You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
			
				
					323 lines
				
				9.4 KiB
			
		
		
			
		
	
	
					323 lines
				
				9.4 KiB
			| 
								 
											4 years ago
										 
									 | 
							
								//      
							 | 
						||
| 
								 | 
							
								'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';
							 | 
						||
| 
								 | 
							
								}
							 |