@npmcli/arborist 2.4.2 → 2.6.0

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/license.js CHANGED
@@ -22,7 +22,7 @@ a.loadVirtual().then(tree => {
22
22
  set.push([tree.inventory.query('license', license).size, license])
23
23
 
24
24
  for (const [count, license] of set.sort((a, b) =>
25
- a[1] && b[1] ? b[0] - a[0] || a[1].localeCompare(b[1])
25
+ a[1] && b[1] ? b[0] - a[0] || a[1].localeCompare(b[1], 'en')
26
26
  : a[1] ? -1
27
27
  : b[1] ? 1
28
28
  : 0))
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 })))
@@ -75,7 +75,7 @@ const addSingle = ({pkg, spec, saveBundle, saveType, log}) => {
75
75
  // keep it sorted, keep it unique
76
76
  const bd = new Set(pkg.bundleDependencies || [])
77
77
  bd.add(spec.name)
78
- pkg.bundleDependencies = [...bd].sort((a, b) => a.localeCompare(b))
78
+ pkg.bundleDependencies = [...bd].sort((a, b) => a.localeCompare(b, 'en'))
79
79
  }
80
80
  }
81
81
 
@@ -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
@@ -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
 
@@ -764,7 +751,7 @@ This is a one-time fix-up, please be patient...
764
751
  // sort physically shallower deps up to the front of the queue,
765
752
  // because they'll affect things deeper in, then alphabetical
766
753
  this[_depsQueue].sort((a, b) =>
767
- (a.depth - b.depth) || a.path.localeCompare(b.path))
754
+ (a.depth - b.depth) || a.path.localeCompare(b.path, 'en'))
768
755
 
769
756
  const node = this[_depsQueue].shift()
770
757
  const bd = node.package.bundleDependencies
@@ -902,7 +889,7 @@ This is a one-time fix-up, please be patient...
902
889
  }
903
890
 
904
891
  const placed = tasks
905
- .sort((a, b) => a.edge.name.localeCompare(b.edge.name))
892
+ .sort((a, b) => a.edge.name.localeCompare(b.edge.name, 'en'))
906
893
  .map(({ edge, dep }) => this[_placeDep](dep, node, edge))
907
894
 
908
895
  const promises = []
@@ -1025,7 +1012,8 @@ This is a one-time fix-up, please be patient...
1025
1012
 
1026
1013
  // also skip over any nodes in the tree that failed to load, since those
1027
1014
  // will crash the install later on anyway.
1028
- const bd = node.isProjectRoot ? null : node.package.bundleDependencies
1015
+ const bd = node.isProjectRoot || node.isWorkspace ? null
1016
+ : node.package.bundleDependencies
1029
1017
  const bundled = new Set(bd || [])
1030
1018
 
1031
1019
  return [...node.edgesOut.values()]
@@ -1061,8 +1049,8 @@ This is a one-time fix-up, please be patient...
1061
1049
  if (this[_isVulnerable](edge.to))
1062
1050
  return true
1063
1051
 
1064
- // If the user has explicitly asked to install this package, it's a problem.
1065
- if (node.isProjectRoot && this[_explicitRequests].has(edge))
1052
+ // If the user has explicitly asked to install this package, it's a "problem".
1053
+ if (this[_explicitRequests].has(edge))
1066
1054
  return true
1067
1055
 
1068
1056
  // No problems!
@@ -1147,7 +1135,7 @@ This is a one-time fix-up, please be patient...
1147
1135
  // we typically only install non-optional peers, but we have to
1148
1136
  // factor them into the peerSet so that we can avoid conflicts
1149
1137
  .filter(e => e.peer && !(e.valid && e.to))
1150
- .sort(({name: a}, {name: b}) => a.localeCompare(b))
1138
+ .sort(({name: a}, {name: b}) => a.localeCompare(b, 'en'))
1151
1139
 
