deps/npm/lib/config/core.js

Summary

Maintainability
D
2 days
Test Coverage

var CC = require("config-chain").ConfigChain
var inherits = require("inherits")
var configDefs = require("./defaults.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 Umask = configDefs.Umask
var mkdirp = require("mkdirp")
var umask = require("../utils/umask")

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 () {
  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
      loading = false
    }
    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()
  if (builtin)
    rc.addFile(builtin, "builtin")
  else
    rc.add({}, "builtin")

  rc.on("load", function () {
    load_(builtin, rc, cli, cb)
  })
  rc.on("error", cb)
}

function load_(builtin, rc, cli, cb) {
  var defaults = configDefs.defaults
  var conf = new Conf(rc)

  conf.usingBuiltin = !!builtin
  conf.add(cli, "cli")
  conf.addEnv()

  conf.loadPrefix(function(er) {
    if (er)
      return cb(er)

    // If you're doing `npm --userconfig=~/foo.npmrc` then you'd expect
    // that ~/.npmrc won't override the stuff in ~/foo.npmrc (or, indeed
    // be used at all).
    //
    // However, if the cwd is ~, then ~/.npmrc is the home for the project
    // config, and will override the userconfig.
    //
    // If you're not setting the userconfig explicitly, then it will be loaded
    // twice, which is harmless but excessive.  If you *are* setting the
    // userconfig explicitly then it will override your explicit intent, and
    // that IS harmful and unexpected.
    //
    // Solution: Do not load project config file that is the same as either
    // the default or resolved userconfig value.  npm will log a "verbose"
    // message about this when it happens, but it is a rare enough edge case
    // that we don't have to be super concerned about it.
    var projectConf = path.resolve(conf.localPrefix, ".npmrc")
    var defaultUserConfig = rc.get("userconfig")
    var resolvedUserConfig = conf.get("userconfig")
    if (!conf.get("global") &&
        projectConf !== defaultUserConfig &&
        projectConf !== resolvedUserConfig) {
      conf.addFile(projectConf, "project")
      conf.once("load", afterPrefix)
    } else {
      conf.add({}, "project")
      afterPrefix()
    }
  })

  function afterPrefix() {
    conf.addFile(conf.get("userconfig"), "user")
    conf.once("error", cb)
    conf.once("load", afterUser)
  }

  function afterUser () {
    // 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")
      mkdirp(etc, function (err) {
        defaults.globalconfig = path.resolve(etc, "npmrc")
        defaults.globalignorefile = path.resolve(etc, "npmignore")
        afterUserContinuation()
      })
    } else {
      afterUserContinuation()
    }
  }

  function afterUserContinuation() {
    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 () {
      conf.loadExtras(afterExtras)
    })
  }

  function afterExtras(er) {
    if (er)
      return cb(er)

    // warn about invalid bits.
    validate(conf)

    var cafile = conf.get("cafile")

    if (cafile) {
      return conf.loadCAFile(cafile, finalize)
    }

    finalize()
  }

  function finalize(er) {
    if (er) {
      return cb(er)
    }

    exports.loaded = conf
    cb(er, 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.loadPrefix = require("./load-prefix.js")
Conf.prototype.loadCAFile = require("./load-cafile.js")
Conf.prototype.loadUid = require("./load-uid.js")
Conf.prototype.setUser = require("./set-user.js")
Conf.prototype.findPrefix = require("./find-prefix.js")
Conf.prototype.getCredentialsByURI = require("./get-credentials-by-uri.js")
Conf.prototype.setCredentialsByURI = require("./set-credentials-by-uri.js")
Conf.prototype.clearCredentialsByURI = require("./clear-credentials-by-uri.js")

Conf.prototype.loadExtras = function(cb) {
  this.setUser(function(er) {
    if (er)
      return cb(er)
    this.loadUid(function(er) {
      if (er)
        return cb(er)
      // Without prefix, nothing will ever work
      mkdirp(this.prefix, cb)
    }.bind(this))
  }.bind(this))
}

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 = ini.stringify(target.data)

  then = then.bind(this)
  done = done.bind(this)
  this._saving ++

  var mode = where === "user" ? "0600" : "0666"
  if (!data.trim()) {
    fs.unlink(target.path, function () {
      // ignore the possible error (e.g. the file doesn't exist)
      done(null)
    })
  } 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) {
  try {
    Object.keys(data).forEach(function (k) {
      data[k] = parseField(data[k], k)
    })
  }
  catch (e) {
    this.emit("error", e)
    return this
  }
  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

      // leave first char untouched, even if
      // it is a "_" - convert all other to "-"
      var p = k.toLowerCase()
               .replace(/^npm_config_/, "")
               .replace(/(?!^)_/g, "-")
      conf[p] = env[k]
    })
  return CC.prototype.addEnv.call(this, "", conf, "env")
}

function parseField (f, k) {
  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 isUmask = -1 !== typeList.indexOf(Umask)
  var isNumber = -1 !== typeList.indexOf(Number)

  f = (""+f).trim()

  if (f.match(/^".*"$/)) {
    try {
      f = JSON.parse(f)
    }
    catch (e) {
      throw new Error("Failed parsing JSON config key " + k + ": " + 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 (isUmask)
    f = umask.fromString(f)

  if (isNumber && !isNaN(f))
    f = +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) {
    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) {
    nopt.clean(conf, configDefs.types)
  })

  nopt.clean(cl.root, configDefs.types)
}