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';
 | |
| }
 | 
