@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 +4 -0
- package/bin/prune.js +46 -0
- package/lib/arborist/audit.js +5 -2
- package/lib/arborist/build-ideal-tree.js +60 -58
- package/lib/arborist/index.js +8 -1
- package/lib/arborist/load-actual.js +16 -4
- package/lib/arborist/rebuild.js +9 -2
- package/lib/arborist/reify.js +56 -62
- package/lib/audit-report.js +17 -12
- package/lib/calc-dep-flags.js +14 -2
- package/lib/diff.js +28 -2
- package/lib/inventory.js +17 -1
- package/lib/node.js +24 -5
- package/lib/shrinkwrap.js +7 -2
- package/lib/tracker.js +1 -1
- package/package.json +6 -2
- package/lib/proc-log.js +0 -21
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
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -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 && !
|
|
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 =
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
package/lib/arborist/index.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
const {resolve} = require('path')
|
|
30
30
|
const {homedir} = require('os')
|
|
31
|
-
const procLog = require('
|
|
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
|
-
|
|
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
|
|
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
|
@@ -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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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(
|
|
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
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
1037
|
-
|
|
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
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
|
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
|
-
|
|
1117
|
+
|
|
1118
|
+
reroot.add(actual)
|
|
1129
1119
|
}
|
|
1130
1120
|
|
|
1131
|
-
//
|
|
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
|
package/lib/audit-report.js
CHANGED
|
@@ -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('
|
|
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
|
-
|
|
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/calc-dep-flags.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npmcli/arborist",
|
|
3
|
-
"version": "2.
|
|
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": "^
|
|
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
|