@npmcli/arborist 2.7.0 → 2.8.2

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.
@@ -0,0 +1,402 @@
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._treeSnapshot = JSON.stringify([...target.root.inventory.entries()]
77
+ .map(([loc, {packageName, version, resolved}]) => {
78
+ return [loc, packageName, version, resolved]
79
+ }).sort(([a], [b]) => a.localeCompare(b, 'en')))
80
+ })
81
+
82
+ // the result of whether we can place it or not
83
+ this.canPlace = null
84
+ // if peers conflict, but this one doesn't, then that is useful info
85
+ this.canPlaceSelf = null
86
+
87
+ this.dep = dep
88
+ this.target = target
89
+ this.edge = edge
90
+ this.explicitRequest = explicitRequest
91
+
92
+ // preventing cycles when we check peer sets
93
+ this.peerPath = peerPath
94
+ // we always prefer to dedupe peers, because they are trying
95
+ // a bit harder to be singletons.
96
+ this.preferDedupe = !!preferDedupe || edge.peer
97
+ this.parent = parent
98
+ this.children = []
99
+
100
+ this.isSource = target === this.peerSetSource
101
+ this.name = edge.name
102
+ this.current = target.children.get(this.name)
103
+ this.targetEdge = target.edgesOut.get(this.name)
104
+ this.conflicts = new Map()
105
+
106
+ // check if this dep was already subject to a peerDep override while
107
+ // building the peerSet.
108
+ this.edgeOverride = !dep.satisfies(edge)
109
+
110
+ this.canPlace = this.checkCanPlace()
111
+ if (!this.canPlaceSelf)
112
+ this.canPlaceSelf = this.canPlace
113
+
114
+ debug(() => {
115
+ const treeSnapshot = JSON.stringify([...target.root.inventory.entries()]
116
+ .map(([loc, {packageName, version, resolved}]) => {
117
+ return [loc, packageName, version, resolved]
118
+ }).sort(([a], [b]) => a.localeCompare(b, 'en')))
119
+ /* istanbul ignore if */
120
+ if (this._treeSnapshot !== treeSnapshot) {
121
+ throw Object.assign(new Error('tree changed in CanPlaceDep'), {
122
+ expect: this._treeSnapshot,
123
+ actual: treeSnapshot,
124
+ })
125
+ }
126
+ })
127
+ }
128
+
129
+ checkCanPlace () {
130
+ const { target, targetEdge, current, dep } = this
131
+
132
+ // if the dep failed to load, we're going to fail the build or
133
+ // prune it out anyway, so just move forward placing/replacing it.
134
+ if (dep.errors.length)
135
+ return current ? REPLACE : OK
136
+
137
+ // cannot place peers inside their dependents, except for tops
138
+ if (targetEdge && targetEdge.peer && !target.isTop)
139
+ return CONFLICT
140
+
141
+ if (targetEdge && !dep.satisfies(targetEdge) && targetEdge !== this.edge)
142
+ return CONFLICT
143
+
144
+ return current ? this.checkCanPlaceCurrent() : this.checkCanPlaceNoCurrent()
145
+ }
146
+
147
+ // we know that the target has a dep by this name in its node_modules
148
+ // already. Can return KEEP, REPLACE, or CONFLICT.
149
+ checkCanPlaceCurrent () {
150
+ const { preferDedupe, explicitRequest, current, target, edge, dep } = this
151
+
152
+ if (dep.matches(current)) {
153
+ if (current.satisfies(edge) || this.edgeOverride)
154
+ return explicitRequest ? REPLACE : KEEP
155
+ }
156
+
157
+ const { version: curVer } = current
158
+ const { version: newVer } = dep
159
+ const tryReplace = curVer && newVer && semver.gte(newVer, curVer)
160
+ if (tryReplace && dep.canReplace(current)) {
161
+ /* XXX-istanbul ignore else - It's extremely rare that a replaceable
162
+ * node would be a conflict, if the current one wasn't a conflict,
163
+ * but it is theoretically possible if peer deps are pinned. In
164
+ * that case we treat it like any other conflict, and keep trying */
165
+ const cpp = this.canPlacePeers(REPLACE)
166
+ if (cpp !== CONFLICT)
167
+ return cpp
168
+ }
169
+
170
+ // ok, can't replace the current with new one, but maybe current is ok?
171
+ if (current.satisfies(edge) && (!explicitRequest || preferDedupe))
172
+ return KEEP
173
+
174
+ // if we prefer deduping, then try replacing newer with older
175
+ if (preferDedupe && !tryReplace && dep.canReplace(current)) {
176
+ const cpp = this.canPlacePeers(REPLACE)
177
+ if (cpp !== CONFLICT)
178
+ return cpp
179
+ }
180
+
181
+ // Check for interesting cases!
182
+ // First, is this the deepest place that this thing can go, and NOT the
183
+ // deepest place where the conflicting dep can go? If so, replace it,
184
+ // and let it re-resolve deeper in the tree.
185
+ const myDeepest = this.deepestNestingTarget
186
+
187
+ // ok, i COULD be placed deeper, so leave the current one alone.
188
+ if (target !== myDeepest)
189
+ return CONFLICT
190
+
191
+ // if we are not checking a peerDep, then we MUST place it here, in the
192
+ // target that has a non-peer dep on it.
193
+ if (!edge.peer && target === edge.from)
194
+ return this.canPlacePeers(REPLACE)
195
+
196
+ // if we aren't placing a peer in a set, then we're done here.
197
+ // This is ignored because it SHOULD be redundant, as far as I can tell,
198
+ // with the deepest target and target===edge.from tests. But until we
199
+ // can prove that isn't possible, this condition is here for safety.
200
+ /* istanbul ignore if - allegedly impossible */
201
+ if (!this.parent && !edge.peer)
202
+ return CONFLICT
203
+
204
+ // check the deps in the peer group for each edge into that peer group
205
+ // if ALL of them can be pushed deeper, or if it's ok to replace its
206
+ // members with the contents of the new peer group, then we're good.
207
+ let canReplace = true
208
+ for (const [entryEdge, currentPeers] of peerEntrySets(current)) {
209
+ if (entryEdge === this.edge || entryEdge === this.peerEntryEdge)
210
+ continue
211
+
212
+ // First, see if it's ok to just replace the peerSet entirely.
213
+ // we do this by walking out from the entryEdge, because in a case like
214
+ // this:
215
+ //
216
+ // v -> PEER(a@1||2)
217
+ // a@1 -> PEER(b@1)
218
+ // a@2 -> PEER(b@2)
219
+ // b@1 -> PEER(a@1)
220
+ // b@2 -> PEER(a@2)
221
+ //
222
+ // root
223
+ // +-- v
224
+ // +-- a@2
225
+ // +-- b@2
226
+ //
227
+ // Trying to place a peer group of (a@1, b@1) would fail to note that
228
+ // they can be replaced, if we did it by looping 1 by 1. If we are
229
+ // replacing something, we don't have to check its peer deps, because
230
+ // the peerDeps in the placed peerSet will presumably satisfy.
231
+ const entryNode = entryEdge.to
232
+ const entryRep = dep.parent.children.get(entryNode.name)
233
+ if (entryRep) {
234
+ if (entryRep.canReplace(entryNode, dep.parent.children.keys()))
235
+ continue
236
+ }
237
+
238
+ let canClobber = !entryRep
239
+ if (!entryRep) {
240
+ const peerReplacementWalk = new Set([entryNode])
241
+ OUTER: for (const currentPeer of peerReplacementWalk) {
242
+ for (const edge of currentPeer.edgesOut.values()) {
243
+ if (!edge.peer || !edge.valid)
244
+ continue
245
+ const rep = dep.parent.children.get(edge.name)
246
+ if (!rep) {
247
+ if (edge.to)
248
+ peerReplacementWalk.add(edge.to)
249
+ continue
250
+ }
251
+ if (!rep.satisfies(edge)) {
252
+ canClobber = false
253
+ break OUTER
254
+ }
255
+ }
256
+ }
257
+ }
258
+ if (canClobber)
259
+ continue
260
+
261
+ // ok, we can't replace, but maybe we can nest the current set deeper?
262
+ let canNestCurrent = true
263
+ for (const currentPeer of currentPeers) {
264
+ if (!canNestCurrent)
265
+ break
266
+
267
+ // still possible to nest this peerSet
268
+ const curDeep = deepestNestingTarget(entryEdge.from, currentPeer.name)
269
+ if (curDeep === target || target.isDescendantOf(curDeep)) {
270
+ canNestCurrent = false
271
+ canReplace = false
272
+ }
273
+ if (canNestCurrent)
274
+ continue
275
+ }
276
+ }
277
+
278
+ // if we can nest or replace all the current peer groups, we can replace.
279
+ if (canReplace)
280
+ return this.canPlacePeers(REPLACE)
281
+
282
+ return CONFLICT
283
+ }
284
+
285
+ checkCanPlaceNoCurrent () {
286
+ const { target, peerEntryEdge, dep, name } = this
287
+
288
+ // check to see what that name resolves to here, and who may depend on
289
+ // being able to reach it by crawling up past the parent. we know
290
+ // that it's not the target's direct child node, and if it was a direct
291
+ // dep of the target, we would have conflicted earlier.
292
+ const current = target !== peerEntryEdge.from && target.resolve(name)
293
+ if (current) {
294
+ for (const edge of current.edgesIn.values()) {
295
+ if (edge.from.isDescendantOf(target) && edge.valid) {
296
+ if (!dep.satisfies(edge))
297
+ return CONFLICT
298
+ }
299
+ }
300
+ }
301
+
302
+ // no objections, so this is fine as long as peers are ok here.
303
+ return this.canPlacePeers(OK)
304
+ }
305
+
306
+ get deepestNestingTarget () {
307
+ const start = this.parent ? this.parent.deepestNestingTarget
308
+ : this.edge.from
309
+ return deepestNestingTarget(start, this.name)
310
+ }
311
+
312
+ get conflictChildren () {
313
+ return this.allChildren.filter(c => c.canPlace === CONFLICT)
314
+ }
315
+
316
+ get allChildren () {
317
+ const set = new Set(this.children)
318
+ for (const child of set) {
319
+ for (const grandchild of child.children)
320
+ set.add(grandchild)
321
+ }
322
+ return [...set]
323
+ }
324
+
325
+ get top () {
326
+ return this.parent ? this.parent.top : this
327
+ }
328
+
329
+ // check if peers can go here. returns state or CONFLICT
330
+ canPlacePeers (state) {
331
+ this.canPlaceSelf = state
332
+ if (this._canPlacePeers)
333
+ return this._canPlacePeers
334
+
335
+ // TODO: represent peerPath in ERESOLVE error somehow?
336
+ const peerPath = [...this.peerPath, this.dep]
337
+ let sawConflict = false
338
+ for (const peerEdge of this.dep.edgesOut.values()) {
339
+ if (!peerEdge.peer || !peerEdge.to || peerPath.includes(peerEdge.to))
340
+ continue
341
+ const peer = peerEdge.to
342
+ // it may be the case that the *initial* dep can be nested, but a peer
343
+ // of that dep needs to be placed shallower, because the target has
344
+ // a peer dep on the peer as well.
345
+ const target = deepestNestingTarget(this.target, peer.name)
346
+ const cpp = new CanPlaceDep({
347
+ dep: peer,
348
+ target,
349
+ parent: this,
350
+ edge: peerEdge,
351
+ peerPath,
352
+ // always place peers in preferDedupe mode
353
+ preferDedupe: true,
354
+ })
355
+ /* istanbul ignore next */
356
+ debug(() => {
357
+ if (this.children.some(c => c.dep === cpp.dep))
358
+ throw new Error('checking same dep repeatedly')
359
+ })
360
+ this.children.push(cpp)
361
+
362
+ if (cpp.canPlace === CONFLICT)
363
+ sawConflict = true
364
+ }
365
+
366
+ this._canPlacePeers = sawConflict ? CONFLICT : state
367
+ return this._canPlacePeers
368
+ }
369
+
370
+ // what is the node that is causing this peerSet to be placed?
371
+ get peerSetSource () {
372
+ return this.parent ? this.parent.peerSetSource : this.edge.from
373
+ }
374
+
375
+ get peerEntryEdge () {
376
+ return this.top.edge
377
+ }
378
+
379
+ static get CONFLICT () {
380
+ return CONFLICT
381
+ }
382
+
383
+ static get OK () {
384
+ return OK
385
+ }
386
+
387
+ static get REPLACE () {
388
+ return REPLACE
389
+ }
390
+
391
+ static get KEEP () {
392
+ return KEEP
393
+ }
394
+
395
+ get description () {
396
+ const { canPlace } = this
397
+ return canPlace && canPlace.description ||
398
+ /* istanbul ignore next - old node affordance */ canPlace
399
+ }
400
+ }
401
+
402
+ module.exports = CanPlaceDep
@@ -0,0 +1,48 @@
1
+ // package children are represented with a Map object, but many file systems
2
+ // are case-insensitive and unicode-normalizing, so we need to treat
3
+ // node.children.get('FOO') and node.children.get('foo') as the same thing.
4
+
5
+ const _keys = Symbol('keys')
6
+ const _normKey = Symbol('normKey')
7
+ const normalize = s => s.normalize('NFKD').toLowerCase()
8
+ const OGMap = Map
9
+ module.exports = class Map extends OGMap {
10
+ constructor (items = []) {
11
+ super()
12
+ this[_keys] = new OGMap()
13
+ for (const [key, val] of items)
14
+ this.set(key, val)
15
+ }
16
+
17
+ [_normKey] (key) {
18
+ return typeof key === 'string' ? normalize(key) : key
19
+ }
20
+
21
+ get (key) {
22
+ const normKey = this[_normKey](key)
23
+ return this[_keys].has(normKey) ? super.get(this[_keys].get(normKey))
24
+ : undefined
25
+ }
26
+
27
+ set (key, val) {
28
+ const normKey = this[_normKey](key)
29
+ if (this[_keys].has(normKey))
30
+ super.delete(this[_keys].get(normKey))
31
+ this[_keys].set(normKey, key)
32
+ return super.set(key, val)
33
+ }
34
+
35
+ delete (key) {
36
+ const normKey = this[_normKey](key)
37
+ if (this[_keys].has(normKey)) {
38
+ const prevKey = this[_keys].get(normKey)
39
+ this[_keys].delete(normKey)
40
+ return super.delete(prevKey)
41
+ }
42
+ }
43
+
44
+ has (key) {
45
+ const normKey = this[_normKey](key)
46
+ return this[_keys].has(normKey) && super.has(this[_keys].get(normKey))
47
+ }
48
+ }
@@ -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 || target.globalTop)
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,10 +73,11 @@ 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) {
78
- return depValid(node, this.spec, this.accept, this.from)
80
+ return node.name === this.name && depValid(node, this.spec, this.accept, this.from)
79
81
  }
