@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.
Files changed (50) hide show
  1. package/bin/actual.js +4 -2
  2. package/bin/audit.js +12 -6
  3. package/bin/dedupe.js +49 -0
  4. package/bin/funding.js +4 -2
  5. package/bin/ideal.js +2 -1
  6. package/bin/lib/logging.js +4 -3
  7. package/bin/lib/options.js +14 -12
  8. package/bin/lib/timers.js +6 -3
  9. package/bin/license.js +9 -5
  10. package/bin/prune.js +6 -3
  11. package/bin/reify.js +6 -3
  12. package/bin/virtual.js +4 -2
  13. package/lib/add-rm-pkg-deps.js +25 -14
  14. package/lib/arborist/audit.js +2 -1
  15. package/lib/arborist/build-ideal-tree.js +246 -757
  16. package/lib/arborist/deduper.js +2 -1
  17. package/lib/arborist/index.js +8 -4
  18. package/lib/arborist/load-actual.js +32 -15
  19. package/lib/arborist/load-virtual.js +34 -18
  20. package/lib/arborist/load-workspaces.js +4 -2
  21. package/lib/arborist/rebuild.js +31 -16
  22. package/lib/arborist/reify.js +332 -119
  23. package/lib/audit-report.js +42 -22
  24. package/lib/calc-dep-flags.js +18 -9
  25. package/lib/can-place-dep.js +430 -0
  26. package/lib/case-insensitive-map.js +50 -0
  27. package/lib/consistent-resolve.js +2 -1
  28. package/lib/deepest-nesting-target.js +18 -0
  29. package/lib/dep-valid.js +8 -4
  30. package/lib/diff.js +74 -22
  31. package/lib/edge.js +29 -14
  32. package/lib/gather-dep-set.js +2 -1
  33. package/lib/inventory.js +12 -6
  34. package/lib/link.js +14 -9
  35. package/lib/node.js +269 -118
  36. package/lib/optional-set.js +4 -2
  37. package/lib/peer-entry-sets.js +77 -0
  38. package/lib/place-dep.js +578 -0
  39. package/lib/printable.js +48 -18
  40. package/lib/realpath.js +12 -6
  41. package/lib/shrinkwrap.js +168 -91
  42. package/lib/signal-handling.js +6 -3
  43. package/lib/spec-from-lock.js +7 -4
  44. package/lib/tracker.js +24 -18
  45. package/lib/tree-check.js +12 -6
  46. package/lib/version-from-tgz.js +4 -2
  47. package/lib/vuln.js +28 -16
  48. package/lib/yarn-lock.js +27 -15
  49. package/package.json +9 -13
  50. package/lib/peer-set.js +0 -25
@@ -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
- p.push(this.calculator.calculate(dep.packageName, advisory).then(meta => {
174
- if (meta.testVersion(dep.version, spec))
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].some(n => advisory.testVersion(n.version))
197
- if (!relevant)
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
  }
@@ -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) => [...tree.edgesOut.values()].map(edge => edge.to),
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