1 | ;
|
2 |
|
3 | var path = require('path');
|
4 | var nconf = require('nconf');
|
5 | var shush = require('shush');
|
6 | var caller = require('caller');
|
7 | var thing = require('core-util-is');
|
8 | var shortstop = require('shortstop');
|
9 | var debug = require('debuglog')('confit');
|
10 | var env = require('./lib/env');
|
11 | var util = require('./lib/util');
|
12 |
|
13 |
|
14 | /**
|
15 | * Initializes environment convenience props in the provided nconf provider.
|
16 | * @param config an nconf Provider.
|
17 | * @returns {Object} the newly configured nconf Provider.
|
18 | */
|
19 | function environment(nodeEnv) {
|
20 | var data = {};
|
21 |
|
22 | debug('NODE_ENV set to \'%s\'', nodeEnv);
|
23 |
|
24 | // Normalize env and set convenience values.
|
25 | Object.keys(env).forEach(function (current) {
|
26 | var match;
|
27 |
|
28 | match = env[current].test(nodeEnv);
|
29 | if (match) { nodeEnv = current; }
|
30 |
|
31 | data[current] = match;
|
32 | });
|
33 |
|
34 | debug('env:env set to \'%s\'', nodeEnv);
|
35 |
|
36 | // Set (or re-set) env:{nodeEnv} value in case
|
37 | // NODE_ENV was not one of our predetermined env
|
38 | // keys (so `config.get('env:blah')` will be true).
|
39 | data[nodeEnv] = true;
|
40 | data.env = nodeEnv;
|
41 | return { env: data };
|
42 | }
|
43 |
|
44 |
|
45 | /**
|
46 | * Creates a local nconf provider instance. NO GLOBAL!
|
47 | * @returns {Object} an nconf provider
|
48 | */
|
49 | function provider() {
|
50 | var config;
|
51 |
|
52 | config = new nconf.Provider();
|
53 | config.add('argv');
|
54 | config.add('env');
|
55 |
|
56 | // Put override before memory to ensure env
|
57 | // values are immutable.
|
58 | config.overrides({
|
59 | type: 'literal',
|
60 | store: environment(config.get('NODE_ENV') || 'development')
|
61 | });
|
62 |
|
63 | config.add('memory');
|
64 |
|
65 | return config;
|
66 | }
|
67 |
|
68 |
|
69 | /**
|
70 | * Creates a file loader that uses the provided `basedir`.
|
71 | * @param basedir the root directory against which file paths will be resolved.
|
72 | * @returns {Function} the file loader implementation.
|
73 | */
|
74 | function loader(basedir) {
|
75 |
|
76 | return function load(file) {
|
77 | var name, config;
|
78 |
|
79 | name = path.basename(file, path.extname(file));
|
80 | config = path.join(basedir, file);
|
81 |
|
82 | return {
|
83 | name: name,
|
84 | data: shush(config)
|
85 | };
|
86 | };
|
87 |
|
88 | }
|
89 |
|
90 |
|
91 | /**
|
92 | * Wraps the provided nconf Provider in a simpler convenience API.
|
93 | * @param config an nconf Provider.
|
94 | */
|
95 | function wrap(config) {
|
96 | return {
|
97 |
|
98 | get: function get(key) {
|
99 | return config.get(key);
|
100 | },
|
101 |
|
102 | set: function set(key, value) {
|
103 | // NOTE: There was discussion around potentially warning
|
104 | // on attempts to set immutable values. The would require
|
105 | // a minimum of one additional operation, which was deemed
|
106 | // overkill for a small/unlikely scenrio. Can revisit.
|
107 | config.set(key, value);
|
108 | },
|
109 |
|
110 | use: function use(obj) {
|
111 | // Merge into memory store.
|
112 | // This must be done b/c nconf applies things kind of backward.
|
113 | // If we just used a literal store it would get added to the END
|
114 | // so no values would be overridden. Additionally, only the memory
|
115 | // store is writable at this point so all updates live there.
|
116 | config.merge(obj);
|
117 | }
|
118 |
|
119 | };
|
120 | }
|
121 |
|
122 |
|
123 | /**
|
124 | * Main module entrypoint. Creates a confit config object using the provided
|
125 | * options.
|
126 | * @param options the configuration settings for this config instance.
|
127 | * @param callback the function to which error or config object will be passed.
|
128 | */
|
129 | module.exports = function confit(options, callback) {
|
130 | var shorty, config, tasks, load;
|
131 |
|
132 | // Normalize arguments
|
133 | if (thing.isFunction(options)) {
|
134 | callback = options;
|
135 | options = undefined;
|
136 | }
|
137 |
|
138 | // ... still normalizing
|
139 | if (thing.isString(options)) {
|
140 | options = { basedir: options };
|
141 | }
|
142 |
|
143 | // ¯\_(ツ)_/¯ ... still normalizing
|
144 | options = options || {};
|
145 | options.defaults = options.defaults || 'config.json';
|
146 | options.basedir = options.basedir || path.dirname(caller());
|
147 |
|
148 |
|
149 | // Configure shortstop using provided protocols
|
150 | shorty = shortstop.create();
|
151 | if (thing.isObject(options.protocols)) {
|
152 | Object.keys(options.protocols).forEach(function (protocol) {
|
153 | shorty.use(protocol, options.protocols[protocol]);
|
154 | });
|
155 | }
|
156 |
|
157 | // Create config provider and initialize basedir
|
158 | // TODO: Add basedir to overrides so it's readonly?
|
159 | config = provider();
|
160 | config.set('basedir', options.basedir);
|
161 |
|
162 |
|
163 | tasks = [];
|
164 | load = loader(options.basedir);
|
165 |
|
166 |
|
167 | // Load the env-specific config file as a literal
|
168 | // datastore. Can't use `file` b/c we preprocess it.
|
169 | tasks.push(function (done) {
|
170 | var file = load(config.get('env:env') + '.json');
|
171 | config.use(file.name, {
|
172 | type: 'literal',
|
173 | store: shorty.resolve(file.data)
|
174 | });
|
175 | done();
|
176 | });
|
177 |
|
178 |
|
179 | // Set defaults from `defaults` file.
|
180 | tasks.push(function (done) {
|
181 | var file = load(options.defaults);
|
182 | config.defaults(shorty.resolve(file.data));
|
183 | done();
|
184 | });
|
185 |
|
186 |
|
187 | util.each(tasks, function (err) {
|
188 | // XXX: Force async until shortstop@1.0 is integrated.
|
189 |
|
190 | // Only report unusual errors. MODULE_NOT_FOUND is an
|
191 | // acceptable scenario b/c no files are truly requried.
|
192 | if (thing.isObject(err) && err.code !== 'MODULE_NOT_FOUND') {
|
193 | setImmediate(callback.bind(null, err));
|
194 | return;
|
195 | }
|
196 |
|
197 | config = wrap(config);
|
198 | setImmediate(callback.bind(null, null, config));
|
199 | });
|
200 |
|
201 | };
|