import * as path from 'path' ; import * as fs from 'fs' ; import * as os from 'os' ; const fsReadFileAsync = fs . promises . readFile ; export type LilconfigResult = null | { filepath : string ; config : any ; isEmpty ? : boolean ; } ; interface OptionsBase { stopDir ? : string ; searchPlaces ? : string [ ] ; ignoreEmptySearchPlaces ? : boolean ; packageProp ? : string | string [ ] ; } export type Transform = TransformSync | ( ( result : LilconfigResult ) => Promise < LilconfigResult > ) ; export type TransformSync = ( result : LilconfigResult ) => LilconfigResult ; type LoaderResult = any ; export type LoaderSync = ( filepath : string , content : string ) => LoaderResult ; export type Loader = LoaderSync | ( ( filepath : string , content : string ) => Promise < LoaderResult > ) ; export type Loaders = Record < string , Loader > ; export type LoadersSync = Record < string , LoaderSync > ; export interface Options extends OptionsBase { loaders ? : Loaders ; transform ? : Transform ; } export interface OptionsSync extends OptionsBase { loaders ? : LoadersSync ; transform ? : TransformSync ; } function getDefaultSearchPlaces ( name ) { return [ 'package.json' , `.${ name }rc.json` , `.${ name }rc.js` , `${ name }.config.js` , `.${ name }rc.cjs` , `${ name }.config.cjs` , ] ; } function getSearchPaths ( startDir , stopDir ) { return startDir . split ( path . sep ) . reduceRight < { searchPlaces : string [ ] ; passedStopDir : boolean ; } > ( ( acc , _ , ind , arr ) => { const currentPath = arr . slice ( 0 , ind + 1 ) . join ( path . sep ) ; /**
         * fix #17
         * On *nix, if cwd is not under homedir,
         * the last path will be '', ('/build' -> '')
         * but it should be '/' actually.
         * And on Windows, this will never happen. ('C:\build' -> 'C:')
         */ if ( ! acc . passedStopDir ) acc . searchPlaces . push ( currentPath || path . sep ) ; if ( currentPath === stopDir ) acc . passedStopDir = true ; return acc ; } , { searchPlaces : [ ] , passedStopDir : false } ) . searchPlaces ; } export const defaultLoaders = Object . freeze ( { '.js' : require , '.json' : require , '.cjs' : require , noExt ( _ , content ) { return JSON . parse ( content ) ; } , } ) ; function getExtDesc ( ext ) { return ext === 'noExt' ? 'files without extensions' : `extension "${ ext }"` ; } function getOptions ( name , options ? ) ; function getOptions ( name , options ? ) ; function getOptions ( name , options = { } ) { const conf = { stopDir : os . homedir ( ) , searchPlaces : getDefaultSearchPlaces ( name ) , ignoreEmptySearchPlaces : true , transform : ( x ) : LilconfigResult => x , packageProp : [ name ] , ... options , loaders : { ... defaultLoaders , ... options . loaders } , } ; conf . searchPlaces . forEach ( place => { const key = path . extname ( place ) || 'noExt' ; const loader = conf . loaders [ key ] ; if ( ! loader ) { throw new Error ( `No loader specified for ${ getExtDesc ( key ) }, so searchPlaces item "${ place }" is invalid` ) ; } if ( typeof loader !== 'function' ) { throw new Error ( `loader for ${ getExtDesc ( key ) } is not a function (type provided: "${ typeof loader }"), so searchPlaces item "${ place }" is invalid` ) ; } } ) ; return conf ; } function getPackageProp ( props , obj ) { if ( typeof props === 'string' && props in obj ) return obj [ props ] ; return ( ( Array . isArray ( props ) ? props : props . split ( '.' ) ) . reduce ( ( acc , prop ) : unknown => ( acc === undefined ? acc : acc [ prop ] ) , obj ) || null ) ; } type SearchItem = { filepath : string ; fileName : string ; loaderKey : string ; } ; function getSearchItems ( searchPlaces , searchPaths ) { return searchPaths . reduce < SearchItem [ ] > ( ( acc , searchPath ) => { searchPlaces . forEach ( fileName => acc . push ( { fileName , filepath : path . join ( searchPath , fileName ) , loaderKey : path . extname ( fileName ) || 'noExt' , } ) ) ; return acc ; } , [ ] ) ; } function validateFilePath ( filepath ) { if ( ! filepath ) throw new Error ( 'load must pass a non-empty string' ) ; } function validateLoader ( loader , ext ) { if ( ! loader ) throw new Error ( `No loader specified for extension "${ ext }"` ) ; if ( typeof loader !== 'function' ) throw new Error ( 'loader is not a function' ) ; } type AsyncSearcher = { search ( searchFrom ? ) : Promise < LilconfigResult > ; load ( filepath ) : Promise < LilconfigResult > ; } ; export function lilconfig ( name , options ? ) { const { ignoreEmptySearchPlaces , loaders , packageProp , searchPlaces , stopDir , transform , } = getOptions ( name , options ) ; return { async search ( searchFrom = process . cwd ( ) ) { const searchPaths = getSearchPaths ( searchFrom , stopDir ) ; const result = { config : null , filepath : '' , } ; const searchItems = getSearchItems ( searchPlaces , searchPaths ) ; for ( const { fileName , filepath , loaderKey } of searchItems ) { try { await fs . promises . access ( filepath ) ; } catch { continue ; } const content = String ( await fsReadFileAsync ( filepath ) ) ; const loader = loaders [ loaderKey ] ; if ( fileName === 'package.json' ) { const pkg = loader ( filepath , content ) ; const maybeConfig = getPackageProp ( packageProp , pkg ) ; if ( maybeConfig != null ) { result . config = maybeConfig ; result . filepath = filepath ; break ; } continue ; } const isEmpty = content . trim ( ) === '' ; if ( isEmpty && ignoreEmptySearchPlaces ) continue ; if ( isEmpty ) { result . isEmpty = true ; result . config = undefined ; } else { validateLoader ( loader , loaderKey ) ; result . config = loader ( filepath , content ) ; } result . filepath = filepath ; break ; } if ( result . filepath === '' && result . config === null ) return transform ( null ) ; return transform ( result ) ; } , async load ( filepath ) { validateFilePath ( filepath ) ; const absPath = path . resolve ( process . cwd ( ) , filepath ) ; const { base , ext } = path . parse ( absPath ) ; const loaderKey = ext || 'noExt' ; const loader = loaders [ loaderKey ] ; validateLoader ( loader , loaderKey ) ; const content = String ( await fsReadFileAsync ( absPath ) ) ; if ( base === 'package.json' ) { const pkg = await loader ( absPath , content ) ; return transform ( { config : getPackageProp ( packageProp , pkg ) , filepath : absPath , } ) ; } const result = { config : null , filepath : absPath , } ; const isEmpty = content . trim ( ) === '' ; if ( isEmpty && ignoreEmptySearchPlaces ) return transform ( { config : undefined , filepath : absPath , isEmpty : true , } ) ; result . config = isEmpty ? undefined : await loader ( absPath , content ) ; return transform ( isEmpty ? { ... result , isEmpty , config : undefined } : result ) ; } , } ; } type SyncSearcher = { search ( searchFrom ? ) : LilconfigResult ; load ( filepath ) : LilconfigResult ; } ; export function lilconfigSync ( name , options ? ) { const { ignoreEmptySearchPlaces , loaders , packageProp , searchPlaces , stopDir , transform , } = getOptions ( name , options ) ; return { search ( searchFrom = process . cwd ( ) ) { const searchPaths = getSearchPaths ( searchFrom , stopDir ) ; const result = { config : null , filepath : '' , } ; const searchItems = getSearchItems ( searchPlaces , searchPaths ) ; for ( const { fileName , filepath , loaderKey } of searchItems ) { try { fs . accessSync ( filepath ) ; } catch { continue ; } const loader = loaders [ loaderKey ] ; const content = String ( fs . readFileSync ( filepath ) ) ; if ( fileName === 'package.json' ) { const pkg = loader ( filepath , content ) ; const maybeConfig = getPackageProp ( packageProp , pkg ) ; if ( maybeConfig != null ) { result . config = maybeConfig ; result . filepath = filepath ; break ; } continue ; } const isEmpty = content . trim ( ) === '' ; if ( isEmpty && ignoreEmptySearchPlaces ) continue ; if ( isEmpty ) { result . isEmpty = true ; result . config = undefined ; } else { validateLoader ( loader , loaderKey ) ; result . config = loader ( filepath , content ) ; } result . filepath = filepath ; break ; } if ( result . filepath === '' && result . config === null ) return transform ( null ) ; return transform ( result ) ; } , load ( filepath ) { validateFilePath ( filepath ) ; const absPath = path . resolve ( process . cwd ( ) , filepath ) ; const { base , ext } = path . parse ( absPath ) ; const loaderKey = ext || 'noExt' ; const loader = loaders [ loaderKey ] ; validateLoader ( loader , loaderKey ) ; const content = String ( fs . readFileSync ( absPath ) ) ; if ( base === 'package.json' ) { const pkg = loader ( absPath , content ) ; return transform ( { config : getPackageProp ( packageProp , pkg ) , filepath : absPath , } ) ; } const result = { config : null , filepath : absPath , } ; const isEmpty = content . trim ( ) === '' ; if ( isEmpty && ignoreEmptySearchPlaces ) return transform ( { filepath : absPath , config : undefined , isEmpty : true , } ) ; result . config = isEmpty ? undefined : loader ( absPath , content ) ; return transform ( isEmpty ? { ... result , isEmpty , config : undefined } : result ) ; } , } ; }