confinode
Version:
Node application configuration reader
514 lines • 22 kB
JavaScript
"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