80
82
 
81
83
  explain (seen = []) {
@@ -165,7 +167,7 @@ class Edge {
165
167
  [_loadError] () {
166
168
  return !this[_to] ? (this.optional ? null : 'MISSING')
167
169
  : this.peer && this.from === this.to.parent && !this.from.isTop ? 'PEER LOCAL'
168
- : !depValid(this.to, this.spec, this.accept, this.from) ? 'INVALID'
170
+ : !this.satisfiedBy(this.to) ? 'INVALID'
169
171
  : 'OK'
170
172
  }
171
173
 
package/lib/node.js CHANGED
@@ -66,6 +66,7 @@ const relpath = require('./relpath.js')
66
66
  const consistentResolve = require('./consistent-resolve.js')
67
67
 
68
68
  const printableTree = require('./printable.js')
69
+ const CaseInsensitiveMap = require('./case-insensitive-map.js')
69
70
 
70
71
  class Node {
71
72
  constructor (options) {
@@ -148,7 +149,7 @@ class Node {
148
149
  this.hasShrinkwrap = hasShrinkwrap || pkg._hasShrinkwrap || false
149
150
  this.legacyPeerDeps = legacyPeerDeps
150
151
 
151
- this.children = new Map()
152
+ this.children = new CaseInsensitiveMap()
152
153
  this.fsChildren = new Set()
153
154
  this.inventory = new Inventory({})
154
155
  this.tops = new Set()
@@ -181,7 +182,7 @@ class Node {
181
182
  }
182
183
 
183
184
  this.edgesIn = new Set()
184
- this.edgesOut = new Map()
185
+ this.edgesOut = new CaseInsensitiveMap()
185
186
 
186
187
  // have to set the internal package ref before assigning the parent,
187
188
  // because this.package is read when adding to inventory
@@ -248,7 +249,7 @@ class Node {
248
249
 
249
250
  // true for packages installed directly in the global node_modules folder
250
251
  get globalTop () {
251
- return this.global && this.parent.isProjectRoot
252
+ return this.global && this.parent && this.parent.isProjectRoot
252
253
  }
253
254
 
254
255
  get workspaces () {
@@ -478,9 +479,17 @@ class Node {
478
479
  }
479
480
 
480
481
  get isProjectRoot () {
482
+ // only treat as project root if it's the actual link that is the root,
483
+ // or the target of the root link, but NOT if it's another link to the
484
+ // same root that happens to be somewhere else.
481
485
  return this === this.root || this === this.root.target
482
486
  }
483
487
 
488
+ * ancestry () {
489
+ for (let anc = this; anc; anc = anc.resolveParent)
490
+ yield anc
491
+ }
492
+
484
493
  set root (root) {
485
494
  // setting to null means this is the new root
486
495
  // should only ever be one step
@@ -767,9 +776,15 @@ class Node {
767
776
  this[_loadDepType](this.package.dependencies, 'prod')
768
777
  this[_loadDepType](this.package.optionalDependencies, 'optional')
769
778
 
770
- const { isTop, path, sourceReference } = this
771
- const { isTop: srcTop, path: srcPath } = sourceReference || {}
772
- if (isTop && path && (!sourceReference || srcTop && srcPath))
779
+ const { globalTop, isTop, path, sourceReference } = this
780
+ const {
781
+ globalTop: srcGlobalTop,
782
+ isTop: srcTop,
783
+ path: srcPath,
784
+ } = sourceReference || {}
785
+ const thisDev = isTop && !globalTop && path
786
+ const srcDev = !sourceReference || srcTop && !srcGlobalTop && srcPath
787
+ if (thisDev && srcDev)
773
788
  this[_loadDepType](this.package.devDependencies, 'dev')
774
789
  }
775
790
 
@@ -878,16 +893,31 @@ class Node {
878
893
  // root dependency brings peer deps along with it. In that case, we
879
894
  // will go ahead and create the invalid state, and then try to resolve
880
895
  // it with more tree construction, because it's a user request.
881
- canReplaceWith (node) {
896
+ canReplaceWith (node, ignorePeers = []) {
882
897
  if (node.name !== this.name)
883
898
  return false
884
899
 
900
+ if (node.packageName !== this.packageName)
901
+ return false
902
+
903
+ ignorePeers = new Set(ignorePeers)
904
+
885
905
  // gather up all the deps of this node and that are only depended
886
906
  // upon by deps of this node. those ones don't count, since
887
907
  // they'll be replaced if this node is replaced anyway.
888
908
  const depSet = gatherDepSet([this], e => e.to !== this && e.valid)
889
909
 
890
910
  for (const edge of this.edgesIn) {
911
+ // when replacing peer sets, we need to be able to replace the entire
912
+ // peer group, which means we ignore incoming edges from other peers
913
+ // within the replacement set.
914
+ const ignored = !this.isTop &&
915
+ edge.from.parent === this.parent &&
916
+ edge.peer &&
917
+ ignorePeers.has(edge.from.name)
918
+ if (ignored)
919
+ continue
920
+
891
921
  // only care about edges that don't originate from this node
892
922
  if (!depSet.has(edge.from) && !edge.satisfiedBy(node))
893
923
  return false
@@ -896,8 +926,8 @@ class Node {
896
926
  return true
897
927
  }
898
928
 
899
- canReplace (node) {
900
- return node.canReplaceWith(this)
929
+ canReplace (node, ignorePeers) {
930
+ return node.canReplaceWith(this, ignorePeers)
901
931
  }
902
932
 
903
933
  // return true if it's safe to remove this node, because anything that
@@ -994,8 +1024,20 @@ class Node {
994
1024
 
995
1025
  replace (node) {
996
1026
  this[_delistFromMeta]()
997
- this.path = node.path
998
- this.name = node.name
1027
+
1028
+ // if the name matches, but is not identical, we are intending to clobber
1029
+ // something case-insensitively, so merely setting name and path won't
1030
+ // have the desired effect. just set the path so it'll collide in the
1031
+ // parent's children map, and leave it at that.
1032
+ const nameMatch = node.parent &&
1033
+ node.parent.children.get(this.name) === node
1034
+ if (nameMatch)
1035
+ this.path = resolve(node.parent.path, 'node_modules', this.name)
1036
+ else {
1037
+ this.path = node.path
1038
+ this.name = node.name
1039
+ }
1040
+
999
1041
  if (!this.isLink)
1000
1042
  this.realpath = this.path
1001
1043
  this[_refreshLocation]()
@@ -1190,7 +1232,7 @@ class Node {
1190
1232
  }
1191
1233
 
1192
1234
  get isTop () {
1193
- return !this.parent
1235
+ return !this.parent || this.globalTop
1194
1236
  }
1195
1237
 
1196
1238
  get top () {
@@ -1210,6 +1252,12 @@ class Node {
1210
1252
  }
1211
1253
 
1212
1254
  resolve (name) {
1255
+ /* istanbul ignore next - should be impossible,
1256
+ * but I keep doing this mistake in tests */
1257
+ debug(() => {
1258
+ if (typeof name !== 'string' || !name)
1259
+ throw new Error('non-string passed to Node.resolve')
1260
+ })
1213
1261
  const mine = this.children.get(name)
1214
1262
  if (mine)
1215
1263
  return mine