@npmcli/arborist 2.7.1 → 2.8.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/actual.js +4 -2
- package/bin/audit.js +12 -6
- package/bin/dedupe.js +49 -0
- package/bin/funding.js +4 -2
- package/bin/ideal.js +2 -1
- package/bin/lib/logging.js +4 -3
- package/bin/lib/options.js +14 -12
- package/bin/lib/timers.js +6 -3
- package/bin/license.js +9 -5
- package/bin/prune.js +6 -3
- package/bin/reify.js +6 -3
- package/bin/virtual.js +4 -2
- package/lib/add-rm-pkg-deps.js +25 -14
- package/lib/arborist/audit.js +2 -1
- package/lib/arborist/build-ideal-tree.js +246 -757
- package/lib/arborist/deduper.js +2 -1
- package/lib/arborist/index.js +8 -4
- package/lib/arborist/load-actual.js +32 -15
- package/lib/arborist/load-virtual.js +34 -18
- package/lib/arborist/load-workspaces.js +4 -2
- package/lib/arborist/rebuild.js +31 -16
- package/lib/arborist/reify.js +332 -119
- package/lib/audit-report.js +42 -22
- package/lib/calc-dep-flags.js +18 -9
- package/lib/can-place-dep.js +430 -0
- package/lib/case-insensitive-map.js +50 -0
- package/lib/consistent-resolve.js +2 -1
- package/lib/deepest-nesting-target.js +18 -0
- package/lib/dep-valid.js +8 -4
- package/lib/diff.js +74 -22
- package/lib/edge.js +29 -14
- package/lib/gather-dep-set.js +2 -1
- package/lib/inventory.js +12 -6
- package/lib/link.js +14 -9
- package/lib/node.js +269 -118
- package/lib/optional-set.js +4 -2
- package/lib/peer-entry-sets.js +77 -0
- package/lib/place-dep.js +578 -0
- package/lib/printable.js +48 -18
- package/lib/realpath.js +12 -6
- package/lib/shrinkwrap.js +168 -91
- package/lib/signal-handling.js +6 -3
- package/lib/spec-from-lock.js +7 -4
- package/lib/tracker.js +24 -18
- package/lib/tree-check.js +12 -6
- package/lib/version-from-tgz.js +4 -2
- package/lib/vuln.js +28 -16
- package/lib/yarn-lock.js +27 -15
- package/package.json +9 -13
- package/lib/peer-set.js +0 -25
package/lib/audit-report.js
CHANGED
|
@@ -63,8 +63,9 @@ class AuditReport extends Map {
|
|
|
63
63
|
prod = false
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
if (prod)
|
|
66
|
+
if (prod) {
|
|
67
67
|
dependencies.prod++
|
|
68
|
+
}
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
// if it doesn't have any topVulns, then it's fixable with audit fix
|
|
@@ -104,8 +105,9 @@ class AuditReport extends Map {
|
|
|
104
105
|
async run () {
|
|
105
106
|
this.report = await this[_getReport]()
|
|
106
107
|
this.log.silly('audit report', this.report)
|
|
107
|
-
if (this.report)
|
|
108
|
+
if (this.report) {
|
|
108
109
|
await this[_init]()
|
|
110
|
+
}
|
|
109
111
|
return this
|
|
110
112
|
}
|
|
111
113
|
|
|
@@ -119,8 +121,9 @@ class AuditReport extends Map {
|
|
|
119
121
|
|
|
120
122
|
const promises = []
|
|
121
123
|
for (const [name, advisories] of Object.entries(this.report)) {
|
|
122
|
-
for (const advisory of advisories)
|
|
124
|
+
for (const advisory of advisories) {
|
|
123
125
|
promises.push(this.calculator.calculate(name, advisory))
|
|
126
|
+
}
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
// now the advisories are calculated with a set of versions
|
|
@@ -136,43 +139,51 @@ class AuditReport extends Map {
|
|
|
136
139
|
// adding multiple advisories with the same range is fine, but no
|
|
137
140
|
// need to search for nodes we already would have added.
|
|
138
141
|
const k = `${name}@${range}`
|
|
139
|
-
if (seen.has(k))
|
|
142
|
+
if (seen.has(k)) {
|
|
140
143
|
continue
|
|
144
|
+
}
|
|
141
145
|
|
|
142
146
|
seen.add(k)
|
|
143
147
|
|
|
144
148
|
const vuln = this.get(name) || new Vuln({ name, advisory })
|
|
145
|
-
if (this.has(name))
|
|
149
|
+
if (this.has(name)) {
|
|
146
150
|
vuln.addAdvisory(advisory)
|
|
151
|
+
}
|
|
147
152
|
super.set(name, vuln)
|
|
148
153
|
|
|
149
154
|
const p = []
|
|
150
155
|
for (const node of this.tree.inventory.query('packageName', name)) {
|
|
151
|
-
if (!shouldAudit(node, this[_omit], this.filterSet))
|
|
156
|
+
if (!shouldAudit(node, this[_omit], this.filterSet)) {
|
|
152
157
|
continue
|
|
158
|
+
}
|
|
153
159
|
|
|
154
160
|
// if not vulnerable by this advisory, keep searching
|
|
155
|
-
if (!advisory.testVersion(node.version))
|
|
161
|
+
if (!advisory.testVersion(node.version)) {
|
|
156
162
|
continue
|
|
163
|
+
}
|
|
157
164
|
|
|
158
165
|
// we will have loaded the source already if this is a metavuln
|
|
159
|
-
if (advisory.type === 'metavuln')
|
|
166
|
+
if (advisory.type === 'metavuln') {
|
|
160
167
|
vuln.addVia(this.get(advisory.dependency))
|
|
168
|
+
}
|
|
161
169
|
|
|
162
170
|
// already marked this one, no need to do it again
|
|
163
|
-
if (vuln.nodes.has(node))
|
|
171
|
+
if (vuln.nodes.has(node)) {
|
|
164
172
|
continue
|
|
173
|
+
}
|
|
165
174
|
|
|
166
175
|
// haven't marked this one yet. get its dependents.
|
|
167
176
|
vuln.nodes.add(node)
|
|
168
177
|
for (const { from: dep, spec } of node.edgesIn) {
|
|
169
|
-
if (dep.isTop && !vuln.topNodes.has(dep))
|
|
178
|
+
if (dep.isTop && !vuln.topNodes.has(dep)) {
|
|
170
179
|
this[_checkTopNode](dep, vuln, spec)
|
|
171
|
-
else {
|
|
180
|
+
} else {
|
|
172
181
|
// calculate a metavuln, if necessary
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
const calc = this.calculator.calculate(dep.packageName, advisory)
|
|
183
|
+
p.push(calc.then(meta => {
|
|
184
|
+
if (meta.testVersion(dep.version, spec)) {
|
|
175
185
|
advisories.add(meta)
|
|
186
|
+
}
|
|
176
187
|
}))
|
|
177
188
|
}
|
|
178
189
|
}
|
|
@@ -193,9 +204,11 @@ class AuditReport extends Map {
|
|
|
193
204
|
// the nodes it references, then remove it from the advisory list.
|
|
194
205
|
// happens when using omit with old audit endpoint.
|
|
195
206
|
for (const advisory of vuln.advisories) {
|
|
196
|
-
const relevant = [...vuln.nodes]
|
|
197
|
-
|
|
207
|
+
const relevant = [...vuln.nodes]
|
|
208
|
+
.some(n => advisory.testVersion(n.version))
|
|
209
|
+
if (!relevant) {
|
|
198
210
|
vuln.deleteAdvisory(advisory)
|
|
211
|
+
}
|
|
199
212
|
}
|
|
200
213
|
}
|
|
201
214
|
process.emit('timeEnd', 'auditReport:init')
|
|
@@ -221,18 +234,21 @@ class AuditReport extends Map {
|
|
|
221
234
|
// this will always be set to at least {name, versions:{}}
|
|
222
235
|
const paku = vuln.packument
|
|
223
236
|
|
|
224
|
-
if (!vuln.testSpec(spec))
|
|
237
|
+
if (!vuln.testSpec(spec)) {
|
|
225
238
|
return true
|
|
239
|
+
}
|
|
226
240
|
|
|
227
241
|
// similarly, even if we HAVE a packument, but we're looking for it
|
|
228
242
|
// somewhere other than the registry, and we got something vulnerable,
|
|
229
243
|
// then we're stuck with it.
|
|
230
244
|
const specObj = npa(spec)
|
|
231
|
-
if (!specObj.registry)
|
|
245
|
+
if (!specObj.registry) {
|
|
232
246
|
return false
|
|
247
|
+
}
|
|
233
248
|
|
|
234
|
-
if (specObj.subSpec)
|
|
249
|
+
if (specObj.subSpec) {
|
|
235
250
|
spec = specObj.subSpec.rawSpec
|
|
251
|
+
}
|
|
236
252
|
|
|
237
253
|
// We don't provide fixes for top nodes other than root, but we
|
|
238
254
|
// still check to see if the node is fixable with a different version,
|
|
@@ -287,8 +303,9 @@ class AuditReport extends Map {
|
|
|
287
303
|
|
|
288
304
|
async [_getReport] () {
|
|
289
305
|
// if we're not auditing, just return false
|
|
290
|
-
if (this.options.audit === false || this.tree.inventory.size === 1)
|
|
306
|
+
if (this.options.audit === false || this.tree.inventory.size === 1) {
|
|
291
307
|
return null
|
|
308
|
+
}
|
|
292
309
|
|
|
293
310
|
process.emit('time', 'auditReport:getReport')
|
|
294
311
|
try {
|
|
@@ -299,8 +316,9 @@ class AuditReport extends Map {
|
|
|
299
316
|
|
|
300
317
|
// no sense asking if we don't have anything to audit,
|
|
301
318
|
// we know it'll be empty
|
|
302
|
-
if (!Object.keys(body).length)
|
|
319
|
+
if (!Object.keys(body).length) {
|
|
303
320
|
return null
|
|
321
|
+
}
|
|
304
322
|
|
|
305
323
|
const res = await fetch('/-/npm/v1/security/advisories/bulk', {
|
|
306
324
|
...this.options,
|
|
@@ -353,13 +371,15 @@ const prepareBulkData = (tree, omit, filterSet) => {
|
|
|
353
371
|
for (const name of tree.inventory.query('packageName')) {
|
|
354
372
|
const set = new Set()
|
|
355
373
|
for (const node of tree.inventory.query('packageName', name)) {
|
|
356
|
-
if (!shouldAudit(node, omit, filterSet))
|
|
374
|
+
if (!shouldAudit(node, omit, filterSet)) {
|
|
357
375
|
continue
|
|
376
|
+
}
|
|
358
377
|
|
|
359
378
|
set.add(node.version)
|
|
360
379
|
}
|
|
361
|
-
if (set.size)
|
|
380
|
+
if (set.size) {
|
|
362
381
|
payload[name] = [...set]
|
|
382
|
+
}
|
|
363
383
|
}
|
|
364
384
|
return payload
|
|
365
385
|
}
|
package/lib/calc-dep-flags.js
CHANGED
|
@@ -11,7 +11,8 @@ const calcDepFlags = (tree, resetRoot = true) => {
|
|
|
11
11
|
tree,
|
|
12
12
|
visit: node => calcDepFlagsStep(node),
|
|
13
13
|
filter: node => node,
|
|
14
|
-
getChildren: (node, tree) =>
|
|
14
|
+
getChildren: (node, tree) =>
|
|
15
|
+
[...tree.edgesOut.values()].map(edge => edge.to),
|
|
15
16
|
})
|
|
16
17
|
return ret
|
|
17
18
|
}
|
|
@@ -39,8 +40,9 @@ const calcDepFlagsStep = (node) => {
|
|
|
39
40
|
|
|
40
41
|
node.edgesOut.forEach(({peer, optional, dev, to}) => {
|
|
41
42
|
// if the dep is missing, then its flags are already maximally unset
|
|
42
|
-
if (!to)
|
|
43
|
+
if (!to) {
|
|
43
44
|
return
|
|
45
|
+
}
|
|
44
46
|
|
|
45
47
|
// everything with any kind of edge into it is not extraneous
|
|
46
48
|
to.extraneous = false
|
|
@@ -59,28 +61,34 @@ const calcDepFlagsStep = (node) => {
|
|
|
59
61
|
!node.optional && !optional
|
|
60
62
|
const unsetPeer = !node.peer && !peer
|
|
61
63
|
|
|
62
|
-
if (unsetPeer)
|
|
64
|
+
if (unsetPeer) {
|
|
63
65
|
unsetFlag(to, 'peer')
|
|
66
|
+
}
|
|
64
67
|
|
|
65
|
-
if (unsetDevOpt)
|
|
68
|
+
if (unsetDevOpt) {
|
|
66
69
|
unsetFlag(to, 'devOptional')
|
|
70
|
+
}
|
|
67
71
|
|
|
68
|
-
if (unsetDev)
|
|
72
|
+
if (unsetDev) {
|
|
69
73
|
unsetFlag(to, 'dev')
|
|
74
|
+
}
|
|
70
75
|
|
|
71
|
-
if (unsetOpt)
|
|
76
|
+
if (unsetOpt) {
|
|
72
77
|
unsetFlag(to, 'optional')
|
|
78
|
+
}
|
|
73
79
|
})
|
|
74
80
|
|
|
75
81
|
return node
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
const resetParents = (node, flag) => {
|
|
79
|
-
if (node[flag])
|
|
85
|
+
if (node[flag]) {
|
|
80
86
|
return
|
|
87
|
+
}
|
|
81
88
|
|
|
82
|
-
for (let p = node; p && (p === node || p[flag]); p = p.resolveParent)
|
|
89
|
+
for (let p = node; p && (p === node || p[flag]); p = p.resolveParent) {
|
|
83
90
|
p[flag] = false
|
|
91
|
+
}
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
// typically a short walk, since it only traverses deps that
|
|
@@ -92,8 +100,9 @@ const unsetFlag = (node, flag) => {
|
|
|
92
100
|
tree: node,
|
|
93
101
|
visit: node => {
|
|
94
102
|
node.extraneous = node[flag] = false
|
|
95
|
-
if (node.isLink)
|
|
103
|
+
if (node.isLink) {
|
|
96
104
|
node.target.extraneous = node.target[flag] = false
|
|
105
|
+
}
|
|
97
106
|
},
|
|
98
107
|
getChildren: node => [...node.target.edgesOut.values()]
|
|
99
108
|
.filter(edge => edge.to && edge.to[flag] &&
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
// Internal methods used by buildIdealTree.
|
|
2
|
+
// Answer the question: "can I put this dep here?"
|
|
3
|
+
//
|
|
4
|
+
// IMPORTANT: *nothing* in this class should *ever* modify or mutate the tree
|
|
5
|
+
// at all. The contract here is strictly limited to read operations. We call
|
|
6
|
+
// this in the process of walking through the ideal tree checking many
|
|
7
|
+
// different potential placement targets for a given node. If a change is made
|
|
8
|
+
// to the tree along the way, that can cause serious problems!
|
|
9
|
+
//
|
|
10
|
+
// In order to enforce this restriction, in debug mode, canPlaceDep() will
|
|
11
|
+
// snapshot the tree at the start of the process, and then at the end, will
|
|
12
|
+
// verify that it still matches the snapshot, and throw an error if any changes
|
|
13
|
+
// occurred.
|
|
14
|
+
//
|
|
15
|
+
// The algorithm is roughly like this:
|
|
16
|
+
// - check the node itself:
|
|
17
|
+
// - if there is no version present, and no conflicting edges from target,
|
|
18
|
+
// OK, provided all peers can be placed at or above the target.
|
|
19
|
+
// - if the current version matches, KEEP
|
|
20
|
+
// - if there is an older version present, which can be replaced, then
|
|
21
|
+
// - if satisfying and preferDedupe? KEEP
|
|
22
|
+
// - else: REPLACE
|
|
23
|
+
// - if there is a newer version present, and preferDedupe, REPLACE
|
|
24
|
+
// - if the version present satisfies the edge, KEEP
|
|
25
|
+
// - else: CONFLICT
|
|
26
|
+
// - if the node is not in conflict, check each of its peers:
|
|
27
|
+
// - if the peer can be placed in the target, continue
|
|
28
|
+
// - else if the peer can be placed in a parent, and there is no other
|
|
29
|
+
// conflicting version shadowing it, continue
|
|
30
|
+
// - else CONFLICT
|
|
31
|
+
// - If the peers are not in conflict, return the original node's value
|
|
32
|
+
//
|
|
33
|
+
// An exception to this logic is that if the target is the deepest location
|
|
34
|
+
// that a node can be placed, and the conflicting node can be placed deeper,
|
|
35
|
+
// then we will return REPLACE rather than CONFLICT, and Arborist will queue
|
|
36
|
+
// the replaced node for resolution elsewhere.
|
|
37
|
+
|
|
38
|
+
const semver = require('semver')
|
|
39
|
+
const debug = require('./debug.js')
|
|
40
|
+
const peerEntrySets = require('./peer-entry-sets.js')
|
|
41
|
+
const deepestNestingTarget = require('./deepest-nesting-target.js')
|
|
42
|
+
|
|
43
|
+
const CONFLICT = Symbol('CONFLICT')
|
|
44
|
+
const OK = Symbol('OK')
|
|
45
|
+
const REPLACE = Symbol('REPLACE')
|
|
46
|
+
const KEEP = Symbol('KEEP')
|
|
47
|
+
|
|
48
|
+
class CanPlaceDep {
|
|
49
|
+
// dep is a dep that we're trying to place. it should already live in
|
|
50
|
+
// a virtual tree where its peer set is loaded as children of the root.
|
|
51
|
+
// target is the actual place where we're trying to place this dep
|
|
52
|
+
// in a node_modules folder.
|
|
53
|
+
// edge is the edge that we're trying to satisfy with this placement.
|
|
54
|
+
// parent is the CanPlaceDep object of the entry node when placing a peer.
|
|
55
|
+
constructor (options) {
|
|
56
|
+
const {
|
|
57
|
+
dep,
|
|
58
|
+
target,
|
|
59
|
+
edge,
|
|
60
|
+
preferDedupe,
|
|
61
|
+
parent = null,
|
|
62
|
+
peerPath = [],
|
|
63
|
+
explicitRequest = false,
|
|
64
|
+
} = options
|
|
65
|
+
|
|
66
|
+
debug(() => {
|
|
67
|
+
if (!dep) {
|
|
68
|
+
throw new Error('no dep provided to CanPlaceDep')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!target) {
|
|
72
|
+
throw new Error('no target provided to CanPlaceDep')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!edge) {
|
|
76
|
+
throw new Error('no edge provided to CanPlaceDep')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this._treeSnapshot = JSON.stringify([...target.root.inventory.entries()]
|
|
80
|
+
.map(([loc, {packageName, version, resolved}]) => {
|
|
81
|
+
return [loc, packageName, version, resolved]
|
|
82
|
+
}).sort(([a], [b]) => a.localeCompare(b, 'en')))
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// the result of whether we can place it or not
|
|
86
|
+
this.canPlace = null
|
|
87
|
+
// if peers conflict, but this one doesn't, then that is useful info
|
|
88
|
+
this.canPlaceSelf = null
|
|
89
|
+
|
|
90
|
+
this.dep = dep
|
|
91
|
+
this.target = target
|
|
92
|
+
this.edge = edge
|
|
93
|
+
this.explicitRequest = explicitRequest
|
|
94
|
+
|
|
95
|
+
// preventing cycles when we check peer sets
|
|
96
|
+
this.peerPath = peerPath
|
|
97
|
+
// we always prefer to dedupe peers, because they are trying
|
|
98
|
+
// a bit harder to be singletons.
|
|
99
|
+
this.preferDedupe = !!preferDedupe || edge.peer
|
|
100
|
+
this.parent = parent
|
|
101
|
+
this.children = []
|
|
102
|
+
|
|
103
|
+
this.isSource = target === this.peerSetSource
|
|
104
|
+
this.name = edge.name
|
|
105
|
+
this.current = target.children.get(this.name)
|
|
106
|
+
this.targetEdge = target.edgesOut.get(this.name)
|
|
107
|
+
this.conflicts = new Map()
|
|
108
|
+
|
|
109
|
+
// check if this dep was already subject to a peerDep override while
|
|
110
|
+
// building the peerSet.
|
|
111
|
+
this.edgeOverride = !dep.satisfies(edge)
|
|
112
|
+
|
|
113
|
+
this.canPlace = this.checkCanPlace()
|
|
114
|
+
if (!this.canPlaceSelf) {
|
|
115
|
+
this.canPlaceSelf = this.canPlace
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
debug(() => {
|
|
119
|
+
const treeSnapshot = JSON.stringify([...target.root.inventory.entries()]
|
|
120
|
+
.map(([loc, {packageName, version, resolved}]) => {
|
|
121
|
+
return [loc, packageName, version, resolved]
|
|
122
|
+
}).sort(([a], [b]) => a.localeCompare(b, 'en')))
|
|
123
|
+
/* istanbul ignore if */
|
|
124
|
+
if (this._treeSnapshot !== treeSnapshot) {
|
|
125
|
+
throw Object.assign(new Error('tree changed in CanPlaceDep'), {
|
|
126
|
+
expect: this._treeSnapshot,
|
|
127
|
+
actual: treeSnapshot,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
checkCanPlace () {
|
|
134
|
+
const { target, targetEdge, current, dep } = this
|
|
135
|
+
|
|
136
|
+
// if the dep failed to load, we're going to fail the build or
|
|
137
|
+
// prune it out anyway, so just move forward placing/replacing it.
|
|
138
|
+
if (dep.errors.length) {
|
|
139
|
+
return current ? REPLACE : OK
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// cannot place peers inside their dependents, except for tops
|
|
143
|
+
if (targetEdge && targetEdge.peer && !target.isTop) {
|
|
144
|
+
return CONFLICT
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (targetEdge && !dep.satisfies(targetEdge) && targetEdge !== this.edge) {
|
|
148
|
+
return CONFLICT
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return current ? this.checkCanPlaceCurrent() : this.checkCanPlaceNoCurrent()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// we know that the target has a dep by this name in its node_modules
|
|
155
|
+
// already. Can return KEEP, REPLACE, or CONFLICT.
|
|
156
|
+
checkCanPlaceCurrent () {
|
|
157
|
+
const { preferDedupe, explicitRequest, current, target, edge, dep } = this
|
|
158
|
+
|
|
159
|
+
if (dep.matches(current)) {
|
|
160
|
+
if (current.satisfies(edge) || this.edgeOverride) {
|
|
161
|
+
return explicitRequest ? REPLACE : KEEP
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { version: curVer } = current
|
|
166
|
+
const { version: newVer } = dep
|
|
167
|
+
const tryReplace = curVer && newVer && semver.gte(newVer, curVer)
|
|
168
|
+
if (tryReplace && dep.canReplace(current)) {
|
|
169
|
+
/* XXX-istanbul ignore else - It's extremely rare that a replaceable
|
|
170
|
+
* node would be a conflict, if the current one wasn't a conflict,
|
|
171
|
+
* but it is theoretically possible if peer deps are pinned. In
|
|
172
|
+
* that case we treat it like any other conflict, and keep trying */
|
|
173
|
+
const cpp = this.canPlacePeers(REPLACE)
|
|
174
|
+
if (cpp !== CONFLICT) {
|
|
175
|
+
return cpp
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ok, can't replace the current with new one, but maybe current is ok?
|
|
180
|
+
if (current.satisfies(edge) && (!explicitRequest || preferDedupe)) {
|
|
181
|
+
return KEEP
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// if we prefer deduping, then try replacing newer with older
|
|
185
|
+
if (preferDedupe && !tryReplace && dep.canReplace(current)) {
|
|
186
|
+
const cpp = this.canPlacePeers(REPLACE)
|
|
187
|
+
if (cpp !== CONFLICT) {
|
|
188
|
+
return cpp
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check for interesting cases!
|
|
193
|
+
// First, is this the deepest place that this thing can go, and NOT the
|
|
194
|
+
// deepest place where the conflicting dep can go? If so, replace it,
|
|
195
|
+
// and let it re-resolve deeper in the tree.
|
|
196
|
+
const myDeepest = this.deepestNestingTarget
|
|
197
|
+
|
|
198
|
+
// ok, i COULD be placed deeper, so leave the current one alone.
|
|
199
|
+
if (target !== myDeepest) {
|
|
200
|
+
return CONFLICT
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// if we are not checking a peerDep, then we MUST place it here, in the
|
|
204
|
+
// target that has a non-peer dep on it.
|
|
205
|
+
if (!edge.peer && target === edge.from) {
|
|
206
|
+
return this.canPlacePeers(REPLACE)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// if we aren't placing a peer in a set, then we're done here.
|
|
210
|
+
// This is ignored because it SHOULD be redundant, as far as I can tell,
|
|
211
|
+
// with the deepest target and target===edge.from tests. But until we
|
|
212
|
+
// can prove that isn't possible, this condition is here for safety.
|
|
213
|
+
/* istanbul ignore if - allegedly impossible */
|
|
214
|
+
if (!this.parent && !edge.peer) {
|
|
215
|
+
return CONFLICT
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// check the deps in the peer group for each edge into that peer group
|
|
219
|
+
// if ALL of them can be pushed deeper, or if it's ok to replace its
|
|
220
|
+
// members with the contents of the new peer group, then we're good.
|
|
221
|
+
let canReplace = true
|
|
222
|
+
for (const [entryEdge, currentPeers] of peerEntrySets(current)) {
|
|
223
|
+
if (entryEdge === this.edge || entryEdge === this.peerEntryEdge) {
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// First, see if it's ok to just replace the peerSet entirely.
|
|
228
|
+
// we do this by walking out from the entryEdge, because in a case like
|
|
229
|
+
// this:
|
|
230
|
+
//
|
|
231
|
+
// v -> PEER(a@1||2)
|
|
232
|
+
// a@1 -> PEER(b@1)
|
|
233
|
+
// a@2 -> PEER(b@2)
|
|
234
|
+
// b@1 -> PEER(a@1)
|
|
235
|
+
// b@2 -> PEER(a@2)
|
|
236
|
+
//
|
|
237
|
+
// root
|
|
238
|
+
// +-- v
|
|
239
|
+
// +-- a@2
|
|
240
|
+
// +-- b@2
|
|
241
|
+
//
|
|
242
|
+
// Trying to place a peer group of (a@1, b@1) would fail to note that
|
|
243
|
+
// they can be replaced, if we did it by looping 1 by 1. If we are
|
|
244
|
+
// replacing something, we don't have to check its peer deps, because
|
|
245
|
+
// the peerDeps in the placed peerSet will presumably satisfy.
|
|
246
|
+
const entryNode = entryEdge.to
|
|
247
|
+
const entryRep = dep.parent.children.get(entryNode.name)
|
|
248
|
+
if (entryRep) {
|
|
249
|
+
if (entryRep.canReplace(entryNode, dep.parent.children.keys())) {
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let canClobber = !entryRep
|
|
255
|
+
if (!entryRep) {
|
|
256
|
+
const peerReplacementWalk = new Set([entryNode])
|
|
257
|
+
OUTER: for (const currentPeer of peerReplacementWalk) {
|
|
258
|
+
for (const edge of currentPeer.edgesOut.values()) {
|
|
259
|
+
if (!edge.peer || !edge.valid) {
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
const rep = dep.parent.children.get(edge.name)
|
|
263
|
+
if (!rep) {
|
|
264
|
+
if (edge.to) {
|
|
265
|
+
peerReplacementWalk.add(edge.to)
|
|
266
|
+
}
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
if (!rep.satisfies(edge)) {
|
|
270
|
+
canClobber = false
|
|
271
|
+
break OUTER
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (canClobber) {
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ok, we can't replace, but maybe we can nest the current set deeper?
|
|
281
|
+
let canNestCurrent = true
|
|
282
|
+
for (const currentPeer of currentPeers) {
|
|
283
|
+
if (!canNestCurrent) {
|
|
284
|
+
break
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// still possible to nest this peerSet
|
|
288
|
+
const curDeep = deepestNestingTarget(entryEdge.from, currentPeer.name)
|
|
289
|
+
if (curDeep === target || target.isDescendantOf(curDeep)) {
|
|
290
|
+
canNestCurrent = false
|
|
291
|
+
canReplace = false
|
|
292
|
+
}
|
|
293
|
+
if (canNestCurrent) {
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// if we can nest or replace all the current peer groups, we can replace.
|
|
300
|
+
if (canReplace) {
|
|
301
|
+
return this.canPlacePeers(REPLACE)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return CONFLICT
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
checkCanPlaceNoCurrent () {
|
|
308
|
+
const { target, peerEntryEdge, dep, name } = this
|
|
309
|
+
|
|
310
|
+
// check to see what that name resolves to here, and who may depend on
|
|
311
|
+
// being able to reach it by crawling up past the parent. we know
|
|
312
|
+
// that it's not the target's direct child node, and if it was a direct
|
|
313
|
+
// dep of the target, we would have conflicted earlier.
|
|
314
|
+
const current = target !== peerEntryEdge.from && target.resolve(name)
|
|
315
|
+
if (current) {
|
|
316
|
+
for (const edge of current.edgesIn.values()) {
|
|
317
|
+
if (edge.from.isDescendantOf(target) && edge.valid) {
|
|
318
|
+
if (!dep.satisfies(edge)) {
|
|
319
|
+
return CONFLICT
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// no objections, so this is fine as long as peers are ok here.
|
|
326
|
+
return this.canPlacePeers(OK)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
get deepestNestingTarget () {
|
|
330
|
+
const start = this.parent ? this.parent.deepestNestingTarget
|
|
331
|
+
: this.edge.from
|
|
332
|
+
return deepestNestingTarget(start, this.name)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
get conflictChildren () {
|
|
336
|
+
return this.allChildren.filter(c => c.canPlace === CONFLICT)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
get allChildren () {
|
|
340
|
+
const set = new Set(this.children)
|
|
341
|
+
for (const child of set) {
|
|
342
|
+
for (const grandchild of child.children) {
|
|
343
|
+
set.add(grandchild)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return [...set]
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
get top () {
|
|
350
|
+
return this.parent ? this.parent.top : this
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// check if peers can go here. returns state or CONFLICT
|
|
354
|
+
canPlacePeers (state) {
|
|
355
|
+
this.canPlaceSelf = state
|
|
356
|
+
if (this._canPlacePeers) {
|
|
357
|
+
return this._canPlacePeers
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// TODO: represent peerPath in ERESOLVE error somehow?
|
|
361
|
+
const peerPath = [...this.peerPath, this.dep]
|
|
362
|
+
let sawConflict = false
|
|
363
|
+
for (const peerEdge of this.dep.edgesOut.values()) {
|
|
364
|
+
if (!peerEdge.peer || !peerEdge.to || peerPath.includes(peerEdge.to)) {
|
|
365
|
+
continue
|
|
366
|
+
}
|
|
367
|
+
const peer = peerEdge.to
|
|
368
|
+
// it may be the case that the *initial* dep can be nested, but a peer
|
|
369
|
+
// of that dep needs to be placed shallower, because the target has
|
|
370
|
+
// a peer dep on the peer as well.
|
|
371
|
+
const target = deepestNestingTarget(this.target, peer.name)
|
|
372
|
+
const cpp = new CanPlaceDep({
|
|
373
|
+
dep: peer,
|
|
374
|
+
target,
|
|
375
|
+
parent: this,
|
|
376
|
+
edge: peerEdge,
|
|
377
|
+
peerPath,
|
|
378
|
+
// always place peers in preferDedupe mode
|
|
379
|
+
preferDedupe: true,
|
|
380
|
+
})
|
|
381
|
+
/* istanbul ignore next */
|
|
382
|
+
debug(() => {
|
|
383
|
+
if (this.children.some(c => c.dep === cpp.dep)) {
|
|
384
|
+
throw new Error('checking same dep repeatedly')
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
this.children.push(cpp)
|
|
388
|
+
|
|
389
|
+
if (cpp.canPlace === CONFLICT) {
|
|
390
|
+
sawConflict = true
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
this._canPlacePeers = sawConflict ? CONFLICT : state
|
|
395
|
+
return this._canPlacePeers
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// what is the node that is causing this peerSet to be placed?
|
|
399
|
+
get peerSetSource () {
|
|
400
|
+
return this.parent ? this.parent.peerSetSource : this.edge.from
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
get peerEntryEdge () {
|
|
404
|
+
return this.top.edge
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
static get CONFLICT () {
|
|
408
|
+
return CONFLICT
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
static get OK () {
|
|
412
|
+
return OK
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
static get REPLACE () {
|
|
416
|
+
return REPLACE
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
static get KEEP () {
|
|
420
|
+
return KEEP
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
get description () {
|
|
424
|
+
const { canPlace } = this
|
|
425
|
+
return canPlace && canPlace.description ||
|
|
426
|
+
/* istanbul ignore next - old node affordance */ canPlace
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
module.exports = CanPlaceDep
|