@npmcli/arborist 2.4.0 → 2.4.1

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.
@@ -47,6 +47,7 @@ const _flagsSuspect = Symbol.for('flagsSuspect')
47
47
  const _workspaces = Symbol.for('workspaces')
48
48
  const _prune = Symbol('prune')
49
49
  const _preferDedupe = Symbol('preferDedupe')
50
+ const _pruneDedupable = Symbol('pruneDedupable')
50
51
  const _legacyBundling = Symbol('legacyBundling')
51
52
  const _parseSettings = Symbol('parseSettings')
52
53
  const _initTree = Symbol('initTree')
@@ -1342,6 +1343,21 @@ This is a one-time fix-up, please be patient...
1342
1343
  // this is an overridden peer dep
1343
1344
  this[_warnPeerConflict](edge)
1344
1345
  }
1346
+
1347
+ // if we get a KEEP in a update scenario, then we MAY have something
1348
+ // already duplicating this unnecessarily! For example:
1349
+ // ```
1350
+ // root
1351
+ // +-- x (dep: y@1.x)
1352
+ // | +-- y@1.0.0
1353
+ // +-- y@1.1.0
1354
+ // ```
1355
+ // Now say we do `reify({update:['y']})`, and the latest version is
1356
+ // 1.1.0, which we already have in the root. We'll try to place y@1.1.0
1357
+ // first in x, then in the root, ending with KEEP, because we already
1358
+ // have it. In that case, we ought to REMOVE the nm/x/nm/y node, because
1359
+ // it is an unnecessary duplicate.
1360
+ this[_pruneDedupable](target, true)
1345
1361
  return []
1346
1362
  }
1347
1363
 
@@ -1397,8 +1413,8 @@ This is a one-time fix-up, please be patient...
1397
1413
  // MAY end up putting a better/identical node further up the tree in
1398
1414
  // a way that causes an unnecessary duplication. If so, remove the
1399
1415
  // now-unnecessary node.
1400
- if (edge.valid && edge.to.parent !== target && newDep.canReplace(edge.to))
1401
- edge.to.parent = null
1416
+ if (edge.valid && edge.to && edge.to !== newDep)
1417
+ this[_pruneDedupable](edge.to, false)
1402
1418
 
1403
1419
  // visit any dependents who are upset by this change
1404
1420
  // if it's an angry overridden peer edge, however, make sure we
@@ -1414,30 +1430,8 @@ This is a one-time fix-up, please be patient...
1414
1430
  // prune anything deeper in the tree that can be replaced by this
1415
1431
  if (this.idealTree) {
1416
1432
  for (const node of this.idealTree.inventory.query('name', newDep.name)) {
1417
- if (node !== newDep &&
1418
- node.isDescendantOf(target) &&
1419
- !node.inShrinkwrap &&
1420
- !node.inBundle &&
1421
- node.canReplaceWith(newDep)) {
1422
- // don't prune if the dupe is necessary!
1423
- // root (a, d)
1424
- // +-- a (b, c2)
1425
- // | +-- b (c2) <-- place c2 for b, lands at root
1426
- // +-- d (e)
1427
- // +-- e (c1, d)
1428
- // +-- c1
1429
- // +-- f (c2)
1430
- // +-- c2 <-- pruning this would be bad
1431
-
1432
- const mask = node.parent !== target &&
1433
- node.parent &&
1434
- node.parent.parent &&
1435
- node.parent.parent !== target &&
1436
- node.parent.parent.resolve(newDep.name)
1437
-
1438
- if (!mask || mask === newDep || node.canReplaceWith(mask))
1439
- node.parent = null
1440
- }
1433
+ if (node.isDescendantOf(target))
1434
+ this[_pruneDedupable](node, false)
1441
1435
  }
1442
1436
  }
1443
1437
 
@@ -1470,6 +1464,21 @@ This is a one-time fix-up, please be patient...
1470
1464
  return placed
1471
1465
  }
1472
1466
 
