-
Notifications
You must be signed in to change notification settings - Fork 3.2k
/
can-place-dep.js
436 lines (382 loc) · 13.9 KB
/
can-place-dep.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
// Internal methods used by buildIdealTree.
// Answer the question: "can I put this dep here?"
//
// IMPORTANT: *nothing* in this class should *ever* modify or mutate the tree
// at all. The contract here is strictly limited to read operations. We call
// this in the process of walking through the ideal tree checking many
// different potential placement targets for a given node. If a change is made
// to the tree along the way, that can cause serious problems!
//
// In order to enforce this restriction, in debug mode, canPlaceDep() will
// snapshot the tree at the start of the process, and then at the end, will
// verify that it still matches the snapshot, and throw an error if any changes
// occurred.
//
// The algorithm is roughly like this:
// - check the node itself:
// - if there is no version present, and no conflicting edges from target,
// OK, provided all peers can be placed at or above the target.
// - if the current version matches, KEEP
// - if there is an older version present, which can be replaced, then
// - if satisfying and preferDedupe? KEEP
// - else: REPLACE
// - if there is a newer version present, and preferDedupe, REPLACE
// - if the version present satisfies the edge, KEEP
// - else: CONFLICT
// - if the node is not in conflict, check each of its peers:
// - if the peer can be placed in the target, continue
// - else if the peer can be placed in a parent, and there is no other
// conflicting version shadowing it, continue
// - else CONFLICT
// - If the peers are not in conflict, return the original node's value
//
// An exception to this logic is that if the target is the deepest location
// that a node can be placed, and the conflicting node can be placed deeper,
// then we will return REPLACE rather than CONFLICT, and Arborist will queue
// the replaced node for resolution elsewhere.
const localeCompare = require('@isaacs/string-locale-compare')('en')
const semver = require('semver')
const debug = require('./debug.js')
const peerEntrySets = require('./peer-entry-sets.js')
const deepestNestingTarget = require('./deepest-nesting-target.js')
const CONFLICT = Symbol('CONFLICT')
const OK = Symbol('OK')
const REPLACE = Symbol('REPLACE')
const KEEP = Symbol('KEEP')
class CanPlaceDep {
// dep is a dep that we're trying to place. it should already live in
// a virtual tree where its peer set is loaded as children of the root.
// target is the actual place where we're trying to place this dep
// in a node_modules folder.
// edge is the edge that we're trying to satisfy with this placement.
// parent is the CanPlaceDep object of the entry node when placing a peer.
constructor (options) {
const {
dep,
target,
edge,
preferDedupe,
parent = null,
peerPath = [],
explicitRequest = false,
} = options
debug(() => {
if (!dep) {
throw new Error('no dep provided to CanPlaceDep')
}
if (!target) {
throw new Error('no target provided to CanPlaceDep')
}
if (!edge) {
throw new Error('no edge provided to CanPlaceDep')
}
this._treeSnapshot = JSON.stringify([...target.root.inventory.entries()]
.map(([loc, { packageName, version, resolved }]) => {
return [loc, packageName, version, resolved]
}).sort(([a], [b]) => localeCompare(a, b)))
})
// the result of whether we can place it or not
this.canPlace = null
// if peers conflict, but this one doesn't, then that is useful info
this.canPlaceSelf = null
this.dep = dep
this.target = target
this.edge = edge
this.explicitRequest = explicitRequest
// preventing cycles when we check peer sets
this.peerPath = peerPath
// we always prefer to dedupe peers, because they are trying
// a bit harder to be singletons.
this.preferDedupe = !!preferDedupe || edge.peer
this.parent = parent
this.children = []
this.isSource = target === this.peerSetSource
this.name = edge.name
this.current = target.children.get(this.name)
this.targetEdge = target.edgesOut.get(this.name)
this.conflicts = new Map()
// check if this dep was already subject to a peerDep override while
// building the peerSet.
this.edgeOverride = !dep.satisfies(edge)
this.canPlace = this.checkCanPlace()
if (!this.canPlaceSelf) {
this.canPlaceSelf = this.canPlace
}
debug(() => {
const treeSnapshot = JSON.stringify([...target.root.inventory.entries()]
.map(([loc, { packageName, version, resolved }]) => {
return [loc, packageName, version, resolved]
}).sort(([a], [b]) => localeCompare(a, b)))
/* istanbul ignore if */
if (this._treeSnapshot !== treeSnapshot) {
throw Object.assign(new Error('tree changed in CanPlaceDep'), {
expect: this._treeSnapshot,
actual: treeSnapshot,
})
}
})
}
checkCanPlace () {
const { target, targetEdge, current, dep } = this
// if the dep failed to load, we're going to fail the build or
// prune it out anyway, so just move forward placing/replacing it.
if (dep.errors.length) {
return current ? REPLACE : OK
}
// cannot place peers inside their dependents, except for tops
if (targetEdge && targetEdge.peer && !target.isTop) {
return CONFLICT
}
// skip this test if there's a current node, because we might be able
// to dedupe against it anyway
if (!current &&
targetEdge &&
!dep.satisfies(targetEdge) &&
targetEdge !== this.edge) {
return CONFLICT
}
return current ? this.checkCanPlaceCurrent() : this.checkCanPlaceNoCurrent()
}
// we know that the target has a dep by this name in its node_modules
// already. Can return KEEP, REPLACE, or CONFLICT.
checkCanPlaceCurrent () {
const { preferDedupe, explicitRequest, current, target, edge, dep } = this
if (dep.matches(current)) {
if (current.satisfies(edge) || this.edgeOverride) {
return explicitRequest ? REPLACE : KEEP
}
}
const { version: curVer } = current
const { version: newVer } = dep
const tryReplace = curVer && newVer && semver.gte(newVer, curVer)
if (tryReplace && dep.canReplace(current)) {
// It's extremely rare that a replaceable node would be a conflict, if
// the current one wasn't a conflict, but it is theoretically possible
// if peer deps are pinned. In that case we treat it like any other
// conflict, and keep trying.
const cpp = this.canPlacePeers(REPLACE)
if (cpp !== CONFLICT) {
return cpp
}
}
// ok, can't replace the current with new one, but maybe current is ok?
if (current.satisfies(edge) && (!explicitRequest || preferDedupe)) {
return KEEP
}
// if we prefer deduping, then try replacing newer with older
if (preferDedupe && !tryReplace && dep.canReplace(current)) {
const cpp = this.canPlacePeers(REPLACE)
if (cpp !== CONFLICT) {
return cpp
}
}
// Check for interesting cases!
// First, is this the deepest place that this thing can go, and NOT the
// deepest place where the conflicting dep can go? If so, replace it,
// and let it re-resolve deeper in the tree.
const myDeepest = this.deepestNestingTarget
// ok, i COULD be placed deeper, so leave the current one alone.
if (target !== myDeepest) {
return CONFLICT
}
// if we are not checking a peerDep, then we MUST place it here, in the
// target that has a non-peer dep on it.
if (!edge.peer && target === edge.from) {
return this.canPlacePeers(REPLACE)
}
// if we aren't placing a peer in a set, then we're done here.
// This is ignored because it SHOULD be redundant, as far as I can tell,
// with the deepest target and target===edge.from tests. But until we
// can prove that isn't possible, this condition is here for safety.
/* istanbul ignore if - allegedly impossible */
if (!this.parent && !edge.peer) {
return CONFLICT
}
// check the deps in the peer group for each edge into that peer group
// if ALL of them can be pushed deeper, or if it's ok to replace its
// members with the contents of the new peer group, then we're good.
let canReplace = true
for (const [entryEdge, currentPeers] of peerEntrySets(current)) {
if (entryEdge === this.edge || entryEdge === this.peerEntryEdge) {
continue
}
// First, see if it's ok to just replace the peerSet entirely.
// we do this by walking out from the entryEdge, because in a case like
// this:
//
// v -> PEER(a@1||2)
// a@1 -> PEER(b@1)
// a@2 -> PEER(b@2)
// b@1 -> PEER(a@1)
// b@2 -> PEER(a@2)
//
// root
// +-- v
// +-- a@2
// +-- b@2
//
// Trying to place a peer group of (a@1, b@1) would fail to note that
// they can be replaced, if we did it by looping 1 by 1. If we are
// replacing something, we don't have to check its peer deps, because
// the peerDeps in the placed peerSet will presumably satisfy.
const entryNode = entryEdge.to
const entryRep = dep.parent.children.get(entryNode.name)
if (entryRep) {
if (entryRep.canReplace(entryNode, dep.parent.children.keys())) {
continue
}
}
let canClobber = !entryRep
if (!entryRep) {
const peerReplacementWalk = new Set([entryNode])
OUTER: for (const currentPeer of peerReplacementWalk) {
for (const edge of currentPeer.edgesOut.values()) {
if (!edge.peer || !edge.valid) {
continue
}
const rep = dep.parent.children.get(edge.name)
if (!rep) {
if (edge.to) {
peerReplacementWalk.add(edge.to)
}
continue
}
if (!rep.satisfies(edge)) {
canClobber = false
break OUTER
}
}
}
}
if (canClobber) {
continue
}
// ok, we can't replace, but maybe we can nest the current set deeper?
let canNestCurrent = true
for (const currentPeer of currentPeers) {
if (!canNestCurrent) {
break
}
// still possible to nest this peerSet
const curDeep = deepestNestingTarget(entryEdge.from, currentPeer.name)
if (curDeep === target || target.isDescendantOf(curDeep)) {
canNestCurrent = false
canReplace = false
}
if (canNestCurrent) {
continue
}
}
}
// if we can nest or replace all the current peer groups, we can replace.
if (canReplace) {
return this.canPlacePeers(REPLACE)
}
return CONFLICT
}
checkCanPlaceNoCurrent () {
const { target, peerEntryEdge, dep, name } = this
// check to see what that name resolves to here, and who may depend on
// being able to reach it by crawling up past the parent. we know
// that it's not the target's direct child node, and if it was a direct
// dep of the target, we would have conflicted earlier.
const current = target !== peerEntryEdge.from && target.resolve(name)
if (current) {
for (const edge of current.edgesIn.values()) {
if (edge.from.isDescendantOf(target) && edge.valid) {
if (!dep.satisfies(edge)) {
return CONFLICT
}
}
}
}
// no objections, so this is fine as long as peers are ok here.
return this.canPlacePeers(OK)
}
get deepestNestingTarget () {
const start = this.parent ? this.parent.deepestNestingTarget
: this.edge.from
return deepestNestingTarget(start, this.name)
}
get conflictChildren () {
return this.allChildren.filter(c => c.canPlace === CONFLICT)
}
get allChildren () {
const set = new Set(this.children)
for (const child of set) {
for (const grandchild of child.children) {
set.add(grandchild)
}
}
return [...set]
}
get top () {
return this.parent ? this.parent.top : this
}
// check if peers can go here. returns state or CONFLICT
canPlacePeers (state) {
this.canPlaceSelf = state
if (this._canPlacePeers) {
return this._canPlacePeers
}
// TODO: represent peerPath in ERESOLVE error somehow?
const peerPath = [...this.peerPath, this.dep]
let sawConflict = false
for (const peerEdge of this.dep.edgesOut.values()) {
if (!peerEdge.peer || !peerEdge.to || peerPath.includes(peerEdge.to)) {
continue
}
const peer = peerEdge.to
// it may be the case that the *initial* dep can be nested, but a peer
// of that dep needs to be placed shallower, because the target has
// a peer dep on the peer as well.
const target = deepestNestingTarget(this.target, peer.name)
const cpp = new CanPlaceDep({
dep: peer,
target,
parent: this,
edge: peerEdge,
peerPath,
// always place peers in preferDedupe mode
preferDedupe: true,
})
/* istanbul ignore next */
debug(() => {
if (this.children.some(c => c.dep === cpp.dep)) {
throw new Error('checking same dep repeatedly')
}
})
this.children.push(cpp)
if (cpp.canPlace === CONFLICT) {
sawConflict = true
}
}
this._canPlacePeers = sawConflict ? CONFLICT : state
return this._canPlacePeers
}
// what is the node that is causing this peerSet to be placed?
get peerSetSource () {
return this.parent ? this.parent.peerSetSource : this.edge.from
}
get peerEntryEdge () {
return this.top.edge
}
static get CONFLICT () {
return CONFLICT
}
static get OK () {
return OK
}
static get REPLACE () {
return REPLACE
}
static get KEEP () {
return KEEP
}
get description () {
const { canPlace } = this
return canPlace && canPlace.description ||
/* istanbul ignore next - old node affordance */ canPlace
}
}
module.exports = CanPlaceDep