UNPKG

config

Version:

Configuration control for production node deployments

1,473 lines (1,297 loc) 45.8 kB
// config.js (c) 2010-2026 Loren West and other contributors // May be freely distributed under the MIT license. // For further details and documentation: // http://lorenwest.github.com/node-config // Dependencies /** @typedef {typeof import('./../parser')} Parser */ const { deferConfig, DeferredConfig } = require('./defer.js'); const { resolveAsyncConfigs } = require('../async'); const Path = require('path'); const FileSystem = require('fs'); const OS = require("os"); const DEFAULT_CONFIG_DIR = Path.join( process.cwd(), 'config'); const SEEN_ERRORS = {}; /** * @typedef {Object} BootstrapCallbackContext * @property {Util} util * @property {deferConfig} defer * @property {RawConfig.raw} raw */ /** * A function for deferred configuration * * Executable config files can now be initialized by a callback which receives useful * context in order to avoid the need for require() or import() calls to config while * it is still initializing. * * @callback bootstrapCallback * @param context {BootstrapCallbackContext} - utility functions for startup * @returns {Object} - data for config properties */ /** * A source in the configSources list * * @typedef {Object} ConfigSource * @property {string} name * @property {Object} parsed - parsed representation * @property {string=} original - unparsed representation of the data */ /** * The data used for a Load operation, mostly derived from environment variables * * @typedef {Object} LoadOptions * @property {string=} configDir - config directory location, absolute or relative to cwd() * @property {string[]=} nodeEnv - NODE_ENV value or commo-separated list * @property {string=} hostName - hostName for host-specific loads * @property {string=} appInstance - per-process config ID * @property {boolean=} skipConfigSources - don't track sources * @property {boolean=} gitCrypt - allow gitcrypt files * @property {Parser=} parser - alternative parser implementation */ /** @type {LoadOptions} */ const DEFAULT_OPTIONS = { configDir: DEFAULT_CONFIG_DIR, nodeEnv: ['development'], hostName: OS.hostname(), gitCrypt: true, parser: require("../parser.js") }; /** * Callback for converting loaded data. * * @callback DataConvert * @param {Object} input - An object to modify. * @returns {Object} - converted object */ const DEFAULT_CLONE_DEPTH = 20; const GIT_CRYPT_REGEX = /^.GITCRYPT/; // regular expression to test for gitcrypt files. /** * Util functions that do not require the singleton in order to run. */ class Util { /** * <p>Make a configuration property hidden so it doesn't appear when enumerating * elements of the object.</p> * * <p> * The property still exists and can be read from and written to, but it won't * show up in for ... in loops, Object.keys(), or JSON.stringify() type methods. * </p> * * <p> * If the property already exists, it will be made hidden. Otherwise it will * be created as a hidden property with the specified value. * </p> * * <p><i> * This method was built for hiding configuration values, but it can be applied * to <u>any</u> javascript object. * </i></p> * * <p>Example:</p> * <pre> * const Util = require('config/lib/util.js'); * ... * * // Hide the Amazon S3 credentials * Util.makeHidden(CONFIG.amazonS3, 'access_id'); * Util.makeHidden(CONFIG.amazonS3, 'secret_key'); * </pre> * * @method makeHidden * @param {object} object - The object to make a hidden property into. * @param {string} property - The name of the property to make hidden. * @param {*=} value - (optional) Set the property value to this (otherwise leave alone) * @return {object} - The original object is returned - for chaining. */ static makeHidden(object, property, value) { // If the new value isn't specified, just mark the property as hidden if (typeof value === 'undefined') { Object.defineProperty(object, property, { enumerable: false, configurable: true }); } else { // Otherwise set the value and mark it as hidden Object.defineProperty(object, property, { value: value, enumerable: false, configurable: true }); } return object; } /** * Make a javascript object immutable (assuring it cannot be changed from the current value) * * All attributes of that object are made immutable, including properties of contained * objects, recursively. * * This operation cannot be undone. * * <p>Example:</p> * <pre> * const config = require('config'); * const myObject = {hello:'world'}; * Util.makeImmutable(myObject); * </pre> * * @method makeImmutable * @param object {Object} - The object to freeze * @return object {Object} - The original object is returned - for chaining. */ static makeImmutable(object) { if (Buffer.isBuffer(object)) { return object; } for (let propertyName of Object.keys(object)) { let value = object[propertyName]; if (value instanceof RawConfig) { Object.defineProperty(object, propertyName, { value: value.resolve(), writable: false, configurable: false }); } else if (Array.isArray(value)) { // Ensure object items of this array are also immutable. for (let item of value) { if (this.isObject(item) || Array.isArray(item)) { this.makeImmutable(item); } } Object.defineProperty(object, propertyName, { value: Object.freeze(value) }); } else { // Call recursively if an object. if (this.isObject(value)) { // Create a proxy, to capture user updates of configuration options, and throw an exception for awareness, as per: // https://github.com/lorenwest/node-config/issues/514 value = new Proxy(this.makeImmutable(value), { get(target, property, receiver) { // Bypass proxy receiver for properties directly on the target (e.g., RegExp.prototype.source) // or properties that are not functions to prevent errors related to internal object methods. if (target.hasOwnProperty(property) || (property in target && typeof target[property] !== 'function')) { return Reflect.get(target, property); } // Otherwise, use the proxy receiver to handle the property access const ref = Reflect.get(target, property, receiver); // Binds the method's `this` context to the target object (e.g., Date.prototype.toISOString) // to ensure it behaves correctly when called on the proxy. if (typeof ref === 'function') { return ref.bind(target); } return ref; }, set(target, name) { const message = (Reflect.has(target, name) ? 'update' : 'add'); // Notify the user. throw Error(`Can not ${message} runtime configuration property: "${name}". Configuration objects are immutable unless ALLOW_CONFIG_MUTATIONS is set.`) } }); } // Check if property already has writable: false and configurable: false const currentDescriptor = Object.getOwnPropertyDescriptor(object, propertyName); if (!currentDescriptor || currentDescriptor.writable !== false || currentDescriptor.configurable !== false) { Object.defineProperty(object, propertyName, { value: value, writable: false, configurable: false }); } } } Object.preventExtensions(object) return object; } /** * Looks into an options object for a specific attribute * * <p> * This method looks into the options object, and if an attribute is defined, returns it, * and if not, returns the default value * </p> * * @template T * @method getOption * @param {Object | undefined} options the options object * @param {string} optionName the attribute name to look for * @param {T} defaultValue the default in case the options object is empty, or the attribute does not exist. * @return {T} options[optionName] if defined, defaultValue if not. */ static getOption(options, optionName, defaultValue) { if (options !== undefined && typeof options[optionName] !== 'undefined') { return options[optionName]; } else { return defaultValue; } } /** * Load the individual file configurations. * * <p> * This method builds a map of filename to the configuration object defined * by the file. The search order is: * </p> * * <pre> * default.EXT * (deployment).EXT * (hostname).EXT * (hostname)-(deployment).EXT * local.EXT * local-(deployment).EXT * </pre> * * <p> * EXT can be yml, yaml, coffee, iced, json, jsonc, cson or js signifying the file type. * yaml (and yml) is in YAML format, coffee is a coffee-script, iced is iced-coffee-script, * json is in JSON format, jsonc is in JSONC format, cson is in CSON format, properties is * in .properties format (http://en.wikipedia.org/wiki/.properties), and js is a javascript * executable file that is require()'d with module.exports being the config object. * </p> * * <p> * hostname is the $HOST environment variable (or --HOST command line parameter) * if set, otherwise the $HOSTNAME environment variable (or --HOSTNAME command * line parameter) if set, otherwise the hostname found from * require('os').hostname(). * </p> * * <p> * Once a hostname is found, everything from the first period ('.') onwards * is removed. For example, abc.example.com becomes abc * </p> * * <p> * (deployment) is the deployment type, found in the $NODE_ENV environment * variable (which can be overridden by using $NODE_CONFIG_ENV * environment variable). Defaults to 'development'. * </p> * * <p> * If the $NODE_APP_INSTANCE environment variable (or --NODE_APP_INSTANCE * command line parameter) is set, then files with this appendage will be loaded. * See the Multiple Application Instances section of the main documentation page * for more information. * </p> * * @method loadFileConfigs * @param {LoadOptions | Load} opts parsing options or Load to update * @return {Load} loadConfig */ static loadFileConfigs(opts) { let load; if (opts instanceof Load) { load = opts; } else { load = new Load(opts); } let options = load.options; let dir = options.configDir; dir = _toAbsolutePath(dir); // Read each file in turn const baseNames = ['default'].concat(options.nodeEnv); const hostName = options.hostName; // #236: Also add full hostname when they are different. if (hostName) { const firstDomain = hostName.split('.')[0]; for (let env of options.nodeEnv) { // Backward compatibility baseNames.push(firstDomain, firstDomain + '-' + env); // Add full hostname when it is not the same if (hostName !== firstDomain) { baseNames.push(hostName, hostName + '-' + env); } } } for (let env of options.nodeEnv) { baseNames.push('local', 'local-' + env); } const allowedFiles = {}; let resolutionIndex = 1; const extNames = options.parser.getFilesOrder(); for (let baseName of baseNames) { const fileNames = [baseName]; if (options.appInstance) { fileNames.push(baseName + '-' + options.appInstance); } for (let fileName of fileNames) { for (let extName of extNames) { allowedFiles[fileName + '.' + extName] = resolutionIndex++; } } } const locatedFiles = this.locateMatchingFiles(dir, allowedFiles); for (let fullFilename of locatedFiles) { load.loadFile(fullFilename); } return load; } /** * Return a list of fullFilenames who exists in allowedFiles * Ordered according to allowedFiles argument specifications * * @method locateMatchingFiles * @param {string} configDirs the config dir, or multiple dirs separated by a column (:) * @param {Object} allowedFiles an object. keys and supported filenames * and values are the position in the resolution order * @returns {string[]} fullFilenames - path + filename */ static locateMatchingFiles(configDirs, allowedFiles) { return configDirs.split(Path.delimiter) .filter(Boolean) .reduce(function (files, configDir) { configDir = _toAbsolutePath(configDir); try { FileSystem.readdirSync(configDir) .filter(file => allowedFiles[file]) .forEach(function (file) { files.push([allowedFiles[file], Path.join(configDir, file)]); }); } catch (e) { } return files; }, []) .sort(function (a, b) { return a[0] - b[0]; }) .map(function (file) { return file[1]; }); } /** * * @param {object} config */ static resolveDeferredConfigs(config) { const deferred = []; function _iterate (prop) { if (prop == null || prop.constructor === String) { return; } // We put the properties we are going to look it in an array to keep the order predictable const propsToSort = Object.keys(prop).filter((property) => prop[property] != null); // Second step is to iterate of the elements in a predictable (sorted) order for (let name of propsToSort.sort()) { const property = prop[name]; if (property.constructor === Object) { _iterate(property); } else if (property.constructor === Array) { property.forEach(function (entry, i) { if (entry instanceof DeferredConfig) { deferred.push(entry.prepare(config, property, i)); } else { _iterate(property[i]); } }); } else if (property instanceof DeferredConfig) { deferred.push(property.prepare(config, prop, name)); } // else: Nothing to do. Keep the property how it is. } } _iterate(config); deferred.forEach((defer) => { defer.resolve() }); } /** * Used to resolve configs that have async functions. * * NOTE: Do not use `config.get` before executing this method, it will freeze the config object * * This replaces ./async.js, which is deprecated. * * @param config * @returns {Promise<void>} */ static async resolveAsyncConfigs(config) { const promises = []; await resolveAsyncConfigs(config); function _iterate (prop) { if (prop == null || prop.constructor === String) { return; } // We put the properties we are going to look it in an array to keep the order predictable const propsToSort = Object.keys(prop).filter((property) => prop[property] != null); // Second step is to iterate of the elements in a predictable (sorted) order for (let name of propsToSort.sort()) { const property = prop[name]; if (property.constructor === Object) { _iterate(property); } else if (property.constructor === Array) { for (let entry of property) { if (entry instanceof Promise) { promises.push(entry); } else { _iterate(entry); } } } else if (property instanceof Promise) { promises.push(property); } // else: Nothing to do. Keep the property how it is. } } _iterate(config); await Promise.all(promises); } /** * Return a deep copy of the specified object using cloneDeep(parent, depth, circular, prototype). * * This returns a new object with all elements copied from the specified * object. Deep copies are made of objects and arrays so you can do anything * with the returned object without affecting the input object. * * @method cloneDeep * @param {object} parent The original object to copy from * @param {number} [depth=20] Maximum depth * @param {boolean} [circular=true] Handle circular references * @param {object=} prototype Optional prototype for the new object * @return {object} A new object with the elements copied from the parent object * * This method is copied from https://github.com/pvorb/node-clone/blob/17eea36140d61d97a9954c53417d0e04a00525d9/clone.js * * Copyright © 2011-2014 Paul Vorbach and contributors. * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the “Software”), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: The above copyright notice and this permission * notice shall be included in all copies or substantial portions of the Software. */ static cloneDeep(parent, depth, circular, prototype) { // maintain two arrays for circular references, where corresponding parents // and children have the same index const allParents = []; const allChildren = []; const util = this; if (typeof circular === 'undefined') circular = true; if (typeof depth === 'undefined') depth = 20; // recurse this function so we don't reset allParents and allChildren function _clone(parent, depth) { // cloning null always returns null if (parent === null) return null; if (depth === 0) return parent; let child; if (typeof parent != 'object') { return parent; } if (Array.isArray(parent)) { child = []; } else if (parent instanceof RegExp) { child = new RegExp(parent.source, util.getRegExpFlags(parent)); if (parent.lastIndex) child.lastIndex = parent.lastIndex; } else if (parent instanceof Date) { child = new Date(parent.getTime()); } else if (Buffer.isBuffer(parent)) { child = Buffer.alloc(parent.length); parent.copy(child); return child; } else if (parent instanceof RawConfig) { child = parent; } else { if (typeof prototype === 'undefined') child = Object.create(Object.getPrototypeOf(parent)); else child = Object.create(prototype); } if (circular) { const index = allParents.indexOf(parent); if (index !== -1) { return allChildren[index]; } allParents.push(parent); allChildren.push(child); } for (const i in parent) { const propDescriptor = Object.getOwnPropertyDescriptor(parent, i); const hasGetter = ((typeof propDescriptor !== 'undefined') && (typeof propDescriptor.get !== 'undefined')); if (hasGetter) { Object.defineProperty(child, i, propDescriptor); } else if (util.isPromise(parent[i])) { child[i] = parent[i]; } else { child[i] = _clone(parent[i], depth - 1); } } return child; } return _clone(parent, depth); } /** * Underlying get mechanism * * @method getPath * @param {object} object - Object to get the property for * @param {string|string[]} property - The property name to get (as an array or '.' delimited string) * @return {*} value - Property value, including undefined if not defined. */ static getPath(object, property) { const path = Array.isArray(property) ? property : property.split('.'); let next = object; for (let i = 0; i < path.length; i++) { const name = path[i]; const value = next[name]; if (i === path.length - 1) { return value; } // Note that typeof null === 'object' if (value === null || typeof value !== 'object') { return undefined; } next = value; } } /** * Set objects given a path as a string list * * @method setPath * @param {object} object - Object to set the property on * @param {string|string[]} property - The property name to get (as an array or '.' delimited string) * @param {*} value - value to set, ignoring null * @return {*} - the given value */ static setPath(object, property, value) { const path = Array.isArray(property) ? property : property.split('.'); if (value === null || path.length === 0) { return; } let next = object; for (let i = 0; i < path.length; i++) { let name = path[i]; if (i === path.length - 1) { // no more keys to make, so set the value next[name] = value; } else if (Object.hasOwnProperty.call(next, name)) { next = next[name]; } else { next = next[name] = {}; } } return value; } /** * Return true if two objects have equal contents. * * @method equalsDeep * @param object1 {Object} The object to compare from * @param object2 {Object} The object to compare with * @param depth {number} An optional depth to prevent recursion. Default: 20. * @return {boolean} True if both objects have equivalent contents */ static equalsDeep(object1, object2, depth) { // Recursion detection depth = (depth === null ? DEFAULT_CLONE_DEPTH : depth); if (depth < 0) { return {}; } // Fast comparisons if (!object1 || !object2) { return false; } if (object1 === object2) { return true; } if (typeof (object1) != 'object' || typeof (object2) != 'object') { return false; } // They must have the same keys. If their length isn't the same // then they're not equal. If the keys aren't the same, the value // comparisons will fail. if (Object.keys(object1).length !== Object.keys(object2).length) { return false; } // Compare the values for (const prop in object1) { // Call recursively if an object or array if (object1[prop] && typeof (object1[prop]) === 'object') { if (!this.equalsDeep(object1[prop], object2[prop], depth - 1)) { return false; } } else { if (object1[prop] !== object2[prop]) { return false; } } } // Test passed. return true; } /** * Extend an object, and any object it contains. * * This does not replace deep objects, but dives into them * replacing individual elements instead. * * @method extendDeep * @param mergeInto {Object} The object to merge into * @param mergeFrom... {Object} - Any number of objects to merge from * @param {number} [depth=20] - An optional depth to prevent recursion. Default: 20. * @return {Object} The altered mergeInto object is returned */ static extendDeep(mergeInto, ...vargs) { // Initialize let depth = vargs.pop(); if (typeof (depth) != 'number') { vargs.push(depth); depth = DEFAULT_CLONE_DEPTH; } // Recursion detection if (depth < 0) { return mergeInto; } for (let mergeFrom of vargs) { // Cycle through each element of the object to merge from for (const prop in mergeFrom) { // save original value in deferred elements const fromIsDeferredFunc = mergeFrom[prop] instanceof DeferredConfig; const isDeferredFunc = mergeInto[prop] instanceof DeferredConfig; if (fromIsDeferredFunc && Object.hasOwnProperty.call(mergeInto, prop)) { mergeFrom[prop]._original = isDeferredFunc ? mergeInto[prop]._original : mergeInto[prop]; } // Extend recursively if both elements are objects and target is not really a deferred function if (mergeFrom[prop] instanceof Date) { mergeInto[prop] = mergeFrom[prop]; } if ((mergeFrom[prop] instanceof RegExp) || (mergeFrom[prop] instanceof RawConfig)) { mergeInto[prop] = mergeFrom[prop]; } else if (Util.isObject(mergeInto[prop]) && Util.isObject(mergeFrom[prop]) && !isDeferredFunc) { Util.extendDeep(mergeInto[prop], mergeFrom[prop], depth - 1); } else if (Util.isPromise(mergeFrom[prop])) { mergeInto[prop] = mergeFrom[prop]; } else if (mergeFrom[prop] && typeof mergeFrom[prop] === 'object') { // Copy recursively if the mergeFrom element is an object (or array or fn) mergeInto[prop] = Util.cloneDeep(mergeFrom[prop], depth - 1); } else if (Object.getOwnPropertyDescriptor(Object(mergeFrom), prop)) { // Copy property descriptor otherwise, preserving accessors Object.defineProperty(mergeInto, prop, Object.getOwnPropertyDescriptor(Object(mergeFrom), prop)); } else if (mergeInto[prop] !== mergeFrom[prop]) { mergeInto[prop] = mergeFrom[prop]; } } } // Chain return mergeInto; } /** * Is the specified argument a regular javascript object? * * The argument is an object if it's a JS object, but not an array. * * @method isObject * @param {unknown} obj An argument of any type. * @return {obj is object} TRUE if the arg is an object, FALSE if not */ static isObject(obj) { return (obj !== null) && (typeof obj === 'object') && !(Array.isArray(obj)); } /** * Is the specified argument a javascript promise? * * @method isPromise * @param {unknown} obj An argument of any type. * @returns {obj is Promise} TRUE if the arg is a Promise, FALSE if not */ static isPromise(obj) { return Object.prototype.toString.call(obj) === '[object Promise]'; } /** * Returns a string of flags for regular expression `re`. * * @param {RegExp} re Regular expression * @returns {string} Flags */ static getRegExpFlags = function (re) { let flags = ''; re.global && (flags += 'g'); re.ignoreCase && (flags += 'i'); re.multiline && (flags += 'm'); return flags; } /** * Returns a new deep copy of the current config object, or any part of the config if provided. * * @param {Object} config The part of the config to copy and serialize. * @returns {Object} The cloned config or part of the config */ static toObject(config) { return JSON.parse(JSON.stringify(config)); } /** * Send a message to console.error once * @param {string} key the subject of the error, used to prevent duplicates * @param {string} message helpful message for library users * @param {...object} args additional arguments to console.error */ static errorOnce(key, message, ...args) { if (SEEN_ERRORS[key] === undefined) { SEEN_ERRORS[key] = true; let err = new Error; Error.captureStackTrace(err, this.errorOnce); console.error(message, ...args, err.stack); } } } /** * Record a set of lookups */ class Env { /** * Create an instance */ constructor() { this.lookups = {}; } /** * Create a snapshot of an Env. * @returns {Env} */ clone() { const copy = new Env(); copy.lookups = Util.cloneDeep(this.lookups); return copy; } /** * <p>Initialize a parameter from the command line or process environment</p> * * <p> * This method looks for the parameter from the command line in the format * --PARAMETER=VALUE, then from the process environment, then from the * default specified as an argument. * </p> * * @template T * @method initParam * @param {string} paramName Name of the parameter * @param {T} [defaultValue] Default value of the parameter * @return {T} The found value, or default value */ initParam(paramName, defaultValue) { // Record and return the value const value = this.getCmdLineArg(paramName) || process.env[paramName] || defaultValue; this.setEnv(paramName, value); return value; } /** * <p>Get Command Line Arguments</p> * * <p> * This method allows you to retrieve the value of the specified command line argument. * </p> * * <p> * The argument is case sensitive, and must be of the form '--ARG_NAME=value' * </p> * * @method getCmdLineArg * @param {string} searchFor The argument name to search for * @return {false|string} false if the argument was not found, the argument value if found */ getCmdLineArg(searchFor) { const cmdLineArgs = process.argv.slice(2, process.argv.length); const argName = '--' + searchFor + '='; for (let argvIt = 0; argvIt < cmdLineArgs.length; argvIt++) { if (cmdLineArgs[argvIt].indexOf(argName) === 0) { return cmdLineArgs[argvIt].substr(argName.length); } } return false; } /** * <p>Get a Config Environment Variable Value</p> * * <p> * This method returns the value of the specified config environment variable, * including any defaults or overrides. * </p> * * @method getEnv * @param {string} varName The environment variable name * @return {string} The value of the environment variable */ getEnv(varName) { return this.lookups[varName]; } /** * Set a tracing variable of what was accessed from process.env * * @see fromEnvironment * @param {string} key * @param {*} value */ setEnv(key, value) { this.lookups[key] = value; } } /** * The work horse of loading Config data - without the singleton. * * This class can be used to execute important workflows, such as build-time validations * and Module Defaults. * * @example * //load module defaults * const config = require("config"); * const Load = require("config/util.js").Load; * * let load = Load.fromEnvironment(); * * load.scan(); * * config.setModuleDefaults("my-module", load.config); * * @example * // verify configs * const Load = require("config/util.js").Load; * * for (let environment of ["sandbox", "qa", "qa-hyderabad", "perf", "staging", "prod-east-1", "prod-west-2"] { * let load = Load.fromEnvironment(environment); * * load.scan(); * } * * * @class Load */ class Load { /** * @constructor * @param {LoadOptions=} options - defaults to reading from environment variables * @param {Env=} env - optional Env data, usually from fromEnvironment() */ constructor(options, env = new Env()) { /** @type {Env} */ this.env = env; /** @type {LoadOptions} */ this.options = { ...DEFAULT_OPTIONS, ...options }; /** @type {ConfigSource[] | undefined} */ this.sources = this.options.skipConfigSources ? undefined : []; /** @type {Parser | undefined} */ this.parser = this.options.parser; /** @type {Record<string, any>} */ this.config = {}; /** @type {Record<string, any> | undefined} */ this.defaults = undefined; /** @type {Record<string, any> | undefined} */ this.unmerged = undefined; /** @type {boolean} */ this.updated = true; } /** * Create a snapshot of a Load instance * @returns {Load} */ clone() { const newEnv = this.env.clone(); const copy = new Load(this.options, newEnv); return Util.extendDeep(copy, this); } /** * <p>Initialize a parameter from the command line or process environment</p> * * <p> * This method looks for the parameter from the command line in the format * --PARAMETER=VALUE, then from the process environment, then from the * default specified as an argument. * </p> * * @template T * @method initParam * @param {string} paramName Name of the parameter * @param {T} [defaultValue] Default value of the parameter * @return {T} The found value, or default value */ initParam(paramName, defaultValue) { return this.env.initParam(paramName, defaultValue); } /** * <p>Get Command Line Arguments</p> * * <p> * This method allows you to retrieve the value of the specified command line argument. * </p> * * <p> * The argument is case sensitive, and must be of the form '--ARG_NAME=value' * </p> * * @method getCmdLineArg * @param {string} searchFor The argument name to search for * @return {false|string} false if the argument was not found, the argument value if found */ getCmdLineArg(searchFor) { return this.env.getCmdLineArg(searchFor); } /** * <p>Get a Config Environment Variable Value</p> * * <p> * This method returns the value of the specified config environment variable, * including any defaults or overrides. * </p> * * @method getEnv * @param {string} varName The environment variable name * @return {string} The value of the environment variable */ getEnv(varName) { return this.env.getEnv(varName); } /** * Set a tracing variable of what was accessed from process.env * * @see fromEnvironment * @param {string} key * @param {*} value */ setEnv(key, value) { return this.env.setEnv(key, value); } /** * Add a set of configurations and record the source * * @param {string=} name an entry will be added to sources under this name (if given) * @param {object=} values values to merge in * @param {string=} original Optional unparsed version of the data * @return {Load} this */ addConfig(name, values, original) { this.updated = true; Util.extendDeep(this.config, values); if (name && this.sources) { let source = {name, parsed: values}; if (original !== undefined) { source.original = original; } this.sources.push(source); } return this; } /** * scan and load config files in the same manner that config.js does * * @param {{name: string, config: any}[]=} additional additional values to populate (usually from NODE_CONFIG) */ scan(additional) { Util.loadFileConfigs(this); if (additional) { for (let {name, config} of additional) { this.addConfig(name, config); } } // Override with environment variables if there is a custom-environment-variables.EXT mapping file this.loadCustomEnvVars(); Util.resolveDeferredConfigs(this.config); } /** * Load a file and add it to the configuration * * @param {string} fullFilename an absolute file path * @param {DataConvert=} convert * @returns {null} */ loadFile(fullFilename, convert) { let configObject = null; let fileContent = null; // Note that all methods here are the Sync versions. This is appropriate during // module loading (which is a synchronous operation), but not thereafter. try { // Try loading the file. fileContent = FileSystem.readFileSync(fullFilename, 'utf-8'); fileContent = fileContent.replace(/^\uFEFF/, ''); } catch (e2) { if (e2.code !== 'ENOENT') { throw new Error('Config file ' + fullFilename + ' cannot be read. Error code is: '+e2.code +'. Error message is: '+e2.message); } return null; // file doesn't exists } // Parse the file based on extension try { // skip if it's a gitcrypt file and CONFIG_SKIP_GITCRYPT is true if (!this.options.gitCrypt) { if (GIT_CRYPT_REGEX.test(fileContent)) { console.error('WARNING: ' + fullFilename + ' is a git-crypt file and CONFIG_SKIP_GITCRYPT is set. skipping.'); return null; } } configObject = this.parser.parse(fullFilename, fileContent); } catch (e3) { if (GIT_CRYPT_REGEX.test(fileContent)) { console.error('ERROR: ' + fullFilename + ' is a git-crypt file and CONFIG_SKIP_GITCRYPT is not set.'); } throw new Error("Cannot parse config file: '" + fullFilename + "': " + e3); } if (typeof configObject == 'function') { /** @type bootstrapCallback */ let fn = configObject; configObject = fn({ defer: deferConfig, util: Util, raw: RawConfig.raw }); } if (convert) { configObject = convert(configObject); } this.addConfig(fullFilename, configObject, fileContent); return configObject; } /** * load custom-environment-variables * * @param extNames {string[]=} extensions * @returns {{}} */ loadCustomEnvVars(extNames) { let resolutionIndex = 1; const allowedFiles = {}; extNames = extNames ?? this.parser.getFilesOrder(); extNames.forEach(function (extName) { allowedFiles['custom-environment-variables' + '.' + extName] = resolutionIndex++; }); const locatedFiles = Util.locateMatchingFiles(this.options.configDir, allowedFiles); locatedFiles.forEach((fullFilename) => { this.loadFile(fullFilename, (configObj) => this.substituteDeep(configObj, process.env)); }); } /** * Return the report of where the sources for this load operation came from * @returns {ConfigSource[]} */ getSources() { return (this.sources ?? []).slice(); } /** * <p> * Set default configurations for a node.js module. * </p> * * <p> * This allows module developers to attach their configurations onto the * default configuration object so they can be configured by the consumers * of the module. * </p> * * <p>Using the function within your module:</p> * <pre> * load.setModuleDefaults("MyModule", { * &nbsp;&nbsp;templateName: "t-50", * &nbsp;&nbsp;colorScheme: "green" * }); * <br> * // Template name may be overridden by application config files * console.log("Template: " + CONFIG.MyModule.templateName); * </pre> * * <p> * The above example results in a "MyModule" element of the configuration * object, containing an object with the specified default values. * </p> * * @method setModuleDefaults * @param moduleName {string} - Name of your module. * @param defaultProperties {Object} - The default module configuration. * @return {Object} - The module level configuration object. */ setModuleDefaults(moduleName, defaultProperties) { this.updated = true; if (this.defaults === undefined) { this.defaults = {}; this.unmerged = {}; if (this.sources) { this.sources.splice(0, 0, { name: 'Module Defaults', parsed: this.defaults }); } } const path = moduleName.split('.'); const defaults = Util.setPath(this.defaults, path, Util.getPath(this.defaults, path) ?? {}); Util.extendDeep(defaults, defaultProperties); const original = Util.getPath(this.unmerged, path) ?? Util.setPath(this.unmerged, path, Util.getPath(this.config, path) ?? {}); const moduleConfig = Util.extendDeep({}, defaults, original); Util.setPath(this.config, path, moduleConfig); Util.resolveDeferredConfigs(this.config); return moduleConfig; } /** * Parse and return the specified string with the specified format. * * The format determines the parser to use. * * json = File is parsed using JSON.parse() * yaml (or yml) = Parsed with a YAML parser * toml = Parsed with a TOML parser * cson = Parsed with a CSON parser * hjson = Parsed with a HJSON parser * json5 = Parsed with a JSON5 parser * properties = Parsed with the 'properties' node package * xml = Parsed with a XML parser * * If the file doesn't exist, a null will be returned. If the file can't be * parsed, an exception will be thrown. * * This method performs synchronous file operations, and should not be called * after synchronous module loading. * * @protected * @method parseString * @param {string} content The full content * @param {string} format The format to be parsed * @return {object} configObject The configuration object parsed from the string */ parseString(content, format) { const parser = this.parser.getParser(format); if (typeof parser === 'function') { return parser(null, content); } else { //TODO: throw on missing #753 } } /** * Create a new object patterned after substitutionMap, where: * 1. Terminal string values in substitutionMap are used as keys * 2. To look up values in a key-value store, variables * 3. And parent keys are created as necessary to retain the structure of substitutionMap. * * @protected * @method substituteDeep * @param {object} substitutionMap {Object} - an object whose terminal (non-subobject) values are strings * @param {Record<string, any>} variables - usually process.env, a flat object used to transform * terminal values in a copy of substitutionMap. * @returns {Object} - deep copy of substitutionMap with only those paths whose terminal values * corresponded to a key in `variables` */ substituteDeep(substitutionMap, variables) { const result = {}; const _substituteVars = (map, vars, pathTo) => { for (const prop in map) { const value = map[prop]; if (typeof(value) === 'string') { // We found a leaf variable name if (typeof vars[value] !== 'undefined' && vars[value] !== '') { // if the vars provide a value set the value in the result map Util.setPath(result, pathTo.concat(prop), vars[value]); } } else if (Util.isObject(value)) { // work on the subtree, giving it a clone of the pathTo if ('__name' in value && '__format' in value && typeof vars[value.__name] !== 'undefined' && vars[value.__name] !== '') { let parsedValue; try { parsedValue = this.parseString(vars[value.__name], value.__format); } catch(err) { err.message = '__format parser error in ' + value.__name + ': ' + err.message; throw err; } Util.setPath(result, pathTo.concat(prop), parsedValue); } else { _substituteVars(value, vars, pathTo.concat(prop)); } } else { let msg = "Illegal key type for substitution map at " + pathTo.join('.') + ': ' + typeof(value); throw Error(msg); } } }; _substituteVars(substitutionMap, variables, []); return result; } /** * Populate a LoadConfig entirely from environment variables. * * This is the way a base config is normally accomplished, but not for independent loads. * * This function exists in part to reduce the circular dependency of variable initializations * in the config.js file * @param {string} environments the NODE_CONFIG_ENVs you want to load * @returns {Load} */ static fromEnvironment(environments) { let env = new Env(); if (environments !== undefined) { environments = environments.split(','); env.setEnv('nodeEnv', environments.join(',')); } else { let nodeConfigEnv = env.initParam('NODE_CONFIG_ENV'); let nodeEnv = env.initParam('NODE_ENV'); if (nodeConfigEnv) { env.setEnv('nodeEnv', 'NODE_CONFIG_ENV'); nodeEnv = nodeConfigEnv; } else if (nodeEnv) { env.setEnv('nodeEnv', 'NODE_ENV'); env.setEnv('NODE_CONFIG_ENV', nodeEnv); //TODO: This is a bug asserted in the tests } else { nodeEnv = 'development'; env.setEnv('nodeEnv', 'default'); env.setEnv('NODE_ENV', nodeEnv); env.setEnv('NODE_CONFIG_ENV', nodeEnv); //TODO: This is a bug asserted in the tests } environments = nodeEnv.split(','); } let configDir = env.initParam('NODE_CONFIG_DIR'); let appInstance = env.initParam('NODE_APP_INSTANCE'); let gitCrypt = !env.initParam('CONFIG_SKIP_GITCRYPT'); let parser = _loadParser(env.initParam('NODE_CONFIG_PARSER'), configDir); let hostName = env.initParam('HOST') || env.initParam('HOSTNAME'); // Determine the host name from the OS module, $HOST, or $HOSTNAME // Remove any . appendages, and default to null if not set try { if (!hostName) { const OS = require('os'); hostName = OS.hostname(); } } catch (e) { hostName = ''; } env.setEnv('HOSTNAME', hostName); /** @type {LoadOptions} */ let options = { configDir: configDir ?? DEFAULT_CONFIG_DIR, nodeEnv: environments, hostName, parser, appInstance, gitCrypt }; return new Load(options, env); } } /** * This is meant to wrap configuration objects that should be left as is, * meaning that the object or its prototype will not be modified in any way */ class RawConfig { #rawObject = undefined; constructor(data) { this.#rawObject = data; } resolve() { return this.#rawObject; } /** * create a RawConfig * @param rawObject {Object} properties we don't want to have manipulated by node-config * @returns {RawConfig} */ static raw(rawObject) { return new RawConfig(rawObject); } } // Helper functions shared across object members function _toAbsolutePath (configDir) { if (configDir.indexOf('.') === 0) { return Path.join(process.cwd(), configDir); } return configDir; } function _loadParser(name, dir) { if (name === undefined) { return require("../parser.js"); } try { const parserModule = Path.isAbsolute(name) ? name : Path.join(dir, name); return require(parserModule); } catch (e) { console.warn(`Failed to load config parser from ${name}`); console.log(e); } } module.exports = { Util, Load, RawConfig };