1467
+ // prune all the nodes in a branch of the tree that can be safely removed
1468
+ // This is only the most basic duplication detection; it finds if there
1469
+ // is another satisfying node further up the tree, and if so, dedupes.
1470
+ // Even in legacyBundling mode, we do this amount of deduplication.
1471
+ [_pruneDedupable] (node, descend = true) {
1472
+ if (node.canDedupe(this[_preferDedupe])) {
1473
+ node.root = null
1474
+ return
1475
+ }
1476
+ if (descend) {
1477
+ for (const child of node.children.values())
1478
+ this[_pruneDedupable](child)
1479
+ }
1480
+ }
1481
+
1473
1482
  [_pruneForReplacement] (node, oldDeps) {
1474
1483
  // gather up all the invalid edgesOut, and any now-extraneous
1475
1484
  // deps that the new node doesn't depend on but the old one did.
@@ -1622,32 +1631,137 @@ This is a one-time fix-up, please be patient...
1622
1631
  // placed here as well. the virtualRoot already has the appropriate
1623
1632
  // overrides applied.
1624
1633
  if (peerEntryEdge) {
1625
- const peerSet = getPeerSet(current)
1626
- OUTER: for (const p of peerSet) {
1627
- // if any have a non-peer dep from the target, or a peer dep if
1628
- // the target is root, then cannot safely replace and dupe deeper.
1634
+ const currentPeerSet = getPeerSet(current)
1635
+
1636
+ // We are effectively replacing currentPeerSet with newPeerSet
1637
+ // If there are any non-peer deps coming into the currentPeerSet,
1638
+ // which are currently valid, and are from the target, then that
1639
+ // means that we have to ensure that they're not going to be made
1640
+ // invalid by putting the newPeerSet in place.
1641
+ // If the edge comes from somewhere deeper than the target, then
1642
+ // that's fine, because we'll create an invalid edge, detect it,
1643
+ // and duplicate the node further into the tree.
1644
+ // loop through the currentPeerSet checking for valid edges on
1645
+ // the members of the peer set which will be made invalid.
1646
+ const targetEdges = new Set()
1647
+ for (const p of currentPeerSet) {
1629
1648
  for (const edge of p.edgesIn) {
1630
- if (peerSet.has(edge.from))
1649
+ // edge from within the peerSet, ignore
1650
+ if (currentPeerSet.has(edge.from))
1651
+ continue
1652
+ // only care about valid edges from target.
1653
+ // edges from elsewhere can dupe if offended, invalid edges
1654
+ // are already being fixed or will be later.
1655
+ if (edge.from !== target || !edge.valid)
1631
1656
  continue
1657
+ targetEdges.add(edge)
1658
+ }
1659
+ }
1632
1660
 
1633
- // only respect valid edges, however, since we're likely trying
1634
- // to fix the very one that's currently broken! If the virtual
1635
- // root's replacement is ok, and doesn't have any invalid edges
1636
- // indicating that it was an overridden peer, then ignore the
1637
- // conflict and continue. If it WAS an override, then we need
1638
- // to get the conflict here so that we can decide whether to
1639
- // accept the current dep node, clobber it, or fail the install.
1640
- if (edge.from === target && edge.valid) {
1641
- const rep = dep.parent.children.get(edge.name)
1642
- const override = rep && ([...rep.edgesIn].some(e => !e.valid))
1643
- if (!rep || !rep.satisfies(edge) || override) {
1661
+ for (const edge of targetEdges) {
1662
+ // see if we intend to replace this one anyway
1663
+ const rep = dep.parent.children.get(edge.name)
1664
+ const current = edge.to
1665
+ if (!rep) {
1666
+ // this isn't one we're replacing. but it WAS included in the
1667
+ // peerSet for some reason, so make sure that it's still
1668
+ // ok with the replacements in the new peerSet
1669
+ for (const curEdge of current.edgesOut.values()) {
1670
+ const newRepDep = dep.parent.children.get(curEdge.name)
1671
+ if (curEdge.valid && newRepDep && !newRepDep.satisfies(curEdge)) {
1644
1672
  canReplace = false
1645
- break OUTER
1673
+ break
1674
+ }
1675
+ }
1676
+ continue
1677
+ }
1678
+
1679
+ // was this replacement already an override of some sort?
1680
+ const override = [...rep.edgesIn].some(e => !e.valid)
1681
+ // if we have a rep, and it's ok to put in this location, and
1682
+ // it's not already part of an override in the peerSet, then
1683
+ // we can continue with it.
1684
+ if (rep.satisfies(edge) && !override)
1685
+ continue
1686
+ // Otherwise, we cannot replace.
1687
+ canReplace = false
1688
+ break
1689
+ }
1690
+ // if we're going to be replacing the peerSet, we have to remove
1691
+ // and re-resolve any members of the old peerSet that are not
1692
+ // present in the new one, and which will have invalid edges.
1693
+ // We know that they're not depended upon by the target, or else
1694
+ // they would have caused a conflict, so they'll get landed deeper
1695
+ // in the tree, if possible.
1696
+ if (canReplace) {
1697
+ let needNesting = false
1698
+ OUTER: for (const node of currentPeerSet) {
1699
+ const rep = dep.parent.children.get(node.name)
1700
+ // has a replacement, already addressed above
1701
+ if (rep)
1702
+ continue
1703
+
1704
+ // ok, it has been placed here to dedupe, see if it needs to go
1705
+ // back deeper within the tree.
1706
+ for (const edge of node.edgesOut.values()) {
1707
+ const repDep = dep.parent.children.get(edge.name)
1708
+ // not in new peerSet, maybe fine.
1709
+ if (!repDep)
1710
+ continue
1711
+
1712
+ // new thing will be fine, no worries
1713
+ if (repDep.satisfies(edge))
1714
+ continue
1715
+
1716
+ // uhoh, we'll have to nest them.
1717
+ needNesting = true
1718
+ break OUTER
1719
+ }
1720
+ }
1721
+
1722
+ // to nest, just delete everything without a target dep
1723
+ // that's in the current peerSet, and add their dependants
1724
+ // to the _depsQueue for evaluation. Some of these MAY end
1725
+ // up in the same location again, and that's fine.
1726
+ if (needNesting) {
1727
+ // avoid mutating the tree while we're examining it
1728
+ const dependants = new Set()
1729
+ const reresolve = new Set()
1730
+ OUTER: for (const node of currentPeerSet) {
1731
+ const rep = dep.parent.children.get(node.name)
1732
+ if (rep)
1733
+ continue
1734
+ // create a separate set for each one, so we can skip any
1735
+ // that might somehow have an incoming target edge
1736
+ const deps = new Set()
1737
+ for (const edge of node.edgesIn) {
1738
+ // a target dep, skip this dep entirely, already addressed
1739
+ // ignoring for coverage, because it really ought to be
1740
+ // impossible, but I can't prove it yet, so this is here
1741
+ // for safety.
1742
+ /* istanbul ignore if - should be impossible */
1743
+ if (edge.from === target)
1744
+ continue OUTER
1745
+ // ignore this edge, it'll either be replaced or re-resolved
1746
+ if (currentPeerSet.has(edge.from))
1747
+ continue
1748
+ // ok, we care about this one.
1749
+ deps.add(edge.from)
1646
1750
  }
1751
+ reresolve.add(node)
1752
+ for (const d of deps)
1753
+ dependants.add(d)
1647
1754
  }
1755
+ for (const dependant of dependants) {
1756
+ this[_depsQueue].push(dependant)
1757
+ this[_depsSeen].delete(dependant)
1758
+ }
1759
+ for (const node of reresolve)
1760
+ node.root = null
1648
1761
  }
1649
1762
  }
1650
1763
  }
1764
+
1651
1765
  if (canReplace) {
1652
1766
  const ret = this[_canPlacePeers](dep, target, edge, REPLACE, peerEntryEdge, peerPath, isSource)
1653
1767
  /* istanbul ignore else - extremely rare that the peer set would
package/lib/node.js CHANGED
@@ -28,6 +28,7 @@
28
28
  // where we need to quickly find all instances of a given package name within a
29
29
  // tree.
30
30
 
31
+ const semver = require('semver')
31
32
  const nameFromFolder = require('@npmcli/name-from-folder')
32
33
  const Edge = require('./edge.js')
33
34
  const Inventory = require('./inventory.js')
@@ -885,6 +886,43 @@ class Node {
885
886
  return node.canReplaceWith(this)
886
887
  }
887
888
 
889
+ // return true if it's safe to remove this node, because anything that
890
+ // is depending on it would be fine with the thing that they would resolve
891
+ // to if it was removed, or nothing is depending on it in the first place.
892
+ canDedupe (preferDedupe = false) {
893
+ // not allowed to mess with shrinkwraps or bundles
894
+ if (this.inDepBundle || this.inShrinkwrap)
895
+ return false
896
+
897
+ // it's a top level pkg, or a dep of one
898
+ if (!this.parent || !this.parent.parent)
899
+ return false
900
+
901
+ // no one wants it, remove it
902
+ if (this.edgesIn.size === 0)
903
+ return true
904
+
905
+ const other = this.parent.parent.resolve(this.name)
906
+
907
+ // nothing else, need this one
908
+ if (!other)
909
+ return false
910
+
911
+ // if it's the same thing, then always fine to remove
912
+ if (other.matches(this))
913
+ return true
914
+
915
+ // if the other thing can't replace this, then skip it
916
+ if (!other.canReplace(this))
917
+ return false
918
+
919
+ // if we prefer dedupe, or if the version is greater/equal, take the other
920
+ if (preferDedupe || semver.gte(other.version, this.version))
921
+ return true
922
+
923
+ return false
924
+ }
925
+
888
926
  satisfies (requested) {
889
927
  if (requested instanceof Edge)
890
928
  return this.name === requested.name && requested.satisfiedBy(this)
package/lib/printable.js CHANGED
@@ -29,6 +29,15 @@ class ArboristNode {
29
29
  this.peer = true
30
30
  if (tree.inBundle)
31
31
  this.bundled = true
32
+ if (tree.inDepBundle)
33
+ this.bundler = tree.getBundler().location
34
+ const bd = tree.package && tree.package.bundleDependencies
35
+ if (bd && bd.length)
36
+ this.bundleDependencies = bd
37
+ if (tree.inShrinkwrap)
38
+ this.inShrinkwrap = true
39
+ else if (tree.hasShrinkwrap)
40
+ this.hasShrinkwrap = true
32
41
  if (tree.error)
33
42
  this.error = treeError(tree.error)
34
43
  if (tree.errors && tree.errors.length)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@npmcli/installed-package-contents": "^1.0.7",