323 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			323 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | //      
 | ||
|  | 'use strict'; | ||
|  | 
 | ||
|  | const path = require('path'); | ||
|  | const loaders = require('./loaders'); | ||
|  | const readFile = require('./readFile'); | ||
|  | const cacheWrapper = require('./cacheWrapper'); | ||
|  | const getDirectory = require('./getDirectory'); | ||
|  | const getPropertyByPath = require('./getPropertyByPath'); | ||
|  | 
 | ||
|  | const MODE_SYNC = 'sync'; | ||
|  | 
 | ||
|  | // An object value represents a config object.
 | ||
|  | // null represents that the loader did not find anything relevant.
 | ||
|  | // undefined represents that the loader found something relevant
 | ||
|  | // but it was empty.
 | ||
|  |                                                | ||
|  | 
 | ||
|  | class Explorer { | ||
|  |                                                        | ||
|  |                                                   | ||
|  |                                                          | ||
|  |                                                     | ||
|  |                            | ||
|  | 
 | ||
|  |   constructor(options                 ) { | ||
|  |     this.loadCache = options.cache ? new Map() : null; | ||
|  |     this.loadSyncCache = options.cache ? new Map() : null; | ||
|  |     this.searchCache = options.cache ? new Map() : null; | ||
|  |     this.searchSyncCache = options.cache ? new Map() : null; | ||
|  |     this.config = options; | ||
|  |     this.validateConfig(); | ||
|  |   } | ||
|  | 
 | ||
|  |   clearLoadCache() { | ||
|  |     if (this.loadCache) { | ||
|  |       this.loadCache.clear(); | ||
|  |     } | ||
|  |     if (this.loadSyncCache) { | ||
|  |       this.loadSyncCache.clear(); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   clearSearchCache() { | ||
|  |     if (this.searchCache) { | ||
|  |       this.searchCache.clear(); | ||
|  |     } | ||
|  |     if (this.searchSyncCache) { | ||
|  |       this.searchSyncCache.clear(); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   clearCaches() { | ||
|  |     this.clearLoadCache(); | ||
|  |     this.clearSearchCache(); | ||
|  |   } | ||
|  | 
 | ||
|  |   validateConfig() { | ||
|  |     const config = this.config; | ||
|  | 
 | ||
|  |     config.searchPlaces.forEach(place => { | ||
|  |       const loaderKey = path.extname(place) || 'noExt'; | ||
|  |       const loader = config.loaders[loaderKey]; | ||
|  |       if (!loader) { | ||
|  |         throw new Error( | ||
|  |           `No loader specified for ${getExtensionDescription( | ||
|  |             place | ||
|  |           )}, so searchPlaces item "${place}" is invalid`
 | ||
|  |         ); | ||
|  |       } | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   search(searchFrom         )                             { | ||
|  |     searchFrom = searchFrom || process.cwd(); | ||
|  |     return getDirectory(searchFrom).then(dir => { | ||
|  |       return this.searchFromDirectory(dir); | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   searchFromDirectory(dir        )                             { | ||
|  |     const absoluteDir = path.resolve(process.cwd(), dir); | ||
|  |     const run = () => { | ||
|  |       return this.searchDirectory(absoluteDir).then(result => { | ||
|  |         const nextDir = this.nextDirectoryToSearch(absoluteDir, result); | ||
|  |         if (nextDir) { | ||
|  |           return this.searchFromDirectory(nextDir); | ||
|  |         } | ||
|  |         return this.config.transform(result); | ||
|  |       }); | ||
|  |     }; | ||
|  | 
 | ||
|  |     if (this.searchCache) { | ||
|  |       return cacheWrapper(this.searchCache, absoluteDir, run); | ||
|  |     } | ||
|  |     return run(); | ||
|  |   } | ||
|  | 
 | ||
|  |   searchSync(searchFrom         )                    { | ||
|  |     searchFrom = searchFrom || process.cwd(); | ||
|  |     const dir = getDirectory.sync(searchFrom); | ||
|  |     return this.searchFromDirectorySync(dir); | ||
|  |   } | ||
|  | 
 | ||
|  |   searchFromDirectorySync(dir        )                    { | ||
|  |     const absoluteDir = path.resolve(process.cwd(), dir); | ||
|  |     const run = () => { | ||
|  |       const result = this.searchDirectorySync(absoluteDir); | ||
|  |       const nextDir = this.nextDirectoryToSearch(absoluteDir, result); | ||
|  |       if (nextDir) { | ||
|  |         return this.searchFromDirectorySync(nextDir); | ||
|  |       } | ||
|  |       return this.config.transform(result); | ||
|  |     }; | ||
|  | 
 | ||
|  |     if (this.searchSyncCache) { | ||
|  |       return cacheWrapper(this.searchSyncCache, absoluteDir, run); | ||
|  |     } | ||
|  |     return run(); | ||
|  |   } | ||
|  | 
 | ||
|  |   searchDirectory(dir        )                             { | ||
|  |     return this.config.searchPlaces.reduce((prevResultPromise, place) => { | ||
|  |       return prevResultPromise.then(prevResult => { | ||
|  |         if (this.shouldSearchStopWithResult(prevResult)) { | ||
|  |           return prevResult; | ||
|  |         } | ||
|  |         return this.loadSearchPlace(dir, place); | ||
|  |       }); | ||
|  |     }, Promise.resolve(null)); | ||
|  |   } | ||
|  | 
 | ||
|  |   searchDirectorySync(dir        )                    { | ||
|  |     let result = null; | ||
|  |     for (const place of this.config.searchPlaces) { | ||
|  |       result = this.loadSearchPlaceSync(dir, place); | ||
|  |       if (this.shouldSearchStopWithResult(result)) break; | ||
|  |     } | ||
|  |     return result; | ||
|  |   } | ||
|  | 
 | ||
|  |   shouldSearchStopWithResult(result                   )          { | ||
|  |     if (result === null) return false; | ||
|  |     if (result.isEmpty && this.config.ignoreEmptySearchPlaces) return false; | ||
|  |     return true; | ||
|  |   } | ||
|  | 
 | ||
|  |   loadSearchPlace(dir        , place        )                             { | ||
|  |     const filepath = path.join(dir, place); | ||
|  |     return readFile(filepath).then(content => { | ||
|  |       return this.createCosmiconfigResult(filepath, content); | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   loadSearchPlaceSync(dir        , place        )                    { | ||
|  |     const filepath = path.join(dir, place); | ||
|  |     const content = readFile.sync(filepath); | ||
|  |     return this.createCosmiconfigResultSync(filepath, content); | ||
|  |   } | ||
|  | 
 | ||
|  |   nextDirectoryToSearch( | ||
|  |     currentDir        , | ||
|  |     currentResult                    | ||
|  |   )          { | ||
|  |     if (this.shouldSearchStopWithResult(currentResult)) { | ||
|  |       return null; | ||
|  |     } | ||
|  |     const nextDir = nextDirUp(currentDir); | ||
|  |     if (nextDir === currentDir || currentDir === this.config.stopDir) { | ||
|  |       return null; | ||
|  |     } | ||
|  |     return nextDir; | ||
|  |   } | ||
|  | 
 | ||
|  |   loadPackageProp(filepath        , content        ) { | ||
|  |     const parsedContent = loaders.loadJson(filepath, content); | ||
|  |     const packagePropValue = getPropertyByPath( | ||
|  |       parsedContent, | ||
|  |       this.config.packageProp | ||
|  |     ); | ||
|  |     return packagePropValue || null; | ||
|  |   } | ||
|  | 
 | ||
|  |   getLoaderEntryForFile(filepath        )              { | ||
|  |     if (path.basename(filepath) === 'package.json') { | ||
|  |       const loader = this.loadPackageProp.bind(this); | ||
|  |       return { sync: loader, async: loader }; | ||
|  |     } | ||
|  | 
 | ||
|  |     const loaderKey = path.extname(filepath) || 'noExt'; | ||
|  |     return this.config.loaders[loaderKey] || {}; | ||
|  |   } | ||
|  | 
 | ||
|  |   getSyncLoaderForFile(filepath        )             { | ||
|  |     const entry = this.getLoaderEntryForFile(filepath); | ||
|  |     if (!entry.sync) { | ||
|  |       throw new Error( | ||
|  |         `No sync loader specified for ${getExtensionDescription(filepath)}` | ||
|  |       ); | ||
|  |     } | ||
|  |     return entry.sync; | ||
|  |   } | ||
|  | 
 | ||
|  |   getAsyncLoaderForFile(filepath        )              { | ||
|  |     const entry = this.getLoaderEntryForFile(filepath); | ||
|  |     const loader = entry.async || entry.sync; | ||
|  |     if (!loader) { | ||
|  |       throw new Error( | ||
|  |         `No async loader specified for ${getExtensionDescription(filepath)}` | ||
|  |       ); | ||
|  |     } | ||
|  |     return loader; | ||
|  |   } | ||
|  | 
 | ||
|  |   loadFileContent( | ||
|  |     mode                  , | ||
|  |     filepath        , | ||
|  |     content                | ||
|  |   )                                                 { | ||
|  |     if (content === null) { | ||
|  |       return null; | ||
|  |     } | ||
|  |     if (content.trim() === '') { | ||
|  |       return undefined; | ||
|  |     } | ||
|  |     const loader = | ||
|  |       mode === MODE_SYNC | ||
|  |         ? this.getSyncLoaderForFile(filepath) | ||
|  |         : this.getAsyncLoaderForFile(filepath); | ||
|  |     return loader(filepath, content); | ||
|  |   } | ||
|  | 
 | ||
|  |   loadedContentToCosmiconfigResult( | ||
|  |     filepath        , | ||
|  |     loadedContent                    | ||
|  |   )                    { | ||
|  |     if (loadedContent === null) { | ||
|  |       return null; | ||
|  |     } | ||
|  |     if (loadedContent === undefined) { | ||
|  |       return { filepath, config: undefined, isEmpty: true }; | ||
|  |     } | ||
|  |     return { config: loadedContent, filepath }; | ||
|  |   } | ||
|  | 
 | ||
|  |   createCosmiconfigResult( | ||
|  |     filepath        , | ||
|  |     content                | ||
|  |   )                             { | ||
|  |     return Promise.resolve() | ||
|  |       .then(() => { | ||
|  |         return this.loadFileContent('async', filepath, content); | ||
|  |       }) | ||
|  |       .then(loaderResult => { | ||
|  |         return this.loadedContentToCosmiconfigResult(filepath, loaderResult); | ||
|  |       }); | ||
|  |   } | ||
|  | 
 | ||
|  |   createCosmiconfigResultSync( | ||
|  |     filepath        , | ||
|  |     content                | ||
|  |   )                    { | ||
|  |     const loaderResult = this.loadFileContent('sync', filepath, content); | ||
|  |     return this.loadedContentToCosmiconfigResult(filepath, loaderResult); | ||
|  |   } | ||
|  | 
 | ||
|  |   validateFilePath(filepath         ) { | ||
|  |     if (!filepath) { | ||
|  |       throw new Error('load and loadSync must pass a non-empty string'); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   load(filepath        )                             { | ||
|  |     return Promise.resolve().then(() => { | ||
|  |       this.validateFilePath(filepath); | ||
|  |       const absoluteFilePath = path.resolve(process.cwd(), filepath); | ||
|  |       return cacheWrapper(this.loadCache, absoluteFilePath, () => { | ||
|  |         return readFile(absoluteFilePath, { throwNotFound: true }) | ||
|  |           .then(content => { | ||
|  |             return this.createCosmiconfigResult(absoluteFilePath, content); | ||
|  |           }) | ||
|  |           .then(this.config.transform); | ||
|  |       }); | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   loadSync(filepath        )                    { | ||
|  |     this.validateFilePath(filepath); | ||
|  |     const absoluteFilePath = path.resolve(process.cwd(), filepath); | ||
|  |     return cacheWrapper(this.loadSyncCache, absoluteFilePath, () => { | ||
|  |       const content = readFile.sync(absoluteFilePath, { throwNotFound: true }); | ||
|  |       const result = this.createCosmiconfigResultSync( | ||
|  |         absoluteFilePath, | ||
|  |         content | ||
|  |       ); | ||
|  |       return this.config.transform(result); | ||
|  |     }); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = function createExplorer(options                 ) { | ||
|  |   const explorer = new Explorer(options); | ||
|  | 
 | ||
|  |   return { | ||
|  |     search: explorer.search.bind(explorer), | ||
|  |     searchSync: explorer.searchSync.bind(explorer), | ||
|  |     load: explorer.load.bind(explorer), | ||
|  |     loadSync: explorer.loadSync.bind(explorer), | ||
|  |     clearLoadCache: explorer.clearLoadCache.bind(explorer), | ||
|  |     clearSearchCache: explorer.clearSearchCache.bind(explorer), | ||
|  |     clearCaches: explorer.clearCaches.bind(explorer), | ||
|  |   }; | ||
|  | }; | ||
|  | 
 | ||
|  | function nextDirUp(dir        )         { | ||
|  |   return path.dirname(dir); | ||
|  | } | ||
|  | 
 | ||
|  | function getExtensionDescription(filepath        )         { | ||
|  |   const ext = path.extname(filepath); | ||
|  |   return ext ? `extension "${ext}"` : 'files without extensions'; | ||
|  | } |