@npmcli/arborist 2.5.0 → 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 +4 -0
- package/bin/prune.js +46 -0
- package/lib/arborist/audit.js +5 -2
- package/lib/arborist/build-ideal-tree.js +52 -52
- package/lib/arborist/index.js +7 -0
- package/lib/arborist/rebuild.js +9 -2
- package/lib/arborist/reify.js +56 -27
- package/lib/audit-report.js +16 -11
- package/lib/diff.js +10 -0
- package/lib/node.js +8 -3
- package/package.json +4 -1
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 })))
|
package/lib/arborist/audit.js
CHANGED
|
@@ -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
|
|
25
|
-
|
|
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
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
493
|
-
// to be resolved relative to the CWD the user is in.
|
|
494
|
-
|
|
495
|
-
.then(
|
|
496
|
-
.then(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
|
@@ -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
|
|
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 (
|
|
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!
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
package/lib/arborist/index.js
CHANGED
|
@@ -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
|
}
|
package/lib/arborist/rebuild.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/lib/arborist/reify.js
CHANGED
|
@@ -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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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(
|
|
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
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const
|
|
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 =
|
|
972
|
+
const child = edge.to
|
|
964
973
|
|
|
965
974
|
let newSpec
|
|
966
975
|
if (req.registry) {
|
|
@@ -999,7 +1008,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
999
1008
|
// path initially, in which case we can end up with the wrong
|
|
1000
1009
|
// thing, so just get the ultimate fetchSpec and relativize it.
|
|
1001
1010
|
const p = req.fetchSpec.replace(/^file:/, '')
|
|
1002
|
-
const rel = relpath(
|
|
1011
|
+
const rel = relpath(addTree.realpath, p)
|
|
1003
1012
|
newSpec = `file:${rel}`
|
|
1004
1013
|
} else
|
|
1005
1014
|
newSpec = req.saveSpec
|
|
@@ -1031,10 +1040,9 @@ module.exports = cls => class Reifier extends cls {
|
|
|
1031
1040
|
pkg.optionalDependencies[name] = newSpec
|
|
1032
1041
|
}
|
|
1033
1042
|
}
|
|
1034
|
-
}
|
|
1035
1043
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1044
|
+
updatedTrees.add(addTree)
|
|
1045
|
+
}
|
|
1038
1046
|
}
|
|
1039
1047
|
|
|
1040
1048
|
// preserve indentation, if possible
|
|
@@ -1048,10 +1056,21 @@ module.exports = cls => class Reifier extends cls {
|
|
|
1048
1056
|
: this[_formatPackageLock],
|
|
1049
1057
|
}
|
|
1050
1058
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
|
1055
1074
|
}
|
|
1056
1075
|
|
|
1057
1076
|
async [_saveLockFile] (saveOpt) {
|
|
@@ -1085,11 +1104,11 @@ module.exports = cls => class Reifier extends cls {
|
|
|
1085
1104
|
// Then we move the entire idealTree over to this.actualTree, and
|
|
1086
1105
|
// save the hidden lockfile.
|
|
1087
1106
|
if (this.diff && this.diff.filterSet.size) {
|
|
1107
|
+
const reroot = new Set()
|
|
1108
|
+
|
|
1088
1109
|
const { filterSet } = this.diff
|
|
1089
1110
|
const seen = new Set()
|
|
1090
1111
|
for (const [loc, ideal] of this.idealTree.inventory.entries()) {
|
|
1091
|
-
if (seen.has(loc))
|
|
1092
|
-
continue
|
|
1093
1112
|
seen.add(loc)
|
|
1094
1113
|
|
|
1095
1114
|
// if it's an ideal node from the filter set, then skip it
|
|
@@ -1113,7 +1132,7 @@ module.exports = cls => class Reifier extends cls {
|
|
|
1113
1132
|
if (isLink && ideal.isLink && ideal.realpath === realpath)
|
|
1114
1133
|
continue
|
|
1115
1134
|
else
|
|
1116
|
-
actual
|
|
1135
|
+
reroot.add(actual)
|
|
1117
1136
|
}
|
|
1118
1137
|
}
|
|
1119
1138
|
|
|
@@ -1123,12 +1142,22 @@ module.exports = cls => class Reifier extends cls {
|
|
|
1123
1142
|
if (seen.has(loc))
|
|
1124
1143
|
continue
|
|
1125
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.
|
|
1126
1150
|
if (filterSet.has(actual))
|
|
1127
1151
|
continue
|
|
1128
|
-
|
|
1152
|
+
|
|
1153
|
+
reroot.add(actual)
|
|
1129
1154
|
}
|
|
1130
1155
|
|
|
1131
|
-
//
|
|
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.
|
|
1132
1161
|
for (const top of this.idealTree.tops) {
|
|
1133
1162
|
if (top.linksIn.size === 0)
|
|
1134
1163
|
top.root = null
|
package/lib/audit-report.js
CHANGED
|
@@ -89,7 +89,8 @@ class AuditReport extends Map {
|
|
|
89
89
|
|
|
90
90
|
constructor (tree, opts = {}) {
|
|
91
91
|
super()
|
|
92
|
-
|
|
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 (
|
|
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
|
|
337
|
-
const
|
|
338
|
-
!node.version ?
|
|
339
|
-
: node.isRoot ?
|
|
340
|
-
:
|
|
341
|
-
:
|
|
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 (
|
|
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}),
|
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.
|
|
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.
|
|
911
|
+
const other = this.resolveParent.resolveParent.resolve(this.name)
|
|
907
912
|
|
|
908
913
|
// nothing else, need this one
|
|
909
914
|
if (!other)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npmcli/arborist",
|
|
3
|
-
"version": "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",
|
|
@@ -86,5 +86,8 @@
|
|
|
86
86
|
"--no-deprecation"
|
|
87
87
|
],
|
|
88
88
|
"timeout": "240"
|
|
89
|
+
},
|
|
90
|
+
"engines": {
|
|
91
|
+
"node": ">= 10"
|
|
89
92
|
}
|
|
90
93
|
}
|