UNPKG

11.4 kBJavaScriptView Raw
1
2var CC = require('config-chain').ConfigChain
3var inherits = require('inherits')
4var configDefs = require('./config-defs.js')
5var types = configDefs.types
6var once = require('once')
7var fs = require('fs')
8var path = require('path')
9var nopt = require('nopt')
10var ini = require('ini')
11var Octal = configDefs.Octal
12var mkdirp = require('mkdirp')
13var path = require('path')
14
15exports.load = load
16exports.Conf = Conf
17exports.loaded = false
18exports.rootConf = null
19exports.usingBuiltin = false
20exports.defs = configDefs
21
22Object.defineProperty(exports, 'defaults', { get: function () {
23 return configDefs.defaults
24}, enumerable: true })
25
26Object.defineProperty(exports, 'types', { get: function () {
27 return configDefs.types
28}, enumerable: true })
29
30exports.validate = validate
31
32var myUid = process.env.SUDO_UID !== undefined
33 ? process.env.SUDO_UID : (process.getuid && process.getuid())
34var myGid = process.env.SUDO_GID !== undefined
35 ? process.env.SUDO_GID : (process.getgid && process.getgid())
36
37
38var loading = false
39var loadCbs = []
40function load (cli_, builtin_, cb_) {
41 var cli, builtin, cb
42 for (var i = 0; i < arguments.length; i++)
43 switch (typeof arguments[i]) {
44 case 'string': builtin = arguments[i]; break
45 case 'object': cli = arguments[i]; break
46 case 'function': cb = arguments[i]; break
47 }
48
49 if (!cb)
50 cb = function () {}
51
52 if (exports.loaded) {
53 var ret = exports.loaded
54 if (cli) {
55 ret = new Conf(ret)
56 ret.unshift(cli)
57 }
58 return process.nextTick(cb.bind(null, null, ret))
59 }
60
61 // either a fresh object, or a clone of the passed in obj
62 if (!cli)
63 cli = {}
64 else
65 cli = Object.keys(cli).reduce(function (c, k) {
66 c[k] = cli[k]
67 return c
68 }, {})
69
70 loadCbs.push(cb)
71 if (loading)
72 return
73
74 loading = true
75
76 cb = once(function (er, conf) {
77 if (!er)
78 exports.loaded = conf
79 loadCbs.forEach(function (fn) {
80 fn(er, conf)
81 })
82 loadCbs.length = 0
83 })
84
85 // check for a builtin if provided.
86 exports.usingBuiltin = !!builtin
87 var rc = exports.rootConf = new Conf()
88 if (builtin)
89 rc.addFile(builtin, 'builtin')
90 else
91 rc.add({}, 'builtin')
92
93 rc.on('load', function () {
94 load_(builtin, rc, cli, cb)
95 })
96}
97
98function load_(builtin, rc, cli, cb) {
99 var defaults = configDefs.defaults
100 var conf = new Conf(rc)
101
102 conf.usingBuiltin = !!builtin
103 conf.add(cli, 'cli')
104 conf.addEnv()
105
106 conf.loadPrefix(function(er) {
107 if (er)
108 return cb(er)
109
110 // If you're doing `npm --userconfig=~/foo.npmrc` then you'd expect
111 // that ~/.npmrc won't override the stuff in ~/foo.npmrc (or, indeed
112 // be used at all).
113 //
114 // However, if the cwd is ~, then ~/.npmrc is the home for the project
115 // config, and will override the userconfig.
116 //
117 // If you're not setting the userconfig explicitly, then it will be loaded
118 // twice, which is harmless but excessive. If you *are* setting the
119 // userconfig explicitly then it will override your explicit intent, and
120 // that IS harmful and unexpected.
121 //
122 // Solution: Do not load project config file that is the same as either
123 // the default or resolved userconfig value. npm will log a "verbose"
124 // message about this when it happens, but it is a rare enough edge case
125 // that we don't have to be super concerned about it.
126 var projectConf = path.resolve(conf.localPrefix, '.npmrc')
127 var defaultUserConfig = rc.get('userconfig')
128 var resolvedUserConfig = conf.get('userconfig')
129 if (!conf.get('global') &&
130 projectConf !== defaultUserConfig &&
131 projectConf !== resolvedUserConfig) {
132 conf.addFile(projectConf, 'project')
133 conf.once('load', afterPrefix)
134 } else {
135 conf.add({}, 'project')
136 afterPrefix()
137 }
138 })
139
140 function afterPrefix() {
141 conf.addFile(conf.get('userconfig'), 'user')
142 conf.once('error', cb)
143 conf.once('load', afterUser)
144 }
145
146 function afterUser () {
147 // globalconfig and globalignorefile defaults
148 // need to respond to the 'prefix' setting up to this point.
149 // Eg, `npm config get globalconfig --prefix ~/local` should
150 // return `~/local/etc/npmrc`
151 // annoying humans and their expectations!
152 if (conf.get('prefix')) {
153 var etc = path.resolve(conf.get('prefix'), 'etc')
154 defaults.globalconfig = path.resolve(etc, 'npmrc')
155 defaults.globalignorefile = path.resolve(etc, 'npmignore')
156 }
157
158 conf.addFile(conf.get('globalconfig'), 'global')
159
160 // move the builtin into the conf stack now.
161 conf.root = defaults
162 conf.add(rc.shift(), 'builtin')
163 conf.once('load', function () {
164 conf.loadExtras(afterExtras)
165 })
166 }
167
168 function afterExtras(er) {
169 if (er)
170 return cb(er)
171
172 // warn about invalid bits.
173 validate(conf)
174
175 var cafile = conf.get('cafile')
176
177 if (cafile) {
178 return conf.loadCAFile(cafile, finalize)
179 }
180
181 finalize()
182 }
183
184 function finalize(er, cadata) {
185 if (er) {
186 return cb(er)
187 }
188
189 exports.loaded = conf
190 cb(er, conf)
191 }
192}
193
194// Basically the same as CC, but:
195// 1. Always ini
196// 2. Parses environment variable names in field values
197// 3. Field values that start with ~/ are replaced with process.env.HOME
198// 4. Can inherit from another Conf object, using it as the base.
199inherits(Conf, CC)
200function Conf (base) {
201 if (!(this instanceof Conf))
202 return new Conf(base)
203
204 CC.apply(this)
205
206 if (base)
207 if (base instanceof Conf)
208 this.root = base.list[0] || base.root
209 else
210 this.root = base
211 else
212 this.root = configDefs.defaults
213}
214
215Conf.prototype.loadPrefix = require('./lib/load-prefix.js')
216Conf.prototype.loadCAFile = require('./lib/load-cafile.js')
217Conf.prototype.loadUid = require('./lib/load-uid.js')
218Conf.prototype.setUser = require('./lib/set-user.js')
219Conf.prototype.findPrefix = require('./lib/find-prefix.js')
220
221Conf.prototype.loadExtras = function(cb) {
222 this.setUser(function(er) {
223 if (er)
224 return cb(er)
225 this.loadUid(function(er) {
226 if (er)
227 return cb(er)
228 // Without prefix, nothing will ever work
229 mkdirp(this.prefix, cb)
230 }.bind(this))
231 }.bind(this))
232}
233
234Conf.prototype.save = function (where, cb) {
235 var target = this.sources[where]
236 if (!target || !(target.path || target.source) || !target.data) {
237 if (where !== 'builtin')
238 var er = new Error('bad save target: '+where)
239 if (cb) {
240 process.nextTick(cb.bind(null, er))
241 return this
242 }
243 return this.emit('error', er)
244 }
245
246 if (target.source) {
247 var pref = target.prefix || ''
248 Object.keys(target.data).forEach(function (k) {
249 target.source[pref + k] = target.data[k]
250 })
251 if (cb) process.nextTick(cb)
252 return this
253 }
254
255 var data = target.data
256
257 if (typeof data._password === 'string' &&
258 typeof data.username === 'string') {
259 var auth = data.username + ':' + data._password
260 data = Object.keys(data).reduce(function (c, k) {
261 if (k === 'username' || k === '_password')
262 return c
263 c[k] = data[k]
264 return c
265 }, { _auth: new Buffer(auth, 'utf8').toString('base64') })
266 delete data.username
267 delete data._password
268 }
269
270 data = ini.stringify(data)
271
272 then = then.bind(this)
273 done = done.bind(this)
274 this._saving ++
275
276 var mode = where === 'user' ? 0600 : 0666
277 if (!data.trim()) {
278 fs.unlink(target.path, function (er) {
279 // ignore the possible error (e.g. the file doesn't exist)
280 done(null)
281 })
282 } else {
283 mkdirp(path.dirname(target.path), function (er) {
284 if (er)
285 return then(er)
286 fs.writeFile(target.path, data, 'utf8', function (er) {
287 if (er)
288 return then(er)
289 if (where === 'user' && myUid && myGid)
290 fs.chown(target.path, +myUid, +myGid, then)
291 else
292 then()
293 })
294 })
295 }
296
297 function then (er) {
298 if (er)
299 return done(er)
300 fs.chmod(target.path, mode, done)
301 }
302
303 function done (er) {
304 if (er) {
305 if (cb) return cb(er)
306 else return this.emit('error', er)
307 }
308 this._saving --
309 if (this._saving === 0) {
310 if (cb) cb()
311 this.emit('save')
312 }
313 }
314
315 return this
316}
317
318Conf.prototype.addFile = function (file, name) {
319 name = name || file
320 var marker = {__source__:name}
321 this.sources[name] = { path: file, type: 'ini' }
322 this.push(marker)
323 this._await()
324 fs.readFile(file, 'utf8', function (er, data) {
325 if (er) // just ignore missing files.
326 return this.add({}, marker)
327 this.addString(data, file, 'ini', marker)
328 }.bind(this))
329 return this
330}
331
332// always ini files.
333Conf.prototype.parse = function (content, file) {
334 return CC.prototype.parse.call(this, content, file, 'ini')
335}
336
337Conf.prototype.add = function (data, marker) {
338 Object.keys(data).forEach(function (k) {
339 data[k] = parseField(data[k], k)
340 })
341 if (Object.prototype.hasOwnProperty.call(data, '_auth')) {
342 var auth = new Buffer(data._auth, 'base64').toString('utf8').split(':')
343 var username = auth.shift()
344 var password = auth.join(':')
345 data.username = username
346 data._password = password
347 }
348 return CC.prototype.add.call(this, data, marker)
349}
350
351Conf.prototype.addEnv = function (env) {
352 env = env || process.env
353 var conf = {}
354 Object.keys(env)
355 .filter(function (k) { return k.match(/^npm_config_/i) })
356 .forEach(function (k) {
357 if (!env[k])
358 return
359
360 // leave first char untouched, even if
361 // it is a "_" - convert all other to "-"
362 var p = k.toLowerCase()
363 .replace(/^npm_config_/, '')
364 .replace(/(?!^)_/g, '-')
365 conf[p] = env[k]
366 })
367 return CC.prototype.addEnv.call(this, '', conf, 'env')
368}
369
370function parseField (f, k, emptyIsFalse) {
371 if (typeof f !== 'string' && !(f instanceof String))
372 return f
373
374 // type can be an array or single thing.
375 var typeList = [].concat(types[k])
376 var isPath = -1 !== typeList.indexOf(path)
377 var isBool = -1 !== typeList.indexOf(Boolean)
378 var isString = -1 !== typeList.indexOf(String)
379 var isOctal = -1 !== typeList.indexOf(Octal)
380 var isNumber = isOctal || (-1 !== typeList.indexOf(Number))
381
382 f = (''+f).trim()
383
384 if (f.match(/^".*"$/))
385 f = JSON.parse(f)
386
387 if (isBool && !isString && f === '')
388 return true
389
390 switch (f) {
391 case 'true': return true
392 case 'false': return false
393 case 'null': return null
394 case 'undefined': return undefined
395 }
396
397 f = envReplace(f)
398
399 if (isPath) {
400 var homePattern = process.platform === 'win32' ? /^~(\/|\\)/ : /^~\//
401 if (f.match(homePattern) && process.env.HOME) {
402 f = path.resolve(process.env.HOME, f.substr(2))
403 }
404 f = path.resolve(f)
405 }
406
407 if (isNumber && !isNaN(f))
408 f = isOctal ? parseInt(f, 8) : +f
409
410 return f
411}
412
413function envReplace (f) {
414 if (typeof f !== 'string' || !f) return f
415
416 // replace any ${ENV} values with the appropriate environ.
417 var envExpr = /(\\*)\$\{([^}]+)\}/g
418 return f.replace(envExpr, function (orig, esc, name, i, s) {
419 esc = esc.length && esc.length % 2
420 if (esc)
421 return orig
422 if (undefined === process.env[name])
423 throw new Error('Failed to replace env in config: '+orig)
424 return process.env[name]
425 })
426}
427
428function validate (cl) {
429 // warn about invalid configs at every level.
430 cl.list.forEach(function (conf, level) {
431 nopt.clean(conf, configDefs.types)
432 })
433
434 nopt.clean(cl.root, configDefs.types)
435}