1152
1140
  for (const edge of peerEdges) {
1153
1141
  // already placed this one, and we're happy with it.
@@ -1358,7 +1346,7 @@ This is a one-time fix-up, please be patient...
1358
1346
  // first in x, then in the root, ending with KEEP, because we already
1359
1347
  // have it. In that case, we ought to REMOVE the nm/x/nm/y node, because
1360
1348
  // it is an unnecessary duplicate.
1361
- this[_pruneDedupable](target, true)
1349
+ this[_pruneDedupable](target)
1362
1350
  return []
1363
1351
  }
1364
1352
 
@@ -1475,8 +1463,20 @@ This is a one-time fix-up, please be patient...
1475
1463
  return
1476
1464
  }
1477
1465
  if (descend) {
1478
- for (const child of node.children.values())
1466
+ // sort these so that they're deterministically ordered
1467
+ // otherwise, resulting tree shape is dependent on the order
1468
+ // in which they happened to be resolved.
1469
+ const nodeSort = (a, b) => a.location.localeCompare(b.location, 'en')
1470
+
1471
+ const children = [...node.children.values()].sort(nodeSort)
1472
+ const fsChildren = [...node.fsChildren].sort(nodeSort)
1473
+ for (const child of children)
1479
1474
  this[_pruneDedupable](child)
1475
+ for (const topNode of fsChildren) {
1476
+ const children = [...topNode.children.values()].sort(nodeSort)
1477
+ for (const child of children)
1478
+ this[_pruneDedupable](child)
1479
+ }
1480
1480
  }
1481
1481
  }
1482
1482
 
@@ -45,6 +45,7 @@ const mixins = [
45
45
  ]
46
46
 
47
47
  const Base = mixins.reduce((a, b) => b(a), require('events'))
48
+ const getWorkspaceNodes = require('../get-workspace-nodes.js')
48
49
 
49
50
  class Arborist extends Base {
50
51
  constructor (options = {}) {
@@ -64,6 +65,35 @@ class Arborist extends Base {
64
65
  this.path = resolve(this.options.path)
65
66
  process.emit('timeEnd', 'arborist:ctor')
66
67
  }
68
+
69
+ // returns an array of the actual nodes for all the workspaces
70
+ workspaceNodes (tree, workspaces) {
71
+ return getWorkspaceNodes(tree, workspaces, this.log)
72
+ }
73
+
74
+ // returns a set of workspace nodes and all their deps
75
+ workspaceDependencySet (tree, workspaces) {
76
+ const wsNodes = this.workspaceNodes(tree, workspaces)
77
+ const set = new Set(wsNodes)
78
+ const extraneous = new Set()
79
+ for (const node of set) {
80
+ for (const edge of node.edgesOut.values()) {
81
+ const dep = edge.to
82
+ if (dep) {
83
+ set.add(dep)
84
+ if (dep.target)
85
+ set.add(dep.target)
86
+ }
87
+ }
88
+ for (const child of node.children.values()) {
89
+ if (child.extraneous)
90
+ extraneous.add(child)
91
+ }
92
+ }
93
+ for (const extra of extraneous)
94
+ set.add(extra)
95
+ return set
96
+ }
67
97
  }
68
98
 
69
99
  module.exports = Arborist
@@ -159,12 +159,12 @@ module.exports = cls => class VirtualLoader extends cls {
159
159
  ...depsToEdges('peerOptional', peerOptional),
160
160
  ...lockWS,
161
161
  ].sort(([atype, aname], [btype, bname]) =>
162
- atype.localeCompare(btype) || aname.localeCompare(bname))
162
+ atype.localeCompare(btype, 'en') || aname.localeCompare(bname, 'en'))
163
163
 
164
164
  const rootEdges = [...root.edgesOut.values()]
165
165
  .map(e => [e.type, e.name, e.spec])
166
166
  .sort(([atype, aname], [btype, bname]) =>
167
- atype.localeCompare(btype) || aname.localeCompare(bname))
167
+ atype.localeCompare(btype, 'en') || aname.localeCompare(bname, 'en'))
168
168
 
