@npmcli/arborist 2.7.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ const pacote = require('pacote')
5
5
  const AuditReport = require('../audit-report.js')
6
6
  const {subset, intersects} = require('semver')
7
7
  const npa = require('npm-package-arg')
8
+ const debug = require('../debug.js')
8
9
 
9
10
  const {dirname, resolve, relative} = require('path')
10
11
  const {depth: dfwalk} = require('treeverse')
@@ -50,6 +51,7 @@ const _createSparseTree = Symbol.for('createSparseTree')
50
51
  const _loadShrinkwrapsAndUpdateTrees = Symbol.for('loadShrinkwrapsAndUpdateTrees')
51
52
  const _shrinkwrapInflated = Symbol('shrinkwrapInflated')
52
53
  const _bundleUnpacked = Symbol('bundleUnpacked')
54
+ const _bundleMissing = Symbol('bundleMissing')
53
55
  const _reifyNode = Symbol.for('reifyNode')
54
56
  const _extractOrLink = Symbol('extractOrLink')
55
57
  // defined by rebuild mixin
@@ -83,8 +85,9 @@ const _omitPeer = Symbol('omitPeer')
83
85
 
84
86
  const _global = Symbol.for('global')
85
87
 
88
+ const _pruneBundledMetadeps = Symbol('pruneBundledMetadeps')
89
+
86
90
  // defined by Ideal mixin
87
- const _pruneBundledMetadeps = Symbol.for('pruneBundledMetadeps')
88
91
  const _resolvedAdd = Symbol.for('resolvedAdd')
89
92
  const _usePackageLock = Symbol.for('usePackageLock')
90
93
  const _formatPackageLock = Symbol.for('formatPackageLock')
@@ -112,6 +115,10 @@ module.exports = cls => class Reifier extends cls {
112
115
  this[_sparseTreeDirs] = new Set()
113
116
  this[_sparseTreeRoots] = new Set()
114
117
  this[_trashList] = new Set()
118
+ // the nodes we unpack to read their bundles
119
+ this[_bundleUnpacked] = new Set()
120
+ // child nodes we'd EXPECT to be included in a bundle, but aren't
121
+ this[_bundleMissing] = new Set()
115
122
  }
116
123
 
117
124
  // public method
