UNPKG

multiconf

Version:

Work with JSON configs

302 lines (264 loc) 9.44 kB
// Import. const fs = require('fs'); const path = require('path'); const JSONCParser = require('./lib/jsonc-parser'); const Template = require('./lib/template'); const deepMerge = require('./lib/deep-merge'); // Constants. const DEFAULT_CONF_FILES_DIRECTORY = './conf'; const CONF_ENCODING = 'utf8'; const JSONC_CONF_FILES_EXTENSION = '.jsonc'; const CONF_FILES_EXTENSION = '.json'; const DEFAULT_CONF_FILES_EXTENSION = '.json.default'; /** * Config. */ class Config { static isRenderTemplates = false; static allowedEnvVars = null; static dirDelimiter = ';'; /** * Get. * @param {string|string[]} [configDir] Config directory or array of directories. * @param {string} [envConfigPrefix] Environment config prefix. * @returns {object} Config object. */ static get(configDir = DEFAULT_CONF_FILES_DIRECTORY, envConfigPrefix) { const dirs = Config.#normalizeConfigDirs(configDir); // Define config object container. let configObject = {}; // Append file configs from each directory in sequence, deep-merging results. for (const dir of dirs) { const dirConfig = {}; FileConfigs.append(dirConfig, dir); deepMerge(configObject, dirConfig); } // Append environment configs if need it. EnvConfigs.append(configObject, envConfigPrefix); // Apply template to config object if need it. if (Config.isRenderTemplates) { configObject = Config.#applyTemplate(configObject); } // Return config object. return configObject; } /** * Get default. * @param {string|string[]} [configDir] Config directory or array of directories. * @returns {object} Config object. */ static getDefault(configDir = DEFAULT_CONF_FILES_DIRECTORY) { const dirs = Config.#normalizeConfigDirs(configDir); // Define config object container. let configObject = {}; // Append default file configs from each directory in sequence, deep-merging results. for (const dir of dirs) { const dirConfig = {}; FileConfigs.append(dirConfig, dir, true); deepMerge(configObject, dirConfig); } // Apply template to config object if need it. if (Config.isRenderTemplates) { configObject = Config.#applyTemplate(configObject); } // Return config object. return configObject; } /** * Apply template to config object values recursively. * @private * @param {object} configObject Config object to handle. * @returns {object} Config object. */ static #applyTemplate(configObject) { // Check. if (configObject === null || typeof configObject !== 'object') { return configObject; } if (Array.isArray(configObject)) { return configObject.map((value) => Config.#applyTemplate(value)); } const template = new Template({ }); const context = { env: Config.#getFilteredEnv(), config: configObject, to_base64: (str) => Buffer.from(str).toString('base64'), from_base64: (str) => Buffer.from(str, 'base64').toString('utf8'), }; // Render config object as template. const renderedConfigObject = {}; for (const key in configObject) { const value = configObject[key]; if (typeof value === 'string') { renderedConfigObject[key] = template.renderString(value, context); } else if (value !== null && typeof value === 'object') { renderedConfigObject[key] = Config.#applyTemplate(value); } else { renderedConfigObject[key] = value; } } // Return rendered config object. return renderedConfigObject; } /** * Normalize config directory input into an array. * @private * @param {string|string[]} configDir Config directory or array of directories. * @returns {string[]} Config directories. */ static #normalizeConfigDirs(configDir) { const list = Array.isArray(configDir) ? configDir : [configDir]; return list .map((dir) => dir.split(Config.dirDelimiter)) .flat() .filter((dir) => dir.trim() !== ''); } /** * Set template rendering. * @param {boolean} value Whether to enable or disable template rendering. * @returns {Config} Config class for chaining. */ static setTemplateRendering(value) { Config.isRenderTemplates = value; return Config; } /** * Set allowed environment variable names for template context. * @param {string[]|null} varNames Array of allowed env var names, or null to allow all. * @returns {Config} Config class for chaining. */ static setAllowedEnvVars(varNames) { Config.allowedEnvVars = varNames; return Config; } /** * Set directory delimiter for config directories. * @param {string} delimiter Delimiter string. * @returns {Config} Config class for chaining. */ static setDirDelimiter(delimiter) { Config.dirDelimiter = delimiter; return Config; } /** * Get filtered environment object based on whitelist. * @private * @returns {object} Environment object (filtered if whitelist is set, otherwise all process.env). */ static #getFilteredEnv() { // If no whitelist is set, return full process.env. if (Config.allowedEnvVars === null) { return process.env; } // Build filtered env object from whitelist (supporting exact strings and regex patterns). const filteredEnv = {}; for (const envKey in process.env) { for (const allowedItem of Config.allowedEnvVars) { // Check if allowedItem is a regex pattern. if (allowedItem instanceof RegExp) { if (allowedItem.test(envKey)) { filteredEnv[envKey] = process.env[envKey]; break; } } else if (typeof allowedItem === 'string' && allowedItem === envKey) { // Exact string match. filteredEnv[envKey] = process.env[envKey]; break; } } } return filteredEnv; } } /** * File configs. */ class FileConfigs { /** * Append configs from files. * @param {object} configObject Config object to handle. * @param {string} confDir Config directory. * @returns {object} Config object. */ static append(configObject, confDir, defaultOnly = false) { // Get files from config directory. const files = fs.readdirSync(confDir, CONF_ENCODING); // Handle config files. for (let i = 0; i < files.length; i++) { // File name. const file = files[i]; // If user-defined JSONC config. if (file.endsWith(JSONC_CONF_FILES_EXTENSION) && !defaultOnly) { const jsonConf = fs.readFileSync(path.join(confDir, file), CONF_ENCODING); const confName = file.substring(0, file.length - JSONC_CONF_FILES_EXTENSION.length); try { configObject[confName] = JSONCParser.parse(jsonConf); } catch (error) { throw new Error(`Can't parse user-defined config "${confName}".\n${error}`); } } // If user-defined JSON config. if (file.endsWith(CONF_FILES_EXTENSION) && !defaultOnly) { const jsonConf = fs.readFileSync(path.join(confDir, file), CONF_ENCODING); const confName = file.substring(0, file.length - CONF_FILES_EXTENSION.length); if (configObject[confName] === undefined) { try { configObject[confName] = JSON.parse(jsonConf); } catch (error) { throw new Error(`Can't parse user-defined config "${confName}".\n${error}`); } } } // If default config. if (file.endsWith(DEFAULT_CONF_FILES_EXTENSION)) { const jsonConf = fs.readFileSync(path.join(confDir, file), CONF_ENCODING); const confName = file.substring(0, file.length - DEFAULT_CONF_FILES_EXTENSION.length); if (configObject[confName] === undefined) { try { configObject[confName] = JSON.parse(jsonConf); } catch (error) { throw new Error(`Can't parse default config "${confName}".\n${error}`); } } } } // Return object with appends. return configObject; } } /** * Environment configs. */ class EnvConfigs { /** * Append configs from environment by prefix. * @param {object} configObject Config object to handle. * @param {string} [envConfigPrefix] Environment config prefix. * @returns {object} Config object. */ static append(configObject, envConfigPrefix) { // Check. if (typeof configObject !== 'object' || typeof envConfigPrefix !== 'string') { return; } // Get environment params. for (const envParamKey in process.env) { // Do not handle if without needed prefix. if (!envParamKey.startsWith(envConfigPrefix)) { continue; } // Define config key. const configKey = envParamKey.substring(envConfigPrefix.length); const lowerCaseConfigKey = configKey.toLowerCase(); // Define config value. const configValue = process.env[envParamKey]; // Try to parse and append config value. Keep original string if can not parse. try { const configValueObject = JSON.parse(configValue); configObject[configKey] = configValueObject; configObject[lowerCaseConfigKey] = configValueObject; } catch (error) { throw new Error(`Can't parse env config "${envParamKey}".\n${error}`); } } // Return object with appends. return configObject; } } // Export. module.exports = Config;