123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503 |
- // config.js (c) 2010-2020 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
- var deferConfig = require('../defer').deferConfig,
- DeferredConfig = require('../defer').DeferredConfig,
- RawConfig = require('../raw').RawConfig,
- Parser = require('../parser'),
- Utils = require('util'),
- Path = require('path'),
- FileSystem = require('fs');
- // Static members
- var DEFAULT_CLONE_DEPTH = 20,
- NODE_CONFIG, CONFIG_DIR, RUNTIME_JSON_FILENAME, NODE_ENV, APP_INSTANCE,
- HOST, HOSTNAME, ALLOW_CONFIG_MUTATIONS, CONFIG_SKIP_GITCRYPT, NODE_ENV_VAR_NAME,
- NODE_CONFIG_PARSER,
- env = {},
- privateUtil = {},
- deprecationWarnings = {},
- configSources = [], // Configuration sources - array of {name, original, parsed}
- checkMutability = true, // Check for mutability/immutability on first get
- gitCryptTestRegex = /^.GITCRYPT/; // regular expression to test for gitcrypt files.
- /**
- * <p>Application Configurations</p>
- *
- * <p>
- * The config module exports a singleton object representing all
- * configurations for this application deployment.
- * </p>
- *
- * <p>
- * Application configurations are stored in files within the config directory
- * of your application. The default configuration file is loaded, followed
- * by files specific to the deployment type (development, testing, staging,
- * production, etc.).
- * </p>
- *
- * <p>
- * For example, with the following config/default.yaml file:
- * </p>
- *
- * <pre>
- * ...
- * customer:
- * initialCredit: 500
- * db:
- * name: customer
- * port: 5984
- * ...
- * </pre>
- *
- * <p>
- * The following code loads the customer section into the CONFIG variable:
- * <p>
- *
- * <pre>
- * var CONFIG = require('config').customer;
- * ...
- * newCustomer.creditLimit = CONFIG.initialCredit;
- * database.open(CONFIG.db.name, CONFIG.db.port);
- * ...
- * </pre>
- *
- * @module config
- * @class Config
- */
- /**
- * <p>Get the configuration object.</p>
- *
- * <p>
- * The configuration object is a shared singleton object within the application,
- * attained by calling require('config').
- * </p>
- *
- * <p>
- * Usually you'll specify a CONFIG variable at the top of your .js file
- * for file/module scope. If you want the root of the object, you can do this:
- * </p>
- * <pre>
- * var CONFIG = require('config');
- * </pre>
- *
- * <p>
- * Sometimes you only care about a specific sub-object within the CONFIG
- * object. In that case you could do this at the top of your file:
- * </p>
- * <pre>
- * var CONFIG = require('config').customer;
- * or
- * var CUSTOMER_CONFIG = require('config').customer;
- * </pre>
- *
- * <script type="text/javascript">
- * document.getElementById("showProtected").style.display = "block";
- * </script>
- *
- * @method constructor
- * @return CONFIG {object} - The top level configuration object
- */
- var Config = function() {
- var t = this;
- // Bind all utility functions to this
- for (var fnName in util) {
- if (typeof util[fnName] === 'function') {
- util[fnName] = util[fnName].bind(t);
- }
- }
- // Merge configurations into this
- util.extendDeep(t, util.loadFileConfigs());
- util.attachProtoDeep(t);
- // Perform strictness checks and possibly throw an exception.
- util.runStrictnessChecks(t);
- };
- /**
- * Utilities are under the util namespace vs. at the top level
- */
- var util = Config.prototype.util = {};
- /**
- * Underlying get mechanism
- *
- * @private
- * @method getImpl
- * @param object {object} - Object to get the property for
- * @param property {string|string[]} - The property name to get (as an array or '.' delimited string)
- * @return value {*} - Property value, including undefined if not defined.
- */
- var getImpl= function(object, property) {
- var t = this,
- elems = Array.isArray(property) ? property : property.split('.'),
- name = elems[0],
- value = object[name];
- if (elems.length <= 1) {
- return value;
- }
- // Note that typeof null === 'object'
- if (value === null || typeof value !== 'object') {
- return undefined;
- }
- return getImpl(value, elems.slice(1));
- };
- /**
- * <p>Get a configuration value</p>
- *
- * <p>
- * This will return the specified property value, throwing an exception if the
- * configuration isn't defined. It is used to assure configurations are defined
- * before being used, and to prevent typos.
- * </p>
- *
- * @method get
- * @param property {string} - The configuration property to get. Can include '.' sub-properties.
- * @return value {*} - The property value
- */
- Config.prototype.get = function(property) {
- if(property === null || property === undefined){
- throw new Error("Calling config.get with null or undefined argument");
- }
- // Make configurations immutable after first get (unless disabled)
- if (checkMutability) {
- if (!util.initParam('ALLOW_CONFIG_MUTATIONS', false)) {
- util.makeImmutable(config);
- }
- checkMutability = false;
- }
- var t = this,
- value = getImpl(t, property);
- // Produce an exception if the property doesn't exist
- if (value === undefined) {
- throw new Error('Configuration property "' + property + '" is not defined');
- }
- // Return the value
- return value;
- };
- /**
- * Test that a configuration parameter exists
- *
- * <pre>
- * var config = require('config');
- * if (config.has('customer.dbName')) {
- * console.log('Customer database name: ' + config.customer.dbName);
- * }
- * </pre>
- *
- * @method has
- * @param property {string} - The configuration property to test. Can include '.' sub-properties.
- * @return isPresent {boolean} - True if the property is defined, false if not defined.
- */
- Config.prototype.has = function(property) {
- // While get() throws an exception for undefined input, has() is designed to test validity, so false is appropriate
- if(property === null || property === undefined){
- return false;
- }
- var t = this;
- return (getImpl(t, property) !== undefined);
- };
- /**
- * <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>
- * var CONFIG = require("config");
- * CONFIG.util.setModuleDefaults("MyModule", {
- * templateName: "t-50",
- * 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 moduleConfig {object} - The module level configuration object.
- */
- util.setModuleDefaults = function (moduleName, defaultProperties) {
- // Copy the properties into a new object
- var t = this,
- moduleConfig = util.cloneDeep(defaultProperties);
- // Set module defaults into the first sources element
- if (configSources.length === 0 || configSources[0].name !== 'Module Defaults') {
- configSources.splice(0, 0, {
- name: 'Module Defaults',
- parsed: {}
- });
- }
- util.setPath(configSources[0].parsed, moduleName.split('.'), {});
- util.extendDeep(getImpl(configSources[0].parsed, moduleName), defaultProperties);
- // Create a top level config for this module if it doesn't exist
- util.setPath(t, moduleName.split('.'), getImpl(t, moduleName) || {});
- // Extend local configurations into the module config
- util.extendDeep(moduleConfig, getImpl(t, moduleName));
- // Merge the extended configs without replacing the original
- util.extendDeep(getImpl(t, moduleName), moduleConfig);
- // reset the mutability check for "config.get" method.
- // we are not making t[moduleName] immutable immediately,
- // since there might be more modifications before the first config.get
- if (!util.initParam('ALLOW_CONFIG_MUTATIONS', false)) {
- checkMutability = true;
- }
- // Attach handlers & watchers onto the module config object
- return util.attachProtoDeep(getImpl(t, moduleName));
- };
- /**
- * <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>
- * var CONFIG = require('config');
- * ...
- *
- * // Hide the Amazon S3 credentials
- * CONFIG.util.makeHidden(CONFIG.amazonS3, 'access_id');
- * CONFIG.util.makeHidden(CONFIG.amazonS3, 'secret_key');
- * </pre>
- *
- * @method makeHidden
- * @param object {object} - The object to make a hidden property into.
- * @param property {string} - The name of the property to make hidden.
- * @param value {*} - (optional) Set the property value to this (otherwise leave alone)
- * @return object {object} - The original object is returned - for chaining.
- */
- util.makeHidden = function(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
- });
- }
- // Otherwise set the value and mark it as hidden
- else {
- Object.defineProperty(object, property, {
- value : value,
- enumerable : false
- });
- }
- return object;
- }
- /**
- * <p>Make a javascript object property immutable (assuring it cannot be changed
- * from the current value).</p>
- * <p>
- * If the specified property is an object, all attributes of that object are
- * made immutable, including properties of contained objects, recursively.
- * If a property name isn't supplied, all properties of the object are made
- * immutable.
- * </p>
- * <p>
- *
- * </p>
- * <p>
- * New properties can be added to the object and those properties will not be
- * immutable unless this method is called on those new properties.
- * </p>
- * <p>
- * This operation cannot be undone.
- * </p>
- *
- * <p>Example:</p>
- * <pre>
- * var config = require('config');
- * var myObject = {hello:'world'};
- * config.util.makeImmutable(myObject);
- * </pre>
- *
- * @method makeImmutable
- * @param object {object} - The object to specify immutable properties for
- * @param [property] {string | [string]} - The name of the property (or array of names) to make immutable.
- * If not provided, all owned properties of the object are made immutable.
- * @param [value] {* | [*]} - Property value (or array of values) to set
- * the property to before making immutable. Only used when setting a single
- * property. Retained for backward compatibility.
- * @return object {object} - The original object is returned - for chaining.
- */
- util.makeImmutable = function(object, property, value) {
- if (Buffer.isBuffer(object)) {
- return object;
- }
- var properties = null;
- // Backwards compatibility mode where property/value can be specified
- if (typeof property === 'string') {
- return Object.defineProperty(object, property, {
- value : (typeof value === 'undefined') ? object[property] : value,
- writable : false,
- configurable: false
- });
- }
- // Get the list of properties to work with
- if (Array.isArray(property)) {
- properties = property;
- }
- else {
- properties = Object.keys(object);
- }
- // Process each property
- for (var i = 0; i < properties.length; i++) {
- var propertyName = properties[i],
- 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.
- value.forEach((item, index) => { if (util.isObject(item) || Array.isArray(item)) util.makeImmutable(item) })
- Object.defineProperty(object, propertyName, {
- value: Object.freeze(value)
- });
- } else {
- Object.defineProperty(object, propertyName, {
- value: value,
- writable : false,
- configurable: false
- });
- // Ensure new properties can not be added.
- Object.preventExtensions(object)
- // Call recursively if an object.
- if (util.isObject(value)) {
- util.makeImmutable(value);
- }
- }
- }
- return object;
- };
- /**
- * Return the sources for the configurations
- *
- * <p>
- * All sources for configurations are stored in an array of objects containing
- * the source name (usually the filename), the original source (as a string),
- * and the parsed source as an object.
- * </p>
- *
- * @method getConfigSources
- * @return configSources {Array[Object]} - An array of objects containing
- * name, original, and parsed elements
- */
- util.getConfigSources = function() {
- var t = this;
- return configSources.slice(0);
- };
- /**
- * 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>
- *
- * @method getOption
- * @param options {Object | undefined} the options object
- * @param optionName {string} the attribute name to look for
- * @param defaultValue { any } the default in case the options object is empty, or the attribute does not exist.
- * @return options[optionName] if defined, defaultValue if not.
- */
- util.getOption = function(options, optionName, defaultValue) {
- if (options !== undefined && 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
- * runtime.json
- * </pre>
- *
- * <p>
- * EXT can be yml, yaml, coffee, iced, json, 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, 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>
- * The runtime.json file contains configuration changes made at runtime either
- * manually, or by the application setting a configuration value.
- * </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>
- *
- * @protected
- * @method loadFileConfigs
- * @param configDir { string | null } the path to the directory containing the configurations to load
- * @param options { object | undefined } parsing options. Current supported option: skipConfigSources: true|false
- * @return config {Object} The configuration object
- */
- util.loadFileConfigs = function(configDir, options) {
- // Initialize
- var t = this,
- config = {};
- // Specify variables that can be used to define the environment
- var node_env_var_names = ['NODE_CONFIG_ENV', 'NODE_ENV'];
- // Loop through the variables to try and set environment
- for (const node_env_var_name of node_env_var_names) {
- NODE_ENV = util.initParam(node_env_var_name);
- if (!!NODE_ENV) {
- NODE_ENV_VAR_NAME = node_env_var_name;
- break;
- }
- }
- // If we haven't successfully set the environment using the variables, we'll default it
- if (!NODE_ENV) {
- NODE_ENV = 'development';
- }
- node_env_var_names.forEach(node_env_var_name => {
- env[node_env_var_name] = NODE_ENV;
- });
- // Split files name, for loading multiple files.
- NODE_ENV = NODE_ENV.split(',');
- CONFIG_DIR = configDir || util.initParam('NODE_CONFIG_DIR', Path.join( process.cwd(), 'config') );
- if (CONFIG_DIR.indexOf('.') === 0) {
- CONFIG_DIR = Path.join(process.cwd() , CONFIG_DIR);
- }
- APP_INSTANCE = util.initParam('NODE_APP_INSTANCE');
- HOST = util.initParam('HOST');
- HOSTNAME = util.initParam('HOSTNAME');
- CONFIG_SKIP_GITCRYPT = util.initParam('CONFIG_SKIP_GITCRYPT');
- // This is for backward compatibility
- RUNTIME_JSON_FILENAME = util.initParam('NODE_CONFIG_RUNTIME_JSON', Path.join(CONFIG_DIR , 'runtime.json') );
- NODE_CONFIG_PARSER = util.initParam('NODE_CONFIG_PARSER');
- if (NODE_CONFIG_PARSER) {
- try {
- var parserModule = Path.isAbsolute(NODE_CONFIG_PARSER)
- ? NODE_CONFIG_PARSER
- : Path.join(CONFIG_DIR, NODE_CONFIG_PARSER);
- Parser = require(parserModule);
- }
- catch (e) {
- console.warn('Failed to load config parser from ' + NODE_CONFIG_PARSER);
- console.log(e);
- }
- }
- // Determine the host name from the OS module, $HOST, or $HOSTNAME
- // Remove any . appendages, and default to null if not set
- try {
- var hostName = HOST || HOSTNAME;
- if (!hostName) {
- var OS = require('os');
- hostName = OS.hostname();
- }
- } catch (e) {
- hostName = '';
- }
- // Store the hostname that won.
- env.HOSTNAME = hostName;
- // Read each file in turn
- var baseNames = ['default'].concat(NODE_ENV);
- // #236: Also add full hostname when they are different.
- if (hostName) {
- var firstDomain = hostName.split('.')[0];
- NODE_ENV.forEach(function(env) {
- // Backward compatibility
- baseNames.push(firstDomain, firstDomain + '-' + env);
- // Add full hostname when it is not the same
- if (hostName !== firstDomain) {
- baseNames.push(hostName, hostName + '-' + env);
- }
- });
- }
- NODE_ENV.forEach(function(env) {
- baseNames.push('local', 'local-' + env);
- });
- var allowedFiles = {};
- var resolutionIndex = 1;
- var extNames = Parser.getFilesOrder();
- baseNames.forEach(function(baseName) {
- extNames.forEach(function(extName) {
- allowedFiles[baseName + '.' + extName] = resolutionIndex++;
- if (APP_INSTANCE) {
- allowedFiles[baseName + '-' + APP_INSTANCE + '.' + extName] = resolutionIndex++;
- }
- });
- });
- var locatedFiles = util.locateMatchingFiles(CONFIG_DIR, allowedFiles);
- locatedFiles.forEach(function(fullFilename) {
- var configObj = util.parseFile(fullFilename, options);
- if (configObj) {
- util.extendDeep(config, configObj);
- }
- });
- // Override configurations from the $NODE_CONFIG environment variable
- // NODE_CONFIG only applies to the base config
- if (!configDir) {
- var envConfig = {};
- if (process.env.NODE_CONFIG) {
- try {
- envConfig = JSON.parse(process.env.NODE_CONFIG);
- } catch(e) {
- console.error('The $NODE_CONFIG environment variable is malformed JSON');
- }
- util.extendDeep(config, envConfig);
- var skipConfigSources = util.getOption(options,'skipConfigSources', false);
- if (!skipConfigSources){
- configSources.push({
- name: "$NODE_CONFIG",
- parsed: envConfig,
- });
- }
- }
- // Override configurations from the --NODE_CONFIG command line
- var cmdLineConfig = util.getCmdLineArg('NODE_CONFIG');
- if (cmdLineConfig) {
- try {
- cmdLineConfig = JSON.parse(cmdLineConfig);
- } catch(e) {
- console.error('The --NODE_CONFIG={json} command line argument is malformed JSON');
- }
- util.extendDeep(config, cmdLineConfig);
- var skipConfigSources = util.getOption(options,'skipConfigSources', false);
- if (!skipConfigSources){
- configSources.push({
- name: "--NODE_CONFIG argument",
- parsed: cmdLineConfig,
- });
- }
- }
- // Place the mixed NODE_CONFIG into the environment
- env['NODE_CONFIG'] = JSON.stringify(util.extendDeep(envConfig, cmdLineConfig, {}));
- }
- // Override with environment variables if there is a custom-environment-variables.EXT mapping file
- var customEnvVars = util.getCustomEnvVars(CONFIG_DIR, extNames);
- util.extendDeep(config, customEnvVars);
- // Extend the original config with the contents of runtime.json (backwards compatibility)
- var runtimeJson = util.parseFile(RUNTIME_JSON_FILENAME) || {};
- util.extendDeep(config, runtimeJson);
- util.resolveDeferredConfigs(config);
- // Return the configuration object
- return config;
- };
- /**
- * Return a list of fullFilenames who exists in allowedFiles
- * Ordered according to allowedFiles argument specifications
- *
- * @protected
- * @method locateMatchingFiles
- * @param configDirs {string} the config dir, or multiple dirs separated by a column (:)
- * @param allowedFiles {object} an object. keys and supported filenames
- * and values are the position in the resolution order
- * @returns {string[]} fullFilenames - path + filename
- */
- util.locateMatchingFiles = function(configDirs, allowedFiles) {
- return configDirs.split(Path.delimiter)
- .reduce(function(files, configDir) {
- if (configDir) {
- try {
- FileSystem.readdirSync(configDir).forEach(function(file) {
- if (allowedFiles[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]; });
- };
- // Using basic recursion pattern, find all the deferred values and resolve them.
- util.resolveDeferredConfigs = function (config) {
- var deferred = [];
- function _iterate (prop) {
- // We put the properties we are going to look it in an array to keep the order predictable
- var propsToSort = [];
- // First step is to put the properties of interest in an array
- for (var property in prop) {
- if (Object.hasOwnProperty.call(prop, property) && prop[property] != null) {
- propsToSort.push(property);
- }
- }
- // Second step is to iterate of the elements in a predictable (sorted) order
- propsToSort.sort().forEach(function (property) {
- if (prop[property].constructor === Object) {
- _iterate(prop[property]);
- } else if (prop[property].constructor === Array) {
- for (var i = 0; i < prop[property].length; i++) {
- if (prop[property][i] instanceof DeferredConfig) {
- deferred.push(prop[property][i].prepare(config, prop[property], i));
- }
- else {
- _iterate(prop[property][i]);
- }
- }
- } else {
- if (prop[property] instanceof DeferredConfig) {
- deferred.push(prop[property].prepare(config, prop, property));
- }
- // else: Nothing to do. Keep the property how it is.
- }
- });
- }
- _iterate(config);
- deferred.forEach(function (defer) { defer.resolve(); });
- };
- /**
- * Parse and return the specified configuration file.
- *
- * If the file exists in the application config directory, it will
- * parse and return it as a JavaScript object.
- *
- * The file extension determines the parser to use.
- *
- * .js = File to run that has a module.exports containing the config object
- * .coffee = File to run that has a module.exports with coffee-script containing the config object
- * .iced = File to run that has a module.exports with iced-coffee-script containing the config object
- * All other supported file types (yaml, toml, json, cson, hjson, json5, properties, xml)
- * are parsed with util.parseString.
- *
- * 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 parseFile
- * @param fullFilename {string} The full file path and name
- * @param options { object | undefined } parsing options. Current supported option: skipConfigSources: true|false
- * @return configObject {object|null} The configuration object parsed from the file
- */
- util.parseFile = function(fullFilename, options) {
- var t = this, // Initialize
- configObject = null,
- fileContent = null,
- stat = 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 (CONFIG_SKIP_GITCRYPT) {
- if (gitCryptTestRegex.test(fileContent)) {
- console.error('WARNING: ' + fullFilename + ' is a git-crypt file and CONFIG_SKIP_GITCRYPT is set. skipping.');
- return null;
- }
- }
- configObject = Parser.parse(fullFilename, fileContent);
- }
- catch (e3) {
- if (gitCryptTestRegex.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);
- }
- // Keep track of this configuration sources, including empty ones, unless the skipConfigSources flag is set to true in the options
- var skipConfigSources = util.getOption(options,'skipConfigSources', false);
- if (typeof configObject === 'object' && !skipConfigSources) {
- configSources.push({
- name: fullFilename,
- original: fileContent,
- parsed: configObject,
- });
- }
- return configObject;
- };
- /**
- * 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 content {string} The full content
- * @param format {string} The format to be parsed
- * @return {configObject} The configuration object parsed from the string
- */
- util.parseString = function (content, format) {
- var parser = Parser.getParser(format);
- if (typeof parser === 'function') {
- return parser(null, content);
- }
- };
- /**
- * Attach the Config class prototype to all config objects recursively.
- *
- * <p>
- * This allows you to do anything with CONFIG sub-objects as you can do with
- * the top-level CONFIG object. It's so you can do this:
- * </p>
- *
- * <pre>
- * var CUST_CONFIG = require('config').Customer;
- * CUST_CONFIG.get(...)
- * </pre>
- *
- * @protected
- * @method attachProtoDeep
- * @param toObject
- * @param depth
- * @return toObject
- */
- util.attachProtoDeep = function(toObject, depth) {
- if (toObject instanceof RawConfig) {
- return toObject;
- }
- // Recursion detection
- var t = this;
- depth = (depth === null ? DEFAULT_CLONE_DEPTH : depth);
- if (depth < 0) {
- return toObject;
- }
- // Adding Config.prototype methods directly to toObject as hidden properties
- // because adding to toObject.__proto__ exposes the function in toObject
- for (var fnName in Config.prototype) {
- if (!toObject[fnName]) {
- util.makeHidden(toObject, fnName, Config.prototype[fnName]);
- }
- }
- // Add prototypes to sub-objects
- for (var prop in toObject) {
- if (util.isObject(toObject[prop])) {
- util.attachProtoDeep(toObject[prop], depth - 1);
- }
- }
- // Return the original object
- return toObject;
- };
- /**
- * Return a deep copy of the specified object.
- *
- * 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.
- *
- * @protected
- * @method cloneDeep
- * @param parent {object} The original object to copy from
- * @param [depth=20] {Integer} Maximum depth (default 20)
- * @return {object} A new object with the elements copied from the copyFrom 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.
- */
- util.cloneDeep = function cloneDeep(parent, depth, circular, prototype) {
- // maintain two arrays for circular references, where corresponding parents
- // and children have the same index
- var allParents = [];
- var allChildren = [];
- var useBuffer = typeof Buffer != 'undefined';
- 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;
- var child;
- if (typeof parent != 'object') {
- return parent;
- }
- if (Utils.isArray(parent)) {
- child = [];
- } else if (Utils.isRegExp(parent)) {
- child = new RegExp(parent.source, util.getRegExpFlags(parent));
- if (parent.lastIndex) child.lastIndex = parent.lastIndex;
- } else if (Utils.isDate(parent)) {
- child = new Date(parent.getTime());
- } else if (useBuffer && Buffer.isBuffer(parent)) {
- child = Buffer.alloc(parent.length);
- parent.copy(child);
- return child;
- } else {
- if (typeof prototype === 'undefined') child = Object.create(Object.getPrototypeOf(parent));
- else child = Object.create(prototype);
- }
- if (circular) {
- var index = allParents.indexOf(parent);
- if (index != -1) {
- return allChildren[index];
- }
- allParents.push(parent);
- allChildren.push(child);
- }
- for (var i in parent) {
- var propDescriptor = Object.getOwnPropertyDescriptor(parent,i);
- var hasGetter = ((propDescriptor !== undefined) && (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);
- };
- /**
- * Set objects given a path as a string list
- *
- * @protected
- * @method setPath
- * @param object {object} - Object to set the property on
- * @param path {array[string]} - Array path to the property
- * @param value {*} - value to set, ignoring null
- */
- util.setPath = function (object, path, value) {
- var nextKey = null;
- if (value === null || path.length === 0) {
- return;
- }
- else if (path.length === 1) { // no more keys to make, so set the value
- object[path.shift()] = value;
- }
- else {
- nextKey = path.shift();
- if (!Object.hasOwnProperty.call(object, nextKey)) {
- object[nextKey] = {};
- }
- util.setPath(object[nextKey], path, value);
- }
- };
- /**
- * 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 substitutionMap {object} - an object whose terminal (non-subobject) values are strings
- * @param variables {object[string:value]} - 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`
- */
- util.substituteDeep = function (substitutionMap, variables) {
- var result = {};
- function _substituteVars(map, vars, pathTo) {
- for (var prop in map) {
- var value = map[prop];
- if (typeof(value) === 'string') { // We found a leaf variable name
- if (vars[value] !== undefined) { // 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 && vars[value.__name] !== undefined) {
- try {
- var parsedValue = util.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 {
- msg = "Illegal key type for substitution map at " + pathTo.join('.') + ': ' + typeof(value);
- throw Error(msg);
- }
- }
- }
- _substituteVars(substitutionMap, variables, []);
- return result;
- };
- /* Map environment variables into the configuration if a mapping file,
- * `custom-environment-variables.EXT` exists.
- *
- * @protected
- * @method getCustomEnvVars
- * @param CONFIG_DIR {string} - the passed configuration directory
- * @param extNames {Array[string]} - acceptable configuration file extension names.
- * @returns {object} - mapped environment variables or {} if there are none
- */
- util.getCustomEnvVars = function (CONFIG_DIR, extNames) {
- var result = {};
- var resolutionIndex = 1;
- var allowedFiles = {};
- extNames.forEach(function (extName) {
- allowedFiles['custom-environment-variables' + '.' + extName] = resolutionIndex++;
- });
- var locatedFiles = util.locateMatchingFiles(CONFIG_DIR, allowedFiles);
- locatedFiles.forEach(function (fullFilename) {
- var configObj = util.parseFile(fullFilename);
- if (configObj) {
- var environmentSubstitutions = util.substituteDeep(configObj, process.env);
- util.extendDeep(result, environmentSubstitutions);
- }
- });
- return result;
- };
- /**
- * Return true if two objects have equal contents.
- *
- * @protected
- * @method equalsDeep
- * @param object1 {object} The object to compare from
- * @param object2 {object} The object to compare with
- * @param depth {integer} An optional depth to prevent recursion. Default: 20.
- * @return {boolean} True if both objects have equivalent contents
- */
- util.equalsDeep = function(object1, object2, depth) {
- // Recursion detection
- var t = this;
- 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 (var prop in object1) {
- // Call recursively if an object or array
- if (object1[prop] && typeof(object1[prop]) === 'object') {
- if (!util.equalsDeep(object1[prop], object2[prop], depth - 1)) {
- return false;
- }
- }
- else {
- if (object1[prop] !== object2[prop]) {
- return false;
- }
- }
- }
- // Test passed.
- return true;
- };
- /**
- * Returns an object containing all elements that differ between two objects.
- * <p>
- * This method was designed to be used to create the runtime.json file
- * contents, but can be used to get the diffs between any two Javascript objects.
- * </p>
- * <p>
- * It works best when object2 originated by deep copying object1, then
- * changes were made to object2, and you want an object that would give you
- * the changes made to object1 which resulted in object2.
- * </p>
- *
- * @protected
- * @method diffDeep
- * @param object1 {object} The base object to compare to
- * @param object2 {object} The object to compare with
- * @param depth {integer} An optional depth to prevent recursion. Default: 20.
- * @return {object} A differential object, which if extended onto object1 would
- * result in object2.
- */
- util.diffDeep = function(object1, object2, depth) {
- // Recursion detection
- var t = this, diff = {};
- depth = (depth === null ? DEFAULT_CLONE_DEPTH : depth);
- if (depth < 0) {
- return {};
- }
- // Process each element from object2, adding any element that's different
- // from object 1.
- for (var parm in object2) {
- var value1 = object1[parm];
- var value2 = object2[parm];
- if (value1 && value2 && util.isObject(value2)) {
- if (!(util.equalsDeep(value1, value2))) {
- diff[parm] = util.diffDeep(value1, value2, depth - 1);
- }
- }
- else if (Array.isArray(value1) && Array.isArray(value2)) {
- if(!util.equalsDeep(value1, value2)) {
- diff[parm] = value2;
- }
- }
- else if (value1 !== value2){
- diff[parm] = value2;
- }
- }
- // Return the diff object
- return diff;
- };
- /**
- * Extend an object, and any object it contains.
- *
- * This does not replace deep objects, but dives into them
- * replacing individual elements instead.
- *
- * @protected
- * @method extendDeep
- * @param mergeInto {object} The object to merge into
- * @param mergeFrom... {object...} - Any number of objects to merge from
- * @param depth {integer} An optional depth to prevent recursion. Default: 20.
- * @return {object} The altered mergeInto object is returned
- */
- util.extendDeep = function(mergeInto) {
- // Initialize
- var t = this;
- var vargs = Array.prototype.slice.call(arguments, 1);
- var depth = vargs.pop();
- if (typeof(depth) != 'number') {
- vargs.push(depth);
- depth = DEFAULT_CLONE_DEPTH;
- }
- // Recursion detection
- if (depth < 0) {
- return mergeInto;
- }
- // Cycle through each object to extend
- vargs.forEach(function(mergeFrom) {
- // Cycle through each element of the object to merge from
- for (var prop in mergeFrom) {
- // save original value in deferred elements
- var fromIsDeferredFunc = mergeFrom[prop] instanceof DeferredConfig;
- var 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) {
- 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];
- }
- // Copy recursively if the mergeFrom element is an object (or array or fn)
- else if (mergeFrom[prop] && typeof mergeFrom[prop] === 'object') {
- mergeInto[prop] = util.cloneDeep(mergeFrom[prop], depth -1);
- }
- // Copy property descriptor otherwise, preserving accessors
- else if (Object.getOwnPropertyDescriptor(Object(mergeFrom), prop)){
- Object.defineProperty(mergeInto, prop, Object.getOwnPropertyDescriptor(Object(mergeFrom), prop));
- } else {
- 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.
- *
- * @protected
- * @method isObject
- * @param obj {*} An argument of any type.
- * @return {boolean} TRUE if the arg is an object, FALSE if not
- */
- util.isObject = function(obj) {
- return (obj !== null) && (typeof obj === 'object') && !(Array.isArray(obj));
- };
- /**
- * Is the specified argument a javascript promise?
- *
- * @protected
- * @method isPromise
- * @param obj {*} An argument of any type.
- * @returns {boolean}
- */
- util.isPromise = function(obj) {
- return Object.prototype.toString.call(obj) === '[object Promise]';
- };
- /**
- * <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>
- *
- * @method initParam
- * @param paramName {String} Name of the parameter
- * @param [defaultValue] {Any} Default value of the parameter
- * @return {Any} The found value, or default value
- */
- util.initParam = function (paramName, defaultValue) {
- var t = this;
- // Record and return the value
- var value = util.getCmdLineArg(paramName) || process.env[paramName] || defaultValue;
- env[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 searchFor {String} The argument name to search for
- * @return {*} false if the argument was not found, the argument value if found
- */
- util.getCmdLineArg = function (searchFor) {
- var cmdLineArgs = process.argv.slice(2, process.argv.length),
- argName = '--' + searchFor + '=';
- for (var 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 varName {String} The environment variable name
- * @return {String} The value of the environment variable
- */
- util.getEnv = function (varName) {
- return env[varName];
- }
- /**
- * Returns a string of flags for regular expression `re`.
- *
- * @param {RegExp} re Regular expression
- * @returns {string} Flags
- */
- util.getRegExpFlags = function (re) {
- var 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. Omit this argument to return the entire config.
- * @returns {Object} The cloned config or part of the config
- */
- util.toObject = function(config) {
- return JSON.parse(JSON.stringify(config || this));
- };
- // Run strictness checks on NODE_ENV and NODE_APP_INSTANCE and throw an error if there's a problem.
- util.runStrictnessChecks = function (config) {
- var sources = config.util.getConfigSources();
- var sourceFilenames = sources.map(function (src) {
- return Path.basename(src.name);
- });
- NODE_ENV.forEach(function(env) {
- // Throw an exception if there's no explicit config file for NODE_ENV
- var anyFilesMatchEnv = sourceFilenames.some(function (filename) {
- return filename.match(env);
- });
- // development is special-cased because it's the default value
- if (env && (env !== 'development') && !anyFilesMatchEnv) {
- _warnOrThrow(NODE_ENV_VAR_NAME+" value of '"+env+"' did not match any deployment config file names.");
- }
- // Throw if NODE_ENV matches' default' or 'local'
- if ((env === 'default') || (env === 'local')) {
- _warnOrThrow(NODE_ENV_VAR_NAME+" value of '"+env+"' is ambiguous.");
- }
- });
- // Throw an exception if there's no explicit config file for NODE_APP_INSTANCE
- var anyFilesMatchInstance = sourceFilenames.some(function (filename) {
- return filename.match(APP_INSTANCE);
- });
- if (APP_INSTANCE && !anyFilesMatchInstance) {
- _warnOrThrow("NODE_APP_INSTANCE value of '"+APP_INSTANCE+"' did not match any instance config file names.");
- }
- function _warnOrThrow (msg) {
- var beStrict = process.env.NODE_CONFIG_STRICT_MODE;
- var prefix = beStrict ? 'FATAL: ' : 'WARNING: ';
- var seeURL = 'See https://github.com/lorenwest/node-config/wiki/Strict-Mode';
- console.error(prefix+msg);
- console.error(prefix+seeURL);
- // Accept 1 and true as truthy values. When set via process.env, Node.js casts them to strings.
- if (["true", "1"].indexOf(beStrict) >= 0) {
- throw new Error(prefix+msg+' '+seeURL);
- }
- }
- };
- // Instantiate and export the configuration
- var config = module.exports = new Config();
- // copy methods to util for backwards compatibility
- util.stripComments = Parser.stripComments;
- util.stripYamlComments = Parser.stripYamlComments;
- // Produce warnings if the configuration is empty
- var showWarnings = !(util.initParam('SUPPRESS_NO_CONFIG_WARNING'));
- if (showWarnings && Object.keys(config).length === 0) {
- console.error('WARNING: No configurations found in configuration directory:' +CONFIG_DIR);
- console.error('WARNING: To disable this warning set SUPPRESS_NO_CONFIG_WARNING in the environment.');
- }
|