1// Given a dep, a node that depends on it, and the edge representing that 2// dependency, place the dep somewhere in the node's tree, and all of its 3// peer dependencies. 4// 5// Handles all of the tree updating needed to place the dep, including 6// removing replaced nodes, pruning now-extraneous or invalidated nodes, 7// and saves a set of what was placed and what needs re-evaluation as 8// a result. 9 10const localeCompare = require('@isaacs/string-locale-compare')('en') 11const log = require('proc-log') 12const { cleanUrl } = require('npm-registry-fetch') 13const deepestNestingTarget = require('./deepest-nesting-target.js') 14const CanPlaceDep = require('./can-place-dep.js') 15const { 16 KEEP, 17 CONFLICT, 18} = CanPlaceDep 19const debug = require('./debug.js') 20 21const Link = require('./link.js') 22const gatherDepSet = require('./gather-dep-set.js') 23const peerEntrySets = require('./peer-entry-sets.js') 24 25class PlaceDep { 26 constructor (options) { 27 this.auditReport = options.auditReport 28 this.dep = options.dep 29 this.edge = options.edge 30 this.explicitRequest = options.explicitRequest 31 this.force = options.force 32 this.installLinks = options.installLinks 33 this.installStrategy = options.installStrategy 34 this.legacyPeerDeps = options.legacyPeerDeps 35 this.parent = options.parent || null 36 this.preferDedupe = options.preferDedupe 37 this.strictPeerDeps = options.strictPeerDeps 38 this.updateNames = options.updateNames 39 40 this.canPlace = null 41 this.canPlaceSelf = null 42 // XXX this only appears to be used by tests 43 this.checks = new Map() 44 this.children = [] 45 this.needEvaluation = new Set() 46 this.peerConflict = null 47 this.placed = null 48 this.target = null 49 50 this.current = this.edge.to 51 this.name = this.edge.name 52 this.top = this.parent?.top || this 53 54 // nothing to do if the edge is fine as it is 55 if (this.edge.to && 56 !this.edge.error && 57 !this.explicitRequest && 58 !this.updateNames.includes(this.edge.name) && 59 !this.auditReport?.isVulnerable(this.edge.to)) { 60 return 61 } 62 63 // walk up the tree until we hit either a top/root node, or a place 64 // where the dep is not a peer dep. 65 const start = this.getStartNode() 66 67 for (const target of start.ancestry()) { 68 // if the current location has a peerDep on it, then we can't place here 69 // this is pretty rare to hit, since we always prefer deduping peers, 70 // and the getStartNode will start us out above any peers from the 71 // thing that depends on it. but we could hit it with something like: 72 // 73 // a -> (b@1, c@1) 74 // +-- c@1 75 // +-- b -> PEEROPTIONAL(v) (c@2) 76 // +-- c@2 -> (v) 77 // 78 // So we check if we can place v under c@2, that's fine. 79 // Then we check under b, and can't, because of the optional peer dep. 80 // but we CAN place it under a, so the correct thing to do is keep 81 // walking up the tree. 82 const targetEdge = target.edgesOut.get(this.edge.name) 83 if (!target.isTop && targetEdge && targetEdge.peer) { 84 continue 85 } 86 87 const cpd = new CanPlaceDep({ 88 dep: this.dep, 89 edge: this.edge, 90 // note: this sets the parent's canPlace as the parent of this 91 // canPlace, but it does NOT add this canPlace to the parent's 92 // children. This way, we can know that it's a peer dep, and 93 // get the top edge easily, while still maintaining the 94 // tree of checks that factored into the original decision. 95 parent: this.parent && this.parent.canPlace, 96 target, 97 preferDedupe: this.preferDedupe, 98 explicitRequest: this.explicitRequest, 99 }) 100 this.checks.set(target, cpd) 101 102 // It's possible that a "conflict" is a conflict among the *peers* of 103 // a given node we're trying to place, but there actually is no current 104 // node. Eg, 105 // root -> (a, b) 106 // a -> PEER(c) 107 // b -> PEER(d) 108 // d -> PEER(c@2) 109 // We place (a), and get a peer of (c) along with it. 110 // then we try to place (b), and get CONFLICT in the check, because 111 // of the conflicting peer from (b)->(d)->(c@2). In that case, we 112 // should treat (b) and (d) as OK, and place them in the last place 113 // where they did not themselves conflict, and skip c@2 if conflict 114 // is ok by virtue of being forced or not ours and not strict. 115 if (cpd.canPlaceSelf !== CONFLICT) { 116 this.canPlaceSelf = cpd 117 } 118 119 // we found a place this can go, along with all its peer friends. 120 // we break when we get the first conflict 121 if (cpd.canPlace !== CONFLICT) { 122 this.canPlace = cpd 123 } else { 124 break 125 } 126 127 // if it's a load failure, just plop it in the first place attempted, 128 // since we're going to crash the build or prune it out anyway. 129 // but, this will frequently NOT be a successful canPlace, because 130 // it'll have no version or other information. 131 if (this.dep.errors.length) { 132 break 133 } 134 135 // nest packages like npm v1 and v2 136 // very disk-inefficient 137 if (this.installStrategy === 'nested') { 138 break 139 } 140 141 // when installing globally, or just in global style, we never place 142 // deps above the first level. 143 if (this.installStrategy === 'shallow') { 144 const rp = target.resolveParent 145 if (rp && rp.isProjectRoot) { 146 break 147 } 148 } 149 } 150 151 // if we can't find a target, that means that the last place checked, 152 // and all the places before it, had a conflict. 153 if (!this.canPlace) { 154 // if not forced, and it's our dep, or strictPeerDeps is set, then 155 // this is an ERESOLVE error. 156 if (!this.force && (this.isMine || this.strictPeerDeps)) { 157 return this.failPeerConflict() 158 } 159 160 // ok! we're gonna allow the conflict, but we should still warn 161 // if we have a current, then we treat CONFLICT as a KEEP. 162 // otherwise, we just skip it. Only warn on the one that actually 163 // could not be placed somewhere. 164 if (!this.canPlaceSelf) { 165 this.warnPeerConflict() 166 return 167 } 168 169 this.canPlace = this.canPlaceSelf 170 } 171 172 // now we have a target, a tree of CanPlaceDep results for the peer group, 173 // and we are ready to go 174 175 /* istanbul ignore next */ 176 if (!this.canPlace) { 177 debug(() => { 178 throw new Error('canPlace not set, but trying to place in tree') 179 }) 180 return 181 } 182 183 const { target } = this.canPlace 184 185 log.silly( 186 'placeDep', 187 target.location || 'ROOT', 188 `${this.dep.name}@${this.dep.version}`, 189 this.canPlace.description, 190 `for: ${this.edge.from.package._id || this.edge.from.location}`, 191 `want: ${cleanUrl(this.edge.spec || '*')}` 192 ) 193 194 const placementType = this.canPlace.canPlace === CONFLICT 195 ? this.canPlace.canPlaceSelf 196 : this.canPlace.canPlace 197 198 // if we're placing in the tree with --force, we can get here even though 199 // it's a conflict. Treat it as a KEEP, but warn and move on. 200 if (placementType === KEEP) { 201 // this was a peerConflicted peer dep 202 if (this.edge.peer && !this.edge.valid) { 203 this.warnPeerConflict() 204 } 205 206 // if we get a KEEP in a update scenario, then we MAY have something 207 // already duplicating this unnecessarily! For example: 208 // ``` 209 // root (dep: y@1) 210 // +-- x (dep: y@1.1) 211 // | +-- y@1.1.0 (replacing with 1.1.2, got KEEP at the root) 212 // +-- y@1.1.2 (updated already from 1.0.0) 213 // ``` 214 // Now say we do `reify({update:['y']})`, and the latest version is 215 // 1.1.2, which we now have in the root. We'll try to place y@1.1.2 216 // first in x, then in the root, ending with KEEP, because we already 217 // have it. In that case, we ought to REMOVE the nm/x/nm/y node, because 218 // it is an unnecessary duplicate. 219 this.pruneDedupable(target) 220 return 221 } 222 223 // we were told to place it here in the target, so either it does not 224 // already exist in the tree, OR it's shadowed. 225 // handle otherwise unresolvable dependency nesting loops by 226 // creating a symbolic link 227 // a1 -> b1 -> a2 -> b2 -> a1 -> ... 228 // instead of nesting forever, when the loop occurs, create 229 // a symbolic link to the earlier instance 230 for (let p = target; p; p = p.resolveParent) { 231 if (p.matches(this.dep) && !p.isTop) { 232 this.placed = new Link({ parent: target, target: p }) 233 return 234 } 235 } 236 237 // XXX if we are replacing SOME of a peer entry group, we will need to 238 // remove any that are not being replaced and will now be invalid, and 239 // re-evaluate them deeper into the tree. 240 241 const virtualRoot = this.dep.parent 242 this.placed = new this.dep.constructor({ 243 name: this.dep.name, 244 pkg: this.dep.package, 245 resolved: this.dep.resolved, 246 integrity: this.dep.integrity, 247 installLinks: this.installLinks, 248 legacyPeerDeps: this.legacyPeerDeps, 249 error: this.dep.errors[0], 250 ...(this.dep.overrides ? { overrides: this.dep.overrides } : {}), 251 ...(this.dep.isLink ? { target: this.dep.target, realpath: this.dep.realpath } : {}), 252 }) 253 254 this.oldDep = target.children.get(this.name) 255 if (this.oldDep) { 256 this.replaceOldDep() 257 } else { 258 this.placed.parent = target 259 } 260 261 // if it's a peerConflicted peer dep, warn about it 262 if (this.edge.peer && !this.placed.satisfies(this.edge)) { 263 this.warnPeerConflict() 264 } 265 266 // If the edge is not an error, then we're updating something, and 267 // MAY end up putting a better/identical node further up the tree in 268 // a way that causes an unnecessary duplication. If so, remove the 269 // now-unnecessary node. 270 if (this.edge.valid && this.edge.to && this.edge.to !== this.placed) { 271 this.pruneDedupable(this.edge.to, false) 272 } 273 274 // in case we just made some duplicates that can be removed, 275 // prune anything deeper in the tree that can be replaced by this 276 for (const node of target.root.inventory.query('name', this.name)) { 277 if (node.isDescendantOf(target) && !node.isTop) { 278 this.pruneDedupable(node, false) 279 // only walk the direct children of the ones we kept 280 if (node.root === target.root) { 281 for (const kid of node.children.values()) { 282 this.pruneDedupable(kid, false) 283 } 284 } 285 } 286 } 287 288 // also place its unmet or invalid peer deps at this location 289 // loop through any peer deps from the thing we just placed, and place 290 // those ones as well. it's safe to do this with the virtual nodes, 291 // because we're copying rather than moving them out of the virtual root, 292 // otherwise they'd be gone and the peer set would change throughout 293 // this loop. 294 for (const peerEdge of this.placed.edgesOut.values()) { 295 if (peerEdge.valid || !peerEdge.peer || peerEdge.peerConflicted) { 296 continue 297 } 298 299 const peer = virtualRoot.children.get(peerEdge.name) 300 301 // Note: if the virtualRoot *doesn't* have the peer, then that means 302 // it's an optional peer dep. If it's not being properly met (ie, 303 // peerEdge.valid is false), then this is likely heading for an 304 // ERESOLVE error, unless it can walk further up the tree. 305 if (!peer) { 306 continue 307 } 308 309 // peerConflicted peerEdge, just accept what's there already 310 if (!peer.satisfies(peerEdge)) { 311 continue 312 } 313 314 this.children.push(new PlaceDep({ 315 auditReport: this.auditReport, 316 explicitRequest: this.explicitRequest, 317 force: this.force, 318 installLinks: this.installLinks, 319 installStrategy: this.installStrategy, 320 legacyPeerDeps: this.legaycPeerDeps, 321 preferDedupe: this.preferDedupe, 322 strictPeerDeps: this.strictPeerDeps, 323 updateNames: this.updateName, 324 parent: this, 325 dep: peer, 326 node: this.placed, 327 edge: peerEdge, 328 })) 329 } 330 } 331 332 replaceOldDep () { 333 const target = this.oldDep.parent 334 335 // XXX handle replacing an entire peer group? 336 // what about cases where we need to push some other peer groups deeper 337 // into the tree? all the tree updating should be done here, and track 338 // all the things that we add and remove, so that we can know what 339 // to re-evaluate. 340 341 // if we're replacing, we should also remove any nodes for edges that 342 // are now invalid, and where this (or its deps) is the only dependent, 343 // and also recurse on that pruning. Otherwise leaving that dep node 344 // around can result in spurious conflicts pushing nodes deeper into 345 // the tree than needed in the case of cycles that will be removed 346 // later anyway. 347 const oldDeps = [] 348 for (const [name, edge] of this.oldDep.edgesOut.entries()) { 349 if (!this.placed.edgesOut.has(name) && edge.to) { 350 oldDeps.push(...gatherDepSet([edge.to], e => e.to !== edge.to)) 351 } 352 } 353 354 // gather all peer edgesIn which are at this level, and will not be 355 // satisfied by the new dependency. Those are the peer sets that need 356 // to be either warned about (if they cannot go deeper), or removed and 357 // re-placed (if they can). 358 const prunePeerSets = [] 359 for (const edge of this.oldDep.edgesIn) { 360 if (this.placed.satisfies(edge) || 361 !edge.peer || 362 edge.from.parent !== target || 363 edge.peerConflicted) { 364 // not a peer dep, not invalid, or not from this level, so it's fine 365 // to just let it re-evaluate as a problemEdge later, or let it be 366 // satisfied by the new dep being placed. 367 continue 368 } 369 for (const entryEdge of peerEntrySets(edge.from).keys()) { 370 // either this one needs to be pruned and re-evaluated, or marked 371 // as peerConflicted and warned about. If the entryEdge comes in from 372 // the root or a workspace, then we have to leave it alone, and in that 373 // case, it will have already warned or crashed by getting to this point 374 const entryNode = entryEdge.to 375 const deepestTarget = deepestNestingTarget(entryNode) 376 if (deepestTarget !== target && 377 !(entryEdge.from.isProjectRoot || entryEdge.from.isWorkspace)) { 378 prunePeerSets.push(...gatherDepSet([entryNode], e => { 379 return e.to !== entryNode && !e.peerConflicted 380 })) 381 } else { 382 this.warnPeerConflict(edge, this.dep) 383 } 384 } 385 } 386 387 this.placed.replace(this.oldDep) 388 this.pruneForReplacement(this.placed, oldDeps) 389 for (const dep of prunePeerSets) { 390 for (const edge of dep.edgesIn) { 391 this.needEvaluation.add(edge.from) 392 } 393 dep.root = null 394 } 395 } 396 397 pruneForReplacement (node, oldDeps) { 398 // gather up all the now-invalid/extraneous edgesOut, as long as they are 399 // only depended upon by the old node/deps 400 const invalidDeps = new Set([...node.edgesOut.values()] 401 .filter(e => e.to && !e.valid).map(e => e.to)) 402 for (const dep of oldDeps) { 403 const set = gatherDepSet([dep], e => e.to !== dep && e.valid) 404 for (const dep of set) { 405 invalidDeps.add(dep) 406 } 407 } 408 409 // ignore dependency edges from the node being replaced, but 410 // otherwise filter the set down to just the set with no 411 // dependencies from outside the set, except the node in question. 412 const deps = gatherDepSet(invalidDeps, edge => 413 edge.from !== node && edge.to !== node && edge.valid) 414 415 // now just delete whatever's left, because it's junk 416 for (const dep of deps) { 417 dep.root = null 418 } 419 } 420 421 // prune all the nodes in a branch of the tree that can be safely removed 422 // This is only the most basic duplication detection; it finds if there 423 // is another satisfying node further up the tree, and if so, dedupes. 424 // Even in installStategy is nested, we do this amount of deduplication. 425 pruneDedupable (node, descend = true) { 426 if (node.canDedupe(this.preferDedupe)) { 427 // gather up all deps that have no valid edges in from outside 428 // the dep set, except for this node we're deduping, so that we 429 // also prune deps that would be made extraneous. 430 const deps = gatherDepSet([node], e => e.to !== node && e.valid) 431 for (const node of deps) { 432 node.root = null 433 } 434 return 435 } 436 if (descend) { 437 // sort these so that they're deterministically ordered 438 // otherwise, resulting tree shape is dependent on the order 439 // in which they happened to be resolved. 440 const nodeSort = (a, b) => localeCompare(a.location, b.location) 441 442 const children = [...node.children.values()].sort(nodeSort) 443 for (const child of children) { 444 this.pruneDedupable(child) 445 } 446 const fsChildren = [...node.fsChildren].sort(nodeSort) 447 for (const topNode of fsChildren) { 448 const children = [...topNode.children.values()].sort(nodeSort) 449 for (const child of children) { 450 this.pruneDedupable(child) 451 } 452 } 453 } 454 } 455 456 get isMine () { 457 const { edge } = this.top 458 const { from: node } = edge 459 460 if (node.isWorkspace || node.isProjectRoot) { 461 return true 462 } 463 464 if (!edge.peer) { 465 return false 466 } 467 468 // re-entry case. check if any non-peer edges come from the project, 469 // or any entryEdges on peer groups are from the root. 470 let hasPeerEdges = false 471 for (const edge of node.edgesIn) { 472 if (edge.peer) { 473 hasPeerEdges = true 474 continue 475 } 476 if (edge.from.isWorkspace || edge.from.isProjectRoot) { 477 return true 478 } 479 } 480 if (hasPeerEdges) { 481 for (const edge of peerEntrySets(node).keys()) { 482 if (edge.from.isWorkspace || edge.from.isProjectRoot) { 483 return true 484 } 485 } 486 } 487 488 return false 489 } 490 491 warnPeerConflict (edge, dep) { 492 edge = edge || this.edge 493 dep = dep || this.dep 494 edge.peerConflicted = true 495 const expl = this.explainPeerConflict(edge, dep) 496 log.warn('ERESOLVE', 'overriding peer dependency', expl) 497 } 498 499 failPeerConflict (edge, dep) { 500 edge = edge || this.top.edge 501 dep = dep || this.top.dep 502 const expl = this.explainPeerConflict(edge, dep) 503 throw Object.assign(new Error('could not resolve'), expl) 504 } 505 506 explainPeerConflict (edge, dep) { 507 const { from: node } = edge 508 const curNode = node.resolve(edge.name) 509 510 // XXX decorate more with this.canPlace and this.canPlaceSelf, 511 // this.checks, this.children, walk over conflicted peers, etc. 512 const expl = { 513 code: 'ERESOLVE', 514 edge: edge.explain(), 515 dep: dep.explain(edge), 516 force: this.force, 517 isMine: this.isMine, 518 strictPeerDeps: this.strictPeerDeps, 519 } 520 521 if (this.parent) { 522 // this is the conflicted peer 523 expl.current = curNode && curNode.explain(edge) 524 expl.peerConflict = this.current && this.current.explain(this.edge) 525 } else { 526 expl.current = curNode && curNode.explain() 527 if (this.canPlaceSelf && this.canPlaceSelf.canPlaceSelf !== CONFLICT) { 528 // failed while checking for a child dep 529 const cps = this.canPlaceSelf 530 for (const peer of cps.conflictChildren) { 531 if (peer.current) { 532 expl.peerConflict = { 533 current: peer.current.explain(), 534 peer: peer.dep.explain(peer.edge), 535 } 536 break 537 } 538 } 539 } else { 540 expl.peerConflict = { 541 current: this.current && this.current.explain(), 542 peer: this.dep.explain(this.edge), 543 } 544 } 545 } 546 547 return expl 548 } 549 550 getStartNode () { 551 // if we are a peer, then we MUST be at least as shallow as the peer 552 // dependent 553 const from = this.parent?.getStartNode() || this.edge.from 554 return deepestNestingTarget(from, this.name) 555 } 556 557 // XXX this only appears to be used by tests 558 get allChildren () { 559 const set = new Set(this.children) 560 for (const child of set) { 561 for (const grandchild of child.children) { 562 set.add(grandchild) 563 } 564 } 565 return [...set] 566 } 567} 568 569module.exports = PlaceDep 570