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