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