@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
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) {
@@ -119,8 +120,9 @@ class Node {
119
120
  // should be equal if not a link
120
121
  this.path = path ? resolve(path) : null
121
122
 
122
- if (!this.name && (!this.path || this.path !== dirname(this.path)))
123
+ if (!this.name && (!this.path || this.path !== dirname(this.path))) {
123
124
  throw new TypeError('could not detect node name from path or package')
125
+ }
124
126
 
125
127
  this.realpath = !this.isLink ? this.path : resolve(realpath)
126
128
 
@@ -141,14 +143,15 @@ class Node {
141
143
  //
142
144
  // Otherwise, hopefully a shrinkwrap will help us out.
143
145
  const resolved = consistentResolve(pkg._resolved)
144
- if (resolved && !(/^file:/.test(resolved) && pkg._where))
146
+ if (resolved && !(/^file:/.test(resolved) && pkg._where)) {
145
147
  this.resolved = resolved
148
+ }
146
149
  }
147
150
  this.integrity = integrity || pkg._integrity || null
148
151
  this.hasShrinkwrap = hasShrinkwrap || pkg._hasShrinkwrap || false
149
152
  this.legacyPeerDeps = legacyPeerDeps
150
153
 
151
- this.children = new Map()
154
+ this.children = new CaseInsensitiveMap()
152
155
  this.fsChildren = new Set()
153
156
  this.inventory = new Inventory({})
154
157
  this.tops = new Set()
@@ -181,7 +184,7 @@ class Node {
181
184
  }
182
185
 
183
186
  this.edgesIn = new Set()
184
- this.edgesOut = new Map()
187
+ this.edgesOut = new CaseInsensitiveMap()
185
188
 
186
189
  // have to set the internal package ref before assigning the parent,
187
190
  // because this.package is read when adding to inventory
@@ -214,18 +217,21 @@ class Node {
214
217
  // see parent/root setters below.
215
218
  // root is set to parent's root if we have a parent, otherwise if it's
216
219
  // null, then it's set to the node itself.
217
- if (!parent && !fsParent)
220
+ if (!parent && !fsParent) {
218
221
  this.root = root || null
222
+ }
219
223
 
220
224
  // mostly a convenience for testing, but also a way to create
221
225
  // trees in a more declarative way than setting parent on each
222
226
  if (children) {
223
- for (const c of children)
227
+ for (const c of children) {
224
228
  new Node({ ...c, parent: this })
229
+ }
225
230
  }
226
231
  if (fsChildren) {
227
- for (const c of fsChildren)
232
+ for (const c of fsChildren) {
228
233
  new Node({ ...c, fsParent: this })
234
+ }
229
235
  }
230
236
 
231
237
  // now load all the dep edges
@@ -238,8 +244,9 @@ class Node {
238
244
 
239
245
  set meta (meta) {
240
246
  this[_meta] = meta
241
- if (meta)
247
+ if (meta) {
242
248
  meta.add(this)
249
+ }
243
250
  }
244
251
 
245
252
  get global () {
@@ -248,7 +255,7 @@ class Node {
248
255
 
249
256
  // true for packages installed directly in the global node_modules folder
250
257
  get globalTop () {
251
- return this.global && this.parent.isProjectRoot
258
+ return this.global && this.parent && this.parent.isProjectRoot
252
259
  }
253
260
 
254
261
  get workspaces () {
@@ -259,8 +266,9 @@ class Node {
259
266
  // deletes edges if they already exists
260
267
  if (this[_workspaces]) {
261
268
  for (const name of this[_workspaces].keys()) {
262
- if (!workspaces.has(name))
269
+ if (!workspaces.has(name)) {
263
270
  this.edgesOut.get(name).detach()
271
+ }
264
272
  }
265
273
  }
266
274
 
@@ -270,8 +278,9 @@ class Node {
270
278
  }
271
279
 
272
280
  get binPaths () {
273
- if (!this.parent)
281
+ if (!this.parent) {
274
282
  return []
283
+ }
275
284
 
276
285
  return getBinPaths({
277
286
  pkg: this[_package],
@@ -318,8 +327,9 @@ class Node {
318
327
  // only do this more than once at the root level, so the resolve() calls
319
328
  // are only one level deep, and there's not much to be saved, anyway.
320
329
  // simpler to just toss them all out.
321
- for (const edge of this.edgesOut.values())
330
+ for (const edge of this.edgesOut.values()) {
322
331
  edge.detach()
332
+ }
323
333
 
324
334
  this[_explanation] = null
325
335
  /* istanbul ignore next - should be impossible */
@@ -340,8 +350,9 @@ class Node {
340
350
  // node.explain(nodes seen already, edge we're trying to satisfy
341
351
  // if edge is not specified, it lists every edge into the node.
342
352
  explain (edge = null, seen = []) {
343
- if (this[_explanation])
353
+ if (this[_explanation]) {
344
354
  return this[_explanation]
355
+ }
345
356
 
346
357
  return this[_explanation] = this[_explain](edge, seen)
347
358
  }
@@ -373,11 +384,13 @@ class Node {
373
384
  }
374
385
  }
375
386
 
376
- if (this.sourceReference)
387
+ if (this.sourceReference) {
377
388
  return this.sourceReference.explain(edge, seen)
389
+ }
378
390
 
379
- if (seen.includes(this))
391
+ if (seen.includes(this)) {
380
392
  return why
393
+ }
381
394
 
382
395
  why.location = this.location
383
396
  why.isWorkspace = this.isWorkspace
@@ -386,56 +399,64 @@ class Node {
386
399
  seen = seen.concat(this)
387
400
 
388
401
  why.dependents = []
389
- if (edge)
402
+ if (edge) {
390
403
  why.dependents.push(edge.explain(seen))
391
- else {
404
+ } else {
392
405
  // ignore invalid edges, since those aren't satisfied by this thing,
393
406
  // and are not keeping it held in this spot anyway.
394
407
  const edges = []
395
408
  for (const edge of this.edgesIn) {
396
- if (!edge.valid && !edge.from.isProjectRoot)
409
+ if (!edge.valid && !edge.from.isProjectRoot) {
397
410
  continue
411
+ }
398
412
 
399
413
  edges.push(edge)
400
414
  }
401
- for (const edge of edges)
415
+ for (const edge of edges) {
402
416
  why.dependents.push(edge.explain(seen))
417
+ }
403
418
  }
404
419
 
405
- if (this.linksIn.size)
420
+ if (this.linksIn.size) {
406
421
  why.linksIn = [...this.linksIn].map(link => link[_explain](edge, seen))
422
+ }
407
423
 
408
424
  return why
409
425
  }
410
426
 
411
427
  isDescendantOf (node) {
412
428
  for (let p = this; p; p = p.resolveParent) {
413
- if (p === node)
429
+ if (p === node) {
414
430
  return true
431
+ }
415
432
  }
416
433
  return false
417
434
  }
418
435
 
419
436
  getBundler (path = []) {
420
437
  // made a cycle, definitely not bundled!
421
- if (path.includes(this))
438
+ if (path.includes(this)) {
422
439
  return null
440
+ }
423
441
 
424
442
  path.push(this)
425
443
 
426
444
  const parent = this[_parent]
427
- if (!parent)
445
+ if (!parent) {
428
446
  return null
447
+ }
429
448
 
430
449
  const pBundler = parent.getBundler(path)
431
- if (pBundler)
450
+ if (pBundler) {
432
451
  return pBundler
452
+ }
433
453
 
434
454
  const ppkg = parent.package
435
455
  const bd = ppkg && ppkg.bundleDependencies
436
456
  // explicit bundling
437
- if (Array.isArray(bd) && bd.includes(this.name))
457
+ if (Array.isArray(bd) && bd.includes(this.name)) {
438
458
  return parent
459
+ }
439
460
 
440
461
  // deps that are deduped up to the bundling level are bundled.
441
462
  // however, if they get their dep met further up than that,
@@ -443,11 +464,13 @@ class Node {
443
464
  // unmet bundled deps will not cause your deps to be bundled.
444
465
  for (const edge of this.edgesIn) {
445
466
  const eBundler = edge.from.getBundler(path)
446
- if (!eBundler)
467
+ if (!eBundler) {
447
468
  continue
469
+ }
448
470
 
449
- if (eBundler === parent)
471
+ if (eBundler === parent) {
450
472
  return eBundler
473
+ }
451
474
  }
452
475
 
453
476
  return null
@@ -466,8 +489,9 @@ class Node {
466
489
  }
467
490
 
468
491
  get isWorkspace () {
469
- if (this.isProjectRoot)
492
+ if (this.isProjectRoot) {
470
493
  return false
494
+ }
471
495
  const { root } = this
472
496
  const { type, to } = root.edgesOut.get(this.packageName) || {}
473
497
  return type === 'workspace' && to && (to.target === this || to === this)
@@ -478,14 +502,24 @@ class Node {
478
502
  }
479
503
 
480
504
  get isProjectRoot () {
505
+ // only treat as project root if it's the actual link that is the root,
506
+ // or the target of the root link, but NOT if it's another link to the
507
+ // same root that happens to be somewhere else.
481
508
  return this === this.root || this === this.root.target
482
509
  }
483
510
 
511
+ * ancestry () {
512
+ for (let anc = this; anc; anc = anc.resolveParent) {
513
+ yield anc
514
+ }
515
+ }
516
+
484
517
  set root (root) {
485
518
  // setting to null means this is the new root
486
519
  // should only ever be one step
487
- while (root && root.root !== root)
520
+ while (root && root.root !== root) {
488
521
  root = root.root
522
+ }
489
523
 
490
524
  root = root || this
491
525
 
@@ -495,8 +529,9 @@ class Node {
495
529
  // can't set the root (yet) if there's no way to determine location
496
530
  // this allows us to do new Node({...}) and then set the root later.
497
531
  // just make the assignment so we don't lose it, and move on.
498
- if (!this.path || !root.realpath || !root.path)
532
+ if (!this.path || !root.realpath || !root.path) {
499
533
  return this[_root] = root
534
+ }
500
535
 
501
536
  // temporarily become a root node
502
537
  this[_root] = this
@@ -512,8 +547,9 @@ class Node {
512
547
  if (this.isLink) {
513
548
  if (target) {
514
549
  target.linksIn.delete(this)
515
- if (target.root === this)
550
+ if (target.root === this) {
516
551
  target[_delistFromMeta]()
552
+ }
517
553
  }
518
554
  this[_target] = null
519
555
  }
@@ -530,16 +566,17 @@ class Node {
530
566
  this[_fsParent] = null
531
567
  }
532
568
 
533
- if (root === this)
569
+ if (root === this) {
534
570
  this[_refreshLocation]()
535
- else {
571
+ } else {
536
572
  // setting to some different node.
537
573
  const loc = relpath(root.realpath, this.path)
538
574
  const current = root.inventory.get(loc)
539
575
 
540
576
  // clobber whatever is there now
541
- if (current)
577
+ if (current) {
542
578
  current.root = null
579
+ }
543
580
 
544
581
  this[_root] = root
545
582
  // set this.location and add to inventory
@@ -547,8 +584,9 @@ class Node {
547
584
 
548
585
  // try to find our parent/fsParent in the new root inventory
549
586
  for (const p of walkUp(dirname(this.path))) {
550
- if (p === this.path)
587
+ if (p === this.path) {
551
588
  continue
589
+ }
552
590
  const ploc = relpath(root.realpath, p)
553
591
  const parent = root.inventory.get(ploc)
554
592
  if (parent) {
@@ -567,8 +605,9 @@ class Node {
567
605
  const isParent = this.location === childLoc
568
606
  if (isParent) {
569
607
  const oldChild = parent.children.get(this.name)
570
- if (oldChild && oldChild !== this)
608
+ if (oldChild && oldChild !== this) {
571
609
  oldChild.root = null
610
+ }
572
611
  if (this.parent) {
573
612
  this.parent.children.delete(this.name)
574
613
  this.parent[_reloadNamedEdges](this.name)
@@ -577,13 +616,15 @@ class Node {
577
616
  this[_parent] = parent
578
617
  // don't do it for links, because they don't have a target yet
579
618
  // we'll hit them up a bit later on.
580
- if (!this.isLink)
619
+ if (!this.isLink) {
581
620
  parent[_reloadNamedEdges](this.name)
621
+ }
582
622
  } else {
583
623
  /* istanbul ignore if - should be impossible, since we break
584
624
  * all fsParent/child relationships when moving? */
585
- if (this.fsParent)
625
+ if (this.fsParent) {
586
626
  this.fsParent.fsChildren.delete(this)
627
+ }
587
628
  parent.fsChildren.add(this)
588
629
  this[_fsParent] = parent
589
630
  }
@@ -592,10 +633,11 @@ class Node {
592
633
  }
593
634
 
594
635
  // if it doesn't have a parent, it's a top node
595
- if (!this.parent)
636
+ if (!this.parent) {
596
637
  root.tops.add(this)
597
- else
638
+ } else {
598
639
  root.tops.delete(this)
640
+ }
599
641
 
600
642
  // assign parentage for any nodes that need to have this as a parent
601
643
  // this can happen when we have a node at nm/a/nm/b added *before*
@@ -605,24 +647,30 @@ class Node {
605
647
  const nmloc = `${this.location}${this.location ? '/' : ''}node_modules/`
606
648
  const isChild = n => n.location === nmloc + n.name
607
649
  // check dirname so that /foo isn't treated as the fsparent of /foo-bar
608
- const isFsChild = n => dirname(n.path).startsWith(this.path) &&
609
- n !== this &&
610
- !n.parent &&
611
- (!n.fsParent || n.fsParent === this || dirname(this.path).startsWith(n.fsParent.path))
650
+ const isFsChild = n => {
651
+ return dirname(n.path).startsWith(this.path) &&
652
+ n !== this &&
653
+ !n.parent &&
654
+ (!n.fsParent ||
655
+ n.fsParent === this ||
656
+ dirname(this.path).startsWith(n.fsParent.path))
657
+ }
612
658
  const isKid = n => isChild(n) || isFsChild(n)
613
659
 
614
660
  // only walk top nodes, since anything else already has a parent.
615
661
  for (const child of root.tops) {
616
- if (!isKid(child))
662
+ if (!isKid(child)) {
617
663
  continue
664
+ }
618
665
 
619
666
  // set up the internal parentage links
620
- if (this.isLink)
667
+ if (this.isLink) {
621
668
  child.root = null
622
- else {
669
+ } else {
623
670
  // can't possibly have a parent, because it's in tops
624
- if (child.fsParent)
671
+ if (child.fsParent) {
625
672
  child.fsParent.fsChildren.delete(child)
673
+ }
626
674
  child[_fsParent] = null
627
675
  if (isChild(child)) {
628
676
  this.children.set(child.name, child)
@@ -639,13 +687,15 @@ class Node {
639
687
  // to that realpath, or a thing at that realpath if we're adding a link
640
688
  // (if we're adding a regular node, we already deleted the old one)
641
689
  for (const node of root.inventory.query('realpath', this.realpath)) {
642
- if (node === this)
690
+ if (node === this) {
643
691
  continue
692
+ }
644
693
 
645
694
  /* istanbul ignore next - should be impossible */
646
695
  debug(() => {
647
- if (node.root !== root)
696
+ if (node.root !== root) {
648
697
  throw new Error('inventory contains node from other root')
698
+ }
649
699
  })
650
700
 
651
701
  if (this.isLink) {
@@ -654,8 +704,9 @@ class Node {
654
704
  this[_package] = target.package
655
705
  target.linksIn.add(this)
656
706
  // reload edges here, because now we have a target
657
- if (this.parent)
707
+ if (this.parent) {
658
708
  this.parent[_reloadNamedEdges](this.name)
709
+ }
659
710
  break
660
711
  } else {
661
712
  /* istanbul ignore else - should be impossible */
@@ -663,8 +714,9 @@ class Node {
663
714
  node[_target] = this
664
715
  node[_package] = this.package
665
716
  this.linksIn.add(node)
666
- if (node.parent)
717
+ if (node.parent) {
667
718
  node.parent[_reloadNamedEdges](node.name)
719
+ }
668
720
  } else {
669
721
  debug(() => {
670
722
  throw Object.assign(new Error('duplicate node in root setter'), {
@@ -681,14 +733,16 @@ class Node {
681
733
  // reload all edgesIn where the root doesn't match, so we don't have
682
734
  // cross-tree dependency graphs
683
735
  for (const edge of this.edgesIn) {
684
- if (edge.from.root !== root)
736
+ if (edge.from.root !== root) {
685
737
  edge.reload()
738
+ }
686
739
  }
687
740
  // reload all edgesOut where root doens't match, or is missing, since
688
741
  // it might not be missing in the new tree
689
742
  for (const edge of this.edgesOut.values()) {
690
- if (!edge.to || edge.to.root !== root)
743
+ if (!edge.to || edge.to.root !== root) {
691
744
  edge.reload()
745
+ }
692
746
  }
693
747
 
694
748
  // now make sure our family comes along for the ride!
@@ -712,15 +766,17 @@ class Node {
712
766
  }
713
767
  }
714
768
  for (const child of family) {
715
- if (child.root !== root)
769
+ if (child.root !== root) {
716
770
  child.root = root
771
+ }
717
772
  }
718
773
 
719
774
  // if we had a target, and didn't find one in the new root, then bring
720
775
  // it over as well, but only if we're setting the link into a new root,
721
776
  // as we don't want to lose the target any time we remove a link.
722
- if (this.isLink && target && !this.target && root !== this)
777
+ if (this.isLink && target && !this.target && root !== this) {
723
778
  target.root = root
779
+ }
724
780
 
725
781
  // tree should always be valid upon root setter completion.
726
782
  treeCheck(this)
@@ -732,11 +788,13 @@ class Node {
732
788
  }
733
789
 
734
790
  [_loadWorkspaces] () {
735
- if (!this[_workspaces])
791
+ if (!this[_workspaces]) {
736
792
  return
793
+ }
737
794
 
738
- for (const [name, path] of this[_workspaces].entries())
795
+ for (const [name, path] of this[_workspaces].entries()) {
739
796
  new Edge({ from: this, name, spec: `file:${path}`, type: 'workspace' })
797
+ }
740
798
  }
741
799
 
742
800
  [_loadDeps] () {
@@ -755,10 +813,11 @@ class Node {
755
813
  const peerDependencies = {}
756
814
  const peerOptional = {}
757
815
  for (const [name, dep] of Object.entries(pd)) {
758
- if (pm[name] && pm[name].optional)
816
+ if (pm[name] && pm[name].optional) {
759
817
  peerOptional[name] = dep
760
- else
818
+ } else {
761
819
  peerDependencies[name] = dep
820
+ }
762
821
  }
763
822
  this[_loadDepType](peerDependencies, 'peer')
764
823
  this[_loadDepType](peerOptional, 'peerOptional')
@@ -767,10 +826,17 @@ class Node {
767
826
  this[_loadDepType](this.package.dependencies, 'prod')
768
827
  this[_loadDepType](this.package.optionalDependencies, 'optional')
769
828
 
770
- const { isTop, path, sourceReference } = this
771
- const { isTop: srcTop, path: srcPath } = sourceReference || {}
772
- if (isTop && path && (!sourceReference || srcTop && srcPath))
829
+ const { globalTop, isTop, path, sourceReference } = this
830
+ const {
831
+ globalTop: srcGlobalTop,
832
+ isTop: srcTop,
833
+ path: srcPath,
834
+ } = sourceReference || {}
835
+ const thisDev = isTop && !globalTop && path
836
+ const srcDev = !sourceReference || srcTop && !srcGlobalTop && srcPath
837
+ if (thisDev && srcDev) {
773
838
  this[_loadDepType](this.package.devDependencies, 'dev')
839
+ }
774
840
  }
775
841
 
776
842
  [_loadDepType] (deps, type) {
@@ -779,8 +845,9 @@ class Node {
779
845
  // prioritize a new edge over an existing one
780
846
  for (const [name, spec] of Object.entries(deps || {})) {
781
847
  const current = this.edgesOut.get(name)
782
- if (!current || current.type !== 'workspace')
848
+ if (!current || current.type !== 'workspace') {
783
849
  new Edge({ from: this, name, spec, accept: ad[name], type })
850
+ }
784
851
  }
785
852
  }
786
853
 
@@ -788,25 +855,29 @@ class Node {
788
855
  const parent = this[_fsParent]
789
856
  /* istanbul ignore next - should be impossible */
790
857
  debug(() => {
791
- if (parent === this)
858
+ if (parent === this) {
792
859
  throw new Error('node set to its own fsParent')
860
+ }
793
861
  })
794
862
  return parent
795
863
  }
796
864
 
797
865
  set fsParent (fsParent) {
798
866
  if (!fsParent) {
799
- if (this[_fsParent])
867
+ if (this[_fsParent]) {
800
868
  this.root = null
869
+ }
801
870
  return
802
871
  }
803
872
 
804
873
  debug(() => {
805
- if (fsParent === this)
874
+ if (fsParent === this) {
806
875
  throw new Error('setting node to its own fsParent')
876
+ }
807
877
 
808
- if (fsParent.realpath === this.realpath)
878
+ if (fsParent.realpath === this.realpath) {
809
879
  throw new Error('setting fsParent to same path')
880
+ }
810
881
 
811
882
  // the initial set MUST be an actual walk-up from the realpath
812
883
  // subsequent sets will re-root on the new fsParent's path.
@@ -822,16 +893,19 @@ class Node {
822
893
  }
823
894
  })
824
895
 
825
- if (fsParent.isLink)
896
+ if (fsParent.isLink) {
826
897
  fsParent = fsParent.target
898
+ }
827
899
 
828
900
  // setting a thing to its own fsParent is not normal, but no-op for safety
829
- if (this === fsParent || fsParent.realpath === this.realpath)
901
+ if (this === fsParent || fsParent.realpath === this.realpath) {
830
902
  return
903
+ }
831
904
 
832
905
  // nothing to do
833
- if (this[_fsParent] === fsParent)
906
+ if (this[_fsParent] === fsParent) {
834
907
  return
908
+ }
835
909
 
836
910
  const oldFsParent = this[_fsParent]
837
911
  const newPath = !oldFsParent ? this.path
@@ -859,11 +933,13 @@ class Node {
859
933
  }
860
934
 
861
935
  // update this.path/realpath for this and all children/fsChildren
862
- if (pathChange)
936
+ if (pathChange) {
863
937
  this[_changePath](newPath)
938
+ }
864
939
 
865
- if (oldParent)
940
+ if (oldParent) {
866
941
  oldParent[_reloadNamedEdges](oldName)
942
+ }
867
943
 
868
944
  // clobbers anything at that path, resets all appropriate references
869
945
  this.root = fsParent.root
@@ -878,9 +954,16 @@ class Node {
878
954
  // root dependency brings peer deps along with it. In that case, we
879
955
  // will go ahead and create the invalid state, and then try to resolve
880
956
  // it with more tree construction, because it's a user request.
881
- canReplaceWith (node) {
882
- if (node.name !== this.name)
957
+ canReplaceWith (node, ignorePeers = []) {
958
+ if (node.name !== this.name) {
883
959
  return false
960
+ }
961
+
962
+ if (node.packageName !== this.packageName) {
963
+ return false
964
+ }
965
+
966
+ ignorePeers = new Set(ignorePeers)
884
967
 
885
968
  // gather up all the deps of this node and that are only depended
886
969
  // upon by deps of this node. those ones don't count, since
@@ -888,16 +971,28 @@ class Node {
888
971
  const depSet = gatherDepSet([this], e => e.to !== this && e.valid)
889
972
 
890
973
  for (const edge of this.edgesIn) {
974
+ // when replacing peer sets, we need to be able to replace the entire
975
+ // peer group, which means we ignore incoming edges from other peers
976
+ // within the replacement set.
977
+ const ignored = !this.isTop &&
978
+ edge.from.parent === this.parent &&
979
+ edge.peer &&
980
+ ignorePeers.has(edge.from.name)
981
+ if (ignored) {
982
+ continue
983
+ }
984
+
891
985
  // only care about edges that don't originate from this node
892
- if (!depSet.has(edge.from) && !edge.satisfiedBy(node))
986
+ if (!depSet.has(edge.from) && !edge.satisfiedBy(node)) {
893
987
  return false
988
+ }
894
989
  }
895
990
 
896
991
  return true
897
992
  }
898
993
 
899
- canReplace (node) {
900
- return node.canReplaceWith(this)
994
+ canReplace (node, ignorePeers) {
995
+ return node.canReplaceWith(this, ignorePeers)
901
996
  }
902
997
 
903
998
  // return true if it's safe to remove this node, because anything that
@@ -905,41 +1000,49 @@ class Node {
905
1000
  // to if it was removed, or nothing is depending on it in the first place.
906
1001
  canDedupe (preferDedupe = false) {
907
1002
  // not allowed to mess with shrinkwraps or bundles
908
- if (this.inDepBundle || this.inShrinkwrap)
1003
+ if (this.inDepBundle || this.inShrinkwrap) {
909
1004
  return false
1005
+ }
910
1006
 
911
1007
  // it's a top level pkg, or a dep of one
912
- if (!this.resolveParent || !this.resolveParent.resolveParent)
1008
+ if (!this.resolveParent || !this.resolveParent.resolveParent) {
913
1009
  return false
1010
+ }
914
1011
 
915
1012
  // no one wants it, remove it
916
- if (this.edgesIn.size === 0)
1013
+ if (this.edgesIn.size === 0) {
917
1014
  return true
1015
+ }
918
1016
 
919
1017
  const other = this.resolveParent.resolveParent.resolve(this.name)
920
1018
 
921
1019
  // nothing else, need this one
922
- if (!other)
1020
+ if (!other) {
923
1021
  return false
1022
+ }
924
1023
 
925
1024
  // if it's the same thing, then always fine to remove
926
- if (other.matches(this))
1025
+ if (other.matches(this)) {
927
1026
  return true
1027
+ }
928
1028
 
929
1029
  // if the other thing can't replace this, then skip it
930
- if (!other.canReplace(this))
1030
+ if (!other.canReplace(this)) {
931
1031
  return false
1032
+ }
932
1033
 
933
1034
  // if we prefer dedupe, or if the version is greater/equal, take the other
934
- if (preferDedupe || semver.gte(other.version, this.version))
1035
+ if (preferDedupe || semver.gte(other.version, this.version)) {
935
1036
  return true
1037
+ }
936
1038
 
937
1039
  return false
938
1040
  }
939
1041
 
940
1042
  satisfies (requested) {
941
- if (requested instanceof Edge)
1043
+ if (requested instanceof Edge) {
942
1044
  return this.name === requested.name && requested.satisfiedBy(this)
1045
+ }
943
1046
 
944
1047
  const parsed = npa(requested)
945
1048
  const { name = this.name, rawSpec: spec } = parsed
@@ -953,29 +1056,35 @@ class Node {
953
1056
 
954
1057
  matches (node) {
955
1058
  // if the nodes are literally the same object, obviously a match.
956
- if (node === this)
1059
+ if (node === this) {
957
1060
  return true
1061
+ }
958
1062
 
959
1063
  // if the names don't match, they're different things, even if
960
1064
  // the package contents are identical.
961
- if (node.name !== this.name)
1065
+ if (node.name !== this.name) {
962
1066
  return false
1067
+ }
963
1068
 
964
1069
  // if they're links, they match if the targets match
965
- if (this.isLink)
1070
+ if (this.isLink) {
966
1071
  return node.isLink && this.target.matches(node.target)
1072
+ }
967
1073
 
968
1074
  // if they're two project root nodes, they're different if the paths differ
969
- if (this.isProjectRoot && node.isProjectRoot)
1075
+ if (this.isProjectRoot && node.isProjectRoot) {
970
1076
  return this.path === node.path
1077
+ }
971
1078
 
972
1079
  // if the integrity matches, then they're the same.
973
- if (this.integrity && node.integrity)
1080
+ if (this.integrity && node.integrity) {
974
1081
  return this.integrity === node.integrity
1082
+ }
975
1083
 
976
1084
  // if no integrity, check resolved
977
- if (this.resolved && node.resolved)
1085
+ if (this.resolved && node.resolved) {
978
1086
  return this.resolved === node.resolved
1087
+ }
979
1088
 
980
1089
  // if no resolved, check both package name and version
981
1090
  // otherwise, conclude that they are different things
@@ -994,34 +1103,51 @@ class Node {
994
1103
 
995
1104
  replace (node) {
996
1105
  this[_delistFromMeta]()
997
- this.path = node.path
998
- this.name = node.name
999
- if (!this.isLink)
1106
+
1107
+ // if the name matches, but is not identical, we are intending to clobber
1108
+ // something case-insensitively, so merely setting name and path won't
1109
+ // have the desired effect. just set the path so it'll collide in the
1110
+ // parent's children map, and leave it at that.
1111
+ const nameMatch = node.parent &&
1112
+ node.parent.children.get(this.name) === node
1113
+ if (nameMatch) {
1114
+ this.path = resolve(node.parent.path, 'node_modules', this.name)
1115
+ } else {
1116
+ this.path = node.path
1117
+ this.name = node.name
1118
+ }
1119
+
1120
+ if (!this.isLink) {
1000
1121
  this.realpath = this.path
1122
+ }
1001
1123
  this[_refreshLocation]()
1002
1124
 
1003
1125
  // keep children when a node replaces another
1004
1126
  if (!this.isLink) {
1005
- for (const kid of node.children.values())
1127
+ for (const kid of node.children.values()) {
1006
1128
  kid.parent = this
1129
+ }
1007
1130
  }
1008
1131
 
1009
- if (!node.isRoot)
1132
+ if (!node.isRoot) {
1010
1133
  this.root = node.root
1134
+ }
1011
1135
 
1012
1136
  treeCheck(this)
1013
1137
  }
1014
1138
 
1015
1139
  get inShrinkwrap () {
1016
- return this.parent && (this.parent.hasShrinkwrap || this.parent.inShrinkwrap)
1140
+ return this.parent &&
1141
+ (this.parent.hasShrinkwrap || this.parent.inShrinkwrap)
1017
1142
  }
1018
1143
 
1019
1144
  get parent () {
1020
1145
  const parent = this[_parent]
1021
1146
  /* istanbul ignore next - should be impossible */
1022
1147
  debug(() => {
1023
- if (parent === this)
1148
+ if (parent === this) {
1024
1149
  throw new Error('node set to its own parent')
1150
+ }
1025
1151
  })
1026
1152
  return parent
1027
1153
  }
@@ -1041,23 +1167,27 @@ class Node {
1041
1167
  if (!parent) {
1042
1168
  // but only delete it if we actually had a parent in the first place
1043
1169
  // otherwise it's just setting to null when it's already null
1044
- if (this[_parent])
1170
+ if (this[_parent]) {
1045
1171
  this.root = null
1172
+ }
1046
1173
  return
1047
1174
  }
1048
1175
 
1049
- if (parent.isLink)
1176
+ if (parent.isLink) {
1050
1177
  parent = parent.target
1178
+ }
1051
1179
 
1052
1180
  // setting a thing to its own parent is not normal, but no-op for safety
1053
- if (this === parent)
1181
+ if (this === parent) {
1054
1182
  return
1183
+ }
1055
1184
 
1056
1185
  const oldParent = this[_parent]
1057
1186
 
1058
1187
  // nothing to do
1059
- if (oldParent === parent)
1188
+ if (oldParent === parent) {
1060
1189
  return
1190
+ }
1061
1191
 
1062
1192
  // ok now we know something is actually changing, and parent is not a link
1063
1193
  const newPath = resolve(parent.path, 'node_modules', this.name)
@@ -1074,8 +1204,9 @@ class Node {
1074
1204
  }
1075
1205
 
1076
1206
  // update this.path/realpath for this and all children/fsChildren
1077
- if (pathChange)
1207
+ if (pathChange) {
1078
1208
  this[_changePath](newPath)
1209
+ }
1079
1210
 
1080
1211
  // clobbers anything at that path, resets all appropriate references
1081
1212
  this.root = parent.root
@@ -1085,16 +1216,19 @@ class Node {
1085
1216
  // Removes the node from its root the metadata and inventory.
1086
1217
  [_delistFromMeta] () {
1087
1218
  const root = this.root
1088
- if (!root.realpath || !this.path)
1219
+ if (!root.realpath || !this.path) {
1089
1220
  return
1221
+ }
1090
1222
  root.inventory.delete(this)
1091
1223
  root.tops.delete(this)
1092
- if (root.meta)
1224
+ if (root.meta) {
1093
1225
  root.meta.delete(this.path)
1226
+ }
1094
1227
  /* istanbul ignore next - should be impossible */
1095
1228
  debug(() => {
1096
- if ([...root.inventory.values()].includes(this))
1229
+ if ([...root.inventory.values()].includes(this)) {
1097
1230
  throw new Error('failed to delist')
1231
+ }
1098
1232
  })
1099
1233
  }
1100
1234
 
@@ -1106,8 +1240,9 @@ class Node {
1106
1240
  this.path = newPath
1107
1241
  const namePattern = /(?:^|\/|\\)node_modules[\\/](@[^/\\]+[\\/][^\\/]+|[^\\/]+)$/
1108
1242
  const nameChange = newPath.match(namePattern)
1109
- if (nameChange && this.name !== nameChange[1])
1243
+ if (nameChange && this.name !== nameChange[1]) {
1110
1244
  this.name = nameChange[1].replace(/\\/g, '/')
1245
+ }
1111
1246
 
1112
1247
  // if we move a link target, update link realpaths
1113
1248
  if (!this.isLink) {
@@ -1119,10 +1254,12 @@ class Node {
1119
1254
  }
1120
1255
  }
1121
1256
  // if we move /x to /y, then a module at /x/a/b becomes /y/a/b
1122
- for (const child of this.fsChildren)
1257
+ for (const child of this.fsChildren) {
1123
1258
  child[_changePath](resolve(newPath, relative(oldPath, child.path)))
1124
- for (const [name, child] of this.children.entries())
1259
+ }
1260
+ for (const [name, child] of this.children.entries()) {
1125
1261
  child[_changePath](resolve(newPath, 'node_modules', name))
1262
+ }
1126
1263
 
1127
1264
  this[_refreshLocation]()
1128
1265
  }
@@ -1137,8 +1274,9 @@ class Node {
1137
1274
  this.location = loc
1138
1275
 
1139
1276
  root.inventory.add(this)
1140
- if (root.meta)
1277
+ if (root.meta) {
1141
1278
  root.meta.add(this)
1279
+ }
1142
1280
  }
1143
1281
 
1144
1282
  addEdgeOut (edge) {
@@ -1149,8 +1287,9 @@ class Node {
1149
1287
  this.edgesIn.add(edge)
1150
1288
 
1151
1289
  // try to get metadata from the yarn.lock file
1152
- if (this.root.meta)
1290
+ if (this.root.meta) {
1153
1291
  this.root.meta.addEdge(edge)
1292
+ }
1154
1293
  }
1155
1294
 
1156
1295
  [_reloadNamedEdges] (name, rootLoc = this.location) {
@@ -1160,13 +1299,16 @@ class Node {
1160
1299
  edge.to.location === `${rootLoc}/node_modules/${edge.name}`
1161
1300
  const sameResolved = edge && this.resolve(name) === edge.to
1162
1301
  const recheck = rootLocResolved || !sameResolved
1163
- if (edge && recheck)
1302
+ if (edge && recheck) {
1164
1303
  edge.reload(true)
1165
- for (const c of this.children.values())
1304
+ }
1305
+ for (const c of this.children.values()) {
1166
1306
  c[_reloadNamedEdges](name, rootLoc)
1307
+ }
1167
1308
 
1168
- for (const c of this.fsChildren)
1309
+ for (const c of this.fsChildren) {
1169
1310
  c[_reloadNamedEdges](name, rootLoc)
1311
+ }
1170
1312
  }
1171
1313
 
1172
1314
  get isLink () {
@@ -1190,7 +1332,7 @@ class Node {
1190
1332
  }
1191
1333
 
1192
1334
  get isTop () {
1193
- return !this.parent
1335
+ return !this.parent || this.globalTop
1194
1336
  }
1195
1337
 
1196
1338
  get top () {
@@ -1210,12 +1352,21 @@ class Node {
1210
1352
  }
1211
1353
 
1212
1354
  resolve (name) {
1355
+ /* istanbul ignore next - should be impossible,
1356
+ * but I keep doing this mistake in tests */
1357
+ debug(() => {
1358
+ if (typeof name !== 'string' || !name) {
1359
+ throw new Error('non-string passed to Node.resolve')
1360
+ }
1361
+ })
1213
1362
  const mine = this.children.get(name)
1214
- if (mine)
1363
+ if (mine) {
1215
1364
  return mine
1365
+ }
1216
1366
  const resolveParent = this.resolveParent
1217
- if (resolveParent)
1367
+ if (resolveParent) {
1218
1368
  return resolveParent.resolve(name)
1369
+ }
1219
1370
  return null
1220
1371
  }
1221
1372