169
169
  if (rootEdges.length !== lockEdges.length) {
170
170
  // something added or removed
@@ -14,8 +14,9 @@ const {
14
14
  } = require('@npmcli/node-gyp')
15
15
 
16
16
  const boolEnv = b => b ? '1' : ''
17
- const sortNodes = (a, b) => (a.depth - b.depth) || a.path.localeCompare(b.path)
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
@@ -133,12 +133,12 @@ module.exports = cls => class Reifier extends cls {
133
133
  this.addTracker('reify')
134
134
  process.emit('time', 'reify')
135
135
  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]())
136
+ await this[_loadTrees](options)
137
+ await this[_diffTrees]()
138
+ await this[_reifyPackages]()
139
+ await this[_saveIdealTree](options)
140
+ await this[_copyIdealToActual]()
141
+ await this[_awaitQuickAudit]()
142
142
 
143
143
  this.finishTracker('reify')
144
144
  process.emit('timeEnd', 'reify')
@@ -774,8 +774,14 @@ module.exports = cls => class Reifier extends cls {
774
774
  // NOT return the promise, as the intent is for this to run in parallel
775
775
  // with the reification, and be resolved at a later time.
776
776
  process.emit('time', 'reify:audit')
777
+ const options = { ...this.options }
778
+ const tree = this.idealTree
779
+
780
+ // if we're operating on a workspace, only audit the workspace deps
781
+ if (this[_workspaces] && this[_workspaces].length)
782
+ options.filterSet = this.workspaceDependencySet(tree, this[_workspaces])
777
783
 
778
- this.auditReport = AuditReport.load(this.idealTree, this.options)
784
+ this.auditReport = AuditReport.load(tree, options)
779
785
  .then(res => {
780
786
  process.emit('timeEnd', 'reify:audit')
781
787
  this.auditReport = res
@@ -936,7 +942,7 @@ module.exports = cls => class Reifier extends cls {
936
942
 
937
943
  // last but not least, we save the ideal tree metadata to the package-lock
938
944
  // or shrinkwrap file, and any additions or removals to package.json
939
- [_saveIdealTree] (options) {
945
+ async [_saveIdealTree] (options) {
940
946
  // the ideal tree is actualized now, hooray!
941
947
  // it still contains all the references to optional nodes that were removed
942
948
  // for install failures. Those still end up in the shrinkwrap, so we
@@ -944,23 +950,26 @@ module.exports = cls => class Reifier extends cls {
944
950
 
945
951
  // support save=false option
946
952
  if (options.save === false || this[_global] || this[_dryRun])
947
- return
953
+ return false
948
954
 
949
955
  process.emit('time', 'reify:save')
950
956
 
957
+ const updatedTrees = new Set()
958
+
951
959
  // resolvedAdd is the list of user add requests, but with names added
952
960
  // to things like git repos and tarball file/urls. However, if the
953
961
  // user requested 'foo@', and we have a foo@file:../foo, then we should
954
962
  // end up saving the spec we actually used, not whatever they gave us.
955
963
  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)
964
+ for (const { name, tree: addTree } of this[_resolvedAdd]) {
965
+ // addTree either the root, or a workspace
966
+ const edge = addTree.edgesOut.get(name)
967
+ const pkg = addTree.package
968
+ const req = npa.resolve(name, edge.spec, addTree.realpath)
960
969
  const {rawSpec, subSpec} = req
961
970
 
962
971
  const spec = subSpec ? subSpec.rawSpec : rawSpec
963
- const child = root.children.get(name)
972
+ const child = edge.to
964
973
 
965
974
  let newSpec
966
975
  if (req.registry) {
@@ -972,8 +981,15 @@ module.exports = cls => class Reifier extends cls {
972
981
  // would allow versions outside the requested range. Tags and
973
982
  // specific versions save with the save-prefix.
974
983
  const isRange = (subSpec || req).type === 'range'
975
- const range = !isRange || subset(prefixRange, spec, { loose: true })
976
- ? prefixRange : spec
984
+
985
+ let range = spec
986
+ if (
987
+ !isRange ||
988
+ spec === '*' ||
989
+ subset(prefixRange, spec, { loose: true })
990
+ )
991
+ range = prefixRange
992
+
977
993
  const pname = child.packageName
978
994
  const alias = name !== pname
979
995
  newSpec = alias ? `npm:${pname}@${range}` : range
@@ -992,7 +1008,7 @@ module.exports = cls => class Reifier extends cls {
992
1008
  // path initially, in which case we can end up with the wrong
993
1009
  // thing, so just get the ultimate fetchSpec and relativize it.
994
1010
  const p = req.fetchSpec.replace(/^file:/, '')
995
- const rel = relpath(root.realpath, p)
1011
+ const rel = relpath(addTree.realpath, p)
996
1012
  newSpec = `file:${rel}`
997
1013
  } else
998
1014
  newSpec = req.saveSpec
@@ -1024,10 +1040,9 @@ module.exports = cls => class Reifier extends cls {
1024
1040
  pkg.optionalDependencies[name] = newSpec
1025
1041
  }
1026
1042
  }
1027
- }
1028
1043
 
1029
- // refresh the edges so they have the correct specs
1030
- this.idealTree.package = pkg
1044
+ updatedTrees.add(addTree)
1045
+ }
1031
1046
  }
1032
1047
 
1033
1048
  // preserve indentation, if possible
@@ -1041,10 +1056,21 @@ module.exports = cls => class Reifier extends cls {
1041
1056
  : this[_formatPackageLock],
1042
1057
  }
1043
1058
 
1044
- return Promise.all([
1045
- this[_saveLockFile](saveOpt),
1046
- updateRootPackageJson(this.idealTree),
1047
- ]).then(() => process.emit('timeEnd', 'reify:save'))
1059
+ const promises = [this[_saveLockFile](saveOpt)]
1060
+
1061
+ // grab any from explicitRequests that had deps removed
1062
+ for (const { from: tree } of this.explicitRequests)
1063
+ updatedTrees.add(tree)
1064
+
1065
+ for (const tree of updatedTrees) {
1066
+ // refresh the edges so they have the correct specs
1067
+ tree.package = tree.package
1068
+ promises.push(updateRootPackageJson(tree))
1069
+ }
1070
+
1071
+ await Promise.all(promises)
1072
+ process.emit('timeEnd', 'reify:save')
1073
+ return true
1048
1074
  }
1049
1075
 
1050
1076
  async [_saveLockFile] (saveOpt) {
@@ -1078,11 +1104,11 @@ module.exports = cls => class Reifier extends cls {
1078
1104
  // Then we move the entire idealTree over to this.actualTree, and
1079
1105
  // save the hidden lockfile.
1080
1106
  if (this.diff && this.diff.filterSet.size) {
1107
+ const reroot = new Set()
1108
+
1081
1109
  const { filterSet } = this.diff
1082
1110
  const seen = new Set()
1083
1111
  for (const [loc, ideal] of this.idealTree.inventory.entries()) {
1084
- if (seen.has(loc))
1085
- continue
1086
1112
  seen.add(loc)
1087
1113
 
1088
1114
  // if it's an ideal node from the filter set, then skip it
@@ -1106,7 +1132,7 @@ module.exports = cls => class Reifier extends cls {
1106
1132
  if (isLink && ideal.isLink && ideal.realpath === realpath)
1107
1133
  continue
1108
1134
  else
1109
- actual.root = this.idealTree
1135
+ reroot.add(actual)
1110
1136
  }
1111
1137
  }
1112
1138
 
@@ -1116,12 +1142,22 @@ module.exports = cls => class Reifier extends cls {
1116
1142
  if (seen.has(loc))
1117
1143
  continue
1118
1144
  seen.add(loc)
1145
+
1146
+ // we know that this is something that ISN'T in the idealTree,
1147
+ // or else we will have addressed it in the previous loop.
1148
+ // If it's in the filterSet, that means we intentionally removed
1149
+ // it, so nothing to do here.
1119
1150
  if (filterSet.has(actual))
1120
1151
  continue
1121
- actual.root = this.idealTree
1152
+
1153
+ reroot.add(actual)
1122
1154
  }
1123
1155
 
1124
- // prune out any tops that lack a linkIn
1156
+ // go through the rerooted actual nodes, and move them over.
1157
+ for (const actual of reroot)
1158
+ actual.root = this.idealTree
1159
+
1160
+ // prune out any tops that lack a linkIn, they are no longer relevant.
1125
1161
  for (const top of this.idealTree.tops) {
1126
1162
  if (top.linksIn.size === 0)
1127
1163
  top.root = null
@@ -78,7 +78,7 @@ class AuditReport extends Map {
78
78
  }
79
79
 
80
80
  obj.vulnerabilities = vulnerabilities
81
- .sort(([a], [b]) => a.localeCompare(b))
81
+ .sort(([a], [b]) => a.localeCompare(b, 'en'))
82
82
  .reduce((set, [name, vuln]) => {
83
83
  set[name] = vuln
84
84
  return set
@@ -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)
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}),
@@ -0,0 +1,33 @@
1
+ // Get the actual nodes corresponding to a root node's child workspaces,
2
+ // given a list of workspace names.
3
+ const relpath = require('./relpath.js')
4
+ const getWorkspaceNodes = (tree, workspaces, log) => {
5
+ const wsMap = tree.workspaces
6
+ if (!wsMap) {
7
+ log.warn('workspaces', 'filter set, but no workspaces present')
8
+ return []
9
+ }
10
+
11
+ const nodes = []
12
+ for (const name of workspaces) {
13
+ const path = wsMap.get(name)
14
+ if (!path) {
15
+ log.warn('workspaces', `${name} in filter set, but not in workspaces`)
16
+ continue
17
+ }
18
+
19
+ const loc = relpath(tree.realpath, path)
20
+ const node = tree.inventory.get(loc)
21
+
22
+ if (!node) {
23
+ log.warn('workspaces', `${name} in filter set, but no workspace folder present`)
24
+ continue
25
+ }
26
+
27
+ nodes.push(node)
28
+ }
29
+
30
+ return nodes
31
+ }
32
+
33
+ module.exports = getWorkspaceNodes
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
 
@@ -896,14 +901,14 @@ class Node {
896
901
  return false
897
902
 
898
903
  // it's a top level pkg, or a dep of one
899
- if (!this.parent || !this.parent.parent)
904
+ if (!this.resolveParent || !this.resolveParent.resolveParent)
900
905
  return false
901
906
 
902
907
  // no one wants it, remove it
903
908
  if (this.edgesIn.size === 0)
904
909
  return true
905
910
 
906
- const other = this.parent.parent.resolve(this.name)
911
+ const other = this.resolveParent.resolveParent.resolve(this.name)
907
912
 
908
913
  // nothing else, need this one
909
914
  if (!other)
package/lib/printable.js CHANGED
@@ -46,14 +46,14 @@ class ArboristNode {
46
46
  // edgesOut sorted by name
47
47
  if (tree.edgesOut.size) {
48
48
  this.edgesOut = new Map([...tree.edgesOut.entries()]
49
- .sort(([a], [b]) => a.localeCompare(b))
49
+ .sort(([a], [b]) => a.localeCompare(b, 'en'))
50
50
  .map(([name, edge]) => [name, new EdgeOut(edge)]))
51
51
  }
52
52
 
53
53
  // edgesIn sorted by location
54
54
  if (tree.edgesIn.size) {
55
55
  this.edgesIn = new Set([...tree.edgesIn]
56
- .sort((a, b) => a.from.location.localeCompare(b.from.location))
56
+ .sort((a, b) => a.from.location.localeCompare(b.from.location, 'en'))
57
57
  .map(edge => new EdgeIn(edge)))
58
58
  }
59
59
 
@@ -65,14 +65,14 @@ class ArboristNode {
65
65
  // fsChildren sorted by path
66
66
  if (tree.fsChildren.size) {
67
67
  this.fsChildren = new Set([...tree.fsChildren]
68
- .sort(({path: a}, {path: b}) => a.localeCompare(b))
68
+ .sort(({path: a}, {path: b}) => a.localeCompare(b, 'en'))
69
69
  .map(tree => printableTree(tree, path)))
70
70
  }
71
71
 
72
72
  // children sorted by name
73
73
  if (tree.children.size) {
74
74
  this.children = new Map([...tree.children.entries()]
75
- .sort(([a], [b]) => a.localeCompare(b))
75
+ .sort(([a], [b]) => a.localeCompare(b, 'en'))
76
76
  .map(([name, tree]) => [name, printableTree(tree, path)]))
77
77
  }
78
78
  }
package/lib/shrinkwrap.js CHANGED
@@ -844,7 +844,7 @@ class Shrinkwrap {
844
844
  /* istanbul ignore next - sort calling order is indeterminate */
845
845
  return aloc.length > bloc.length ? 1
846
846
  : bloc.length > aloc.length ? -1
847
- : aloc[aloc.length - 1].localeCompare(bloc[bloc.length - 1])
847
+ : aloc[aloc.length - 1].localeCompare(bloc[bloc.length - 1], 'en')
848
848
  })[0]
849
849
 
850
850
  const res = consistentResolve(node.resolved, this.path, this.path, true)
@@ -18,7 +18,7 @@ const orderDeps = (pkg) => {
18
18
  for (const type of depTypes) {
19
19
  if (pkg && pkg[type]) {
20
20
  pkg[type] = Object.keys(pkg[type])
21
- .sort((a, b) => a.localeCompare(b))
21
+ .sort((a, b) => a.localeCompare(b, 'en'))
22
22
  .reduce((res, key) => {
23
23
  res[key] = pkg[type][key]
24
24
  return res
package/lib/vuln.js CHANGED
@@ -106,12 +106,12 @@ class Vuln {
106
106
  vulnerableVersions: undefined,
107
107
  id: undefined,
108
108
  }).sort((a, b) =>
109
- String(a.source || a).localeCompare(String(b.source || b))),
109
+ String(a.source || a).localeCompare(String(b.source || b, 'en'))),
110
110
  effects: [...this.effects].map(v => v.name)
111
- .sort(/* istanbul ignore next */(a, b) => a.localeCompare(b)),
111
+ .sort(/* istanbul ignore next */(a, b) => a.localeCompare(b, 'en')),
112
112
  range: this.simpleRange,
113
113
  nodes: [...this.nodes].map(n => n.location)
114
- .sort(/* istanbul ignore next */(a, b) => a.localeCompare(b)),
114
+ .sort(/* istanbul ignore next */(a, b) => a.localeCompare(b, 'en')),
115
115
  fixAvailable: this[_fixAvailable],
116
116
  }
117
117
  }
package/lib/yarn-lock.js CHANGED
@@ -34,7 +34,7 @@ const {breadth} = require('treeverse')
34
34
 
35
35
  // sort a key/value object into a string of JSON stringified keys and vals
36
36
  const sortKV = obj => Object.keys(obj)
37
- .sort((a, b) => a.localeCompare(b))
37
+ .sort((a, b) => a.localeCompare(b, 'en'))
38
38
  .map(k => ` ${JSON.stringify(k)} ${JSON.stringify(obj[k])}`)
39
39
  .join('\n')
40
40
 
@@ -165,7 +165,7 @@ class YarnLock {
165
165
  toString () {
166
166
  return prefix + [...new Set([...this.entries.values()])]
167
167
  .map(e => e.toString())
168
- .sort((a, b) => a.localeCompare(b)).join('\n\n') + '\n'
168
+ .sort((a, b) => a.localeCompare(b, 'en')).join('\n\n') + '\n'
169
169
  }
170
170
 
171
171
  fromTree (tree) {
@@ -175,7 +175,7 @@ class YarnLock {
175
175
  tree,
176
176
  visit: node => this.addEntryFromNode(node),
177
177
  getChildren: node => [...node.children.values(), ...node.fsChildren]
178
- .sort((a, b) => a.depth - b.depth || a.name.localeCompare(b.name)),
178
+ .sort((a, b) => a.depth - b.depth || a.name.localeCompare(b.name, 'en')),
179
179
  })
180
180
  return this
181
181
  }
@@ -183,7 +183,7 @@ class YarnLock {
183
183
  addEntryFromNode (node) {
184
184
  const specs = [...node.edgesIn]
185
185
  .map(e => `${node.name}@${e.spec}`)
186
- .sort((a, b) => a.localeCompare(b))
186
+ .sort((a, b) => a.localeCompare(b, 'en'))
187
187
 
188
188
  // Note:
189
189
  // yarn will do excessive duplication in a case like this:
@@ -309,7 +309,7 @@ class YarnLockEntry {
309
309
  toString () {
310
310
  // sort objects to the bottom, then alphabetical
311
311
  return ([...this[_specs]]
312
- .sort((a, b) => a.localeCompare(b))
312
+ .sort((a, b) => a.localeCompare(b, 'en'))
313
313
  .map(JSON.stringify).join(', ') +
314
314
  ':\n' +
315
315
  Object.getOwnPropertyNames(this)
@@ -318,7 +318,7 @@ class YarnLockEntry {
318
318
  (a, b) =>
319
319
  /* istanbul ignore next - sort call order is unpredictable */
320
320
  (typeof this[a] === 'object') === (typeof this[b] === 'object')
321
- ? a.localeCompare(b)
321
+ ? a.localeCompare(b, 'en')
322
322
  : typeof this[a] === 'object' ? 1 : -1)
323
323
  .map(prop =>
324
324
  typeof this[prop] !== 'object'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "2.4.2",
3
+ "version": "2.6.0",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@npmcli/installed-package-contents": "^1.0.7",
@@ -14,7 +14,7 @@
14
14
  "cacache": "^15.0.3",
15
15
  "common-ancestor-path": "^1.0.1",
16
16
  "json-parse-even-better-errors": "^2.3.1",
17
- "json-stringify-nice": "^1.1.2",
17
+ "json-stringify-nice": "^1.1.4",
18
18
  "mkdirp-infer-owner": "^2.0.0",
19
19
  "npm-install-checks": "^4.0.0",
20
20
  "npm-package-arg": "^8.1.0",
@@ -40,9 +40,8 @@
40
40
  "eslint-plugin-promise": "^4.2.1",
41
41
  "eslint-plugin-standard": "^4.0.1",
42
42
  "minify-registry-metadata": "^2.1.0",
43
- "mutate-fs": "^2.1.1",
44
- "tap": "^15.0.4",
45
- "tcompare": "^3.0.4"
43
+ "tap": "^15.0.9",
44
+ "tcompare": "^5.0.6"
46
45
  },
47
46
  "scripts": {
48
47
  "test": "npm run test-only --",
@@ -74,16 +73,21 @@
74
73
  "bin": {
75
74
  "arborist": "bin/index.js"
76
75
  },
76
+ "//": "sk test-env locale to catch locale-specific sorting",
77
77
  "tap": {
78
78
  "after": "test/fixtures/cleanup.js",
79
79
  "coverage-map": "map.js",
80
80
  "test-env": [
81
- "NODE_OPTIONS=--no-warnings"
81
+ "NODE_OPTIONS=--no-warnings",
82
+ "LC_ALL=sk"
82
83
  ],
83
84
  "node-arg": [
84
85
  "--no-warnings",
85
86
  "--no-deprecation"
86
87
  ],
87
88
  "timeout": "240"
89
+ },
90
+ "engines": {
91
+ "node": ">= 10"
88
92
  }
89
93
  }
package/CHANGELOG.md DELETED
@@ -1,19 +0,0 @@
1
- # CHANGELOG
2
-
3
- ## 2.0
4
-
5
- * BREAKING CHANGE: root node is now included in inventory
6
- * All parent/target/fsParent/etc. references set in `root` setter, rather
7
- than the hodgepodge of setters that existed before.
8
- * `treeCheck` function added, to enforce strict correctness guarantees when
9
- `ARBORIST_DEBUG=1` in the environment (on by default in Arborist tests).
10
-
11
- ## 1.0
12
-
13
- * Release for npm v7 beta
14
- * Fully functional
15
-
16
- ## 0.0
17
-
18
- * Proof of concept
19
- * Before this, it was [`read-package-tree`](http://npm.im/read-package-tree)