multiconf
Version:
Work with JSON configs
302 lines (264 loc) • 9.44 kB
JavaScript
// 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;