@reimorg/config 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/History.md +710 -0
- package/LICENSE +19 -0
- package/README.md +121 -0
- package/async.d.ts +29 -0
- package/async.js +69 -0
- package/defer.d.ts +30 -0
- package/defer.js +23 -0
- package/index.d.ts +30 -0
- package/index.js +20 -0
- package/lib/config.js +627 -0
- package/lib/get.js +1 -0
- package/lib/util.js +1220 -0
- package/package.json +68 -0
- package/parser.d.ts +134 -0
- package/parser.js +299 -0
- package/raw.d.ts +18 -0
- package/raw.js +15 -0
package/lib/util.js
ADDED
|
@@ -0,0 +1,1220 @@
|
|
|
1
|
+
// config.js (c) 2010-2022 Loren West and other contributors
|
|
2
|
+
// May be freely distributed under the MIT license.
|
|
3
|
+
// For further details and documentation:
|
|
4
|
+
// http://lorenwest.github.com/node-config
|
|
5
|
+
|
|
6
|
+
// Dependencies
|
|
7
|
+
const DeferredConfig = require('../defer').DeferredConfig;
|
|
8
|
+
const Path = require('path');
|
|
9
|
+
const FileSystem = require('fs');
|
|
10
|
+
const OS = require("os");
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CONFIG_DIR = Path.join( process.cwd(), 'config');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A source in the configSources list
|
|
16
|
+
*
|
|
17
|
+
* @typedef {Object} ConfigSource
|
|
18
|
+
* @property {string} name
|
|
19
|
+
* @property {Object} parsed - parsed representation
|
|
20
|
+
* @property {string=} original - unparsed representation of the data
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The data used for a Load operation, mostly derived from environment variables
|
|
25
|
+
*
|
|
26
|
+
* @typedef {Object} LoadOptions
|
|
27
|
+
* @property {string} configDir - config directory location, absolute or relative to cwd()
|
|
28
|
+
* @property {string} nodeEnv - NODE_ENV value or commo-separated list
|
|
29
|
+
* @property {string} hostName - hostName for host-specific loads
|
|
30
|
+
* @property {string=} appInstance - per-process config ID
|
|
31
|
+
* @property {boolean} skipConfigSources - don't track sources
|
|
32
|
+
* @property {boolean} gitCrypt - allow gitcrypt files
|
|
33
|
+
* @property {Parser} parser - alternative parser implementation
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** @type {LoadOptions} */
|
|
37
|
+
const DEFAULT_OPTIONS = {
|
|
38
|
+
configDir: DEFAULT_CONFIG_DIR,
|
|
39
|
+
nodeEnv: ['development'],
|
|
40
|
+
hostName: OS.hostname(),
|
|
41
|
+
gitCrypt: true,
|
|
42
|
+
parser: require("../parser.js")
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Callback for converting loaded data.
|
|
47
|
+
*
|
|
48
|
+
* @callback DataConvert
|
|
49
|
+
* @param {Object} input - An object to modify.
|
|
50
|
+
* @returns {Object} - converted object
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
const DEFAULT_CLONE_DEPTH = 20;
|
|
55
|
+
const GIT_CRYPT_REGEX = /^.GITCRYPT/; // regular expression to test for gitcrypt files.
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Util functions that do not require the singleton in order to run.
|
|
59
|
+
*/
|
|
60
|
+
class Util {
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* <p>Make a configuration property hidden so it doesn't appear when enumerating
|
|
64
|
+
* elements of the object.</p>
|
|
65
|
+
*
|
|
66
|
+
* <p>
|
|
67
|
+
* The property still exists and can be read from and written to, but it won't
|
|
68
|
+
* show up in for ... in loops, Object.keys(), or JSON.stringify() type methods.
|
|
69
|
+
* </p>
|
|
70
|
+
*
|
|
71
|
+
* <p>
|
|
72
|
+
* If the property already exists, it will be made hidden. Otherwise it will
|
|
73
|
+
* be created as a hidden property with the specified value.
|
|
74
|
+
* </p>
|
|
75
|
+
*
|
|
76
|
+
* <p><i>
|
|
77
|
+
* This method was built for hiding configuration values, but it can be applied
|
|
78
|
+
* to <u>any</u> javascript object.
|
|
79
|
+
* </i></p>
|
|
80
|
+
*
|
|
81
|
+
* <p>Example:</p>
|
|
82
|
+
* <pre>
|
|
83
|
+
* const Util = require('config/lib/util.js');
|
|
84
|
+
* ...
|
|
85
|
+
*
|
|
86
|
+
* // Hide the Amazon S3 credentials
|
|
87
|
+
* Util.makeHidden(CONFIG.amazonS3, 'access_id');
|
|
88
|
+
* Util.makeHidden(CONFIG.amazonS3, 'secret_key');
|
|
89
|
+
* </pre>
|
|
90
|
+
*
|
|
91
|
+
* @method makeHidden
|
|
92
|
+
* @param object {Object} - The object to make a hidden property into.
|
|
93
|
+
* @param property {string} - The name of the property to make hidden.
|
|
94
|
+
* @param value {*} - (optional) Set the property value to this (otherwise leave alone)
|
|
95
|
+
* @return object {Object} - The original object is returned - for chaining.
|
|
96
|
+
*/
|
|
97
|
+
static makeHidden(object, property, value) {
|
|
98
|
+
// If the new value isn't specified, just mark the property as hidden
|
|
99
|
+
if (typeof value === 'undefined') {
|
|
100
|
+
Object.defineProperty(object, property, {
|
|
101
|
+
enumerable: false,
|
|
102
|
+
configurable: true
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
// Otherwise set the value and mark it as hidden
|
|
106
|
+
Object.defineProperty(object, property, {
|
|
107
|
+
value: value,
|
|
108
|
+
enumerable: false,
|
|
109
|
+
configurable: true
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return object;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Looks into an options object for a specific attribute
|
|
118
|
+
*
|
|
119
|
+
* <p>
|
|
120
|
+
* This method looks into the options object, and if an attribute is defined, returns it,
|
|
121
|
+
* and if not, returns the default value
|
|
122
|
+
* </p>
|
|
123
|
+
*
|
|
124
|
+
* @method getOption
|
|
125
|
+
* @param options {Object | undefined} the options object
|
|
126
|
+
* @param optionName {string} the attribute name to look for
|
|
127
|
+
* @param defaultValue { any } the default in case the options object is empty, or the attribute does not exist.
|
|
128
|
+
* @return options[optionName] if defined, defaultValue if not.
|
|
129
|
+
*/
|
|
130
|
+
static getOption(options, optionName, defaultValue) {
|
|
131
|
+
if (options !== undefined && typeof options[optionName] !== 'undefined') {
|
|
132
|
+
return options[optionName];
|
|
133
|
+
} else {
|
|
134
|
+
return defaultValue;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Load the individual file configurations.
|
|
141
|
+
*
|
|
142
|
+
* <p>
|
|
143
|
+
* This method builds a map of filename to the configuration object defined
|
|
144
|
+
* by the file. The search order is:
|
|
145
|
+
* </p>
|
|
146
|
+
*
|
|
147
|
+
* <pre>
|
|
148
|
+
* default.EXT
|
|
149
|
+
* (deployment).EXT
|
|
150
|
+
* (hostname).EXT
|
|
151
|
+
* (hostname)-(deployment).EXT
|
|
152
|
+
* local.EXT
|
|
153
|
+
* local-(deployment).EXT
|
|
154
|
+
* </pre>
|
|
155
|
+
*
|
|
156
|
+
* <p>
|
|
157
|
+
* EXT can be yml, yaml, coffee, iced, json, jsonc, cson or js signifying the file type.
|
|
158
|
+
* yaml (and yml) is in YAML format, coffee is a coffee-script, iced is iced-coffee-script,
|
|
159
|
+
* json is in JSON format, jsonc is in JSONC format, cson is in CSON format, properties is
|
|
160
|
+
* in .properties format (http://en.wikipedia.org/wiki/.properties), and js is a javascript
|
|
161
|
+
* executable file that is require()'d with module.exports being the config object.
|
|
162
|
+
* </p>
|
|
163
|
+
*
|
|
164
|
+
* <p>
|
|
165
|
+
* hostname is the $HOST environment variable (or --HOST command line parameter)
|
|
166
|
+
* if set, otherwise the $HOSTNAME environment variable (or --HOSTNAME command
|
|
167
|
+
* line parameter) if set, otherwise the hostname found from
|
|
168
|
+
* require('os').hostname().
|
|
169
|
+
* </p>
|
|
170
|
+
*
|
|
171
|
+
* <p>
|
|
172
|
+
* Once a hostname is found, everything from the first period ('.') onwards
|
|
173
|
+
* is removed. For example, abc.example.com becomes abc
|
|
174
|
+
* </p>
|
|
175
|
+
*
|
|
176
|
+
* <p>
|
|
177
|
+
* (deployment) is the deployment type, found in the $NODE_ENV environment
|
|
178
|
+
* variable (which can be overridden by using $NODE_CONFIG_ENV
|
|
179
|
+
* environment variable). Defaults to 'development'.
|
|
180
|
+
* </p>
|
|
181
|
+
*
|
|
182
|
+
* <p>
|
|
183
|
+
* If the $NODE_APP_INSTANCE environment variable (or --NODE_APP_INSTANCE
|
|
184
|
+
* command line parameter) is set, then files with this appendage will be loaded.
|
|
185
|
+
* See the Multiple Application Instances section of the main documentation page
|
|
186
|
+
* for more information.
|
|
187
|
+
* </p>
|
|
188
|
+
*
|
|
189
|
+
* @method loadFileConfigs
|
|
190
|
+
* @param opts {LoadOptions | Load} parsing options or Load to update
|
|
191
|
+
* @return loadConfig {LoadConfig}
|
|
192
|
+
*/
|
|
193
|
+
static loadFileConfigs(opts) {
|
|
194
|
+
let load;
|
|
195
|
+
|
|
196
|
+
if (opts instanceof Load) {
|
|
197
|
+
load = opts;
|
|
198
|
+
} else {
|
|
199
|
+
load = new Load(opts);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let options = load.options;
|
|
203
|
+
let dir = options.configDir;
|
|
204
|
+
dir = _toAbsolutePath(dir);
|
|
205
|
+
|
|
206
|
+
// Read each file in turn
|
|
207
|
+
const baseNames = ['default'].concat(options.nodeEnv);
|
|
208
|
+
const hostName = options.hostName;
|
|
209
|
+
|
|
210
|
+
// #236: Also add full hostname when they are different.
|
|
211
|
+
if (hostName) {
|
|
212
|
+
const firstDomain = hostName.split('.')[0];
|
|
213
|
+
|
|
214
|
+
for (let env of options.nodeEnv) {
|
|
215
|
+
// Backward compatibility
|
|
216
|
+
baseNames.push(firstDomain, firstDomain + '-' + env);
|
|
217
|
+
|
|
218
|
+
// Add full hostname when it is not the same
|
|
219
|
+
if (hostName !== firstDomain) {
|
|
220
|
+
baseNames.push(hostName, hostName + '-' + env);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (let env of options.nodeEnv) {
|
|
226
|
+
baseNames.push('local', 'local-' + env);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const allowedFiles = {};
|
|
230
|
+
let resolutionIndex = 1;
|
|
231
|
+
const extNames = options.parser.getFilesOrder();
|
|
232
|
+
|
|
233
|
+
for (let baseName of baseNames) {
|
|
234
|
+
const fileNames = [baseName];
|
|
235
|
+
if (options.appInstance) {
|
|
236
|
+
fileNames.push(baseName + '-' + options.appInstance);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (let fileName of fileNames) {
|
|
240
|
+
for (let extName of extNames) {
|
|
241
|
+
allowedFiles[fileName + '.' + extName] = resolutionIndex++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const locatedFiles = this.locateMatchingFiles(dir, allowedFiles);
|
|
247
|
+
for (let fullFilename of locatedFiles) {
|
|
248
|
+
load.loadFile(fullFilename);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return load;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Return a list of fullFilenames who exists in allowedFiles
|
|
256
|
+
* Ordered according to allowedFiles argument specifications
|
|
257
|
+
*
|
|
258
|
+
* @method locateMatchingFiles
|
|
259
|
+
* @param configDirs {string} the config dir, or multiple dirs separated by a column (:)
|
|
260
|
+
* @param allowedFiles {Object} an object. keys and supported filenames
|
|
261
|
+
* and values are the position in the resolution order
|
|
262
|
+
* @returns {string[]} fullFilenames - path + filename
|
|
263
|
+
*/
|
|
264
|
+
static locateMatchingFiles(configDirs, allowedFiles) {
|
|
265
|
+
return configDirs.split(Path.delimiter)
|
|
266
|
+
.filter(Boolean)
|
|
267
|
+
.reduce(function (files, configDir) {
|
|
268
|
+
configDir = _toAbsolutePath(configDir);
|
|
269
|
+
try {
|
|
270
|
+
FileSystem.readdirSync(configDir)
|
|
271
|
+
.filter(file => allowedFiles[file])
|
|
272
|
+
.forEach(function (file) {
|
|
273
|
+
files.push([allowedFiles[file], Path.join(configDir, file)]);
|
|
274
|
+
});
|
|
275
|
+
} catch (e) {
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return files;
|
|
279
|
+
}, [])
|
|
280
|
+
.sort(function (a, b) {
|
|
281
|
+
return a[0] - b[0];
|
|
282
|
+
})
|
|
283
|
+
.map(function (file) {
|
|
284
|
+
return file[1];
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
*
|
|
290
|
+
* @param config
|
|
291
|
+
*/
|
|
292
|
+
static resolveDeferredConfigs(config) {
|
|
293
|
+
const deferred = [];
|
|
294
|
+
|
|
295
|
+
function _iterate (prop) {
|
|
296
|
+
if (prop == null || prop.constructor === String) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// We put the properties we are going to look it in an array to keep the order predictable
|
|
301
|
+
const propsToSort = Object.keys(prop).filter((property) => prop[property] != null);
|
|
302
|
+
|
|
303
|
+
// Second step is to iterate of the elements in a predictable (sorted) order
|
|
304
|
+
propsToSort.sort().forEach(function (property) {
|
|
305
|
+
if (prop[property].constructor === Object) {
|
|
306
|
+
_iterate(prop[property]);
|
|
307
|
+
} else if (prop[property].constructor === Array) {
|
|
308
|
+
for (let i = 0; i < prop[property].length; i++) {
|
|
309
|
+
if (prop[property][i] instanceof DeferredConfig) {
|
|
310
|
+
deferred.push(prop[property][i].prepare(config, prop[property], i));
|
|
311
|
+
} else {
|
|
312
|
+
_iterate(prop[property][i]);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
if (prop[property] instanceof DeferredConfig) {
|
|
317
|
+
deferred.push(prop[property].prepare(config, prop, property));
|
|
318
|
+
}
|
|
319
|
+
// else: Nothing to do. Keep the property how it is.
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
_iterate(config);
|
|
325
|
+
|
|
326
|
+
deferred.forEach(function (defer) { defer.resolve(); });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Return a deep copy of the specified object.
|
|
331
|
+
*
|
|
332
|
+
* This returns a new object with all elements copied from the specified
|
|
333
|
+
* object. Deep copies are made of objects and arrays so you can do anything
|
|
334
|
+
* with the returned object without affecting the input object.
|
|
335
|
+
*
|
|
336
|
+
* @method cloneDeep
|
|
337
|
+
* @param parent {Object} The original object to copy from
|
|
338
|
+
* @param [depth=20] {number} Maximum depth (default 20)
|
|
339
|
+
* @return {Object} A new object with the elements copied from the copyFrom object
|
|
340
|
+
*
|
|
341
|
+
* This method is copied from https://github.com/pvorb/node-clone/blob/17eea36140d61d97a9954c53417d0e04a00525d9/clone.js
|
|
342
|
+
*
|
|
343
|
+
* Copyright © 2011-2014 Paul Vorbach and contributors.
|
|
344
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
345
|
+
* of this software and associated documentation files (the “Software”), to deal
|
|
346
|
+
* in the Software without restriction, including without limitation the rights
|
|
347
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
348
|
+
* of the Software, and to permit persons to whom the Software is furnished to do so,
|
|
349
|
+
* subject to the following conditions: The above copyright notice and this permission
|
|
350
|
+
* notice shall be included in all copies or substantial portions of the Software.
|
|
351
|
+
*/
|
|
352
|
+
static cloneDeep(parent, depth, circular, prototype) {
|
|
353
|
+
// maintain two arrays for circular references, where corresponding parents
|
|
354
|
+
// and children have the same index
|
|
355
|
+
const allParents = [];
|
|
356
|
+
const allChildren = [];
|
|
357
|
+
const util = this;
|
|
358
|
+
|
|
359
|
+
const useBuffer = typeof Buffer !== 'undefined';
|
|
360
|
+
|
|
361
|
+
if (typeof circular === 'undefined')
|
|
362
|
+
circular = true;
|
|
363
|
+
|
|
364
|
+
if (typeof depth === 'undefined')
|
|
365
|
+
depth = 20;
|
|
366
|
+
|
|
367
|
+
// recurse this function so we don't reset allParents and allChildren
|
|
368
|
+
function _clone(parent, depth) {
|
|
369
|
+
// cloning null always returns null
|
|
370
|
+
if (parent === null)
|
|
371
|
+
return null;
|
|
372
|
+
|
|
373
|
+
if (depth === 0)
|
|
374
|
+
return parent;
|
|
375
|
+
|
|
376
|
+
let child;
|
|
377
|
+
if (typeof parent != 'object') {
|
|
378
|
+
return parent;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (Array.isArray(parent)) {
|
|
382
|
+
child = [];
|
|
383
|
+
} else if (parent instanceof RegExp) {
|
|
384
|
+
child = new RegExp(parent.source, util.getRegExpFlags(parent));
|
|
385
|
+
if (parent.lastIndex) child.lastIndex = parent.lastIndex;
|
|
386
|
+
} else if (parent instanceof Date) {
|
|
387
|
+
child = new Date(parent.getTime());
|
|
388
|
+
} else if (useBuffer && Buffer.isBuffer(parent)) {
|
|
389
|
+
child = Buffer.alloc(parent.length);
|
|
390
|
+
parent.copy(child);
|
|
391
|
+
return child;
|
|
392
|
+
} else {
|
|
393
|
+
if (typeof prototype === 'undefined') child = Object.create(Object.getPrototypeOf(parent));
|
|
394
|
+
else child = Object.create(prototype);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (circular) {
|
|
398
|
+
const index = allParents.indexOf(parent);
|
|
399
|
+
|
|
400
|
+
if (index !== -1) {
|
|
401
|
+
return allChildren[index];
|
|
402
|
+
}
|
|
403
|
+
allParents.push(parent);
|
|
404
|
+
allChildren.push(child);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
for (const i in parent) {
|
|
408
|
+
const propDescriptor = Object.getOwnPropertyDescriptor(parent, i);
|
|
409
|
+
const hasGetter = ((typeof propDescriptor !== 'undefined') && (typeof propDescriptor.get !== 'undefined'));
|
|
410
|
+
|
|
411
|
+
if (hasGetter) {
|
|
412
|
+
Object.defineProperty(child, i, propDescriptor);
|
|
413
|
+
} else if (util.isPromise(parent[i])) {
|
|
414
|
+
child[i] = parent[i];
|
|
415
|
+
} else {
|
|
416
|
+
child[i] = _clone(parent[i], depth - 1);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return child;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return _clone(parent, depth);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Underlying get mechanism
|
|
428
|
+
*
|
|
429
|
+
* @method getPath
|
|
430
|
+
* @param object {Object} - Object to get the property for
|
|
431
|
+
* @param property {string|string[]} - The property name to get (as an array or '.' delimited string)
|
|
432
|
+
* @return value {*} - Property value, including undefined if not defined.
|
|
433
|
+
*/
|
|
434
|
+
static getPath(object, property) {
|
|
435
|
+
const path = Array.isArray(property) ? property : property.split('.');
|
|
436
|
+
|
|
437
|
+
let next = object;
|
|
438
|
+
for (let i = 0; i < path.length; i++) {
|
|
439
|
+
const name = path[i];
|
|
440
|
+
const value = next[name];
|
|
441
|
+
|
|
442
|
+
if (i === path.length - 1) {
|
|
443
|
+
return value;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Note that typeof null === 'object'
|
|
447
|
+
if (value === null || typeof value !== 'object') {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
next = value;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Set objects given a path as a string list
|
|
457
|
+
*
|
|
458
|
+
* @method setPath
|
|
459
|
+
* @param object {Object} - Object to set the property on
|
|
460
|
+
* @param property {string|string[]} - The property name to get (as an array or '.' delimited string)
|
|
461
|
+
* @param value {*} - value to set, ignoring null
|
|
462
|
+
* @return {*} - the given value
|
|
463
|
+
*/
|
|
464
|
+
static setPath(object, property, value) {
|
|
465
|
+
const path = Array.isArray(property) ? property : property.split('.');
|
|
466
|
+
|
|
467
|
+
if (value === null || path.length === 0) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
let next = object;
|
|
472
|
+
|
|
473
|
+
for (let i = 0; i < path.length; i++) {
|
|
474
|
+
let name = path[i];
|
|
475
|
+
|
|
476
|
+
if (i === path.length - 1) { // no more keys to make, so set the value
|
|
477
|
+
next[name] = value;
|
|
478
|
+
} else if (Object.hasOwnProperty.call(next, name)) {
|
|
479
|
+
next = next[name];
|
|
480
|
+
} else {
|
|
481
|
+
next = next[name] = {};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return value;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Return true if two objects have equal contents.
|
|
490
|
+
*
|
|
491
|
+
* @method equalsDeep
|
|
492
|
+
* @param object1 {Object} The object to compare from
|
|
493
|
+
* @param object2 {Object} The object to compare with
|
|
494
|
+
* @param depth {number} An optional depth to prevent recursion. Default: 20.
|
|
495
|
+
* @return {boolean} True if both objects have equivalent contents
|
|
496
|
+
*/
|
|
497
|
+
static equalsDeep(object1, object2, depth) {
|
|
498
|
+
// Recursion detection
|
|
499
|
+
depth = (depth === null ? DEFAULT_CLONE_DEPTH : depth);
|
|
500
|
+
if (depth < 0) {
|
|
501
|
+
return {};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Fast comparisons
|
|
505
|
+
if (!object1 || !object2) {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
if (object1 === object2) {
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
if (typeof (object1) != 'object' || typeof (object2) != 'object') {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// They must have the same keys. If their length isn't the same
|
|
516
|
+
// then they're not equal. If the keys aren't the same, the value
|
|
517
|
+
// comparisons will fail.
|
|
518
|
+
if (Object.keys(object1).length != Object.keys(object2).length) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Compare the values
|
|
523
|
+
for (const prop in object1) {
|
|
524
|
+
|
|
525
|
+
// Call recursively if an object or array
|
|
526
|
+
if (object1[prop] && typeof (object1[prop]) === 'object') {
|
|
527
|
+
if (!this.equalsDeep(object1[prop], object2[prop], depth - 1)) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
} else {
|
|
531
|
+
if (object1[prop] !== object2[prop]) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Test passed.
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Extend an object, and any object it contains.
|
|
543
|
+
*
|
|
544
|
+
* This does not replace deep objects, but dives into them
|
|
545
|
+
* replacing individual elements instead.
|
|
546
|
+
*
|
|
547
|
+
* @method extendDeep
|
|
548
|
+
* @param mergeInto {Object} The object to merge into
|
|
549
|
+
* @param mergeFrom... {Object} - Any number of objects to merge from
|
|
550
|
+
* @param depth {integer} An optional depth to prevent recursion. Default: 20.
|
|
551
|
+
* @return {Object} The altered mergeInto object is returned
|
|
552
|
+
*/
|
|
553
|
+
static extendDeep(mergeInto, ...vargs) {
|
|
554
|
+
// Initialize
|
|
555
|
+
let depth = vargs.pop();
|
|
556
|
+
if (typeof (depth) != 'number') {
|
|
557
|
+
vargs.push(depth);
|
|
558
|
+
depth = DEFAULT_CLONE_DEPTH;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Recursion detection
|
|
562
|
+
if (depth < 0) {
|
|
563
|
+
return mergeInto;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
for (let mergeFrom of vargs) {
|
|
567
|
+
// Cycle through each element of the object to merge from
|
|
568
|
+
for (const prop in mergeFrom) {
|
|
569
|
+
// save original value in deferred elements
|
|
570
|
+
const fromIsDeferredFunc = mergeFrom[prop] instanceof DeferredConfig;
|
|
571
|
+
const isDeferredFunc = mergeInto[prop] instanceof DeferredConfig;
|
|
572
|
+
|
|
573
|
+
if (fromIsDeferredFunc && Object.hasOwnProperty.call(mergeInto, prop)) {
|
|
574
|
+
mergeFrom[prop]._original = isDeferredFunc ? mergeInto[prop]._original : mergeInto[prop];
|
|
575
|
+
}
|
|
576
|
+
// Extend recursively if both elements are objects and target is not really a deferred function
|
|
577
|
+
if (mergeFrom[prop] instanceof Date) {
|
|
578
|
+
mergeInto[prop] = mergeFrom[prop];
|
|
579
|
+
}
|
|
580
|
+
if (mergeFrom[prop] instanceof RegExp) {
|
|
581
|
+
mergeInto[prop] = mergeFrom[prop];
|
|
582
|
+
} else if (Util.isObject(mergeInto[prop]) && Util.isObject(mergeFrom[prop]) && !isDeferredFunc) {
|
|
583
|
+
Util.extendDeep(mergeInto[prop], mergeFrom[prop], depth - 1);
|
|
584
|
+
} else if (Util.isPromise(mergeFrom[prop])) {
|
|
585
|
+
mergeInto[prop] = mergeFrom[prop];
|
|
586
|
+
}
|
|
587
|
+
// Copy recursively if the mergeFrom element is an object (or array or fn)
|
|
588
|
+
else if (mergeFrom[prop] && typeof mergeFrom[prop] === 'object') {
|
|
589
|
+
mergeInto[prop] = Util.cloneDeep(mergeFrom[prop], depth - 1);
|
|
590
|
+
}
|
|
591
|
+
// Copy property descriptor otherwise, preserving accessors
|
|
592
|
+
else if (Object.getOwnPropertyDescriptor(Object(mergeFrom), prop)) {
|
|
593
|
+
Object.defineProperty(mergeInto, prop, Object.getOwnPropertyDescriptor(Object(mergeFrom), prop));
|
|
594
|
+
} else if (mergeInto[prop] !== mergeFrom[prop]) {
|
|
595
|
+
mergeInto[prop] = mergeFrom[prop];
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Chain
|
|
601
|
+
return mergeInto;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Is the specified argument a regular javascript object?
|
|
606
|
+
*
|
|
607
|
+
* The argument is an object if it's a JS object, but not an array.
|
|
608
|
+
*
|
|
609
|
+
* @method isObject
|
|
610
|
+
* @param obj {*} An argument of any type.
|
|
611
|
+
* @return {boolean} TRUE if the arg is an object, FALSE if not
|
|
612
|
+
*/
|
|
613
|
+
static isObject(obj) {
|
|
614
|
+
return (obj !== null) && (typeof obj === 'object') && !(Array.isArray(obj));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Is the specified argument a javascript promise?
|
|
619
|
+
*
|
|
620
|
+
* @method isPromise
|
|
621
|
+
* @param obj {*} An argument of any type.
|
|
622
|
+
* @returns {boolean}
|
|
623
|
+
*/
|
|
624
|
+
static isPromise(obj) {
|
|
625
|
+
return Object.prototype.toString.call(obj) === '[object Promise]';
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Returns a string of flags for regular expression `re`.
|
|
630
|
+
*
|
|
631
|
+
* @param {RegExp} re Regular expression
|
|
632
|
+
* @returns {string} Flags
|
|
633
|
+
*/
|
|
634
|
+
static getRegExpFlags = function (re) {
|
|
635
|
+
let flags = '';
|
|
636
|
+
re.global && (flags += 'g');
|
|
637
|
+
re.ignoreCase && (flags += 'i');
|
|
638
|
+
re.multiline && (flags += 'm');
|
|
639
|
+
|
|
640
|
+
return flags;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Returns a new deep copy of the current config object, or any part of the config if provided.
|
|
645
|
+
*
|
|
646
|
+
* @param {Object} config The part of the config to copy and serialize.
|
|
647
|
+
* @returns {Object} The cloned config or part of the config
|
|
648
|
+
*/
|
|
649
|
+
static toObject(config) {
|
|
650
|
+
return JSON.parse(JSON.stringify(config));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Record a set of lookups
|
|
656
|
+
*/
|
|
657
|
+
class Env {
|
|
658
|
+
constructor() {
|
|
659
|
+
this.lookups = {};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* <p>Initialize a parameter from the command line or process environment</p>
|
|
664
|
+
*
|
|
665
|
+
* <p>
|
|
666
|
+
* This method looks for the parameter from the command line in the format
|
|
667
|
+
* --PARAMETER=VALUE, then from the process environment, then from the
|
|
668
|
+
* default specified as an argument.
|
|
669
|
+
* </p>
|
|
670
|
+
*
|
|
671
|
+
* @method initParam
|
|
672
|
+
* @param paramName {String} Name of the parameter
|
|
673
|
+
* @param [defaultValue] {*} Default value of the parameter
|
|
674
|
+
* @return {*} The found value, or default value
|
|
675
|
+
*/
|
|
676
|
+
initParam(paramName, defaultValue) {
|
|
677
|
+
// Record and return the value
|
|
678
|
+
const value = this.getCmdLineArg(paramName) || process.env[paramName] || defaultValue;
|
|
679
|
+
this.setEnv(paramName, value);
|
|
680
|
+
|
|
681
|
+
return value;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* <p>Get Command Line Arguments</p>
|
|
686
|
+
*
|
|
687
|
+
* <p>
|
|
688
|
+
* This method allows you to retrieve the value of the specified command line argument.
|
|
689
|
+
* </p>
|
|
690
|
+
*
|
|
691
|
+
* <p>
|
|
692
|
+
* The argument is case sensitive, and must be of the form '--ARG_NAME=value'
|
|
693
|
+
* </p>
|
|
694
|
+
*
|
|
695
|
+
* @method getCmdLineArg
|
|
696
|
+
* @param searchFor {String} The argument name to search for
|
|
697
|
+
* @return {*} false if the argument was not found, the argument value if found
|
|
698
|
+
*/
|
|
699
|
+
getCmdLineArg(searchFor) {
|
|
700
|
+
const cmdLineArgs = process.argv.slice(2, process.argv.length);
|
|
701
|
+
const argName = '--' + searchFor + '=';
|
|
702
|
+
|
|
703
|
+
for (let argvIt = 0; argvIt < cmdLineArgs.length; argvIt++) {
|
|
704
|
+
if (cmdLineArgs[argvIt].indexOf(argName) === 0) {
|
|
705
|
+
return cmdLineArgs[argvIt].substr(argName.length);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* <p>Get a Config Environment Variable Value</p>
|
|
714
|
+
*
|
|
715
|
+
* <p>
|
|
716
|
+
* This method returns the value of the specified config environment variable,
|
|
717
|
+
* including any defaults or overrides.
|
|
718
|
+
* </p>
|
|
719
|
+
*
|
|
720
|
+
* @method getEnv
|
|
721
|
+
* @param varName {String} The environment variable name
|
|
722
|
+
* @return {String} The value of the environment variable
|
|
723
|
+
*/
|
|
724
|
+
getEnv(varName) {
|
|
725
|
+
return this.lookups[varName];
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Set a tracing variable of what was accessed from process.env
|
|
730
|
+
*
|
|
731
|
+
* @see fromEnvironment
|
|
732
|
+
* @param key {string}
|
|
733
|
+
* @param value
|
|
734
|
+
*/
|
|
735
|
+
setEnv(key, value) {
|
|
736
|
+
this.lookups[key] = value;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* The work horse of loading Config data - without the singleton.
|
|
743
|
+
*
|
|
744
|
+
* This class can be used to execute important workflows, such as build-time validations
|
|
745
|
+
* and Module Defaults.
|
|
746
|
+
*
|
|
747
|
+
* @example
|
|
748
|
+
* //load module defaults
|
|
749
|
+
* const config = require("config");
|
|
750
|
+
* const Load = require("config/util.js").Load;
|
|
751
|
+
*
|
|
752
|
+
* let load = Load.fromEnvironment();
|
|
753
|
+
*
|
|
754
|
+
* load.scan();
|
|
755
|
+
*
|
|
756
|
+
* config.setModuleDefaults("my-module", load.config);
|
|
757
|
+
*
|
|
758
|
+
* @example
|
|
759
|
+
* // verify configs
|
|
760
|
+
* const Load = require("config/util.js").Load;
|
|
761
|
+
*
|
|
762
|
+
* for (let environment of ["sandbox", "qa", "qa-hyderabad", "perf", "staging", "prod-east-1", "prod-west-2"] {
|
|
763
|
+
* let load = Load.fromEnvironment(environment);
|
|
764
|
+
*
|
|
765
|
+
* load.scan();
|
|
766
|
+
* }
|
|
767
|
+
*
|
|
768
|
+
*
|
|
769
|
+
* @class Load
|
|
770
|
+
*/
|
|
771
|
+
class Load {
|
|
772
|
+
/**
|
|
773
|
+
* @constructor
|
|
774
|
+
* @param options {LoadOptions=} - defaults to reading from environment variables
|
|
775
|
+
*/
|
|
776
|
+
constructor(options, env = new Env()) {
|
|
777
|
+
this.env = env;
|
|
778
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
779
|
+
this.sources = this.options.skipConfigSources ? undefined : [];
|
|
780
|
+
this.parser = this.options.parser;
|
|
781
|
+
this.config = {};
|
|
782
|
+
this.defaults = undefined;
|
|
783
|
+
this.unmerged = undefined;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* <p>Initialize a parameter from the command line or process environment</p>
|
|
788
|
+
*
|
|
789
|
+
* <p>
|
|
790
|
+
* This method looks for the parameter from the command line in the format
|
|
791
|
+
* --PARAMETER=VALUE, then from the process environment, then from the
|
|
792
|
+
* default specified as an argument.
|
|
793
|
+
* </p>
|
|
794
|
+
*
|
|
795
|
+
* @method initParam
|
|
796
|
+
* @param paramName {String} Name of the parameter
|
|
797
|
+
* @param [defaultValue] {*} Default value of the parameter
|
|
798
|
+
* @return {*} The found value, or default value
|
|
799
|
+
*/
|
|
800
|
+
initParam(paramName, defaultValue) {
|
|
801
|
+
return this.env.initParam(paramName, defaultValue);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* <p>Get Command Line Arguments</p>
|
|
806
|
+
*
|
|
807
|
+
* <p>
|
|
808
|
+
* This method allows you to retrieve the value of the specified command line argument.
|
|
809
|
+
* </p>
|
|
810
|
+
*
|
|
811
|
+
* <p>
|
|
812
|
+
* The argument is case sensitive, and must be of the form '--ARG_NAME=value'
|
|
813
|
+
* </p>
|
|
814
|
+
*
|
|
815
|
+
* @method getCmdLineArg
|
|
816
|
+
* @param searchFor {String} The argument name to search for
|
|
817
|
+
* @return {*} false if the argument was not found, the argument value if found
|
|
818
|
+
*/
|
|
819
|
+
getCmdLineArg(searchFor) {
|
|
820
|
+
return this.env.getCmdLineArg(searchFor);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* <p>Get a Config Environment Variable Value</p>
|
|
825
|
+
*
|
|
826
|
+
* <p>
|
|
827
|
+
* This method returns the value of the specified config environment variable,
|
|
828
|
+
* including any defaults or overrides.
|
|
829
|
+
* </p>
|
|
830
|
+
*
|
|
831
|
+
* @method getEnv
|
|
832
|
+
* @param varName {String} The environment variable name
|
|
833
|
+
* @return {String} The value of the environment variable
|
|
834
|
+
*/
|
|
835
|
+
getEnv(varName) {
|
|
836
|
+
return this.env.getEnv(varName);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Set a tracing variable of what was accessed from process.env
|
|
841
|
+
*
|
|
842
|
+
* @see fromEnvironment
|
|
843
|
+
* @param key {string}
|
|
844
|
+
* @param value
|
|
845
|
+
*/
|
|
846
|
+
setEnv(key, value) {
|
|
847
|
+
return this.env.setEnv(key, value);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Add a set of configurations and record the source
|
|
852
|
+
*
|
|
853
|
+
* @param name {string=} an entry will be added to sources under this name (if given)
|
|
854
|
+
* @param values {Object} values to merge in
|
|
855
|
+
* @param original {string=} Optional unparsed version of the data
|
|
856
|
+
*/
|
|
857
|
+
addConfig(name, values, original) {
|
|
858
|
+
Util.extendDeep(this.config, values);
|
|
859
|
+
|
|
860
|
+
if (name && this.sources) {
|
|
861
|
+
let source = {name, parsed: values};
|
|
862
|
+
|
|
863
|
+
if (original !== undefined) {
|
|
864
|
+
source.original = original;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
this.sources.push(source);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return this;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* scan and load config files in the same manner that config.js does
|
|
876
|
+
*
|
|
877
|
+
* @param load {Load}
|
|
878
|
+
* @param additional {{name, value}[]=} additional values to populate (usually from NODE_CONFIG
|
|
879
|
+
*/
|
|
880
|
+
scan(additional) {
|
|
881
|
+
Util.loadFileConfigs(this);
|
|
882
|
+
|
|
883
|
+
if (additional) {
|
|
884
|
+
for (let {name, config} of additional) {
|
|
885
|
+
this.addConfig(name, config);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Override with environment variables if there is a custom-environment-variables.EXT mapping file
|
|
890
|
+
this.loadCustomEnvVars();
|
|
891
|
+
|
|
892
|
+
Util.resolveDeferredConfigs(this.config);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Load a file and add it to the configuration
|
|
897
|
+
*
|
|
898
|
+
* @param fullFilename {string} an absolute file path
|
|
899
|
+
* @param convert {DataConvert=}
|
|
900
|
+
* @returns {null}
|
|
901
|
+
*/
|
|
902
|
+
loadFile(fullFilename, convert) {
|
|
903
|
+
let configObject = null;
|
|
904
|
+
let fileContent = null;
|
|
905
|
+
|
|
906
|
+
// Note that all methods here are the Sync versions. This is appropriate during
|
|
907
|
+
// module loading (which is a synchronous operation), but not thereafter.
|
|
908
|
+
|
|
909
|
+
try {
|
|
910
|
+
// Try loading the file.
|
|
911
|
+
fileContent = FileSystem.readFileSync(fullFilename, 'utf-8');
|
|
912
|
+
fileContent = fileContent.replace(/^\uFEFF/, '');
|
|
913
|
+
}
|
|
914
|
+
catch (e2) {
|
|
915
|
+
if (e2.code !== 'ENOENT') {
|
|
916
|
+
throw new Error('Config file ' + fullFilename + ' cannot be read. Error code is: '+e2.code
|
|
917
|
+
+'. Error message is: '+e2.message);
|
|
918
|
+
}
|
|
919
|
+
return null; // file doesn't exists
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Parse the file based on extension
|
|
923
|
+
try {
|
|
924
|
+
|
|
925
|
+
// skip if it's a gitcrypt file and CONFIG_SKIP_GITCRYPT is true
|
|
926
|
+
if (!this.options.gitCrypt) {
|
|
927
|
+
if (GIT_CRYPT_REGEX.test(fileContent)) {
|
|
928
|
+
console.error('WARNING: ' + fullFilename + ' is a git-crypt file and CONFIG_SKIP_GITCRYPT is set. skipping.');
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
configObject = this.parser.parse(fullFilename, fileContent);
|
|
934
|
+
} catch (e3) {
|
|
935
|
+
if (GIT_CRYPT_REGEX.test(fileContent)) {
|
|
936
|
+
console.error('ERROR: ' + fullFilename + ' is a git-crypt file and CONFIG_SKIP_GITCRYPT is not set.');
|
|
937
|
+
}
|
|
938
|
+
throw new Error("Cannot parse config file: '" + fullFilename + "': " + e3);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (convert) {
|
|
942
|
+
configObject = convert(configObject);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
this.addConfig(fullFilename, configObject, fileContent);
|
|
946
|
+
|
|
947
|
+
return configObject;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* load custom-environment-variables
|
|
952
|
+
*
|
|
953
|
+
* @param extNames {string[]=} extensions
|
|
954
|
+
* @returns {{}}
|
|
955
|
+
*/
|
|
956
|
+
loadCustomEnvVars(extNames) {
|
|
957
|
+
let resolutionIndex = 1;
|
|
958
|
+
const allowedFiles = {};
|
|
959
|
+
|
|
960
|
+
extNames = extNames ?? this.parser.getFilesOrder();
|
|
961
|
+
|
|
962
|
+
extNames.forEach(function (extName) {
|
|
963
|
+
allowedFiles['custom-environment-variables' + '.' + extName] = resolutionIndex++;
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
const locatedFiles = Util.locateMatchingFiles(this.options.configDir, allowedFiles);
|
|
967
|
+
locatedFiles.forEach((fullFilename) => {
|
|
968
|
+
this.loadFile(fullFilename, (configObj) => this.substituteDeep(configObj, process.env));
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Return the report of where the sources for this load operation came from
|
|
974
|
+
* @returns {ConfigSource[]}
|
|
975
|
+
*/
|
|
976
|
+
getSources() {
|
|
977
|
+
return (this.sources ?? []).slice();
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* <p>
|
|
982
|
+
* Set default configurations for a node.js module.
|
|
983
|
+
* </p>
|
|
984
|
+
*
|
|
985
|
+
* <p>
|
|
986
|
+
* This allows module developers to attach their configurations onto the
|
|
987
|
+
* default configuration object so they can be configured by the consumers
|
|
988
|
+
* of the module.
|
|
989
|
+
* </p>
|
|
990
|
+
*
|
|
991
|
+
* <p>Using the function within your module:</p>
|
|
992
|
+
* <pre>
|
|
993
|
+
* load.setModuleDefaults("MyModule", {
|
|
994
|
+
* templateName: "t-50",
|
|
995
|
+
* colorScheme: "green"
|
|
996
|
+
* });
|
|
997
|
+
* <br>
|
|
998
|
+
* // Template name may be overridden by application config files
|
|
999
|
+
* console.log("Template: " + CONFIG.MyModule.templateName);
|
|
1000
|
+
* </pre>
|
|
1001
|
+
*
|
|
1002
|
+
* <p>
|
|
1003
|
+
* The above example results in a "MyModule" element of the configuration
|
|
1004
|
+
* object, containing an object with the specified default values.
|
|
1005
|
+
* </p>
|
|
1006
|
+
*
|
|
1007
|
+
* @method setModuleDefaults
|
|
1008
|
+
* @param moduleName {string} - Name of your module.
|
|
1009
|
+
* @param defaultProperties {Object} - The default module configuration.
|
|
1010
|
+
* @return {Object} - The module level configuration object.
|
|
1011
|
+
*/
|
|
1012
|
+
setModuleDefaults(moduleName, defaultProperties) {
|
|
1013
|
+
if (this.defaults === undefined) {
|
|
1014
|
+
this.defaults = {};
|
|
1015
|
+
this.unmerged = {};
|
|
1016
|
+
|
|
1017
|
+
if (this.sources) {
|
|
1018
|
+
this.sources.splice(0, 0, { name: 'Module Defaults', parsed: this.defaults });
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const path = moduleName.split('.');
|
|
1023
|
+
const defaults = Util.setPath(this.defaults, path, Util.getPath(this.defaults, path) ?? {});
|
|
1024
|
+
|
|
1025
|
+
Util.extendDeep(defaults, defaultProperties);
|
|
1026
|
+
|
|
1027
|
+
const original =
|
|
1028
|
+
Util.getPath(this.unmerged, path) ??
|
|
1029
|
+
Util.setPath(this.unmerged, path, Util.getPath(this.config, path) ?? {});
|
|
1030
|
+
|
|
1031
|
+
const moduleConfig = Util.extendDeep({}, defaults, original);
|
|
1032
|
+
Util.setPath(this.config, path, moduleConfig);
|
|
1033
|
+
Util.resolveDeferredConfigs(this.config);
|
|
1034
|
+
|
|
1035
|
+
return moduleConfig;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Parse and return the specified string with the specified format.
|
|
1040
|
+
*
|
|
1041
|
+
* The format determines the parser to use.
|
|
1042
|
+
*
|
|
1043
|
+
* json = File is parsed using JSON.parse()
|
|
1044
|
+
* yaml (or yml) = Parsed with a YAML parser
|
|
1045
|
+
* toml = Parsed with a TOML parser
|
|
1046
|
+
* cson = Parsed with a CSON parser
|
|
1047
|
+
* hjson = Parsed with a HJSON parser
|
|
1048
|
+
* json5 = Parsed with a JSON5 parser
|
|
1049
|
+
* properties = Parsed with the 'properties' node package
|
|
1050
|
+
* xml = Parsed with a XML parser
|
|
1051
|
+
*
|
|
1052
|
+
* If the file doesn't exist, a null will be returned. If the file can't be
|
|
1053
|
+
* parsed, an exception will be thrown.
|
|
1054
|
+
*
|
|
1055
|
+
* This method performs synchronous file operations, and should not be called
|
|
1056
|
+
* after synchronous module loading.
|
|
1057
|
+
*
|
|
1058
|
+
* @protected
|
|
1059
|
+
* @method parseString
|
|
1060
|
+
* @param content {string} The full content
|
|
1061
|
+
* @param format {string} The format to be parsed
|
|
1062
|
+
* @return {configObject} The configuration object parsed from the string
|
|
1063
|
+
*/
|
|
1064
|
+
parseString = function (content, format) {
|
|
1065
|
+
const parser = this.parser.getParser(format);
|
|
1066
|
+
|
|
1067
|
+
if (typeof parser === 'function') {
|
|
1068
|
+
return parser(null, content);
|
|
1069
|
+
} else {
|
|
1070
|
+
//TODO: throw on missing #753
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Create a new object patterned after substitutionMap, where:
|
|
1076
|
+
* 1. Terminal string values in substitutionMap are used as keys
|
|
1077
|
+
* 2. To look up values in a key-value store, variables
|
|
1078
|
+
* 3. And parent keys are created as necessary to retain the structure of substitutionMap.
|
|
1079
|
+
*
|
|
1080
|
+
* @protected
|
|
1081
|
+
* @method substituteDeep
|
|
1082
|
+
* @param substitutionMap {Object} - an object whose terminal (non-subobject) values are strings
|
|
1083
|
+
* @param variables {object[string:value]} - usually process.env, a flat object used to transform
|
|
1084
|
+
* terminal values in a copy of substitutionMap.
|
|
1085
|
+
* @returns {Object} - deep copy of substitutionMap with only those paths whose terminal values
|
|
1086
|
+
* corresponded to a key in `variables`
|
|
1087
|
+
*/
|
|
1088
|
+
substituteDeep(substitutionMap, variables) {
|
|
1089
|
+
const result = {};
|
|
1090
|
+
|
|
1091
|
+
const _substituteVars = (map, vars, pathTo) => {
|
|
1092
|
+
for (const prop in map) {
|
|
1093
|
+
const value = map[prop];
|
|
1094
|
+
|
|
1095
|
+
if (typeof(value) === 'string') { // We found a leaf variable name
|
|
1096
|
+
if (typeof vars[value] !== 'undefined' && vars[value] !== '') { // if the vars provide a value set the value in the result map
|
|
1097
|
+
Util.setPath(result, pathTo.concat(prop), vars[value]);
|
|
1098
|
+
}
|
|
1099
|
+
} else if (Util.isObject(value)) { // work on the subtree, giving it a clone of the pathTo
|
|
1100
|
+
if ('__name' in value && '__format' in value && typeof vars[value.__name] !== 'undefined' && vars[value.__name] !== '') {
|
|
1101
|
+
let parsedValue;
|
|
1102
|
+
try {
|
|
1103
|
+
parsedValue = this.parseString(vars[value.__name], value.__format);
|
|
1104
|
+
} catch(err) {
|
|
1105
|
+
err.message = '__format parser error in ' + value.__name + ': ' + err.message;
|
|
1106
|
+
throw err;
|
|
1107
|
+
}
|
|
1108
|
+
Util.setPath(result, pathTo.concat(prop), parsedValue);
|
|
1109
|
+
} else {
|
|
1110
|
+
_substituteVars(value, vars, pathTo.concat(prop));
|
|
1111
|
+
}
|
|
1112
|
+
} else {
|
|
1113
|
+
let msg = "Illegal key type for substitution map at " + pathTo.join('.') + ': ' + typeof(value);
|
|
1114
|
+
throw Error(msg);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
_substituteVars(substitutionMap, variables, []);
|
|
1120
|
+
return result;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Populate a LoadConfig entirely from environment variables.
|
|
1125
|
+
*
|
|
1126
|
+
* This is the way a base config is normally accomplished, but not for independent loads.
|
|
1127
|
+
*
|
|
1128
|
+
* This function exists in part to reduce the circular dependency of variable initializations
|
|
1129
|
+
* in the config.js file
|
|
1130
|
+
* @param environments {string} the NODE_CONFIG_ENVs you want to load
|
|
1131
|
+
* @private
|
|
1132
|
+
* @returns {Load}
|
|
1133
|
+
*/
|
|
1134
|
+
static fromEnvironment(environments) {
|
|
1135
|
+
let env = new Env();
|
|
1136
|
+
|
|
1137
|
+
if (environments !== undefined) {
|
|
1138
|
+
environments = environments.split(',');
|
|
1139
|
+
env.setEnv('nodeEnv', environments.join(','));
|
|
1140
|
+
} else {
|
|
1141
|
+
let nodeConfigEnv = env.initParam('NODE_CONFIG_ENV');
|
|
1142
|
+
let nodeEnv = env.initParam('NODE_ENV');
|
|
1143
|
+
|
|
1144
|
+
if (nodeConfigEnv) {
|
|
1145
|
+
env.setEnv('nodeEnv', 'NODE_CONFIG_ENV');
|
|
1146
|
+
nodeEnv = nodeConfigEnv;
|
|
1147
|
+
} else if (nodeEnv) {
|
|
1148
|
+
env.setEnv('nodeEnv', 'NODE_ENV');
|
|
1149
|
+
env.setEnv('NODE_CONFIG_ENV', nodeEnv); //TODO: This is a bug asserted in the tests
|
|
1150
|
+
} else {
|
|
1151
|
+
nodeEnv = 'development';
|
|
1152
|
+
env.setEnv('nodeEnv', 'default');
|
|
1153
|
+
env.setEnv('NODE_ENV', nodeEnv);
|
|
1154
|
+
env.setEnv('NODE_CONFIG_ENV', nodeEnv); //TODO: This is a bug asserted in the tests
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
environments = nodeEnv.split(',');
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
let configDir = env.initParam('NODE_CONFIG_DIR');
|
|
1161
|
+
configDir = configDir && _toAbsolutePath(configDir);
|
|
1162
|
+
|
|
1163
|
+
let appInstance = env.initParam('NODE_APP_INSTANCE');
|
|
1164
|
+
let gitCrypt = !env.initParam('CONFIG_SKIP_GITCRYPT');
|
|
1165
|
+
let parser = _loadParser(env.initParam('NODE_CONFIG_PARSER'), configDir);
|
|
1166
|
+
let hostName = env.initParam('HOST') || env.initParam('HOSTNAME');
|
|
1167
|
+
|
|
1168
|
+
// Determine the host name from the OS module, $HOST, or $HOSTNAME
|
|
1169
|
+
// Remove any . appendages, and default to null if not set
|
|
1170
|
+
try {
|
|
1171
|
+
if (!hostName) {
|
|
1172
|
+
const OS = require('os');
|
|
1173
|
+
hostName = OS.hostname();
|
|
1174
|
+
}
|
|
1175
|
+
} catch (e) {
|
|
1176
|
+
hostName = '';
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
env.setEnv('HOSTNAME', hostName);
|
|
1180
|
+
|
|
1181
|
+
/** @type {LoadOptions} */
|
|
1182
|
+
let options = {
|
|
1183
|
+
configDir: configDir ?? DEFAULT_CONFIG_DIR,
|
|
1184
|
+
nodeEnv: environments,
|
|
1185
|
+
hostName,
|
|
1186
|
+
parser,
|
|
1187
|
+
appInstance,
|
|
1188
|
+
gitCrypt
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
return new Load(options, env);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Helper functions shared across object members
|
|
1196
|
+
function _toAbsolutePath (configDir) {
|
|
1197
|
+
if (configDir.indexOf('.') === 0) {
|
|
1198
|
+
return Path.join(process.cwd(), configDir);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
return configDir;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function _loadParser(name, dir) {
|
|
1205
|
+
if (name === undefined) {
|
|
1206
|
+
return require("../parser.js");
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
try {
|
|
1210
|
+
const parserModule = Path.isAbsolute(name) ? name : Path.join(dir, name);
|
|
1211
|
+
|
|
1212
|
+
return require(parserModule);
|
|
1213
|
+
}
|
|
1214
|
+
catch (e) {
|
|
1215
|
+
console.warn(`Failed to load config parser from ${name}`);
|
|
1216
|
+
console.log(e);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
module.exports = { Util, Load: Load };
|