@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.
- package/lib/arborist/build-ideal-tree.js +106 -686
- package/lib/arborist/reify.js +111 -35
- package/lib/can-place-dep.js +405 -0
- package/lib/deepest-nesting-target.js +16 -0
- package/lib/edge.js +2 -0
- package/lib/node.js +29 -3
- package/lib/peer-entry-sets.js +72 -0
- package/lib/place-dep.js +536 -0
- package/lib/printable.js +10 -0
- package/lib/shrinkwrap.js +3 -1
- package/package.json +4 -4
- package/lib/peer-set.js +0 -25
package/lib/arborist/reify.js
CHANGED
|
@@ -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 =>
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
|
777
|
+
if (!bundleShadowed.has(edge.from)) {
|
|
714
778
|
shadow.extraneous = false
|
|
715
779
|
bundleShadowed.delete(shadow)
|
|
716
780
|
changed = true
|
|
717
|
-
|
|
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
|
-
|
|
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) {
|