core.js 11 KB

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