UNPKG

35.6 kBJavaScriptView Raw
1'use strict'
2/* eslint-disable camelcase */
3/* eslint-disable standard/no-callback-literal */
4// npm install <pkg> <pkg> <pkg>
5//
6// See doc/cli/npm-install.md for more description
7//
8// Managing contexts...
9// there's a lot of state associated with an "install" operation, including
10// packages that are already installed, parent packages, current shrinkwrap, and
11// so on. We maintain this state in a "context" object that gets passed around.
12// every time we dive into a deeper node_modules folder, the "family" list that
13// gets passed along uses the previous "family" list as its __proto__. Any
14// "resolved precise dependency" things that aren't already on this object get
15// added, and then that's passed to the next generation of installation.
16
17module.exports = install
18module.exports.Installer = Installer
19
20var usage = require('./utils/usage')
21
22install.usage = usage(
23 'install',
24 '\nnpm install (with no args, in package dir)' +
25 '\nnpm install [<@scope>/]<pkg>' +
26 '\nnpm install [<@scope>/]<pkg>@<tag>' +
27 '\nnpm install [<@scope>/]<pkg>@<version>' +
28 '\nnpm install [<@scope>/]<pkg>@<version range>' +
29 '\nnpm install <folder>' +
30 '\nnpm install <tarball file>' +
31 '\nnpm install <tarball url>' +
32 '\nnpm install <git:// url>' +
33 '\nnpm install <github username>/<github project>',
34 '[--save-prod|--save-dev|--save-optional] [--save-exact] [--no-save]'
35)
36
37install.completion = function (opts, cb) {
38 validate('OF', arguments)
39 // install can complete to a folder with a package.json, or any package.
40 // if it has a slash, then it's gotta be a folder
41 // if it starts with https?://, then just give up, because it's a url
42 if (/^https?:\/\//.test(opts.partialWord)) {
43 // do not complete to URLs
44 return cb(null, [])
45 }
46
47 if (/\//.test(opts.partialWord)) {
48 // Complete fully to folder if there is exactly one match and it
49 // is a folder containing a package.json file. If that is not the
50 // case we return 0 matches, which will trigger the default bash
51 // complete.
52 var lastSlashIdx = opts.partialWord.lastIndexOf('/')
53 var partialName = opts.partialWord.slice(lastSlashIdx + 1)
54 var partialPath = opts.partialWord.slice(0, lastSlashIdx)
55 if (partialPath === '') partialPath = '/'
56
57 var annotatePackageDirMatch = function (sibling, cb) {
58 var fullPath = path.join(partialPath, sibling)
59 if (sibling.slice(0, partialName.length) !== partialName) {
60 return cb(null, null) // not name match
61 }
62 fs.readdir(fullPath, function (err, contents) {
63 if (err) return cb(null, { isPackage: false })
64
65 cb(
66 null,
67 {
68 fullPath: fullPath,
69 isPackage: contents.indexOf('package.json') !== -1
70 }
71 )
72 })
73 }
74
75 return fs.readdir(partialPath, function (err, siblings) {
76 if (err) return cb(null, []) // invalid dir: no matching
77
78 asyncMap(siblings, annotatePackageDirMatch, function (err, matches) {
79 if (err) return cb(err)
80
81 var cleaned = matches.filter(function (x) { return x !== null })
82 if (cleaned.length !== 1) return cb(null, [])
83 if (!cleaned[0].isPackage) return cb(null, [])
84
85 // Success - only one match and it is a package dir
86 return cb(null, [cleaned[0].fullPath])
87 })
88 })
89 }
90
91 // FIXME: there used to be registry completion here, but it stopped making
92 // sense somewhere around 50,000 packages on the registry
93 cb()
94}
95
96// system packages
97var fs = require('fs')
98var path = require('path')
99
100// dependencies
101var log = require('npmlog')
102var readPackageTree = require('read-package-tree')
103var readPackageJson = require('read-package-json')
104var chain = require('slide').chain
105var asyncMap = require('slide').asyncMap
106var archy = require('archy')
107var mkdirp = require('mkdirp')
108var rimraf = require('rimraf')
109var iferr = require('iferr')
110var validate = require('aproba')
111var uniq = require('lodash.uniq')
112var Bluebird = require('bluebird')
113
114// npm internal utils
115var npm = require('./npm.js')
116var locker = require('./utils/locker.js')
117var lock = locker.lock
118var unlock = locker.unlock
119var parseJSON = require('./utils/parse-json.js')
120var output = require('./utils/output.js')
121var saveMetrics = require('./utils/metrics.js').save
122
123// install specific libraries
124var copyTree = require('./install/copy-tree.js')
125var readShrinkwrap = require('./install/read-shrinkwrap.js')
126var computeMetadata = require('./install/deps.js').computeMetadata
127var prefetchDeps = require('./install/deps.js').prefetchDeps
128var loadDeps = require('./install/deps.js').loadDeps
129var loadDevDeps = require('./install/deps.js').loadDevDeps
130var getAllMetadata = require('./install/deps.js').getAllMetadata
131var loadRequestedDeps = require('./install/deps.js').loadRequestedDeps
132var loadExtraneous = require('./install/deps.js').loadExtraneous
133var diffTrees = require('./install/diff-trees.js')
134var checkPermissions = require('./install/check-permissions.js')
135var decomposeActions = require('./install/decompose-actions.js')
136var validateTree = require('./install/validate-tree.js')
137var validateArgs = require('./install/validate-args.js')
138var saveRequested = require('./install/save.js').saveRequested
139var saveShrinkwrap = require('./install/save.js').saveShrinkwrap
140var audit = require('./install/audit.js')
141var getSaveType = require('./install/save.js').getSaveType
142var doSerialActions = require('./install/actions.js').doSerial
143var doReverseSerialActions = require('./install/actions.js').doReverseSerial
144var doParallelActions = require('./install/actions.js').doParallel
145var doOneAction = require('./install/actions.js').doOne
146var removeObsoleteDep = require('./install/deps.js').removeObsoleteDep
147var removeExtraneous = require('./install/deps.js').removeExtraneous
148var computeVersionSpec = require('./install/deps.js').computeVersionSpec
149var packageId = require('./utils/package-id.js')
150var moduleName = require('./utils/module-name.js')
151var errorMessage = require('./utils/error-message.js')
152var isExtraneous = require('./install/is-extraneous.js')
153
154function unlockCB (lockPath, name, cb) {
155 validate('SSF', arguments)
156 return function (installEr) {
157 var args = arguments
158 try {
159 unlock(lockPath, name, reportErrorAndReturn)
160 } catch (unlockEx) {
161 process.nextTick(function () {
162 reportErrorAndReturn(unlockEx)
163 })
164 }
165 function reportErrorAndReturn (unlockEr) {
166 if (installEr) {
167 if (unlockEr && unlockEr.code !== 'ENOTLOCKED') {
168 log.warn('unlock' + name, unlockEr)
169 }
170 return cb.apply(null, args)
171 }
172 if (unlockEr) return cb(unlockEr)
173 return cb.apply(null, args)
174 }
175 }
176}
177
178function install (where, args, cb) {
179 if (!cb) {
180 cb = args
181 args = where
182 where = null
183 }
184 var globalTop = path.resolve(npm.globalDir, '..')
185 if (!where) {
186 where = npm.config.get('global')
187 ? globalTop
188 : npm.prefix
189 }
190 validate('SAF', [where, args, cb])
191 // the /path/to/node_modules/..
192 var dryrun = !!npm.config.get('dry-run')
193
194 if (npm.config.get('dev')) {
195 log.warn('install', 'Usage of the `--dev` option is deprecated. Use `--only=dev` instead.')
196 }
197
198 if (where === globalTop && !args.length) {
199 args = ['.']
200 }
201 args = args.filter(function (a) {
202 return path.resolve(a) !== npm.prefix
203 })
204
205 new Installer(where, dryrun, args).run(cb)
206}
207
208function Installer (where, dryrun, args, opts) {
209 validate('SBA|SBAO', arguments)
210 if (!opts) opts = {}
211 this.where = where
212 this.dryrun = dryrun
213 this.args = args
214 // fakechildren are children created from the lockfile and lack relationship data
215 // the only exist when the tree does not match the lockfile
216 // this is fine when doing full tree installs/updates but not ok when modifying only
217 // a few deps via `npm install` or `npm uninstall`.
218 this.currentTree = null
219 this.idealTree = null
220 this.differences = []
221 this.todo = []
222 this.progress = {}
223 this.noPackageJsonOk = !!args.length
224 this.topLevelLifecycles = !args.length
225
226 this.autoPrune = npm.config.get('package-lock')
227
228 const dev = npm.config.get('dev')
229 const only = npm.config.get('only')
230 const onlyProd = /^prod(uction)?$/.test(only)
231 const onlyDev = /^dev(elopment)?$/.test(only)
232 const prod = npm.config.get('production')
233 this.dev = opts.dev != null ? opts.dev : dev || (!onlyProd && !prod) || onlyDev
234 this.prod = opts.prod != null ? opts.prod : !onlyDev
235
236 this.packageLockOnly = opts.packageLockOnly != null
237 ? opts.packageLockOnly : npm.config.get('package-lock-only')
238 this.rollback = opts.rollback != null ? opts.rollback : npm.config.get('rollback')
239 this.link = opts.link != null ? opts.link : npm.config.get('link')
240 this.saveOnlyLock = opts.saveOnlyLock
241 this.global = opts.global != null ? opts.global : this.where === path.resolve(npm.globalDir, '..')
242 this.audit = npm.config.get('audit') && !this.global
243 this.started = Date.now()
244}
245Installer.prototype = {}
246
247Installer.prototype.run = function (_cb) {
248 validate('F|', arguments)
249
250 var result
251 var cb
252 if (_cb) {
253 cb = function (err) {
254 saveMetrics(!err)
255 return _cb.apply(this, arguments)
256 }
257 } else {
258 result = new Promise((resolve, reject) => {
259 cb = (err, value) => err ? reject(err) : resolve(value)
260 })
261 }
262 // FIXME: This is bad and I should feel bad.
263 // lib/install needs to have some way of sharing _limited_
264 // state with the things it calls. Passing the object is too
265 // much. The global config is WAY too much. =( =(
266 // But not having this is gonna break linked modules in
267 // subtle stupid ways, and refactoring all this code isn't
268 // the right thing to do just yet.
269 if (this.global) {
270 var prevGlobal = npm.config.get('global')
271 npm.config.set('global', true)
272 var next = cb
273 cb = function () {
274 npm.config.set('global', prevGlobal)
275 next.apply(null, arguments)
276 }
277 }
278
279 var installSteps = []
280 var postInstallSteps = []
281 if (!this.dryrun) {
282 installSteps.push(
283 [this.newTracker(log, 'runTopLevelLifecycles', 2)],
284 [this, this.runPreinstallTopLevelLifecycles])
285 }
286 installSteps.push(
287 [this.newTracker(log, 'loadCurrentTree', 4)],
288 [this, this.loadCurrentTree],
289 [this, this.finishTracker, 'loadCurrentTree'],
290
291 [this.newTracker(log, 'loadIdealTree', 12)],
292 [this, this.loadIdealTree],
293 [this, this.finishTracker, 'loadIdealTree'],
294
295 [this, this.debugTree, 'currentTree', 'currentTree'],
296 [this, this.debugTree, 'idealTree', 'idealTree'],
297
298 [this.newTracker(log, 'generateActionsToTake')],
299 [this, this.generateActionsToTake],
300 [this, this.finishTracker, 'generateActionsToTake'],
301
302 [this, this.debugActions, 'diffTrees', 'differences'],
303 [this, this.debugActions, 'decomposeActions', 'todo'],
304 [this, this.startAudit]
305 )
306
307 if (this.packageLockOnly) {
308 postInstallSteps.push(
309 [this, this.saveToDependencies])
310 } else if (!this.dryrun) {
311 installSteps.push(
312 [this.newTracker(log, 'executeActions', 8)],
313 [this, this.executeActions],
314 [this, this.finishTracker, 'executeActions'])
315 var node_modules = path.resolve(this.where, 'node_modules')
316 var staging = path.resolve(node_modules, '.staging')
317 postInstallSteps.push(
318 [this.newTracker(log, 'rollbackFailedOptional', 1)],
319 [this, this.rollbackFailedOptional, staging, this.todo],
320 [this, this.finishTracker, 'rollbackFailedOptional'],
321 [this, this.commit, staging, this.todo],
322
323 [this, this.runPostinstallTopLevelLifecycles],
324 [this, this.finishTracker, 'runTopLevelLifecycles']
325 )
326 if (getSaveType()) {
327 postInstallSteps.push(
328 // this is necessary as we don't fill in `dependencies` and `devDependencies` in deps loaded from shrinkwrap
329 // until after we extract them
330 [this, (next) => { computeMetadata(this.idealTree); next() }],
331 [this, this.pruneIdealTree],
332 [this, this.debugLogicalTree, 'saveTree', 'idealTree'],
333 [this, this.saveToDependencies])
334 }
335 }
336 postInstallSteps.push(
337 [this, this.printWarnings],
338 [this, this.printInstalled])
339
340 var self = this
341 chain(installSteps, function (installEr) {
342 if (installEr) self.failing = true
343 chain(postInstallSteps, function (postInstallEr) {
344 if (installEr && postInstallEr) {
345 var msg = errorMessage(postInstallEr)
346 msg.summary.forEach(function (logline) {
347 log.warn.apply(log, logline)
348 })
349 msg.detail.forEach(function (logline) {
350 log.verbose.apply(log, logline)
351 })
352 }
353 cb(installEr || postInstallEr, self.getInstalledModules(), self.idealTree)
354 })
355 })
356 return result
357}
358
359Installer.prototype.loadArgMetadata = function (next) {
360 getAllMetadata(this.args, this.currentTree, process.cwd(), iferr(next, (args) => {
361 this.args = args
362 next()
363 }))
364}
365
366Installer.prototype.newTracker = function (tracker, name, size) {
367 validate('OS', [tracker, name])
368 if (size) validate('N', [size])
369 this.progress[name] = tracker.newGroup(name, size)
370 return function (next) {
371 process.emit('time', 'stage:' + name)
372 next()
373 }
374}
375
376Installer.prototype.finishTracker = function (name, cb) {
377 validate('SF', arguments)
378 process.emit('timeEnd', 'stage:' + name)
379 cb()
380}
381
382Installer.prototype.loadCurrentTree = function (cb) {
383 validate('F', arguments)
384 log.silly('install', 'loadCurrentTree')
385 var todo = []
386 if (this.global) {
387 todo.push([this, this.readGlobalPackageData])
388 } else {
389 todo.push([this, this.readLocalPackageData])
390 }
391 todo.push([this, this.normalizeCurrentTree])
392 chain(todo, cb)
393}
394
395var createNode = require('./install/node.js').create
396var flatNameFromTree = require('./install/flatten-tree.js').flatNameFromTree
397Installer.prototype.normalizeCurrentTree = function (cb) {
398 this.currentTree.isTop = true
399 normalizeTree(this.currentTree)
400 // If the user didn't have a package.json then fill in deps with what was on disk
401 if (this.currentTree.error) {
402 for (let child of this.currentTree.children) {
403 if (!child.fakeChild && isExtraneous(child)) {
404 this.currentTree.package.dependencies[child.package.name] = computeVersionSpec(this.currentTree, child)
405 }
406 }
407 }
408 computeMetadata(this.currentTree)
409 return cb()
410
411 function normalizeTree (tree, seen) {
412 if (!seen) seen = new Set()
413 if (seen.has(tree)) return
414 seen.add(tree)
415 createNode(tree)
416 tree.location = flatNameFromTree(tree)
417 tree.children.forEach((child) => normalizeTree(child, seen))
418 }
419}
420
421Installer.prototype.loadIdealTree = function (cb) {
422 validate('F', arguments)
423 log.silly('install', 'loadIdealTree')
424
425 chain([
426 [this.newTracker(this.progress.loadIdealTree, 'loadIdealTree:cloneCurrentTree')],
427 [this, this.cloneCurrentTreeToIdealTree],
428 [this, this.finishTracker, 'loadIdealTree:cloneCurrentTree'],
429
430 [this.newTracker(this.progress.loadIdealTree, 'loadIdealTree:loadShrinkwrap')],
431 [this, this.loadShrinkwrap],
432 [this, this.finishTracker, 'loadIdealTree:loadShrinkwrap'],
433
434 [this.newTracker(this.progress.loadIdealTree, 'loadIdealTree:loadAllDepsIntoIdealTree', 10)],
435 [this, this.loadAllDepsIntoIdealTree],
436 [this, this.finishTracker, 'loadIdealTree:loadAllDepsIntoIdealTree'],
437 [this, function (next) { computeMetadata(this.idealTree); next() }],
438 [this, this.pruneIdealTree]
439 ], cb)
440}
441
442Installer.prototype.pruneIdealTree = function (cb) {
443 if (!this.idealTree) return cb()
444 // if our lock file didn't have the requires field and there
445 // are any fake children then forgo pruning until we have more info.
446 if (!this.idealTree.hasRequiresFromLock && this.idealTree.children.some((n) => n.fakeChild)) return cb()
447 const toPrune = this.idealTree.children
448 .filter((child) => isExtraneous(child) && (this.autoPrune || child.removing))
449 .map((n) => ({name: moduleName(n)}))
450 return removeExtraneous(toPrune, this.idealTree, cb)
451}
452
453Installer.prototype.loadAllDepsIntoIdealTree = function (cb) {
454 validate('F', arguments)
455 log.silly('install', 'loadAllDepsIntoIdealTree')
456 var saveDeps = getSaveType()
457
458 var cg = this.progress['loadIdealTree:loadAllDepsIntoIdealTree']
459 var installNewModules = !!this.args.length
460 var steps = []
461
462 if (installNewModules) {
463 steps.push([validateArgs, this.idealTree, this.args])
464 steps.push([loadRequestedDeps, this.args, this.idealTree, saveDeps, cg.newGroup('loadRequestedDeps')])
465 } else {
466 const depsToPreload = Object.assign({},
467 this.idealTree.package.devDependencies,
468 this.idealTree.package.dependencies
469 )
470 steps.push(
471 [prefetchDeps, this.idealTree, depsToPreload, cg.newGroup('prefetchDeps')],
472 [loadDeps, this.idealTree, cg.newGroup('loadDeps')],
473 [loadDevDeps, this.idealTree, cg.newGroup('loadDevDeps')])
474 }
475 steps.push(
476 [loadExtraneous.andResolveDeps, this.idealTree, cg.newGroup('loadExtraneous')])
477 chain(steps, cb)
478}
479
480Installer.prototype.generateActionsToTake = function (cb) {
481 validate('F', arguments)
482 log.silly('install', 'generateActionsToTake')
483 var cg = this.progress.generateActionsToTake
484 chain([
485 [validateTree, this.idealTree, cg.newGroup('validateTree')],
486 [diffTrees, this.currentTree, this.idealTree, this.differences, cg.newGroup('diffTrees')],
487 [this, this.computeLinked],
488 [checkPermissions, this.differences],
489 [decomposeActions, this.differences, this.todo]
490 ], cb)
491}
492
493Installer.prototype.computeLinked = function (cb) {
494 validate('F', arguments)
495 if (!this.link || this.global) return cb()
496 var linkTodoList = []
497 var self = this
498 asyncMap(this.differences, function (action, next) {
499 var cmd = action[0]
500 var pkg = action[1]
501 if (cmd !== 'add' && cmd !== 'update') return next()
502 var isReqByTop = pkg.requiredBy.filter(function (mod) { return mod.isTop }).length
503 var isReqByUser = pkg.userRequired
504 var isExtraneous = pkg.requiredBy.length === 0
505 if (!isReqByTop && !isReqByUser && !isExtraneous) return next()
506 isLinkable(pkg, function (install, link) {
507 if (install) linkTodoList.push(['global-install', pkg])
508 if (link) linkTodoList.push(['global-link', pkg])
509 if (install || link) removeObsoleteDep(pkg)
510 next()
511 })
512 }, function () {
513 if (linkTodoList.length === 0) return cb()
514 self.differences.length = 0
515 Array.prototype.push.apply(self.differences, linkTodoList)
516 diffTrees(self.currentTree, self.idealTree, self.differences, log.newGroup('d2'), cb)
517 })
518}
519
520function isLinkable (pkg, cb) {
521 var globalPackage = path.resolve(npm.globalPrefix, 'lib', 'node_modules', moduleName(pkg))
522 var globalPackageJson = path.resolve(globalPackage, 'package.json')
523 fs.stat(globalPackage, function (er) {
524 if (er) return cb(true, true)
525 fs.readFile(globalPackageJson, function (er, data) {
526 var json = parseJSON.noExceptions(data)
527 cb(false, json && json.version === pkg.package.version)
528 })
529 })
530}
531
532Installer.prototype.executeActions = function (cb) {
533 validate('F', arguments)
534 log.silly('install', 'executeActions')
535 var todo = this.todo
536 var cg = this.progress.executeActions
537
538 var node_modules = path.resolve(this.where, 'node_modules')
539 var staging = path.resolve(node_modules, '.staging')
540 var steps = []
541 var trackLifecycle = cg.newGroup('lifecycle')
542
543 cb = unlockCB(node_modules, '.staging', cb)
544
545 steps.push(
546 [doSerialActions, 'global-install', staging, todo, trackLifecycle.newGroup('global-install')],
547 [lock, node_modules, '.staging'],
548 [rimraf, staging],
549 [doParallelActions, 'extract', staging, todo, cg.newGroup('extract', 100)],
550 [doReverseSerialActions, 'unbuild', staging, todo, cg.newGroup('unbuild')],
551 [doSerialActions, 'remove', staging, todo, cg.newGroup('remove')],
552 [doSerialActions, 'move', staging, todo, cg.newGroup('move')],
553 [doSerialActions, 'finalize', staging, todo, cg.newGroup('finalize')],
554 [doParallelActions, 'refresh-package-json', staging, todo, cg.newGroup('refresh-package-json')],
555 [doParallelActions, 'preinstall', staging, todo, trackLifecycle.newGroup('preinstall')],
556 [doSerialActions, 'build', staging, todo, trackLifecycle.newGroup('build')],
557 [doSerialActions, 'global-link', staging, todo, trackLifecycle.newGroup('global-link')],
558 [doParallelActions, 'update-linked', staging, todo, trackLifecycle.newGroup('update-linked')],
559 [doSerialActions, 'install', staging, todo, trackLifecycle.newGroup('install')],
560 [doSerialActions, 'postinstall', staging, todo, trackLifecycle.newGroup('postinstall')])
561
562 var self = this
563 chain(steps, function (er) {
564 if (!er || self.rollback) {
565 rimraf(staging, function () { cb(er) })
566 } else {
567 cb(er)
568 }
569 })
570}
571
572Installer.prototype.rollbackFailedOptional = function (staging, actionsToRun, cb) {
573 if (!this.rollback) return cb()
574 var failed = uniq(actionsToRun.map(function (action) {
575 return action[1]
576 }).filter(function (pkg) {
577 return pkg.failed && pkg.rollback
578 }))
579 var top = this.currentTree && this.currentTree.path
580 Bluebird.map(failed, (pkg) => {
581 return Bluebird.map(pkg.rollback, (rollback) => rollback(top, staging, pkg))
582 }).asCallback(cb)
583}
584
585Installer.prototype.commit = function (staging, actionsToRun, cb) {
586 var toCommit = actionsToRun.map(function (action) { return action[1] }).filter(function (pkg) { return !pkg.failed && pkg.commit })
587 asyncMap(toCommit, function (pkg, next) {
588 asyncMap(pkg.commit, function (commit, done) {
589 commit(staging, pkg, done)
590 }, function () {
591 pkg.commit = []
592 next.apply(null, arguments)
593 })
594 }, cb)
595}
596
597Installer.prototype.runPreinstallTopLevelLifecycles = function (cb) {
598 validate('F', arguments)
599 if (this.failing) return cb()
600 if (!this.topLevelLifecycles) return cb()
601 log.silly('install', 'runPreinstallTopLevelLifecycles')
602
603 readPackageJson(path.join(this.where, 'package.json'), log, false, (err, data) => {
604 if (err) return cb()
605 this.currentTree = createNode({
606 isTop: true,
607 package: data,
608 path: this.where
609 })
610 doOneAction('preinstall', this.where, this.currentTree, log.newGroup('preinstall:.'), cb)
611 })
612}
613
614Installer.prototype.runPostinstallTopLevelLifecycles = function (cb) {
615 validate('F', arguments)
616 if (this.failing) return cb()
617 if (!this.topLevelLifecycles) return cb()
618 log.silly('install', 'runPostinstallTopLevelLifecycles')
619 var steps = []
620 var trackLifecycle = this.progress.runTopLevelLifecycles
621
622 steps.push(
623 [doOneAction, 'build', this.idealTree.path, this.idealTree, trackLifecycle.newGroup('build:.')],
624 [doOneAction, 'install', this.idealTree.path, this.idealTree, trackLifecycle.newGroup('install:.')],
625 [doOneAction, 'postinstall', this.idealTree.path, this.idealTree, trackLifecycle.newGroup('postinstall:.')])
626 if (this.dev) {
627 steps.push(
628 [doOneAction, 'prepare', this.idealTree.path, this.idealTree, trackLifecycle.newGroup('prepare')])
629 }
630 chain(steps, cb)
631}
632
633Installer.prototype.startAudit = function (cb) {
634 if (!this.audit) return cb()
635 this.auditSubmission = Bluebird.try(() => {
636 return audit.generateFromInstall(this.idealTree, this.differences, this.args, this.remove)
637 }).then((auditData) => {
638 return audit.submitForInstallReport(auditData)
639 }).catch(_ => {})
640 cb()
641}
642
643Installer.prototype.saveToDependencies = function (cb) {
644 validate('F', arguments)
645 if (this.failing) return cb()
646 log.silly('install', 'saveToDependencies')
647 if (this.saveOnlyLock) {
648 return saveShrinkwrap(this.idealTree).nodeify(cb)
649 } else {
650 return saveRequested(this.idealTree).nodeify(cb)
651 }
652}
653
654Installer.prototype.readGlobalPackageData = function (cb) {
655 validate('F', arguments)
656 log.silly('install', 'readGlobalPackageData')
657 var self = this
658 this.loadArgMetadata(iferr(cb, function () {
659 mkdirp(self.where, iferr(cb, function () {
660 var pkgs = {}
661 self.args.forEach(function (pkg) {
662 pkgs[pkg.name] = true
663 })
664 readPackageTree(self.where, function (ctx, kid) { return ctx.parent || pkgs[kid] }, iferr(cb, function (currentTree) {
665 self.currentTree = currentTree
666 return cb()
667 }))
668 }))
669 }))
670}
671
672Installer.prototype.readLocalPackageData = function (cb) {
673 validate('F', arguments)
674 log.silly('install', 'readLocalPackageData')
675 var self = this
676 mkdirp(this.where, iferr(cb, function () {
677 readPackageTree(self.where, iferr(cb, function (currentTree) {
678 self.currentTree = currentTree
679 self.currentTree.warnings = []
680 if (currentTree.error && currentTree.error.code === 'EJSONPARSE') {
681 return cb(currentTree.error)
682 }
683 if (!self.noPackageJsonOk && !currentTree.package) {
684 log.error('install', "Couldn't read dependencies")
685 var er = new Error("ENOENT, open '" + path.join(self.where, 'package.json') + "'")
686 er.code = 'ENOPACKAGEJSON'
687 er.errno = 34
688 return cb(er)
689 }
690 if (!currentTree.package) currentTree.package = {}
691 readShrinkwrap(currentTree, function (err) {
692 if (err) {
693 cb(err)
694 } else {
695 self.loadArgMetadata(cb)
696 }
697 })
698 }))
699 }))
700}
701
702Installer.prototype.cloneCurrentTreeToIdealTree = function (cb) {
703 validate('F', arguments)
704 log.silly('install', 'cloneCurrentTreeToIdealTree')
705
706 this.idealTree = copyTree(this.currentTree)
707 this.idealTree.warnings = []
708 cb()
709}
710
711Installer.prototype.loadShrinkwrap = function (cb) {
712 validate('F', arguments)
713 log.silly('install', 'loadShrinkwrap')
714 readShrinkwrap.andInflate(this.idealTree, iferr(cb, () => {
715 computeMetadata(this.idealTree)
716 cb()
717 }))
718}
719
720Installer.prototype.getInstalledModules = function () {
721 return this.differences.filter(function (action) {
722 var mutation = action[0]
723 return (mutation === 'add' || mutation === 'update')
724 }).map(function (action) {
725 var child = action[1]
726 return [child.package._id, child.path]
727 })
728}
729
730Installer.prototype.printWarnings = function (cb) {
731 if (!this.idealTree) return cb()
732
733 var self = this
734 var warned = false
735 this.idealTree.warnings.forEach(function (warning) {
736 if (warning.code === 'EPACKAGEJSON' && self.global) return
737 if (warning.code === 'ENOTDIR') return
738 warned = true
739 var msg = errorMessage(warning)
740 msg.summary.forEach(function (logline) {
741 log.warn.apply(log, logline)
742 })
743 msg.detail.forEach(function (logline) {
744 log.verbose.apply(log, logline)
745 })
746 })
747 if (warned && log.levels[npm.config.get('loglevel')] <= log.levels.warn) console.error()
748 cb()
749}
750
751Installer.prototype.printInstalled = function (cb) {
752 validate('F', arguments)
753 if (this.failing) return cb()
754 log.silly('install', 'printInstalled')
755 const diffs = this.differences
756 if (!this.idealTree.error && this.idealTree.removedChildren) {
757 const deps = this.currentTree.package.dependencies || {}
758 const dev = this.currentTree.package.devDependencies || {}
759 this.idealTree.removedChildren.forEach((r) => {
760 if (diffs.some((d) => d[0] === 'remove' && d[1].path === r.path)) return
761 if (!deps[moduleName(r)] && !dev[moduleName(r)]) return
762 diffs.push(['remove', r])
763 })
764 }
765 return Bluebird.try(() => {
766 if (!this.auditSubmission) return
767 return Bluebird.resolve(this.auditSubmission).timeout(10000).catch(() => null)
768 }).then((auditResult) => {
769 if (auditResult && !auditResult.metadata) {
770 log.warn('audit', 'Audit result from registry missing metadata. This is probably an issue with the registry.')
771 }
772 // maybe write audit report w/ hash of pjson & shrinkwrap for later reading by `npm audit`
773 if (npm.config.get('json')) {
774 return this.printInstalledForJSON(diffs, auditResult)
775 } else if (npm.config.get('parseable')) {
776 return this.printInstalledForParseable(diffs, auditResult)
777 } else {
778 return this.printInstalledForHuman(diffs, auditResult)
779 }
780 }).asCallback(cb)
781}
782
783Installer.prototype.printInstalledForHuman = function (diffs, auditResult) {
784 var removed = 0
785 var added = 0
786 var updated = 0
787 var moved = 0
788 // Count the number of contributors to packages added, tracking
789 // contributors we've seen, so we can produce a running unique count.
790 var contributors = new Set()
791 diffs.forEach(function (action) {
792 var mutation = action[0]
793 var pkg = action[1]
794 if (pkg.failed) return
795 if (mutation === 'remove') {
796 ++removed
797 } else if (mutation === 'move') {
798 ++moved
799 } else if (mutation === 'add') {
800 ++added
801 // Count contributors to added packages. Start by combining `author`
802 // and `contributors` data into a single array of contributor-people
803 // for this package.
804 var people = []
805 var meta = pkg.package
806 if (meta.author) people.push(meta.author)
807 if (meta.contributors && Array.isArray(meta.contributors)) {
808 people = people.concat(meta.contributors)
809 }
810 // Make sure a normalized string for every person behind this
811 // package is in `contributors`.
812 people.forEach(function (person) {
813 // Ignore errors from malformed `author` and `contributors`.
814 try {
815 var normalized = normalizePerson(person)
816 } catch (error) {
817 return
818 }
819 if (!contributors.has(normalized)) contributors.add(normalized)
820 })
821 } else if (mutation === 'update' || mutation === 'update-linked') {
822 ++updated
823 }
824 })
825 var report = ''
826 if (this.args.length && (added || updated)) {
827 report += this.args.map((p) => {
828 return `+ ${p.name}@${p.version}`
829 }).join('\n') + '\n'
830 }
831 var actions = []
832 if (added) {
833 var action = 'added ' + packages(added)
834 if (contributors.size) action += from(contributors.size)
835 actions.push(action)
836 }
837 if (removed) actions.push('removed ' + packages(removed))
838 if (updated) actions.push('updated ' + packages(updated))
839 if (moved) actions.push('moved ' + packages(moved))
840 if (auditResult && auditResult.metadata && auditResult.metadata.totalDependencies) {
841 actions.push('audited ' + packages(auditResult.metadata.totalDependencies))
842 }
843 if (actions.length === 0) {
844 report += 'up to date'
845 } else if (actions.length === 1) {
846 report += actions[0]
847 } else {
848 var lastAction = actions.pop()
849 report += actions.join(', ') + ' and ' + lastAction
850 }
851 report += ' in ' + ((Date.now() - this.started) / 1000) + 's'
852
853 output(report)
854 return auditResult && audit.printInstallReport(auditResult)
855
856 function packages (num) {
857 return num + ' package' + (num > 1 ? 's' : '')
858 }
859
860 function from (num) {
861 return ' from ' + num + ' contributor' + (num > 1 ? 's' : '')
862 }
863
864 // Values of `author` and elements of `contributors` in `package.json`
865 // files can be e-mail style strings or Objects with `name`, `email,
866 // and `url` String properties. Convert Objects to Strings so that
867 // we can efficiently keep a set of contributors we have already seen.
868 function normalizePerson (argument) {
869 if (typeof argument === 'string') return argument
870 var returned = ''
871 if (argument.name) returned += argument.name
872 if (argument.email) returned += ' <' + argument.email + '>'
873 if (argument.url) returned += ' (' + argument.email + ')'
874 return returned
875 }
876}
877
878Installer.prototype.printInstalledForJSON = function (diffs, auditResult) {
879 var result = {
880 added: [],
881 removed: [],
882 updated: [],
883 moved: [],
884 failed: [],
885 warnings: [],
886 audit: auditResult,
887 elapsed: Date.now() - this.started
888 }
889 var self = this
890 this.idealTree.warnings.forEach(function (warning) {
891 if (warning.code === 'EPACKAGEJSON' && self.global) return
892 if (warning.code === 'ENOTDIR') return
893 var output = errorMessage(warning)
894 var message = flattenMessage(output.summary)
895 if (output.detail.length) {
896 message += '\n' + flattenMessage(output.detail)
897 }
898 result.warnings.push(message)
899 })
900 diffs.forEach(function (action) {
901 var mutation = action[0]
902 var child = action[1]
903 var record = recordAction(action)
904 if (child.failed) {
905 result.failed.push(record)
906 } else if (mutation === 'add') {
907 result.added.push(record)
908 } else if (mutation === 'update' || mutation === 'update-linked') {
909 result.updated.push(record)
910 } else if (mutation === 'move') {
911 result.moved.push(record)
912 } else if (mutation === 'remove') {
913 result.removed.push(record)
914 }
915 })
916 output(JSON.stringify(result, null, 2))
917
918 function flattenMessage (msg) {
919 return msg.map(function (logline) { return logline.slice(1).join(' ') }).join('\n')
920 }
921
922 function recordAction (action) {
923 var mutation = action[0]
924 var child = action[1]
925 var result = {
926 action: mutation,
927 name: moduleName(child),
928 version: child.package && child.package.version,
929 path: child.path
930 }
931 if (mutation === 'move') {
932 result.previousPath = child.fromPath
933 } else if (mutation === 'update') {
934 result.previousVersion = child.oldPkg.package && child.oldPkg.package.version
935 }
936 return result
937 }
938}
939
940Installer.prototype.printInstalledForParseable = function (diffs) {
941 var self = this
942 diffs.forEach(function (action) {
943 var mutation = action[0]
944 var child = action[1]
945 if (mutation === 'move') {
946 var previousPath = path.relative(self.where, child.fromPath)
947 } else if (mutation === 'update') {
948 var previousVersion = child.oldPkg.package && child.oldPkg.package.version
949 }
950 output(
951 mutation + '\t' +
952 moduleName(child) + '\t' +
953 (child.package ? child.package.version : '') + '\t' +
954 (child.path ? path.relative(self.where, child.path) : '') + '\t' +
955 (previousVersion || '') + '\t' +
956 (previousPath || ''))
957 })
958}
959
960Installer.prototype.debugActions = function (name, actionListName, cb) {
961 validate('SSF', arguments)
962 var actionsToLog = this[actionListName]
963 log.silly(name, 'action count', actionsToLog.length)
964 actionsToLog.forEach(function (action) {
965 log.silly(name, action.map(function (value) {
966 return (value && value.package) ? packageId(value) : value
967 }).join(' '))
968 })
969 cb()
970}
971
972// This takes an object and a property name instead of a value to allow us
973// to define the arguments for use by chain before the property exists yet.
974Installer.prototype.debugTree = function (name, treeName, cb) {
975 validate('SSF', arguments)
976 log.silly(name, this.archyDebugTree(this[treeName]).trim())
977 cb()
978}
979
980Installer.prototype.archyDebugTree = function (tree) {
981 validate('O', arguments)
982 var seen = new Set()
983 function byName (aa, bb) {
984 return packageId(aa).localeCompare(packageId(bb))
985 }
986 function expandTree (tree) {
987 seen.add(tree)
988 return {
989 label: packageId(tree),
990 nodes: tree.children.filter((tree) => { return !seen.has(tree) && !tree.removed }).sort(byName).map(expandTree)
991 }
992 }
993 return archy(expandTree(tree), '', { unicode: npm.config.get('unicode') })
994}
995
996Installer.prototype.debugLogicalTree = function (name, treeName, cb) {
997 validate('SSF', arguments)
998 this[treeName] && log.silly(name, this.archyDebugLogicalTree(this[treeName]).trim())
999 cb()
1000}
1001
1002Installer.prototype.archyDebugLogicalTree = function (tree) {
1003 validate('O', arguments)
1004 var seen = new Set()
1005 function byName (aa, bb) {
1006 return packageId(aa).localeCompare(packageId(bb))
1007 }
1008 function expandTree (tree) {
1009 seen.add(tree)
1010 return {
1011 label: packageId(tree),
1012 nodes: tree.requires.filter((tree) => { return !seen.has(tree) && !tree.removed }).sort(byName).map(expandTree)
1013 }
1014 }
1015 return archy(expandTree(tree), '', { unicode: npm.config.get('unicode') })
1016}