@npmcli/arborist 2.5.0 → 2.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -11,6 +11,7 @@ Version: ${require('../package.json').version}
11
11
  # COMMANDS
12
12
 
13
13
  * reify: reify ideal tree to node_modules (install, update, rm, ...)
14
+ * prune: prune the ideal tree and reify (like npm prune)
14
15
  * ideal: generate and print the ideal tree
15
16
  * actual: read and print the actual tree in node_modules
16
17
  * virtual: read and print the virtual tree in the local shrinkwrap file
@@ -50,6 +51,9 @@ switch (cmd) {
50
51
  case 'ideal':
51
52
  require('./ideal.js')
52
53
  break
54
+ case 'prune':
55
+ require('./prune.js')
56
+ break
53
57
  case 'reify':
54
58
  require('./reify.js')
55
59
  break
package/bin/prune.js ADDED
@@ -0,0 +1,46 @@
1
+ const Arborist = require('../')
2
+
3
+ const options = require('./lib/options.js')
4
+ const print = require('./lib/print-tree.js')
5
+ require('./lib/logging.js')
6
+ require('./lib/timers.js')
7
+
8
+ const printDiff = diff => {
9
+ const {depth} = require('treeverse')
10
+ depth({
11
+ tree: diff,
12
+ visit: d => {
13
+ if (d.location === '')
14
+ return
15
+ switch (d.action) {
16
+ case 'REMOVE':
17
+ console.error('REMOVE', d.actual.location)
18
+ break
19
+ case 'ADD':
20
+ console.error('ADD', d.ideal.location, d.ideal.resolved)
21
+ break
22
+ case 'CHANGE':
23
+ console.error('CHANGE', d.actual.location, {
24
+ from: d.actual.resolved,
25
+ to: d.ideal.resolved,
26
+ })
27
+ break
28
+ }
29
+ },
30
+ getChildren: d => d.children,
31
+ })
32
+ }
33
+
34
+ const start = process.hrtime()
35
+ process.emit('time', 'install')
36
+ const arb = new Arborist(options)
37
+ arb.prune(options).then(tree => {
38
+ process.emit('timeEnd', 'install')
39
+ const end = process.hrtime(start)
40
+ print(tree)
41
+ if (options.dryRun)
42
+ printDiff(arb.diff)
43
+ console.error(`resolved ${tree.inventory.size} deps in ${end[0] + end[1] / 1e9}s`)
44
+ if (tree.meta && options.save)
45
+ tree.meta.save()
46
+ }).catch(er => console.error(require('util').inspect(er, { depth: Infinity })))
@@ -4,6 +4,7 @@ const AuditReport = require('../audit-report.js')
4
4
 
5
5
  // shared with reify
6
6
  const _global = Symbol.for('global')
7
+ const _workspaces = Symbol.for('workspaces')
7
8
 
8
9
  module.exports = cls => class Auditor extends cls {
9
10
  async audit (options = {}) {
@@ -21,8 +22,10 @@ module.exports = cls => class Auditor extends cls {
21
22
 
22
23
  process.emit('time', 'audit')
23
24
  const tree = await this.loadVirtual()
24
- this.auditReport = await AuditReport.load(tree, this.options)
25
- const ret = options.fix ? this.reify() : this.auditReport
25
+ if (this[_workspaces] && this[_workspaces].length)
26
+ options.filterSet = this.workspaceDependencySet(tree, this[_workspaces])
27
+ this.auditReport = await AuditReport.load(tree, options)
28
+ const ret = options.fix ? this.reify(options) : this.auditReport
26
29
  process.emit('timeEnd', 'audit')
27
30
  this.finishTracker('audit')
28
31
  return ret
@@ -7,7 +7,7 @@ const semver = require('semver')
7
7
  const promiseCallLimit = require('promise-call-limit')
8
8
  const getPeerSet = require('../peer-set.js')
9
9
  const realpath = require('../../lib/realpath.js')
10
- const { resolve } = require('path')
10
+ const { resolve, dirname } = require('path')
11
11
  const { promisify } = require('util')
12
12
  const treeCheck = require('../tree-check.js')
13
13
  const readdir = promisify(require('readdir-scoped-modules'))
@@ -398,38 +398,14 @@ module.exports = cls => class IdealTreeBuilder extends cls {
398
398
  process.emit('time', 'idealTree:userRequests')
399
399
  const tree = this.idealTree.target || this.idealTree
400
400
 
401
- if (!this[_workspaces].length) {
402
- return this[_applyUserRequestsToNode](tree, options).then(() =>
403
- process.emit('timeEnd', 'idealTree:userRequests'))
404
- }
405
-
406
- const wsMap = tree.workspaces
407
- if (!wsMap) {
408
- this.log.warn('idealTree', 'Workspace filter set, but no workspaces present')
409
- return
410
- }
411
-
412
- const promises = []
413
- for (const name of this[_workspaces]) {
414
- const path = wsMap.get(name)
415
- if (!path) {
416
- this.log.warn('idealTree', `Workspace ${name} in filter set, but not in workspaces`)
417
- continue
418
- }
419
- const loc = relpath(tree.realpath, path)
420
- const node = tree.inventory.get(loc)
421
-
422
- /* istanbul ignore if - should be impossible */
423
- if (!node) {
424
- this.log.warn('idealTree', `Workspace ${name} in filter set, but no workspace folder present`)
425
- continue
426
- }
427
-
428
- promises.push(this[_applyUserRequestsToNode](node, options))
401
+ if (!this[_workspaces].length)
402
+ await this[_applyUserRequestsToNode](tree, options)
403
+ else {
404
+ await Promise.all(this.workspaceNodes(tree, this[_workspaces])
405
+ .map(node => this[_applyUserRequestsToNode](node, options)))
429
406
  }
430
407
 
431
- return Promise.all(promises).then(() =>
432
- process.emit('timeEnd', 'idealTree:userRequests'))
408
+ process.emit('timeEnd', 'idealTree:userRequests')
433
409
  }
434
410
 
435
411
  async [_applyUserRequestsToNode] (tree, options) {
@@ -456,7 +432,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
456
432
  }
457
433
 
458
434
  if (this.auditReport && this.auditReport.size > 0)
459
- this[_queueVulnDependents](options)
435
+ await this[_queueVulnDependents](options)
460
436
 
461
437
  const { add, rm } = options
462
438
 
@@ -475,10 +451,14 @@ module.exports = cls => class IdealTreeBuilder extends cls {
475
451
  if (add && add.length || rm && rm.length || this[_global])
476
452
  tree.package = tree.package
477
453
 
478
- for (const spec of this[_resolvedAdd])
479
- this[_explicitRequests].add(tree.edgesOut.get(spec.name))
454
+ for (const spec of this[_resolvedAdd]) {
455
+ if (spec.tree === tree)
456
+ this[_explicitRequests].add(tree.edgesOut.get(spec.name))
457
+ }
480
458
  for (const name of globalExplicitUpdateNames)
481
459
  this[_explicitRequests].add(tree.edgesOut.get(name))
460
+
461
+ this[_depsQueue].push(tree)
482
462
  }
483
463
 
484
464
  // This returns a promise because we might not have the name yet,
@@ -488,12 +468,14 @@ module.exports = cls => class IdealTreeBuilder extends cls {
488
468
  // ie, doing `foo@bar` we just return foo
489
469
  // but if it's a url or git, we don't know the name until we
490
470
  // fetch it and look in its manifest.
491
- return Promise.all(add.map(rawSpec => {
492
- // We do NOT provide the path here, because user-additions need
493
- // to be resolved relative to the CWD the user is in.
494
- return this[_retrieveSpecName](npa(rawSpec))
495
- .then(add => this[_updateFilePath](add))
496
- .then(add => this[_followSymlinkPath](add))
471
+ return Promise.all(add.map(async rawSpec => {
472
+ // We do NOT provide the path to npa here, because user-additions
473
+ // need to be resolved relative to the CWD the user is in.
474
+ const spec = await this[_retrieveSpecName](npa(rawSpec))
475
+ .then(spec => this[_updateFilePath](spec))
476
+ .then(spec => this[_followSymlinkPath](spec))
477
+ spec.tree = tree
478
+ return spec
497
479
  })).then(add => {
498
480
  this[_resolvedAdd].push(...add)
499
481
  // now add is a list of spec objects with names.
@@ -528,7 +510,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
528
510
 
529
511
  async [_updateFilePath] (spec) {
530
512
  if (spec.type === 'file')
531
- spec = this[_getRelpathSpec](spec, spec.fetchSpec)
513
+ return this[_getRelpathSpec](spec, spec.fetchSpec)
532
514
 
533
515
  return spec
534
516
  }
@@ -541,7 +523,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
541
523
  .catch(/* istanbul ignore next */() => null)
542
524
  )
543
525
 
544
- spec = this[_getRelpathSpec](spec, real)
526
+ return this[_getRelpathSpec](spec, real)
545
527
  }
546
528
  return spec
547
529
  }
@@ -561,9 +543,9 @@ module.exports = cls => class IdealTreeBuilder extends cls {
561
543
  // what's in the bundle at each published manifest. Without that, we
562
544
  // can't possibly fix bundled deps without breaking a ton of other stuff,
563
545
  // and leaving the user subject to getting it overwritten later anyway.
564
- [_queueVulnDependents] (options) {
565
- for (const {nodes} of this.auditReport.values()) {
566
- for (const node of nodes) {
546
+ async [_queueVulnDependents] (options) {
547
+ for (const vuln of this.auditReport.values()) {
548
+ for (const node of vuln.nodes) {
567
549
  const bundler = node.getBundler()
568
550
 
569
551
  // XXX this belongs in the audit report itself, not here.
@@ -595,6 +577,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
595
577
  if (this[_force] && this.auditReport && this.auditReport.topVulns.size) {
596
578
  options.add = options.add || []
597
579
  options.rm = options.rm || []
580
+ const nodesTouched = new Set()
598
581
  for (const [name, topVuln] of this.auditReport.topVulns.entries()) {
599
582
  const {
600
583
  simpleRange,
@@ -602,7 +585,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
602
585
  fixAvailable,
603
586
  } = topVuln
604
587
  for (const node of topNodes) {
605
- if (node !== this.idealTree && node !== this.idealTree.target) {
588
+ if (!node.isProjectRoot && !node.isWorkspace) {
606
589
  // not something we're going to fix, sorry. have to cd into
607
590
  // that directory and fix it yourself.
608
591
  this.log.warn('audit', 'Manual fix required in linked project ' +
@@ -622,9 +605,13 @@ module.exports = cls => class IdealTreeBuilder extends cls {
622
605
  : 'outside your stated dependency range'
623
606
  this.log.warn('audit', `Updating ${name} to ${version},` +
624
607
  `which is ${breakingMessage}.`)
625
- options.add.push(`${name}@${version}`)
608
+
609
+ await this[_add](node, { add: [`${name}@${version}`] })
610
+ nodesTouched.add(node)
626
611
  }
627
612
  }
613
+ for (const node of nodesTouched)
614
+ node.package = node.package
628
615
  }
629
616
  }
630
617
 
@@ -646,7 +633,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
646
633
  // probably have their own project associated with them.
647
634
 
648
635
  // for every node with one of the names on the list, we add its
649
- // dependents to the queue to be evaluated. in buildDepStem,
636
+ // dependents to the queue to be evaluated. in buildDepStep,
650
637
  // anything on the update names list will get refreshed, even if
651
638
  // it isn't a problem.
652
639
 
@@ -674,7 +661,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
674
661
  const ancient = meta.ancientLockfile
675
662
  const old = meta.loadedFromDisk && !(meta.originalLockfileVersion >= 2)
676
663
 
677
- if (inventory.size === 0 || !ancient && !(old && this[_complete]))
664
+ if (inventory.size === 0 || !ancient && !old)
678
665
  return
679
666
 
680
667
  // if the lockfile is from node v5 or earlier, then we'll have to reload
@@ -701,10 +688,12 @@ This is a one-time fix-up, please be patient...
701
688
  this.log.silly('inflate', node.location)
702
689
  const { resolved, version, path, name, location, integrity } = node
703
690
  // don't try to hit the registry for linked deps
704
- const useResolved = !version ||
705
- resolved && resolved.startsWith('file:')
706
- const id = useResolved ? resolved : version
707
- const spec = npa.resolve(name, id, path)
691
+ const useResolved = resolved && (
692
+ !version || resolved.startsWith('file:')
693
+ )
694
+ const id = useResolved ? resolved
695
+ : version || `file:${node.path}`
696
+ const spec = npa.resolve(name, id, dirname(path))
708
697
  const sloc = location.substr('node_modules/'.length)
709
698
  const t = `idealTree:inflate:${sloc}`
710
699
  this.addTracker(t)
@@ -1025,7 +1014,8 @@ This is a one-time fix-up, please be patient...
1025
1014
 
1026
1015
  // also skip over any nodes in the tree that failed to load, since those
1027
1016
  // will crash the install later on anyway.
1028
- const bd = node.isProjectRoot ? null : node.package.bundleDependencies
1017
+ const bd = node.isProjectRoot || node.isWorkspace ? null
1018
+ : node.package.bundleDependencies
1029
1019
  const bundled = new Set(bd || [])
1030
1020
 
1031
1021
  return [...node.edgesOut.values()]
@@ -1061,8 +1051,8 @@ This is a one-time fix-up, please be patient...
1061
1051
  if (this[_isVulnerable](edge.to))
1062
1052
  return true
1063
1053
 
1064
- // If the user has explicitly asked to install this package, it's a problem.
1065
- if (node.isProjectRoot && this[_explicitRequests].has(edge))
1054
+ // If the user has explicitly asked to install this package, it's a "problem".
1055
+ if (this[_explicitRequests].has(edge))
1066
1056
  return true
1067
1057
 
1068
1058
  // No problems!
@@ -1358,7 +1348,7 @@ This is a one-time fix-up, please be patient...
1358
1348
  // first in x, then in the root, ending with KEEP, because we already
1359
1349
  // have it. In that case, we ought to REMOVE the nm/x/nm/y node, because
1360
1350
  // it is an unnecessary duplicate.
1361
- this[_pruneDedupable](target, true)
1351
+ this[_pruneDedupable](target)
1362
1352
  return []
1363
1353
  }
1364
1354
 
@@ -1475,8 +1465,20 @@ This is a one-time fix-up, please be patient...
1475
1465
  return
1476
1466
  }
1477
1467
  if (descend) {
1478
- for (const child of node.children.values())
1468
+ // sort these so that they're deterministically ordered
1469
+ // otherwise, resulting tree shape is dependent on the order
1470
+ // in which they happened to be resolved.
1471
+ const nodeSort = (a, b) => a.location.localeCompare(b.location, 'en')
1472
+
1473
+ const children = [...node.children.values()].sort(nodeSort)
1474
+ const fsChildren = [...node.fsChildren].sort(nodeSort)
1475
+ for (const child of children)
1479
1476
  this[_pruneDedupable](child)
1477
+ for (const topNode of fsChildren) {
1478
+ const children = [...topNode.children.values()].sort(nodeSort)
1479
+ for (const child of children)
1480
+ this[_pruneDedupable](child)
1481
+ }
1480
1482
  }
1481
1483
  }
1482
1484
 
@@ -28,7 +28,7 @@
28
28
 
29
29
  const {resolve} = require('path')
30
30
  const {homedir} = require('os')
31
- const procLog = require('../proc-log.js')
31
+ const procLog = require('proc-log')
32
32
  const { saveTypeMap } = require('../add-rm-pkg-deps.js')
33
33
 
34
34
  const mixins = [
@@ -75,6 +75,7 @@ class Arborist extends Base {
75
75
  workspaceDependencySet (tree, workspaces) {
76
76
  const wsNodes = this.workspaceNodes(tree, workspaces)
77
77
  const set = new Set(wsNodes)
78
+ const extraneous = new Set()
78
79
  for (const node of set) {
79
80
  for (const edge of node.edgesOut.values()) {
80
81
  const dep = edge.to
@@ -84,7 +85,13 @@ class Arborist extends Base {
84
85
  set.add(dep.target)
85
86
  }
86
87
  }
88
+ for (const child of node.children.values()) {
89
+ if (child.extraneous)
90
+ extraneous.add(child)
91
+ }
87
92
  }
93
+ for (const extra of extraneous)
94
+ set.add(extra)
88
95
  return set
89
96
  }
90
97
  }
@@ -22,6 +22,7 @@ const _loadFSTree = Symbol('loadFSTree')
22
22
  const _loadFSChildren = Symbol('loadFSChildren')
23
23
  const _findMissingEdges = Symbol('findMissingEdges')
24
24
  const _findFSParents = Symbol('findFSParents')
25
+ const _resetDepFlags = Symbol('resetDepFlags')
25
26
 
26
27
  const _actualTreeLoaded = Symbol('actualTreeLoaded')
27
28
  const _rpcache = Symbol.for('realpathCache')
@@ -74,6 +75,19 @@ module.exports = cls => class ActualLoader extends cls {
74
75
  this[_topNodes] = new Set()
75
76
  }
76
77
 
78
+ [_resetDepFlags] (tree, root) {
79
+ // reset all deps to extraneous prior to recalc
80
+ if (!root) {
81
+ for (const node of tree.inventory.values())
82
+ node.extraneous = true
83
+ }
84
+
85
+ // only reset root flags if we're not re-rooting,
86
+ // otherwise leave as-is
87
+ calcDepFlags(tree, !root)
88
+ return tree
89
+ }
90
+
77
91
  // public method
78
92
  async loadActual (options = {}) {
79
93
  // allow the user to set options on the ctor as well.
@@ -88,6 +102,7 @@ module.exports = cls => class ActualLoader extends cls {
88
102
  return this.actualTree ? this.actualTree
89
103
  : this[_actualTreePromise] ? this[_actualTreePromise]
90
104
  : this[_actualTreePromise] = this[_loadActual](options)
105
+ .then(tree => this[_resetDepFlags](tree, options.root))
91
106
  .then(tree => this.actualTree = treeCheck(tree))
92
107
  }
93
108
 
@@ -152,8 +167,7 @@ module.exports = cls => class ActualLoader extends cls {
152
167
  root: this[_actualTree],
153
168
  })
154
169
  await this[_loadWorkspaces](this[_actualTree])
155
- if (this[_actualTree].workspaces && this[_actualTree].workspaces.size)
156
- calcDepFlags(this[_actualTree], !root)
170
+
157
171
  this[_transplant](root)
158
172
  return this[_actualTree]
159
173
  }
@@ -178,8 +192,6 @@ module.exports = cls => class ActualLoader extends cls {
178
192
  dependencies[name] = dependencies[name] || '*'
179
193
  actualRoot.package = { ...actualRoot.package, dependencies }
180
194
  }
181
- // only reset root flags if we're not re-rooting, otherwise leave as-is
182
- calcDepFlags(this[_actualTree], !root)
183
195
  return this[_actualTree]
184
196
  }
185
197
 
@@ -16,6 +16,7 @@ const {
16
16
  const boolEnv = b => b ? '1' : ''
17
17
  const sortNodes = (a, b) => (a.depth - b.depth) || a.path.localeCompare(b.path, 'en')
18
18
 
19
+ const _workspaces = Symbol.for('workspaces')
19
20
  const _build = Symbol('build')
20
21
  const _resetQueues = Symbol('resetQueues')
21
22
  const _rebuildBundle = Symbol('rebuildBundle')
@@ -70,8 +71,14 @@ module.exports = cls => class Builder extends cls {
70
71
 
71
72
  // if we don't have a set of nodes, then just rebuild
72
73
  // the actual tree on disk.
73
- if (!nodes)
74
- nodes = (await this.loadActual()).inventory.values()
74
+ if (!nodes) {
75
+ const tree = await this.loadActual()
76
+ if (this[_workspaces] && this[_workspaces].length) {
77
+ const filterSet = this.workspaceDependencySet(tree, this[_workspaces])
78
+ nodes = tree.inventory.filter(node => filterSet.has(node))
79
+ } else
80
+ nodes = tree.inventory.values()
81
+ }
75
82
 
76
83
  // separates links nodes so that it can run
77
84
  // prepare scripts and link bins in the expected order
@@ -2,7 +2,6 @@
2
2
 
3
3
  const onExit = require('../signal-handling.js')
4
4
  const pacote = require('pacote')
5
- const rpj = require('read-package-json-fast')
6
5
  const AuditReport = require('../audit-report.js')
7
6
  const {subset, intersects} = require('semver')
8
7
  const npa = require('npm-package-arg')
@@ -57,7 +56,6 @@ const _extractOrLink = Symbol('extractOrLink')
57
56
  const _checkBins = Symbol.for('checkBins')
58
57
  const _symlink = Symbol('symlink')
59
58
  const _warnDeprecated = Symbol('warnDeprecated')
60
- const _loadAncientPackageDetails = Symbol('loadAncientPackageDetails')
61
59
  const _loadBundlesAndUpdateTrees = Symbol.for('loadBundlesAndUpdateTrees')
62
60
  const _submitQuickAudit = Symbol('submitQuickAudit')
63
61
  const _awaitQuickAudit = Symbol('awaitQuickAudit')
@@ -133,12 +131,12 @@ module.exports = cls => class Reifier extends cls {
133
131
  this.addTracker('reify')
134
132
  process.emit('time', 'reify')
135
133
  await this[_validatePath]()
136
- .then(() => this[_loadTrees](options))
137
- .then(() => this[_diffTrees]())
138
- .then(() => this[_reifyPackages]())
139
- .then(() => this[_saveIdealTree](options))
140
- .then(() => this[_copyIdealToActual]())
141
- .then(() => this[_awaitQuickAudit]())
134
+ await this[_loadTrees](options)
135
+ await this[_diffTrees]()
136
+ await this[_reifyPackages]()
137
+ await this[_saveIdealTree](options)
138
+ await this[_copyIdealToActual]()
139
+ await this[_awaitQuickAudit]()
142
140
 
143
141
  this.finishTracker('reify')
144
142
  process.emit('timeEnd', 'reify')
@@ -522,7 +520,6 @@ module.exports = cls => class Reifier extends cls {
522
520
  await this[_checkBins](node)
523
521
  await this[_extractOrLink](node)
524
522
  await this[_warnDeprecated](node)
525
- await this[_loadAncientPackageDetails](node)
526
523
  })
527
524
 
528
525
  return this[_handleOptionalFailure](node, p)
@@ -583,32 +580,6 @@ module.exports = cls => class Reifier extends cls {
583
580
  this.log.warn('deprecated', `${_id}: ${deprecated}`)
584
581
  }
585
582
 
586
- async [_loadAncientPackageDetails] (node, forceReload = false) {
587
- // If we're loading from a v1 lockfile, load details from the package.json
588
- // that weren't recorded in the old format.
589
- const {meta} = this.idealTree
590
- const ancient = meta.ancientLockfile
591
- const old = meta.loadedFromDisk && !(meta.originalLockfileVersion >= 2)
592
-
593
- // already replaced with the manifest if it's truly ancient
594
- if (node.path && (forceReload || (old && !ancient))) {
595
- // XXX should have a shared location where package.json is read,
596
- // so we don't ever read the same pj more than necessary.
597
- let pkg
598
- try {
599
- pkg = await rpj(node.path + '/package.json')
600
- } catch (err) {}
601
-
602
- if (pkg) {
603
- node.package.bin = pkg.bin
604
- node.package.os = pkg.os
605
- node.package.cpu = pkg.cpu
606
- node.package.engines = pkg.engines
607
- meta.add(node)
608
- }
609
- }
610
- }
611
-
612
583
  // if the node is optional, then the failure of the promise is nonfatal
613
584
  // just add it and its optional set to the trash list.
614
585
  [_handleOptionalFailure] (node, p) {
@@ -774,8 +745,14 @@ module.exports = cls => class Reifier extends cls {
774
745
  // NOT return the promise, as the intent is for this to run in parallel
775
746
  // with the reification, and be resolved at a later time.
776
747
  process.emit('time', 'reify:audit')
748
+ const options = { ...this.options }
749
+ const tree = this.idealTree
750
+
751
+ // if we're operating on a workspace, only audit the workspace deps
752
+ if (this[_workspaces] && this[_workspaces].length)
753
+ options.filterSet = this.workspaceDependencySet(tree, this[_workspaces])
777
754
 
778
- this.auditReport = AuditReport.load(this.idealTree, this.options)
755
+ this.auditReport = AuditReport.load(tree, options)
779
756
  .then(res => {
780
757
  process.emit('timeEnd', 'reify:audit')
781
758
  this.auditReport = res
@@ -936,7 +913,7 @@ module.exports = cls => class Reifier extends cls {
936
913
 
937
914
  // last but not least, we save the ideal tree metadata to the package-lock
938
915
  // or shrinkwrap file, and any additions or removals to package.json
939
- [_saveIdealTree] (options) {
916
+ async [_saveIdealTree] (options) {
940
917
  // the ideal tree is actualized now, hooray!
941
918
  // it still contains all the references to optional nodes that were removed
942
919
  // for install failures. Those still end up in the shrinkwrap, so we
@@ -944,23 +921,26 @@ module.exports = cls => class Reifier extends cls {
944
921
 
945
922
  // support save=false option
946
923
  if (options.save === false || this[_global] || this[_dryRun])
947
- return
924
+ return false
948
925
 
949
926
  process.emit('time', 'reify:save')
950
927
 
928
+ const updatedTrees = new Set()
929
+
951
930
  // resolvedAdd is the list of user add requests, but with names added
952
931
  // to things like git repos and tarball file/urls. However, if the
953
932
  // user requested 'foo@', and we have a foo@file:../foo, then we should
954
933
  // end up saving the spec we actually used, not whatever they gave us.
955
934
  if (this[_resolvedAdd].length) {
956
- const root = this.idealTree
957
- const pkg = root.package
958
- for (const { name } of this[_resolvedAdd]) {
959
- const req = npa.resolve(name, root.edgesOut.get(name).spec, root.realpath)
935
+ for (const { name, tree: addTree } of this[_resolvedAdd]) {
936
+ // addTree either the root, or a workspace
937
+ const edge = addTree.edgesOut.get(name)
938
+ const pkg = addTree.package
939
+ const req = npa.resolve(name, edge.spec, addTree.realpath)
960
940
  const {rawSpec, subSpec} = req
961
941
 
962
942
  const spec = subSpec ? subSpec.rawSpec : rawSpec
963
- const child = root.children.get(name)
943
+ const child = edge.to
964
944
 
965
945
  let newSpec
966
946
  if (req.registry) {
@@ -999,7 +979,7 @@ module.exports = cls => class Reifier extends cls {
999
979
  // path initially, in which case we can end up with the wrong
1000
980
  // thing, so just get the ultimate fetchSpec and relativize it.
1001
981
  const p = req.fetchSpec.replace(/^file:/, '')
1002
- const rel = relpath(root.realpath, p)
982
+ const rel = relpath(addTree.realpath, p)
1003
983
  newSpec = `file:${rel}`
1004
984
  } else
1005
985
  newSpec = req.saveSpec
@@ -1031,10 +1011,9 @@ module.exports = cls => class Reifier extends cls {
1031
1011
  pkg.optionalDependencies[name] = newSpec
1032
1012
  }
1033
1013
  }
1034
- }
1035
1014
 
1036
- // refresh the edges so they have the correct specs
1037
- this.idealTree.package = pkg
1015
+ updatedTrees.add(addTree)
1016
+ }
1038
1017
  }
1039
1018
 
1040
1019
  // preserve indentation, if possible
@@ -1048,10 +1027,21 @@ module.exports = cls => class Reifier extends cls {
1048
1027
  : this[_formatPackageLock],
1049
1028
  }
1050
1029
 
1051
- return Promise.all([
1052
- this[_saveLockFile](saveOpt),
1053
- updateRootPackageJson(this.idealTree),
1054
- ]).then(() => process.emit('timeEnd', 'reify:save'))
1030
+ const promises = [this[_saveLockFile](saveOpt)]
1031
+
1032
+ // grab any from explicitRequests that had deps removed
1033
+ for (const { from: tree } of this.explicitRequests)
1034
+ updatedTrees.add(tree)
1035
+
1036
+ for (const tree of updatedTrees) {
1037
+ // refresh the edges so they have the correct specs
1038
+ tree.package = tree.package
1039
+ promises.push(updateRootPackageJson(tree))
1040
+ }
1041
+
1042
+ await Promise.all(promises)
1043
+ process.emit('timeEnd', 'reify:save')
1044
+ return true
1055
1045
  }
1056
1046
 
1057
1047
  async [_saveLockFile] (saveOpt) {
@@ -1060,12 +1050,6 @@ module.exports = cls => class Reifier extends cls {
1060
1050
 
1061
1051
  const { meta } = this.idealTree
1062
1052
 
1063
- // might have to update metadata for bins and stuff that gets lost
1064
- if (meta.loadedFromDisk && !(meta.originalLockfileVersion >= 2)) {
1065
- for (const node of this.idealTree.inventory.values())
1066
- await this[_loadAncientPackageDetails](node, true)
1067
- }
1068
-
1069
1053
  return meta.save(saveOpt)
1070
1054
  }
1071
1055
 
@@ -1085,11 +1069,11 @@ module.exports = cls => class Reifier extends cls {
1085
1069
  // Then we move the entire idealTree over to this.actualTree, and
1086
1070
  // save the hidden lockfile.
1087
1071
  if (this.diff && this.diff.filterSet.size) {
1072
+ const reroot = new Set()
1073
+
1088
1074
  const { filterSet } = this.diff
1089
1075
  const seen = new Set()
1090
1076
  for (const [loc, ideal] of this.idealTree.inventory.entries()) {
1091
- if (seen.has(loc))
1092
- continue
1093
1077
  seen.add(loc)
1094
1078
 
1095
1079
  // if it's an ideal node from the filter set, then skip it
@@ -1113,7 +1097,7 @@ module.exports = cls => class Reifier extends cls {
1113
1097
  if (isLink && ideal.isLink && ideal.realpath === realpath)
1114
1098
  continue
1115
1099
  else
1116
- actual.root = this.idealTree
1100
+ reroot.add(actual)
1117
1101
  }
1118
1102
  }
1119
1103
 
@@ -1123,12 +1107,22 @@ module.exports = cls => class Reifier extends cls {
1123
1107
  if (seen.has(loc))
1124
1108
  continue
1125
1109
  seen.add(loc)
1110
+
1111
+ // we know that this is something that ISN'T in the idealTree,
1112
+ // or else we will have addressed it in the previous loop.
1113
+ // If it's in the filterSet, that means we intentionally removed
1114
+ // it, so nothing to do here.
1126
1115
  if (filterSet.has(actual))
1127
1116
  continue
1128
- actual.root = this.idealTree
1117
+
1118
+ reroot.add(actual)
1129
1119
  }
1130
1120
 
1131
- // prune out any tops that lack a linkIn
1121
+ // go through the rerooted actual nodes, and move them over.
1122
+ for (const actual of reroot)
1123
+ actual.root = this.idealTree
1124
+
1125
+ // prune out any tops that lack a linkIn, they are no longer relevant.
1132
1126
  for (const top of this.idealTree.tops) {
1133
1127
  if (top.linksIn.size === 0)
1134
1128
  top.root = null
@@ -12,7 +12,7 @@ const _fixAvailable = Symbol('fixAvailable')
12
12
  const _checkTopNode = Symbol('checkTopNode')
13
13
  const _init = Symbol('init')
14
14
  const _omit = Symbol('omit')
15
- const procLog = require('./proc-log.js')
15
+ const procLog = require('proc-log')
16
16
 
17
17
  const fetch = require('npm-registry-fetch')
18
18
 
@@ -89,7 +89,8 @@ class AuditReport extends Map {
89
89
 
90
90
  constructor (tree, opts = {}) {
91
91
  super()
92
- this[_omit] = new Set(opts.omit || [])
92
+ const { omit } = opts
93
+ this[_omit] = new Set(omit || [])
93
94
  this.topVulns = new Map()
94
95
 
95
96
  this.calculator = new Calculator(opts)
@@ -97,6 +98,7 @@ class AuditReport extends Map {
97
98
  this.options = opts
98
99
  this.log = opts.log || procLog
99
100
  this.tree = tree
101
+ this.filterSet = opts.filterSet
100
102
  }
101
103
 
102
104
  async run () {
@@ -146,7 +148,7 @@ class AuditReport extends Map {
146
148
 
147
149
  const p = []
148
150
  for (const node of this.tree.inventory.query('packageName', name)) {
149
- if (shouldOmit(node, this[_omit]))
151
+ if (!shouldAudit(node, this[_omit], this.filterSet))
150
152
  continue
151
153
 
152
154
  // if not vulnerable by this advisory, keep searching
@@ -292,7 +294,7 @@ class AuditReport extends Map {
292
294
  try {
293
295
  try {
294
296
  // first try the super fast bulk advisory listing
295
- const body = prepareBulkData(this.tree, this[_omit])
297
+ const body = prepareBulkData(this.tree, this[_omit], this.filterSet)
296
298
  this.log.silly('audit', 'bulk request', body)
297
299
 
298
300
  // no sense asking if we don't have anything to audit,
@@ -333,22 +335,25 @@ class AuditReport extends Map {
333
335
  }
334
336
  }
335
337
 
336
- // return true if we should ignore this one
337
- const shouldOmit = (node, omit) =>
338
- !node.version ? true
339
- : node.isRoot ? true
340
- : omit.size === 0 ? false
341
- : node.dev && omit.has('dev') ||
338
+ // return true if we should audit this one
339
+ const shouldAudit = (node, omit, filterSet) =>
340
+ !node.version ? false
341
+ : node.isRoot ? false
342
+ : filterSet && filterSet.size !== 0 && !filterSet.has(node) ? false
343
+ : omit.size === 0 ? true
344
+ : !( // otherwise, just ensure we're not omitting this one
345
+ node.dev && omit.has('dev') ||
342
346
  node.optional && omit.has('optional') ||
343
347
  node.devOptional && omit.has('dev') && omit.has('optional') ||
344
348
  node.peer && omit.has('peer')
349
+ )
345
350
 
346
- const prepareBulkData = (tree, omit) => {
351
+ const prepareBulkData = (tree, omit, filterSet) => {
347
352
  const payload = {}
348
353
  for (const name of tree.inventory.query('packageName')) {
349
354
  const set = new Set()
350
355
  for (const node of tree.inventory.query('packageName', name)) {
351
- if (shouldOmit(node, omit))
356
+ if (!shouldAudit(node, omit, filterSet))
352
357
  continue
353
358
 
354
359
  set.add(node.version)
@@ -22,6 +22,11 @@ const calcDepFlagsStep = (node) => {
22
22
  // Since we're only walking through deps that are not already flagged
23
23
  // as non-dev/non-optional, it's typically a very shallow traversal
24
24
  node.extraneous = false
25
+ resetParents(node, 'extraneous')
26
+ resetParents(node, 'dev')
27
+ resetParents(node, 'peer')
28
+ resetParents(node, 'devOptional')
29
+ resetParents(node, 'optional')
25
30
 
26
31
  // for links, map their hierarchy appropriately
27
32
  if (node.target) {
@@ -29,8 +34,7 @@ const calcDepFlagsStep = (node) => {
29
34
  node.target.optional = node.optional
30
35
  node.target.devOptional = node.devOptional
31
36
  node.target.peer = node.peer
32
- node.target.extraneous = false
33
- node = node.target
37
+ return calcDepFlagsStep(node.target)
34
38
  }
35
39
 
36
40
  node.edgesOut.forEach(({peer, optional, dev, to}) => {
@@ -71,6 +75,14 @@ const calcDepFlagsStep = (node) => {
71
75
  return node
72
76
  }
73
77
 
78
+ const resetParents = (node, flag) => {
79
+ if (node[flag])
80
+ return
81
+
82
+ for (let p = node; p && (p === node || p[flag]); p = p.resolveParent)
83
+ p[flag] = false
84
+ }
85
+
74
86
  // typically a short walk, since it only traverses deps that
75
87
  // have the flag set.
76
88
  const unsetFlag = (node, flag) => {
package/lib/diff.js CHANGED
@@ -40,6 +40,7 @@ class Diff {
40
40
  // - Add set of Nodes depended on by the filterNode to filterSet.
41
41
  // - Anything outside of that set should be ignored by getChildren
42
42
  const filterSet = new Set()
43
+ const extraneous = new Set()
43
44
  for (const filterNode of filterNodes) {
44
45
  const { root } = filterNode
45
46
  if (root !== ideal && root !== actual)
@@ -72,10 +73,19 @@ class Diff {
72
73
  const actualNode = actual.inventory.get(loc)
73
74
  const actuals = !actualNode ? []
74
75
  : [...actualNode.edgesOut.values()].filter(e => e.to).map(e => e.to)
76
+ if (actualNode) {
77
+ for (const child of actualNode.children.values()) {
78
+ if (child.extraneous)
79
+ extraneous.add(child)
80
+ }
81
+ }
82
+
75
83
  return ideals.concat(actuals)
76
84
  },
77
85
  })
78
86
  }
87
+ for (const extra of extraneous)
88
+ filterSet.add(extra)
79
89
 
80
90
  return depth({
81
91
  tree: new Diff({actual, ideal, filterSet, shrinkwrapInflated}),
@@ -100,16 +110,32 @@ const getAction = ({actual, ideal}) => {
100
110
  if (ideal.isRoot && actual.isRoot)
101
111
  return null
102
112
 
113
+ // if the versions don't match, it's a change no matter what
114
+ if (ideal.version !== actual.version)
115
+ return 'CHANGE'
116
+
103
117
  const binsExist = ideal.binPaths.every((path) => existsSync(path))
104
118
 
105
119
  // top nodes, links, and git deps won't have integrity, but do have resolved
106
- if (!ideal.integrity && !actual.integrity && ideal.resolved === actual.resolved && binsExist)
120
+ // if neither node has integrity, the bins exist, and either (a) neither
121
+ // node has a resolved value or (b) they both do and match, then we can
122
+ // leave this one alone since we already know the versions match due to
123
+ // the condition above. The "neither has resolved" case (a) cannot be
124
+ // treated as a 'mark CHANGE and refetch', because shrinkwraps, bundles,
125
+ // and link deps may lack this information, and we don't want to try to
126
+ // go to the registry for something that isn't there.
127
+ const noIntegrity = !ideal.integrity && !actual.integrity
128
+ const noResolved = !ideal.resolved && !actual.resolved
129
+ const resolvedMatch = ideal.resolved && ideal.resolved === actual.resolved
130
+ if (noIntegrity && binsExist && (resolvedMatch || noResolved))
107
131
  return null
108
132
 
109
133
  // otherwise, verify that it's the same bits
110
134
  // note that if ideal has integrity, and resolved doesn't, we treat
111
135
  // that as a 'change', so that it gets re-fetched and locked down.
112
- if (!ideal.integrity || !actual.integrity || !ssri.parse(ideal.integrity).match(actual.integrity) || !binsExist)
136
+ const integrityMismatch = !ideal.integrity || !actual.integrity ||
137
+ !ssri.parse(ideal.integrity).match(actual.integrity)
138
+ if (integrityMismatch || !binsExist)
113
139
  return 'CHANGE'
114
140
 
115
141
  return null
package/lib/inventory.js CHANGED
@@ -7,6 +7,20 @@ const _index = Symbol('_index')
7
7
  const defaultKeys = ['name', 'license', 'funding', 'realpath', 'packageName']
8
8
  const { hasOwnProperty } = Object.prototype
9
9
  const debug = require('./debug.js')
10
+
11
+ // handling for the outdated "licenses" array, just pick the first one
12
+ // also support the alternative spelling "licence"
13
+ const getLicense = pkg => {
14
+ if (pkg) {
15
+ const lic = pkg.license || pkg.licence
16
+ if (lic)
17
+ return lic
18
+ const lics = pkg.licenses || pkg.licences
19
+ if (Array.isArray(lics))
20
+ return lics[0]
21
+ }
22
+ }
23
+
10
24
  class Inventory extends Map {
11
25
  constructor (opt = {}) {
12
26
  const { primary, keys } = opt
@@ -56,7 +70,9 @@ class Inventory extends Map {
56
70
  for (const [key, map] of this[_index].entries()) {
57
71
  // if the node has the value, but it's false, then use that
58
72
  const val_ = hasOwnProperty.call(node, key) ? node[key]
59
- : node[key] || (node.package && node.package[key])
73
+ : key === 'license' ? getLicense(node.package)
74
+ : node[key] ? node[key]
75
+ : node.package && node.package[key]
60
76
  const val = typeof val_ === 'string' ? val_
61
77
  : !val_ || typeof val_ !== 'object' ? val_
62
78
  : key === 'license' ? val_.type
package/lib/node.js CHANGED
@@ -354,7 +354,7 @@ class Node {
354
354
  }
355
355
 
356
356
  const why = {
357
- name: this.isProjectRoot ? this.packageName : this.name,
357
+ name: this.isProjectRoot || this.isTop ? this.packageName : this.name,
358
358
  version: this.package.version,
359
359
  }
360
360
  if (this.errors.length || !this.packageName || !this.package.version) {
@@ -380,6 +380,7 @@ class Node {
380
380
  return why
381
381
 
382
382
  why.location = this.location
383
+ why.isWorkspace = this.isWorkspace
383
384
 
384
385
  // make a new list each time. we can revisit, but not loop.
385
386
  seen = seen.concat(this)
@@ -400,6 +401,10 @@ class Node {
400
401
  for (const edge of edges)
401
402
  why.dependents.push(edge.explain(seen))
402
403
  }
404
+
405
+ if (this.linksIn.size)
406
+ why.linksIn = [...this.linksIn].map(link => link[_explain](edge, seen))
407
+
403
408
  return why
404
409
  }
405
410
 
@@ -542,6 +547,8 @@ class Node {
542
547
 
543
548
  // try to find our parent/fsParent in the new root inventory
544
549
  for (const p of walkUp(dirname(this.path))) {
550
+ if (p === this.path)
551
+ continue
545
552
  const ploc = relpath(root.realpath, p)
546
553
  const parent = root.inventory.get(ploc)
547
554
  if (parent) {
@@ -778,7 +785,13 @@ class Node {
778
785
  }
779
786
 
780
787
  get fsParent () {
781
- return this[_fsParent]
788
+ const parent = this[_fsParent]
789
+ /* istanbul ignore next - should be impossible */
790
+ debug(() => {
791
+ if (parent === this)
792
+ throw new Error('node set to its own fsParent')
793
+ })
794
+ return parent
782
795
  }
783
796
 
784
797
  set fsParent (fsParent) {
@@ -896,14 +909,14 @@ class Node {
896
909
  return false
897
910
 
898
911
  // it's a top level pkg, or a dep of one
899
- if (!this.parent || !this.parent.parent)
912
+ if (!this.resolveParent || !this.resolveParent.resolveParent)
900
913
  return false
901
914
 
902
915
  // no one wants it, remove it
903
916
  if (this.edgesIn.size === 0)
904
917
  return true
905
918
 
906
- const other = this.parent.parent.resolve(this.name)
919
+ const other = this.resolveParent.resolveParent.resolve(this.name)
907
920
 
908
921
  // nothing else, need this one
909
922
  if (!other)
@@ -1004,7 +1017,13 @@ class Node {
1004
1017
  }
1005
1018
 
1006
1019
  get parent () {
1007
- return this[_parent]
1020
+ const parent = this[_parent]
1021
+ /* istanbul ignore next - should be impossible */
1022
+ debug(() => {
1023
+ if (parent === this)
1024
+ throw new Error('node set to its own parent')
1025
+ })
1026
+ return parent
1008
1027
  }
1009
1028
 
1010
1029
  // This setter keeps everything in order when we move a node from
package/lib/shrinkwrap.js CHANGED
@@ -32,7 +32,7 @@ const mismatch = (a, b) => a && b && a !== b
32
32
  // After calling this.commit(), any nodes not present in the tree will have
33
33
  // been removed from the shrinkwrap data as well.
34
34
 
35
- const procLog = require('./proc-log.js')
35
+ const procLog = require('proc-log')
36
36
  const YarnLock = require('./yarn-lock.js')
37
37
  const {promisify} = require('util')
38
38
  const rimraf = promisify(require('rimraf'))
@@ -714,6 +714,7 @@ class Shrinkwrap {
714
714
  resolved,
715
715
  integrity,
716
716
  hasShrinkwrap,
717
+ version,
717
718
  } = this.get(node.path)
718
719
 
719
720
  const pathFixed = !resolved ? null
@@ -727,8 +728,12 @@ class Shrinkwrap {
727
728
  node.resolved === pathFixed
728
729
  const integrityOk = !integrity || !node.integrity ||
729
730
  node.integrity === integrity
731
+ const versionOk = !version || !node.version || version === node.version
730
732
 
731
- if ((resolved || integrity) && resolvedOk && integrityOk) {
733
+ const allOk = (resolved || integrity || version) &&
734
+ resolvedOk && integrityOk && versionOk
735
+
736
+ if (allOk) {
732
737
  node.resolved = node.resolved || pathFixed || null
733
738
  node.integrity = node.integrity || integrity || null
734
739
  node.hasShrinkwrap = node.hasShrinkwrap || hasShrinkwrap || false
package/lib/tracker.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const _progress = Symbol('_progress')
2
2
  const _onError = Symbol('_onError')
3
- const procLog = require('./proc-log.js')
3
+ const procLog = require('proc-log')
4
4
 
5
5
  module.exports = cls => class Tracker extends cls {
6
6
  constructor (options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "2.5.0",
3
+ "version": "2.6.3",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@npmcli/installed-package-contents": "^1.0.7",
@@ -19,9 +19,10 @@
19
19
  "npm-install-checks": "^4.0.0",
20
20
  "npm-package-arg": "^8.1.0",
21
21
  "npm-pick-manifest": "^6.1.0",
22
- "npm-registry-fetch": "^10.0.0",
22
+ "npm-registry-fetch": "^11.0.0",
23
23
  "pacote": "^11.2.6",
24
24
  "parse-conflict-json": "^1.1.1",
25
+ "proc-log": "^1.0.0",
25
26
  "promise-all-reject-late": "^1.0.0",
26
27
  "promise-call-limit": "^1.0.1",
27
28
  "read-package-json-fast": "^2.0.2",
@@ -86,5 +87,8 @@
86
87
  "--no-deprecation"
87
88
  ],
88
89
  "timeout": "240"
90
+ },
91
+ "engines": {
92
+ "node": ">= 10"
89
93
  }
90
94
  }
package/lib/proc-log.js DELETED
@@ -1,21 +0,0 @@
1
- // default logger.
2
- // emits 'log' events on the process
3
- const LEVELS = [
4
- 'notice',
5
- 'error',
6
- 'warn',
7
- 'info',
8
- 'verbose',
9
- 'http',
10
- 'silly',
11
- 'pause',
12
- 'resume',
13
- ]
14
-
15
- const log = level => (...args) => process.emit('log', level, ...args)
16
-
17
- const logger = {}
18
- for (const level of LEVELS)
19
- logger[level] = log(level)
20
-
21
- module.exports = logger