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