forked from zhurui/management
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';
|
||
|
}
|