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