var CC = require('config-chain').ConfigChain var inherits = require('inherits') var configDefs = require('./config-defs.js') var types = configDefs.types var once = require('once') var fs = require('fs') var path = require('path') var nopt = require('nopt') var ini = require('ini') var Octal = configDefs.Octal var mkdirp = require('mkdirp') exports.load = load exports.Conf = Conf exports.loaded = false exports.rootConf = null exports.usingBuiltin = false exports.defs = configDefs Object.defineProperty(exports, 'defaults', { get: function () { return configDefs.defaults }, enumerable: true }) Object.defineProperty(exports, 'types', { get: function () { return configDefs.types }, enumerable: true }) exports.validate = validate var myUid = process.env.SUDO_UID !== undefined ? process.env.SUDO_UID : (process.getuid && process.getuid()) var myGid = process.env.SUDO_GID !== undefined ? process.env.SUDO_GID : (process.getgid && process.getgid()) var loading = false var loadCbs = [] function load (cli_, builtin_, cb_) { var cli, builtin, cb for (var i = 0; i < arguments.length; i++) switch (typeof arguments[i]) { case 'string': builtin = arguments[i]; break case 'object': cli = arguments[i]; break case 'function': cb = arguments[i]; break } if (!cb) cb = function () {} if (exports.loaded) { var ret = exports.loaded if (cli) { ret = new Conf(ret) ret.unshift(cli) } return process.nextTick(cb.bind(null, null, ret)) } // either a fresh object, or a clone of the passed in obj if (!cli) cli = {} else cli = Object.keys(cli).reduce(function (c, k) { c[k] = cli[k] return c }, {}) loadCbs.push(cb) if (loading) return loading = true cb = once(function (er, conf) { if (!er) exports.loaded = conf loadCbs.forEach(function (fn) { fn(er, conf) }) loadCbs.length = 0 }) // check for a builtin if provided. exports.usingBuiltin = !!builtin var rc = exports.rootConf = new Conf() var defaults = configDefs.defaults if (builtin) rc.addFile(builtin, 'builtin') else rc.add({}, 'builtin') rc.on('load', function () { var conf = new Conf(rc) conf.usingBuiltin = !!builtin conf.add(cli, 'cli') conf.addEnv() conf.addFile(conf.get('userconfig'), 'user') conf.once('error', cb) conf.once('load', function () { // globalconfig and globalignorefile defaults // need to respond to the "prefix" setting up to this point. // Eg, `npm config get globalconfig --prefix ~/local` should // return `~/local/etc/npmrc` // annoying humans and their expectations! if (conf.get('prefix')) { var etc = path.resolve(conf.get("prefix"), "etc") defaults.globalconfig = path.resolve(etc, "npmrc") defaults.globalignorefile = path.resolve(etc, "npmignore") } conf.addFile(conf.get('globalconfig'), 'global') // move the builtin into the conf stack now. conf.root = defaults conf.add(rc.shift(), 'builtin') conf.once('load', function () { // warn about invalid bits. validate(conf) exports.loaded = conf cb(null, conf) }) }) }) } // Basically the same as CC, but: // 1. Always ini // 2. Parses environment variable names in field values // 3. Field values that start with ~/ are replaced with process.env.HOME // 4. Can inherit from another Conf object, using it as the base. inherits(Conf, CC) function Conf (base) { if (!(this instanceof Conf)) return new Conf(base) CC.apply(this) if (base) if (base instanceof Conf) this.root = base.list[0] || base.root else this.root = base else this.root = configDefs.defaults } Conf.prototype.save = function (where, cb) { var target = this.sources[where] if (!target || !(target.path || target.source) || !target.data) { if (where !== 'builtin') var er = new Error('bad save target: '+where) if (cb) { process.nextTick(cb.bind(null, er)) return this } return this.emit('error', er) } if (target.source) { var pref = target.prefix || '' Object.keys(target.data).forEach(function (k) { target.source[pref + k] = target.data[k] }) if (cb) process.nextTick(cb) return this } var data = target.data if (typeof data._password === 'string' && typeof data.username === 'string') { var auth = data.username + ':' + data._password data = Object.keys(data).reduce(function (c, k) { if (k === 'username' || k === '_password') return c c[k] = data[k] return c }, { _auth: new Buffer(auth, 'utf8').toString('base64') }) delete data.username delete data._password } data = ini.stringify(data) then = then.bind(this) done = done.bind(this) this._saving ++ var mode = where === 'user' ? 0600 : 0666 if (!data.trim()) fs.unlink(target.path, done) else { mkdirp(path.dirname(target.path), function (er) { if (er) return then(er) fs.writeFile(target.path, data, 'utf8', function (er) { if (er) return then(er) if (where === 'user' && myUid && myGid) fs.chown(target.path, +myUid, +myGid, then) else then() }) }) } function then (er) { if (er) return done(er) fs.chmod(target.path, mode, done) } function done (er) { if (er) { if (cb) return cb(er) else return this.emit('error', er) } this._saving -- if (this._saving === 0) { if (cb) cb() this.emit('save') } } return this } Conf.prototype.addFile = function (file, name) { name = name || file var marker = {__source__:name} this.sources[name] = { path: file, type: 'ini' } this.push(marker) this._await() fs.readFile(file, 'utf8', function (er, data) { if (er) // just ignore missing files. return this.add({}, marker) this.addString(data, file, 'ini', marker) }.bind(this)) return this } // always ini files. Conf.prototype.parse = function (content, file) { return CC.prototype.parse.call(this, content, file, 'ini') } Conf.prototype.add = function (data, marker) { Object.keys(data).forEach(function (k) { data[k] = parseField(data[k], k) }) if (Object.prototype.hasOwnProperty.call(data, '_auth')) { var auth = new Buffer(data._auth, 'base64').toString('utf8').split(':') var username = auth.shift() var password = auth.join(':') data.username = username data._password = password } return CC.prototype.add.call(this, data, marker) } Conf.prototype.addEnv = function (env) { env = env || process.env var conf = {} Object.keys(env) .filter(function (k) { return k.match(/^npm_config_[^_]/i) }) .forEach(function (k) { if (!env[k]) return conf[k.replace(/^npm_config_/i, '') .toLowerCase() .replace(/_/g, '-')] = env[k] }) return CC.prototype.addEnv.call(this, '', conf, 'env') } function parseField (f, k, emptyIsFalse) { if (typeof f !== 'string' && !(f instanceof String)) return f // type can be an array or single thing. var typeList = [].concat(types[k]) var isPath = -1 !== typeList.indexOf(path) var isBool = -1 !== typeList.indexOf(Boolean) var isString = -1 !== typeList.indexOf(String) var isOctal = -1 !== typeList.indexOf(Octal) var isNumber = isOctal || (-1 !== typeList.indexOf(Number)) f = (''+f).trim() if (f.match(/^".*"$/)) f = JSON.parse(f) if (isBool && !isString && f === '') return true switch (f) { case 'true': return true case 'false': return false case 'null': return null case 'undefined': return undefined } f = envReplace(f) if (isPath) { var homePattern = process.platform === 'win32' ? /^~(\/|\\)/ : /^~\// if (f.match(homePattern) && process.env.HOME) { f = path.resolve(process.env.HOME, f.substr(2)) } f = path.resolve(f) } if (isNumber && !isNaN(f)) f = isOctal ? parseInt(f, 8) : +f return f } function envReplace (f) { if (typeof f !== "string" || !f) return f // replace any ${ENV} values with the appropriate environ. var envExpr = /(\\*)\$\{([^}]+)\}/g return f.replace(envExpr, function (orig, esc, name, i, s) { esc = esc.length && esc.length % 2 if (esc) return orig if (undefined === process.env[name]) throw new Error("Failed to replace env in config: "+orig) return process.env[name] }) } function validate (cl) { // warn about invalid configs at every level. cl.list.forEach(function (conf, level) { nopt.clean(conf, configDefs.types) }) }