@@ -334,7 +341,7 @@ module.exports = cls => class Reifier extends cls {
334
341
  // removed later on in the process. optionally, also mark them
335
342
  // as a retired paths, so that we move them out of the way and
336
343
  // replace them when rolling back on failure.
337
- [_addNodeToTrashList] (node, retire) {
344
+ [_addNodeToTrashList] (node, retire = false) {
338
345
  const paths = [node.path, ...node.binPaths]
339
346
  const moves = this[_retiredPaths]
340
347
  this.log.silly('reify', 'mark', retire ? 'retired' : 'deleted', paths)
@@ -610,10 +617,9 @@ module.exports = cls => class Reifier extends cls {
610
617
  [_loadBundlesAndUpdateTrees] (
611
618
  depth = 0, bundlesByDepth = this[_getBundlesByDepth]()
612
619
  ) {
613
- if (depth === 0) {
614
- this[_bundleUnpacked] = new Set()
620
+ if (depth === 0)
615
621
  process.emit('time', 'reify:loadBundles')
616
- }
622
+
617
623
  const maxBundleDepth = bundlesByDepth.get('maxBundleDepth')
618
624
  if (depth > maxBundleDepth) {
619
625
  // if we did something, then prune the tree and update the diffs
@@ -642,14 +648,30 @@ module.exports = cls => class Reifier extends cls {
642
648
  }))
643
649
  // then load their unpacked children and move into the ideal tree
644
650
  .then(nodes =>
645
- promiseAllRejectLate(nodes.map(node => new this.constructor({
646
- ...this.options,
647
- path: node.path,
648
- }).loadActual({
649
- root: node,
650
- // don't transplant any sparse folders we created
651
- transplantFilter: node => node.package._id,
652
- }))))
651
+ promiseAllRejectLate(nodes.map(async node => {
652
+ const arb = new this.constructor({
653
+ ...this.options,
654
+ path: node.path,
655
+ })
656
+ const notTransplanted = new Set(node.children.keys())
657
+ await arb.loadActual({
658
+ root: node,
659
+ // don't transplant any sparse folders we created
660
+ // loadActual will set node.package to {} for empty directories
661
+ // if by chance there are some empty folders in the node_modules
662
+ // tree for some other reason, then ok, ignore those too.
663
+ transplantFilter: node => {
664
+ if (node.package._id) {
665
+ // it's actually in the bundle if it gets transplanted
666
+ notTransplanted.delete(node.name)
667
+ return true
668
+ } else
669
+ return false
670
+ },
671
+ })
672
+ for (const name of notTransplanted)
673
+ this[_bundleMissing].add(node.children.get(name))
674
+ })))
653
675
  // move onto the next level of bundled items
654
676
  .then(() => this[_loadBundlesAndUpdateTrees](depth + 1, bundlesByDepth))
655
677
  }
@@ -685,6 +707,27 @@ module.exports = cls => class Reifier extends cls {
685
707
  // https://github.com/npm/cli/issues/1597#issuecomment-667639545
686
708
  [_pruneBundledMetadeps] (bundlesByDepth) {
687
709
  const bundleShadowed = new Set()
710
+
711
+ // Example dep graph:
712
+ // root -> (a, c)
713
+ // a -> BUNDLE(b)
714
+ // b -> c
715
+ // c -> b
716
+ //
717
+ // package tree:
718
+ // root
719
+ // +-- a
720
+ // | +-- b(1)
721
+ // | +-- c(1)
722
+ // +-- b(2)
723
+ // +-- c(2)
724
+ // 1. mark everything that's shadowed by anything in the bundle. This
725
+ // marks b(2) and c(2).
726
+ // 2. anything with edgesIn from outside the set, mark not-extraneous,
727
+ // remove from set. This unmarks c(2).
728
+ // 3. continue until no change
729
+ // 4. remove everything in the set from the tree. b(2) is pruned
730
+
688
731
  // create the list of nodes shadowed by children of bundlers
689
732
  for (const bundles of bundlesByDepth.values()) {
690
733
  // skip the 'maxBundleDepth' item
@@ -700,36 +743,50 @@ module.exports = cls => class Reifier extends cls {
700
743
  }
701
744
  }
702
745
  }
703
- let changed = true
704
- while (changed) {
705
- changed = false
706
- for (const shadow of bundleShadowed) {
707
- if (!shadow.extraneous) {
708
- bundleShadowed.delete(shadow)
709
- continue
746
+
747
+ // lib -> (a@1.x) BUNDLE(a@1.2.3 (b@1.2.3))
748
+ // a@1.2.3 -> (b@1.2.3)
749
+ // a@1.3.0 -> (b@2)
750
+ // b@1.2.3 -> ()
751
+ // b@2 -> (c@2)
752
+ //
753
+ // root
754
+ // +-- lib
755
+ // | +-- a@1.2.3
756
+ // | +-- b@1.2.3
757
+ // +-- b@2 <-- shadowed, now extraneous
758
+ // +-- c@2 <-- also shadowed, because only dependent is shadowed
759
+ for (const shadow of bundleShadowed) {
760
+ for (const shadDep of shadow.edgesOut.values()) {
761
+ /* istanbul ignore else - pretty unusual situation, just being
762
+ * defensive here. Would mean that a bundled dep has a dependency
763
+ * that is unmet. which, weird, but if you bundle it, we take
764
+ * whatever you put there and assume the publisher knows best. */
765
+ if (shadDep.to) {
766
+ bundleShadowed.add(shadDep.to)
767
+ shadDep.to.extraneous = true
710
768
  }
769
+ }
770
+ }
711
771
 
772
+ let changed
773
+ do {
774
+ changed = false
775
+ for (const shadow of bundleShadowed) {
712
776
  for (const edge of shadow.edgesIn) {
713
- if (!edge.from.extraneous) {
777
+ if (!bundleShadowed.has(edge.from)) {
714
778
  shadow.extraneous = false
715
779
  bundleShadowed.delete(shadow)
716
780
  changed = true
717
- } else {
718
- for (const shadDep of shadow.edgesOut.values()) {
719
- /* istanbul ignore else - pretty unusual situation, just being
720
- * defensive here. Would mean that a bundled dep has a dependency
721
- * that is unmet. which, weird, but if you bundle it, we take
722
- * whatever you put there and assume the publisher knows best. */
723
- if (shadDep.to)
724
- bundleShadowed.add(shadDep.to)
725
- }
781
+ break
726
782
  }
727
783
  }
728
784
  }
729
- }
785
+ } while (changed)
786
+
730
787
  for (const shadow of bundleShadowed) {
731
- shadow.parent = null
732
788
  this[_addNodeToTrashList](shadow)
789
+ shadow.root = null
733
790
  }
734
791
  }
735
792
 
@@ -780,6 +837,7 @@ module.exports = cls => class Reifier extends cls {
780
837
  const node = diff.ideal
781
838
  const bd = this[_bundleUnpacked].has(node)
782
839
  const sw = this[_shrinkwrapInflated].has(node)
840
+ const bundleMissing = this[_bundleMissing].has(node)
783
841
 
784
842
  // check whether we still need to unpack this one.
785
843
  // test the inDepBundle last, since that's potentially a tree walk.
@@ -787,7 +845,7 @@ module.exports = cls => class Reifier extends cls {
787
845
  !node.isRoot && // root node already exists
788
846
  !bd && // already unpacked to read bundle
789
847
  !sw && // already unpacked to read sw
790
- !node.inDepBundle // already unpacked by another dep's bundle
848
+ (bundleMissing || !node.inDepBundle) // already unpacked by another dep's bundle
791
849
 
792
850
  if (doUnpack)
793
851
  unpacks.push(this[_reifyNode](node))
@@ -814,8 +872,26 @@ module.exports = cls => class Reifier extends cls {
814
872
  const moves = this[_retiredPaths]
815
873
  this[_retiredUnchanged] = {}
816
874
  return promiseAllRejectLate(this.diff.children.map(diff => {
817
- const realFolder = (diff.actual || diff.ideal).path
875
+ // skip if nothing was retired
876
+ if (diff.action !== 'CHANGE' && diff.action !== 'REMOVE')
877
+ return
878
+
879
+ const { path: realFolder } = diff.actual
818
880
  const retireFolder = moves[realFolder]
881
+ /* istanbul ignore next - should be impossible */
882
+ debug(() => {
883
+ if (!retireFolder) {
884
+ const er = new Error('trying to un-retire but not retired')
885
+ throw Object.assign(er, {
886
+ realFolder,
887
+ retireFolder,
888
+ actual: diff.actual,
889
+ ideal: diff.ideal,
890
+ action: diff.action,
891
+ })
892
+ }
893
+ })
894
+
819
895
  this[_retiredUnchanged][retireFolder] = []
820
896
  return promiseAllRejectLate(diff.unchanged.map(node => {
821
897
  // no need to roll back links, since we'll just delete them anyway
@@ -823,7 +899,7 @@ module.exports = cls => class Reifier extends cls {
823
899
  return mkdirp(dirname(node.path)).then(() => this[_reifyNode](node))
824
900
 
825
901
  // will have been moved/unpacked along with bundler
826
- if (node.inDepBundle)
902
+ if (node.inDepBundle && !this[_bundleMissing].has(node))
827
903
  return
828
904
 
829
905
  this[_retiredUnchanged][retireFolder].push(node)
@@ -0,0 +1,405 @@
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
+ if (!target)
71
+ throw new Error('no target provided to CanPlaceDep')
72
+
73
+ if (!edge)
74
+ throw new Error('no edge provided to CanPlaceDep')
75
+
76
+ this._nodeSnapshot = JSON.stringify(dep)
77
+ this._treeSnapshot = JSON.stringify(target.root)
78
+ })
79
+
80
+ // the result of whether we can place it or not
81
+ this.canPlace = null
82
+ // if peers conflict, but this one doesn't, then that is useful info
83
+ this.canPlaceSelf = null
84
+
85
+ this.dep = dep
86
+ this.target = target
87
+ this.edge = edge
88
+ this.explicitRequest = explicitRequest
89
+
90
+ // preventing cycles when we check peer sets
91
+ this.peerPath = peerPath
92
+ // we always prefer to dedupe peers, because they are trying
93
+ // a bit harder to be singletons.
94
+ this.preferDedupe = !!preferDedupe || edge.peer
95
+ this.parent = parent
96
+ this.children = []
97
+
98
+ this.isSource = target === this.peerSetSource
99
+ this.name = edge.name
100
+ this.current = target.children.get(this.name)
101
+ this.targetEdge = target.edgesOut.get(this.name)
102
+ this.conflicts = new Map()
103
+
104
+ // check if this dep was already subject to a peerDep override while
105
+ // building the peerSet.
106
+ this.edgeOverride = !dep.satisfies(edge)
107
+
108
+ this.canPlace = this.checkCanPlace()
109
+ if (!this.canPlaceSelf)
110
+ this.canPlaceSelf = this.canPlace
111
+
112
+ debug(() => {
113
+ const nodeSnapshot = JSON.stringify(dep)
114
+ const treeSnapshot = JSON.stringify(target.root)
115
+ /* istanbul ignore if */
116
+ if (this._nodeSnapshot !== nodeSnapshot) {
117
+ throw Object.assign(new Error('dep changed in CanPlaceDep'), {
118
+ expect: this._nodeSnapshot,
119
+ actual: nodeSnapshot,
120
+ })
121
+ }
122
+ /* istanbul ignore if */
123
+ if (this._treeSnapshot !== treeSnapshot) {
124
+ throw Object.assign(new Error('tree changed in CanPlaceDep'), {
125
+ expect: this._treeSnapshot,
126
+ actual: treeSnapshot,
127
+ })
128
+ }
129
+ })
130
+ }
131
+
132
+ checkCanPlace () {
133
+ const { target, targetEdge, current, dep } = this
134
+
135
+ // if the dep failed to load, we're going to fail the build or
136
+ // prune it out anyway, so just move forward placing/replacing it.
137
+ if (dep.errors.length)
138
+ return current ? REPLACE : OK
139
+
140
+ // cannot place peers inside their dependents, except for tops
141
+ if (targetEdge && targetEdge.peer && !target.isTop)
142
+ return CONFLICT
143
+
144
+ if (targetEdge && !dep.satisfies(targetEdge) && targetEdge !== this.edge)
145
+ return CONFLICT
146
+
147
+ return current ? this.checkCanPlaceCurrent() : this.checkCanPlaceNoCurrent()
148
+ }
149
+
150
+ // we know that the target has a dep by this name in its node_modules
151
+ // already. Can return KEEP, REPLACE, or CONFLICT.
152
+ checkCanPlaceCurrent () {
153
+ const { preferDedupe, explicitRequest, current, target, edge, dep } = this
154
+
155
+ if (dep.matches(current)) {
156
+ if (current.satisfies(edge) || this.edgeOverride)
157
+ return explicitRequest ? REPLACE : KEEP
158
+ }
159
+
160
+ const { version: curVer } = current
161
+ const { version: newVer } = dep
162
+ const tryReplace = curVer && newVer && semver.gte(newVer, curVer)
163
+ if (tryReplace && dep.canReplace(current)) {
164
+ /* XXX-istanbul ignore else - It's extremely rare that a replaceable
165
+ * node would be a conflict, if the current one wasn't a conflict,
166
+ * but it is theoretically possible if peer deps are pinned. In
167
+ * that case we treat it like any other conflict, and keep trying */
168
+ const cpp = this.canPlacePeers(REPLACE)
169
+ if (cpp !== CONFLICT)
170
+ return cpp
171
+ }
172
+
173
+ // ok, can't replace the current with new one, but maybe current is ok?
174
+ if (current.satisfies(edge) && (!explicitRequest || preferDedupe))
175
+ return KEEP
176
+
177
+ // if we prefer deduping, then try replacing newer with older
178
+ if (preferDedupe && !tryReplace && dep.canReplace(current)) {
179
+ const cpp = this.canPlacePeers(REPLACE)
180
+ if (cpp !== CONFLICT)
181
+ return cpp
182
+ }
183
+
184
+ // Check for interesting cases!
185
+ // First, is this the deepest place that this thing can go, and NOT the
186
+ // deepest place where the conflicting dep can go? If so, replace it,
187
+ // and let it re-resolve deeper in the tree.
188
+ const myDeepest = this.deepestNestingTarget
189
+
190
+ // ok, i COULD be placed deeper, so leave the current one alone.
191
+ if (target !== myDeepest)
192
+ return CONFLICT
193
+
194
+ // if we are not checking a peerDep, then we MUST place it here, in the
195
+ // target that has a non-peer dep on it.
196
+ if (!edge.peer && target === edge.from)
197
+ return this.canPlacePeers(REPLACE)
198
+
199
+ // if we aren't placing a peer in a set, then we're done here.
200
+ // This is ignored because it SHOULD be redundant, as far as I can tell,
201
+ // with the deepest target and target===edge.from tests. But until we
202
+ // can prove that isn't possible, this condition is here for safety.
203
+ /* istanbul ignore if - allegedly impossible */
204
+ if (!this.parent && !edge.peer)
205
+ return CONFLICT
206
+
207
+ // check the deps in the peer group for each edge into that peer group
208
+ // if ALL of them can be pushed deeper, or if it's ok to replace its
209
+ // members with the contents of the new peer group, then we're good.
210
+ let canReplace = true
211
+ for (const [entryEdge, currentPeers] of peerEntrySets(current)) {
212
+ if (entryEdge === this.edge || entryEdge === this.peerEntryEdge)
213
+ continue
214
+
215
+ // First, see if it's ok to just replace the peerSet entirely.
216
+ // we do this by walking out from the entryEdge, because in a case like
217
+ // this:
218
+ //
219
+ // v -> PEER(a@1||2)
220
+ // a@1 -> PEER(b@1)
221
+ // a@2 -> PEER(b@2)
222
+ // b@1 -> PEER(a@1)
223
+ // b@2 -> PEER(a@2)
224
+ //
225
+ // root
226
+ // +-- v
227
+ // +-- a@2
228
+ // +-- b@2
229
+ //
230
+ // Trying to place a peer group of (a@1, b@1) would fail to note that
231
+ // they can be replaced, if we did it by looping 1 by 1. If we are
232
+ // replacing something, we don't have to check its peer deps, because
233
+ // the peerDeps in the placed peerSet will presumably satisfy.
234
+ const entryNode = entryEdge.to
235
+ const entryRep = dep.parent.children.get(entryNode.name)
236
+ if (entryRep) {
237
+ if (entryRep.canReplace(entryNode, dep.parent.children.keys()))
238
+ continue
239
+ }
240
+
241
+ let canClobber = !entryRep
242
+ if (!entryRep) {
243
+ const peerReplacementWalk = new Set([entryNode])
244
+ OUTER: for (const currentPeer of peerReplacementWalk) {
245
+ for (const edge of currentPeer.edgesOut.values()) {
246
+ if (!edge.peer || !edge.valid)
247
+ continue
248
+ const rep = dep.parent.children.get(edge.name)
249
+ if (!rep) {
250
+ if (edge.to)
251
+ peerReplacementWalk.add(edge.to)
252
+ continue
253
+ }
254
+ if (!rep.satisfies(edge)) {
255
+ canClobber = false
256
+ break OUTER
257
+ }
258
+ }
259
+ }
260
+ }
261
+ if (canClobber)
262
+ continue
263
+
264
+ // ok, we can't replace, but maybe we can nest the current set deeper?
265
+ let canNestCurrent = true
266
+ for (const currentPeer of currentPeers) {
267
+ if (!canNestCurrent)
268
+ break
269
+
270
+ // still possible to nest this peerSet
271
+ const curDeep = deepestNestingTarget(entryEdge.from, currentPeer.name)
272
+ if (curDeep === target || target.isDescendantOf(curDeep)) {
273
+ canNestCurrent = false
274
+ canReplace = false
275
+ }
276
+ if (canNestCurrent)
277
+ continue
278
+ }
279
+ }
280
+
281
+ // if we can nest or replace all the current peer groups, we can replace.
282
+ if (canReplace)
283
+ return this.canPlacePeers(REPLACE)
284
+
285
+ return CONFLICT
286
+ }
287
+
288
+ checkCanPlaceNoCurrent () {
289
+ const { target, peerEntryEdge, dep, name } = this
290
+
291
+ // check to see what that name resolves to here, and who may depend on
292
+ // being able to reach it by crawling up past the parent. we know
293
+ // that it's not the target's direct child node, and if it was a direct
294
+ // dep of the target, we would have conflicted earlier.
295
+ const current = target !== peerEntryEdge.from && target.resolve(name)
296
+ if (current) {
297
+ for (const edge of current.edgesIn.values()) {
298
+ if (edge.from.isDescendantOf(target) && edge.valid) {
299
+ if (!dep.satisfies(edge))
300
+ return CONFLICT
301
+ }
302
+ }
303
+ }
304
+
305
+ // no objections, so this is fine as long as peers are ok here.
306
+ return this.canPlacePeers(OK)
307
+ }
308
+
309
+ get deepestNestingTarget () {
310
+ const start = this.parent ? this.parent.deepestNestingTarget
311
+ : this.edge.from
312
+ return deepestNestingTarget(start, this.name)
313
+ }
314
+
315
+ get conflictChildren () {
316
+ return this.allChildren.filter(c => c.canPlace === CONFLICT)
317
+ }
318
+
319
+ get allChildren () {
320
+ const set = new Set(this.children)
321
+ for (const child of set) {
322
+ for (const grandchild of child.children)
323
+ set.add(grandchild)
324
+ }
325
+ return [...set]
326
+ }
327
+
328
+ get top () {
329
+ return this.parent ? this.parent.top : this
330
+ }
331
+
332
+ // check if peers can go here. returns state or CONFLICT
333
+ canPlacePeers (state) {
334
+ this.canPlaceSelf = state
335
+ if (this._canPlacePeers)
336
+ return this._canPlacePeers
337
+
338
+ // TODO: represent peerPath in ERESOLVE error somehow?
339
+ const peerPath = [...this.peerPath, this.dep]
340
+ let sawConflict = false
341
+ for (const peerEdge of this.dep.edgesOut.values()) {
342
+ if (!peerEdge.peer || !peerEdge.to || peerPath.includes(peerEdge.to))
343
+ continue
344
+ const peer = peerEdge.to
345
+ // it may be the case that the *initial* dep can be nested, but a peer
346
+ // of that dep needs to be placed shallower, because the target has
347
+ // a peer dep on the peer as well.
348
+ const target = deepestNestingTarget(this.target, peer.name)
349
+ const cpp = new CanPlaceDep({
350
+ dep: peer,
351
+ target,
352
+ parent: this,
353
+ edge: peerEdge,
354
+ peerPath,
355
+ // always place peers in preferDedupe mode
356
+ preferDedupe: true,
357
+ })
358
+ /* istanbul ignore next */
359
+ debug(() => {
360
+ if (this.children.some(c => c.dep === cpp.dep))
361
+ throw new Error('checking same dep repeatedly')
362
+ })
363
+ this.children.push(cpp)
364
+
365
+ if (cpp.canPlace === CONFLICT)
366
+ sawConflict = true
367
+ }
368
+
369
+ this._canPlacePeers = sawConflict ? CONFLICT : state
370
+ return this._canPlacePeers
371
+ }
372
+
373
+ // what is the node that is causing this peerSet to be placed?
374
+ get peerSetSource () {
375
+ return this.parent ? this.parent.peerSetSource : this.edge.from
376
+ }
377
+
378
+ get peerEntryEdge () {
379
+ return this.top.edge
380
+ }
381
+
382
+ static get CONFLICT () {
383
+ return CONFLICT
384
+ }
385
+
386
+ static get OK () {
387
+ return OK
388
+ }
389
+
390
+ static get REPLACE () {
391
+ return REPLACE
392
+ }
393
+
394
+ static get KEEP () {
395
+ return KEEP
396
+ }
397
+
398
+ get description () {
399
+ const { canPlace } = this
400
+ return canPlace && canPlace.description ||
401
+ /* istanbul ignore next - old node affordance */ canPlace
402
+ }
403
+ }
404
+
405
+ module.exports = CanPlaceDep
@@ -0,0 +1,16 @@
1
+ // given a starting node, what is the *deepest* target where name could go?
2
+ // This is not on the Node class for the simple reason that we sometimes
3
+ // need to check the deepest *potential* target for a Node that is not yet
4
+ // added to the tree where we are checking.
5
+ const deepestNestingTarget = (start, name) => {
6
+ for (const target of start.ancestry()) {
7
+ // note: this will skip past the first target if edge is peer
8
+ if (target.isProjectRoot || !target.resolveParent)
9
+ return target
10
+ const targetEdge = target.edgesOut.get(name)
11
+ if (!targetEdge || !targetEdge.peer)
12
+ return target
13
+ }
14
+ }
15
+
16
+ module.exports = deepestNestingTarget
package/lib/edge.js CHANGED
@@ -37,6 +37,7 @@ const printableEdge = (edge) => {
37
37
  ...(edgeFrom != null ? { from: edgeFrom } : {}),
38
38
  ...(edgeTo ? { to: edgeTo } : {}),
39
39
  ...(edge.error ? { error: edge.error } : {}),
40
+ ...(edge.overridden ? { overridden: true } : {}),
40
41
  })
41
42
  }
42
43
 
@@ -72,6 +73,7 @@ class Edge {
72
73
  throw new TypeError('must provide "from" node')
73
74
  this[_setFrom](from)
74
75
  this[_error] = this[_loadError]()
76
+ this.overridden = false
75
77
  }
76
78
 
77
79
  satisfiedBy (node) {