UNPKG

confinode

Version:

Node application configuration reader

514 lines 22 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path_1 = require("path"); const ConfigDescription_1 = require("../ConfigDescription"); const ConfinodeError_1 = require("../ConfinodeError"); const ConfinodeResult_1 = require("../ConfinodeResult"); const FileDescription_1 = require("../FileDescription"); const Loader_1 = require("../Loader"); const messages_1 = require("../messages"); const utils_1 = require("../utils"); const ConfinodeOptions_1 = require("./ConfinodeOptions"); const synchronization_1 = require("./synchronization"); /** * Test if the given parameter is a set of loading process parameter. * * @param parameter - The parameter to test. * @returns True if loading process parameters. */ function isLoadingParameters(parameter) { return !(Symbol.iterator in Object(parameter)); } /** * The default loading parameters, for the first loaded file. */ const defaultLoadingParameters = { alreadyLoaded: [], intermediateResult: undefined, disableCache: false, final: true, }; /** * Check if content is extending another configuration. * * @param content - The content to check. * @returns True if content has extends. */ function isExtending(content) { if (typeof content === 'object' && !!content && 'extends' in content) { const extensions = content.extends; if (typeof extensions !== 'string' && (!Array.isArray(extensions) || extensions.some(extension => typeof extension !== 'string'))) { throw new ConfinodeError_1.default('badExtends'); } return true; } else { return false; } } /** * The main Confinode class. */ class Confinode { constructor(name, description = ConfigDescription_1.anyItem(), options) { var _a; this.name = name; this.description = description; this.folderCache = new utils_1.Cache(60 * 1000, 18); this.contentCache = new utils_1.Cache(300 * 1000, 24); this.configCache = new utils_1.Cache(300 * 1000, 36); // Load default option (prevent null or undefined provided options to remove default ones) this.parameters = Object.entries(options !== null && options !== void 0 ? options : {}).reduce((previous, [key, value]) => { if (value !== undefined && value !== null) { previous[key] = value; } return previous; }, { ...ConfinodeOptions_1.defaultConfig }); // Prepare options this.parameters.modulePaths = utils_1.ensureArray((_a = options === null || options === void 0 ? void 0 : options.modulePaths) !== null && _a !== void 0 ? _a : []).map(path => path_1.resolve(process.cwd(), path)); this.parameters.modulePaths.unshift(process.cwd()); if (!(options === null || options === void 0 ? void 0 : options.files) || ConfinodeOptions_1.filesAreFilters(options.files)) { this.parameters.files = FileDescription_1.defaultFiles(name); } if ((options === null || options === void 0 ? void 0 : options.files) && ConfinodeOptions_1.filesAreFilters(options.files)) { this.parameters.files = options.files.reduce((previous, filter) => filter(previous), this.parameters.files); } if (!(options === null || options === void 0 ? void 0 : options.mode)) { this.parameters.mode = 'async'; } // Prepare polymorphic methods let _search; if (this.parameters.mode === 'async') { _search = this.asyncSearch; _search.sync = (searchStart) => this.syncSearch(searchStart); } else { _search = this.syncSearch; _search.async = (searchStart) => this.asyncSearch(searchStart); } this.search = _search; let _load; if (this.parameters.mode === 'async') { _load = this.asyncLoad; _load.sync = (file) => this.syncLoad(file); } else { _load = this.syncLoad; _load.async = (file) => this.asyncLoad(file); } this.load = _load; // Create loader manager this.loaderManager = new Loader_1.LoaderManager(name, this.parameters.customLoaders); } /** * Clear the cache. */ clearCache() { this.folderCache.clear(); this.contentCache.clear(); this.configCache.clear(); } /** * Asynchronously search for configuration. * * @param searchStart - The place where search will start, current folder by default. * @returns A promise resolving to the configuration if found, undefined otherwise. */ async asyncSearch(searchStart) { return synchronization_1.asyncExecute(this.searchConfig(false, searchStart)); } /** * Synchronously search for configuration. * * @param searchStart - The place where search will start, current folder by default. * @returns The configuration if found, undefined otherwise. */ syncSearch(searchStart) { return synchronization_1.syncExecute(this.searchConfig(true, searchStart)); } /** * Search for configuration. * * @param syncOnly - Do not use loaders if they cannot load synchronously. * @param searchStart - The place where search will start, current folder by default. * @returns A promise resolving to the configuration if found, undefined otherwise. */ *searchConfig(syncOnly, searchStart = process.cwd()) { try { return yield* this.searchConfigInFolder(syncOnly, (yield synchronization_1.requestIsFolder(searchStart)) ? searchStart : path_1.dirname(searchStart), false); } catch (e) { /* istanbul ignore next */ this.log(true, e); /* istanbul ignore next */ return undefined; } } /** * Search for configuration in given folder. This is a recursive method. * * @param syncOnly - Do not use loaders if they cannot load synchronously. * @param folder - The folder to search in. * @param ignoreAbsolute - True to ignore absolute file names. * @returns A promise resolving to the configuration if found, undefined otherwise. */ *searchConfigInFolder(syncOnly, folder, ignoreAbsolute) { // Get absolute folder const absoluteFolder = path_1.resolve(process.cwd(), folder); this.log(messages_1.Level.Trace, 'searchInFolder', absoluteFolder); // See if already in cache if (this.configCache.has(absoluteFolder)) { this.log(messages_1.Level.Trace, 'loadedFromCache'); return this.configCache.get(absoluteFolder); } // Search configuration files let result; try { result = yield* this.searchConfigUsingDescriptions(syncOnly, absoluteFolder, ignoreAbsolute); // Search in parent if not found here if (result === undefined && absoluteFolder !== this.parameters.searchStop) { const parentFolder = path_1.dirname(absoluteFolder); if (parentFolder !== absoluteFolder) { result = yield* this.searchConfigInFolder(syncOnly, parentFolder, true); } } } catch (e) { this.log(true, e); } if (this.parameters.cache) { this.configCache.set(absoluteFolder, result); } return result; } /** * Search for configuration using options descriptions. * * @param syncOnly - Do not use loaders if they cannot load synchronously. * @param folder - The folder in which to search. * @param ignoreAbsolute - True to ignore absolute file names. * @returns The found elements or undefined. */ *searchConfigUsingDescriptions(syncOnly, folder, ignoreAbsolute) { for (const fileDescription of this.parameters.files) { const fileAndLoader = yield* this.searchFileAndLoader(syncOnly, folder, fileDescription, ignoreAbsolute); if (fileAndLoader) { const [fileName, loader] = fileAndLoader; const result = yield* this.loadConfigFile(fileName, syncOnly, loader); if (result) { this.log(messages_1.Level.Information, 'loadedConfiguration', fileName); return result; } } } return undefined; } /** * Search a file and its loader matching the given description in the given folder. * * @param syncOnly - Do not use loaders if they cannot load synchronously. * @param folder - The folder in which to search. * @param description - The file description. * @param ignoreAbsolute - True to ignore absolute file names. * @returns The found file and loader (and possible loader name), or undefined if none. */ *searchFileAndLoader(syncOnly, folder, description, ignoreAbsolute) { if (FileDescription_1.isFileBasename(description)) { const searchedPath = this.buildConfigurationFileName(folder, description, ignoreAbsolute); if (searchedPath) { const folderName = path_1.dirname(searchedPath); const baseName = path_1.basename(description) + '.'; let fileNames; if (this.folderCache.has(folderName)) { fileNames = this.folderCache.get(folderName); } else { fileNames = yield synchronization_1.requestFolderContent(folderName); if (this.parameters.cache) { this.folderCache.set(folderName, fileNames); } } const loaders = this.findLoaderData(syncOnly, folderName, baseName, fileNames); if (loaders.length > 0) { if (loaders.length > 1) { this.log(messages_1.Level.Warning, 'multipleFiles', searchedPath); } return loaders[0]; } } } else { const fileName = this.buildConfigurationFileName(folder, description.name, ignoreAbsolute); if (fileName && (yield synchronization_1.requestFileExits(fileName))) { const knownLoader = this.buildKnownLoaderGenerator(description.loader); knownLoader.next(); return [fileName, knownLoader]; } } return undefined; } /** * Simulate a generator for the known loader. * * @param loader - The loader to use. * @returns The known loader, formatted as if returned by loader manager. */ *buildKnownLoaderGenerator(loader) { const result = [loader, undefined]; yield result; return result; } /** * Build the configuration file name (possibly without extension) based on the file name and the folder. * * @param folder - The folder in which to search for the file. * @param fileName - The name of the file. * @param ignoreAbsolute - True to ignore absolute file names. * @returns The built file name or undefined if should be ignored. */ buildConfigurationFileName(folder, fileName, ignoreAbsolute) { if (path_1.isAbsolute(fileName)) { return ignoreAbsolute ? undefined : fileName; } else { return path_1.join(folder, fileName); } } /** * Find the loader data for the given files. * * @param syncOnly - Do not use loaders if they cannot load synchronously. * @param folder - The folder in which search is done. * @param baseName - The base name (start) of the file, including the `.` preceding extension. * @param fileNames - The name of the files for which to find loaders. * @returns All found loader data as: full path to file, loader generator. */ findLoaderData(syncOnly, folder, baseName, fileNames) { return fileNames .filter(fileName => fileName.startsWith(baseName)) .map(fileName => { const loader = this.loaderManager.getLoaders(this.parameters.modulePaths, syncOnly, fileName, fileName.slice(baseName.length)); return utils_1.isExisting(loader) ? [path_1.join(folder, fileName), loader] : undefined; }) .filter(utils_1.isExisting); } /** * Asynchronously load the configuration file. * * @param name - The name of the configuration file. The name may be an absolute file path, a relative * file path, or a module name and an optional file path. * @returns A promise resolving to the configuration if loaded, undefined otherwise. */ async asyncLoad(name) { return synchronization_1.asyncExecute(this.loadConfig(name, false)); } /** * Synchronously load the configuration file. * * @param name - The name of the configuration file. The name may be an absolute file path, a relative * file path, or a module name and an optional file path. * @returns The configuration if loader, undefined otherwise. */ syncLoad(name) { return synchronization_1.syncExecute(this.loadConfig(name, true)); } /** * Load configuration from file with given name. * * @param name - The name of the configuration file. The name may be an absolute file path, a relative * file path, or a module name and an optional file path. * @param syncOnly - Do not use loaders if they cannot load synchronously. * @param folder - The folder to resolve name from, defaults to current directory. * @param loadingParameters - The parameters for the current loading process. * @returns The configuration if loaded, undefined otherwise. */ *loadConfig(name, syncOnly, folder = process.cwd(), loadingParameters) { // Search for the real file name let fileName; try { fileName = require.resolve(name, { paths: [folder] }); } catch (_a) { fileName = undefined; } // Load the content try { if (!fileName || !(yield synchronization_1.requestFileExits(fileName))) { throw new ConfinodeError_1.default('fileNotFound', name); } return yield* this.loadConfigFile(fileName, syncOnly, loadingParameters !== null && loadingParameters !== void 0 ? loadingParameters : defaultLoadingParameters); } catch (e) { if (loadingParameters) { // Currently inside loading process, rethrow to caller throw e; } else { // Topmost call, display error this.log(true, e); } } // Return result return undefined; } /** * Load the configuration file. * * @param fileName - The name of the file to load. * @param syncOnly - Do not use loaders if they cannot load synchronously. * @param loaderOrLoading - The loader to use (if already found, search file cases) or the loading * parameters (loading in progress). * @returns The method will return the configuration, or undefined if content is empty (the meaning of * empty depends on the loader). May throw an error if loading problem. */ *loadConfigFile(fileName, syncOnly, loaderOrLoading) { const givenLoader = isLoadingParameters(loaderOrLoading) ? undefined : loaderOrLoading; const loadingParameters = isLoadingParameters(loaderOrLoading) ? loaderOrLoading : defaultLoadingParameters; const absoluteFile = path_1.resolve(process.cwd(), fileName); this.log(messages_1.Level.Trace, 'loadingFile', absoluteFile); if (this.configCache.has(absoluteFile) && !loadingParameters.disableCache) { this.log(messages_1.Level.Trace, 'loadedFromCache'); return this.configCache.get(absoluteFile); } // Prevent recursion loop if (loadingParameters.alreadyLoaded.includes(absoluteFile)) { throw new ConfinodeError_1.default('recursion', [...loadingParameters.alreadyLoaded, absoluteFile]); } // Search for the loader if not provided const modulePaths = [ ...this.parameters.modulePaths, ...loadingParameters.alreadyLoaded.map(path_1.dirname), ].filter(utils_1.unique); const usedLoader = givenLoader !== null && givenLoader !== void 0 ? givenLoader : this.loaderManager.getLoaders(modulePaths, syncOnly, path_1.basename(absoluteFile)); if (!usedLoader) { throw new ConfinodeError_1.default('noLoaderFound', absoluteFile); } // Load and parse file content let result; const content = yield* this.loadConfigFileContent(absoluteFile, usedLoader); if (content === undefined) { this.log(messages_1.Level.Trace, 'emptyConfiguration'); result = loadingParameters.intermediateResult; } else { result = yield* this.parseConfigFileContent(absoluteFile, syncOnly, loadingParameters, content); } if (this.parameters.cache && !loadingParameters.disableCache) { this.configCache.set(absoluteFile, result); } return result; } /** * Load configuration file (raw) content or throw an exception if no loader can load the file. * * @param fileName - The name of the file whose content needs to be loaded. * @param loaders - The loader generators. * @returns The file content. */ *loadConfigFileContent(fileName, loaders) { if (this.contentCache.has(fileName)) { return this.contentCache.get(fileName); } let done = false; let content; const errors = []; while (!done) { const nextLoader = loaders.next(); const [loader, loaderName] = nextLoader.value; if (loaderName) { this.log(messages_1.Level.Trace, 'usingLoader', loaderName); } try { content = yield synchronization_1.requestLoadConfigFile(fileName, loader); done = true; } catch (e) { errors.push([loaderName, e.message ? e.message : /* istanbul ignore next */ String(e)]); if (nextLoader.done) { throw new ConfinodeError_1.default('allLoadersFailed', errors); } } } if (this.parameters.cache) { this.contentCache.set(fileName, content); } return content; } /** * Parse the file content. * * @param fileName - The name of file being parsed. * @param syncOnly - Do not use loaders if they cannot load synchronously. * @param loadingParameters - The loading parameters. * @param content - The content of the file. * @returns The parsing result. */ *parseConfigFileContent(fileName, syncOnly, loadingParameters, content) { let result = loadingParameters.intermediateResult; const alreadyLoaded = [...loadingParameters.alreadyLoaded, fileName]; if (typeof content === 'string') { // Indirection return yield* this.loadConfig(content, syncOnly, path_1.dirname(fileName), { ...loadingParameters, alreadyLoaded, }); } else { const resultFile = { name: fileName, extends: [] }; // Inheritance if (isExtending(content)) { const parentConfigs = utils_1.ensureArray(content.extends); delete content.extends; let disableCache = loadingParameters.disableCache; for (const parentConfig of parentConfigs) { result = yield* this.loadConfig(parentConfig, syncOnly, path_1.dirname(fileName), { alreadyLoaded, intermediateResult: result, disableCache, final: false, }); utils_1.pushIfNew(resultFile.extends, result.files, item => item.name === result.files.name); disableCache = true; // Never load siblings with cache as result depends on previous loads } } // Parse file const partialResult = ConfigDescription_1.asDescription(this.description).parse(content, { keyName: '', fileName, parent: result, final: loadingParameters.final, }); partialResult && (result = ConfinodeResult_1.buildResult(partialResult, resultFile)); } return result; } /* * Implementation. */ log(levelOrIsException, messageIdOrException, ...parameters) { let message; if (typeof levelOrIsException === 'boolean') { /* istanbul ignore else */ if (messageIdOrException instanceof ConfinodeError_1.default) { message = messageIdOrException.internalMessage; } else { const errorMessage = messageIdOrException instanceof Error ? messageIdOrException.message : String(messageIdOrException); message = new messages_1.Message(messages_1.Level.Error, 'internalError', errorMessage); } } else { message = new messages_1.Message(levelOrIsException, messageIdOrException, ...parameters); } this.parameters.logger(message); } } exports.default = Confinode; //# sourceMappingURL=Confinode.js.map