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