1// mixin implementing the buildIdealTree method 2const localeCompare = require('@isaacs/string-locale-compare')('en') 3const rpj = require('read-package-json-fast') 4const npa = require('npm-package-arg') 5const pacote = require('pacote') 6const cacache = require('cacache') 7const { callLimit: promiseCallLimit } = require('promise-call-limit') 8const realpath = require('../../lib/realpath.js') 9const { resolve, dirname } = require('path') 10const treeCheck = require('../tree-check.js') 11const { readdirScoped } = require('@npmcli/fs') 12const { lstat, readlink } = require('fs/promises') 13const { depth } = require('treeverse') 14const log = require('proc-log') 15const { cleanUrl } = require('npm-registry-fetch') 16 17const { 18 OK, 19 REPLACE, 20 CONFLICT, 21} = require('../can-place-dep.js') 22const PlaceDep = require('../place-dep.js') 23 24const debug = require('../debug.js') 25const fromPath = require('../from-path.js') 26const calcDepFlags = require('../calc-dep-flags.js') 27const Shrinkwrap = require('../shrinkwrap.js') 28const { defaultLockfileVersion } = Shrinkwrap 29const Node = require('../node.js') 30const Link = require('../link.js') 31const addRmPkgDeps = require('../add-rm-pkg-deps.js') 32const optionalSet = require('../optional-set.js') 33const { checkEngine, checkPlatform } = require('npm-install-checks') 34const relpath = require('../relpath.js') 35const resetDepFlags = require('../reset-dep-flags.js') 36 37// note: some of these symbols are shared so we can hit 38// them with unit tests and reuse them across mixins 39const _updateAll = Symbol.for('updateAll') 40const _flagsSuspect = Symbol.for('flagsSuspect') 41const _setWorkspaces = Symbol.for('setWorkspaces') 42const _updateNames = Symbol.for('updateNames') 43const _resolvedAdd = Symbol.for('resolvedAdd') 44const _usePackageLock = Symbol.for('usePackageLock') 45const _rpcache = Symbol.for('realpathCache') 46const _stcache = Symbol.for('statCache') 47 48// used by Reify mixin 49const _addNodeToTrashList = Symbol.for('addNodeToTrashList') 50 51// Push items in, pop them sorted by depth and then path 52// Sorts physically shallower deps up to the front of the queue, because 53// they'll affect things deeper in, then alphabetical for consistency between 54// installs 55class DepsQueue { 56 // [{ sorted, items }] indexed by depth 57 #deps = [] 58 #sorted = true 59 #minDepth = 0 60 #length = 0 61 62 get length () { 63 return this.#length 64 } 65 66 push (item) { 67 if (!this.#deps[item.depth]) { 68 this.#length++ 69 this.#deps[item.depth] = { sorted: true, items: [item] } 70 // no minDepth check needed, this branch is only reached when we are in 71 // the middle of a shallower depth and creating a new one 72 return 73 } 74 if (!this.#deps[item.depth].items.includes(item)) { 75 this.#length++ 76 this.#deps[item.depth].sorted = false 77 this.#deps[item.depth].items.push(item) 78 if (item.depth < this.#minDepth) { 79 this.#minDepth = item.depth 80 } 81 } 82 } 83 84 pop () { 85 let depth 86 while (!depth?.items.length) { 87 depth = this.#deps[this.#minDepth] 88 if (!depth?.items.length) { 89 this.#minDepth++ 90 } 91 } 92 if (!depth.sorted) { 93 depth.items.sort((a, b) => localeCompare(a.path, b.path)) 94 depth.sorted = true 95 } 96 this.#length-- 97 return depth.items.shift() 98 } 99} 100 101module.exports = cls => class IdealTreeBuilder extends cls { 102 #complete 103 #currentDep = null 104 #depsQueue = new DepsQueue() 105 #depsSeen = new Set() 106 #explicitRequests = new Set() 107 #follow 108 #installStrategy 109 #linkNodes = new Set() 110 #loadFailures = new Set() 111 #manifests = new Map() 112 #mutateTree = false 113 // a map of each module in a peer set to the thing that depended on 114 // that set of peers in the first place. Use a WeakMap so that we 115 // don't hold onto references for nodes that are garbage collected. 116 #peerSetSource = new WeakMap() 117 #preferDedupe = false 118 #prune 119 #strictPeerDeps 120 #virtualRoots = new Map() 121 122 constructor (options) { 123 super(options) 124 125 // normalize trailing slash 126 const registry = options.registry || 'https://registry.npmjs.org' 127 options.registry = this.registry = registry.replace(/\/+$/, '') + '/' 128 129 const { 130 follow = false, 131 installStrategy = 'hoisted', 132 idealTree = null, 133 installLinks = false, 134 legacyPeerDeps = false, 135 packageLock = true, 136 strictPeerDeps = false, 137 workspaces, 138 global, 139 } = options 140 141 this.#strictPeerDeps = !!strictPeerDeps 142 143 this.idealTree = idealTree 144 this.installLinks = installLinks 145 this.legacyPeerDeps = legacyPeerDeps 146 147 this[_usePackageLock] = packageLock 148 this.#installStrategy = global ? 'shallow' : installStrategy 149 this.#follow = !!follow 150 151 if (workspaces?.length && global) { 152 throw new Error('Cannot operate on workspaces in global mode') 153 } 154 155 this[_updateAll] = false 156 this[_updateNames] = [] 157 this[_resolvedAdd] = [] 158 } 159 160 get explicitRequests () { 161 return new Set(this.#explicitRequests) 162 } 163 164 // public method 165 async buildIdealTree (options = {}) { 166 if (this.idealTree) { 167 return this.idealTree 168 } 169 170 // allow the user to set reify options on the ctor as well. 171 // XXX: deprecate separate reify() options object. 172 options = { ...this.options, ...options } 173 174 // an empty array or any falsey value is the same as null 175 if (!options.add || options.add.length === 0) { 176 options.add = null 177 } 178 if (!options.rm || options.rm.length === 0) { 179 options.rm = null 180 } 181 182 process.emit('time', 'idealTree') 183 184 if (!options.add && !options.rm && !options.update && this.options.global) { 185 throw new Error('global requires add, rm, or update option') 186 } 187 188 // first get the virtual tree, if possible. If there's a lockfile, then 189 // that defines the ideal tree, unless the root package.json is not 190 // satisfied by what the ideal tree provides. 191 // from there, we start adding nodes to it to satisfy the deps requested 192 // by the package.json in the root. 193 194 this.#parseSettings(options) 195 196 // start tracker block 197 this.addTracker('idealTree') 198 199 try { 200 await this.#initTree() 201 await this.#inflateAncientLockfile() 202 await this.#applyUserRequests(options) 203 await this.#buildDeps() 204 await this.#fixDepFlags() 205 await this.#pruneFailedOptional() 206 await this.#checkEngineAndPlatform() 207 } finally { 208 process.emit('timeEnd', 'idealTree') 209 this.finishTracker('idealTree') 210 } 211 212 return treeCheck(this.idealTree) 213 } 214 215 async #checkEngineAndPlatform () { 216 const { engineStrict, npmVersion, nodeVersion } = this.options 217 for (const node of this.idealTree.inventory.values()) { 218 if (!node.optional) { 219 try { 220 checkEngine(node.package, npmVersion, nodeVersion, this.options.force) 221 } catch (err) { 222 if (engineStrict) { 223 throw err 224 } 225 log.warn(err.code, err.message, { 226 package: err.pkgid, 227 required: err.required, 228 current: err.current, 229 }) 230 } 231 checkPlatform(node.package, this.options.force) 232 } 233 } 234 } 235 236 #parseSettings (options) { 237 const update = options.update === true ? { all: true } 238 : Array.isArray(options.update) ? { names: options.update } 239 : options.update || {} 240 241 if (update.all || !Array.isArray(update.names)) { 242 update.names = [] 243 } 244 245 this.#complete = !!options.complete 246 this.#preferDedupe = !!options.preferDedupe 247 248 // validates list of update names, they must 249 // be dep names only, no semver ranges are supported 250 for (const name of update.names) { 251 const spec = npa(name) 252 const validationError = 253 new TypeError(`Update arguments must only contain package names, eg: 254 npm update ${spec.name}`) 255 validationError.code = 'EUPDATEARGS' 256 257 // If they gave us anything other than a bare package name 258 if (spec.raw !== spec.name) { 259 throw validationError 260 } 261 } 262 this[_updateNames] = update.names 263 264 this[_updateAll] = update.all 265 // we prune by default unless explicitly set to boolean false 266 this.#prune = options.prune !== false 267 268 // set if we add anything, but also set here if we know we'll make 269 // changes and thus have to maybe prune later. 270 this.#mutateTree = !!( 271 options.add || 272 options.rm || 273 update.all || 274 update.names.length 275 ) 276 } 277 278 // load the initial tree, either the virtualTree from a shrinkwrap, 279 // or just the root node from a package.json 280 async #initTree () { 281 process.emit('time', 'idealTree:init') 282 let root 283 if (this.options.global) { 284 root = await this.#globalRootNode() 285 } else { 286 try { 287 const pkg = await rpj(this.path + '/package.json') 288 root = await this.#rootNodeFromPackage(pkg) 289 } catch (err) { 290 if (err.code === 'EJSONPARSE') { 291 throw err 292 } 293 root = await this.#rootNodeFromPackage({}) 294 } 295 } 296 return this[_setWorkspaces](root) 297 // ok to not have a virtual tree. probably initial install. 298 // When updating all, we load the shrinkwrap, but don't bother 299 // to build out the full virtual tree from it, since we'll be 300 // reconstructing it anyway. 301 .then(root => this.options.global ? root 302 : !this[_usePackageLock] || this[_updateAll] 303 ? Shrinkwrap.reset({ 304 path: this.path, 305 lockfileVersion: this.options.lockfileVersion, 306 resolveOptions: this.options, 307 }).then(meta => Object.assign(root, { meta })) 308 : this.loadVirtual({ root })) 309 310 // if we don't have a lockfile to go from, then start with the 311 // actual tree, so we only make the minimum required changes. 312 // don't do this for global installs or updates, because in those 313 // cases we don't use a lockfile anyway. 314 // Load on a new Arborist object, so the Nodes aren't the same, 315 // or else it'll get super confusing when we change them! 316 .then(async root => { 317 if ((!this[_updateAll] && !this.options.global && !root.meta.loadedFromDisk) || (this.options.global && this[_updateNames].length)) { 318 await new this.constructor(this.options).loadActual({ root }) 319 const tree = root.target 320 // even though we didn't load it from a package-lock.json FILE, 321 // we still loaded it "from disk", meaning we have to reset 322 // dep flags before assuming that any mutations were reflected. 323 if (tree.children.size) { 324 root.meta.loadedFromDisk = true 325 // set these so that we don't try to ancient lockfile reload it 326 root.meta.originalLockfileVersion = root.meta.lockfileVersion = this.options.lockfileVersion || defaultLockfileVersion 327 } 328 } 329 root.meta.inferFormattingOptions(root.package) 330 return root 331 }) 332 333 .then(tree => { 334 // search the virtual tree for invalid edges, if any are found add their source to 335 // the depsQueue so that we'll fix it later 336 depth({ 337 tree, 338 getChildren: (node) => { 339 const children = [] 340 for (const edge of node.edgesOut.values()) { 341 children.push(edge.to) 342 } 343 return children 344 }, 345 filter: node => node, 346 visit: node => { 347 for (const edge of node.edgesOut.values()) { 348 if (!edge.valid) { 349 this.#depsQueue.push(node) 350 break // no need to continue the loop after the first hit 351 } 352 } 353 }, 354 }) 355 // null the virtual tree, because we're about to hack away at it 356 // if you want another one, load another copy. 357 this.idealTree = tree 358 this.virtualTree = null 359 process.emit('timeEnd', 'idealTree:init') 360 return tree 361 }) 362 } 363 364 async #globalRootNode () { 365 const root = await this.#rootNodeFromPackage({ dependencies: {} }) 366 // this is a gross kludge to handle the fact that we don't save 367 // metadata on the root node in global installs, because the "root" 368 // node is something like /usr/local/lib. 369 const meta = new Shrinkwrap({ 370 path: this.path, 371 lockfileVersion: this.options.lockfileVersion, 372 resolveOptions: this.options, 373 }) 374 meta.reset() 375 root.meta = meta 376 return root 377 } 378 379 async #rootNodeFromPackage (pkg) { 380 // if the path doesn't exist, then we explode at this point. Note that 381 // this is not a problem for reify(), since it creates the root path 382 // before ever loading trees. 383 // TODO: make buildIdealTree() and loadActual handle a missing root path, 384 // or a symlink to a missing target, and let reify() create it as needed. 385 const real = await realpath(this.path, this[_rpcache], this[_stcache]) 386 const Cls = real === this.path ? Node : Link 387 const root = new Cls({ 388 path: this.path, 389 realpath: real, 390 pkg, 391 extraneous: false, 392 dev: false, 393 devOptional: false, 394 peer: false, 395 optional: false, 396 global: this.options.global, 397 installLinks: this.installLinks, 398 legacyPeerDeps: this.legacyPeerDeps, 399 loadOverrides: true, 400 }) 401 if (root.isLink) { 402 root.target = new Node({ 403 path: real, 404 realpath: real, 405 pkg, 406 extraneous: false, 407 dev: false, 408 devOptional: false, 409 peer: false, 410 optional: false, 411 global: this.options.global, 412 installLinks: this.installLinks, 413 legacyPeerDeps: this.legacyPeerDeps, 414 root, 415 }) 416 } 417 return root 418 } 419 420 // process the add/rm requests by modifying the root node, and the 421 // update.names request by queueing nodes dependent on those named. 422 async #applyUserRequests (options) { 423 process.emit('time', 'idealTree:userRequests') 424 const tree = this.idealTree.target 425 426 if (!this.options.workspaces.length) { 427 await this.#applyUserRequestsToNode(tree, options) 428 } else { 429 const nodes = this.workspaceNodes(tree, this.options.workspaces) 430 if (this.options.includeWorkspaceRoot) { 431 nodes.push(tree) 432 } 433 const appliedRequests = nodes.map( 434 node => this.#applyUserRequestsToNode(node, options) 435 ) 436 await Promise.all(appliedRequests) 437 } 438 439 process.emit('timeEnd', 'idealTree:userRequests') 440 } 441 442 async #applyUserRequestsToNode (tree, options) { 443 // If we have a list of package names to update, and we know it's 444 // going to update them wherever they are, add any paths into those 445 // named nodes to the buildIdealTree queue. 446 if (!this.options.global && this[_updateNames].length) { 447 this.#queueNamedUpdates() 448 } 449 450 // global updates only update the globalTop nodes, but we need to know 451 // that they're there, and not reinstall the world unnecessarily. 452 const globalExplicitUpdateNames = [] 453 if (this.options.global && (this[_updateAll] || this[_updateNames].length)) { 454 const nm = resolve(this.path, 'node_modules') 455 const paths = await readdirScoped(nm).catch(() => []) 456 for (const p of paths) { 457 const name = p.replace(/\\/g, '/') 458 tree.package.dependencies = tree.package.dependencies || {} 459 const updateName = this[_updateNames].includes(name) 460 if (this[_updateAll] || updateName) { 461 if (updateName) { 462 globalExplicitUpdateNames.push(name) 463 } 464 const dir = resolve(nm, name) 465 const st = await lstat(dir) 466 .catch(/* istanbul ignore next */ er => null) 467 if (st && st.isSymbolicLink()) { 468 const target = await readlink(dir) 469 const real = resolve(dirname(dir), target).replace(/#/g, '%23') 470 tree.package.dependencies[name] = `file:${real}` 471 } else { 472 tree.package.dependencies[name] = '*' 473 } 474 } 475 } 476 } 477 478 if (this.auditReport && this.auditReport.size > 0) { 479 await this.#queueVulnDependents(options) 480 } 481 482 const { add, rm } = options 483 484 if (rm && rm.length) { 485 addRmPkgDeps.rm(tree.package, rm) 486 for (const name of rm) { 487 this.#explicitRequests.add({ from: tree, name, action: 'DELETE' }) 488 } 489 } 490 491 if (add && add.length) { 492 await this.#add(tree, options) 493 } 494 495 // triggers a refresh of all edgesOut. this has to be done BEFORE 496 // adding the edges to explicitRequests, because the package setter 497 // resets all edgesOut. 498 if (add && add.length || rm && rm.length || this.options.global) { 499 tree.package = tree.package 500 } 501 502 for (const spec of this[_resolvedAdd]) { 503 if (spec.tree === tree) { 504 this.#explicitRequests.add(tree.edgesOut.get(spec.name)) 505 } 506 } 507 for (const name of globalExplicitUpdateNames) { 508 this.#explicitRequests.add(tree.edgesOut.get(name)) 509 } 510 511 this.#depsQueue.push(tree) 512 } 513 514 // This returns a promise because we might not have the name yet, and need to 515 // call pacote.manifest to find the name. 516 async #add (tree, { add, saveType = null, saveBundle = false }) { 517 // If we have a link it will need to be added relative to the target's path 518 const path = tree.target.path 519 520 // get the name for each of the specs in the list. 521 // ie, doing `foo@bar` we just return foo but if it's a url or git, we 522 // don't know the name until we fetch it and look in its manifest. 523 await Promise.all(add.map(async rawSpec => { 524 // We do NOT provide the path to npa here, because user-additions need to 525 // be resolved relative to the tree being added to. 526 let spec = npa(rawSpec) 527 528 // if it's just @'' then we reload whatever's there, or get latest 529 // if it's an explicit tag, we need to install that specific tag version 530 const isTag = spec.rawSpec && spec.type === 'tag' 531 532 // look up the names of file/directory/git specs 533 if (!spec.name || isTag) { 534 const mani = await pacote.manifest(spec, { ...this.options }) 535 if (isTag) { 536 // translate tag to a version 537 spec = npa(`${mani.name}@${mani.version}`) 538 } 539 spec.name = mani.name 540 } 541 542 const { name } = spec 543 if (spec.type === 'file') { 544 spec = npa(`file:${relpath(path, spec.fetchSpec).replace(/#/g, '%23')}`, path) 545 spec.name = name 546 } else if (spec.type === 'directory') { 547 try { 548 const real = await realpath(spec.fetchSpec, this[_rpcache], this[_stcache]) 549 spec = npa(`file:${relpath(path, real).replace(/#/g, '%23')}`, path) 550 spec.name = name 551 } catch { 552 // TODO: create synthetic test case to simulate realpath failure 553 } 554 } 555 spec.tree = tree 556 this[_resolvedAdd].push(spec) 557 })) 558 559 // now this._resolvedAdd is a list of spec objects with names. 560 // find a home for each of them! 561 addRmPkgDeps.add({ 562 pkg: tree.package, 563 add: this[_resolvedAdd], 564 saveBundle, 565 saveType, 566 }) 567 } 568 569 // TODO: provide a way to fix bundled deps by exposing metadata about 570 // what's in the bundle at each published manifest. Without that, we 571 // can't possibly fix bundled deps without breaking a ton of other stuff, 572 // and leaving the user subject to getting it overwritten later anyway. 573 async #queueVulnDependents (options) { 574 for (const vuln of this.auditReport.values()) { 575 for (const node of vuln.nodes) { 576 const bundler = node.getBundler() 577 578 // XXX this belongs in the audit report itself, not here. 579 // We shouldn't even get these things here, and they shouldn't 580 // be printed by npm-audit-report as if they can be fixed, because 581 // they can't. 582 if (bundler) { 583 log.warn(`audit fix ${node.name}@${node.version}`, 584 `${node.location}\nis a bundled dependency of\n${ 585 bundler.name}@${bundler.version} at ${bundler.location}\n` + 586 'It cannot be fixed automatically.\n' + 587 `Check for updates to the ${bundler.name} package.`) 588 continue 589 } 590 591 for (const edge of node.edgesIn) { 592 this.addTracker('idealTree', edge.from.name, edge.from.location) 593 this.#depsQueue.push(edge.from) 594 } 595 } 596 } 597 598 // note any that can't be fixed at the root level without --force 599 // if there's a fix, we use that. otherwise, the user has to remove it, 600 // find a different thing, fix the upstream, etc. 601 // 602 // XXX: how to handle top nodes that aren't the root? Maybe the report 603 // just tells the user to cd into that directory and fix it? 604 if (this.options.force && this.auditReport && this.auditReport.topVulns.size) { 605 options.add = options.add || [] 606 options.rm = options.rm || [] 607 const nodesTouched = new Set() 608 for (const [name, topVuln] of this.auditReport.topVulns.entries()) { 609 const { 610 simpleRange, 611 topNodes, 612 fixAvailable, 613 } = topVuln 614 for (const node of topNodes) { 615 if (!node.isProjectRoot && !node.isWorkspace) { 616 // not something we're going to fix, sorry. have to cd into 617 // that directory and fix it yourself. 618 log.warn('audit', 'Manual fix required in linked project ' + 619 `at ./${node.location} for ${name}@${simpleRange}.\n` + 620 `'cd ./${node.location}' and run 'npm audit' for details.`) 621 continue 622 } 623 624 if (!fixAvailable) { 625 log.warn('audit', `No fix available for ${name}@${simpleRange}`) 626 continue 627 } 628 629 // name may be different if parent fixes the dep 630 // see Vuln fixAvailable setter 631 const { isSemVerMajor, version, name: fixName } = fixAvailable 632 const breakingMessage = isSemVerMajor 633 ? 'a SemVer major change' 634 : 'outside your stated dependency range' 635 log.warn('audit', `Updating ${fixName} to ${version}, ` + 636 `which is ${breakingMessage}.`) 637 638 await this.#add(node, { add: [`${fixName}@${version}`] }) 639 nodesTouched.add(node) 640 } 641 } 642 for (const node of nodesTouched) { 643 node.package = node.package 644 } 645 } 646 } 647 648 #avoidRange (name) { 649 if (!this.auditReport) { 650 return null 651 } 652 const vuln = this.auditReport.get(name) 653 if (!vuln) { 654 return null 655 } 656 return vuln.range 657 } 658 659 #queueNamedUpdates () { 660 // ignore top nodes, since they are not loaded the same way, and 661 // probably have their own project associated with them. 662 663 // for every node with one of the names on the list, we add its 664 // dependents to the queue to be evaluated. in buildDepStep, 665 // anything on the update names list will get refreshed, even if 666 // it isn't a problem. 667 668 // XXX this could be faster by doing a series of inventory.query('name') 669 // calls rather than walking over everything in the tree. 670 for (const node of this.idealTree.inventory.values()) { 671 // XXX add any invalid edgesOut to the queue 672 if (this[_updateNames].includes(node.name) && 673 !node.isTop && !node.inDepBundle && !node.inShrinkwrap) { 674 for (const edge of node.edgesIn) { 675 this.addTracker('idealTree', edge.from.name, edge.from.location) 676 this.#depsQueue.push(edge.from) 677 } 678 } 679 } 680 } 681 682 async #inflateAncientLockfile () { 683 const { meta, inventory } = this.idealTree 684 const ancient = meta.ancientLockfile 685 const old = meta.loadedFromDisk && !(meta.originalLockfileVersion >= 2) 686 687 if (inventory.size === 0 || !ancient && !old) { 688 return 689 } 690 691 // if the lockfile is from node v5 or earlier, then we'll have to reload 692 // all the manifests of everything we encounter. this is costly, but at 693 // least it's just a one-time hit. 694 process.emit('time', 'idealTree:inflate') 695 696 // don't warn if we're not gonna actually write it back anyway. 697 const heading = ancient ? 'ancient lockfile' : 'old lockfile' 698 if (ancient || !this.options.lockfileVersion || 699 this.options.lockfileVersion >= defaultLockfileVersion) { 700 log.warn(heading, 701 ` 702The ${meta.type} file was created with an old version of npm, 703so supplemental metadata must be fetched from the registry. 704 705This is a one-time fix-up, please be patient... 706`) 707 } 708 709 this.addTracker('idealTree:inflate') 710 const queue = [] 711 for (const node of inventory.values()) { 712 if (node.isProjectRoot) { 713 continue 714 } 715 716 // if the node's location isn't within node_modules then this is actually 717 // a link target, so skip it. the link node itself will be queued later. 718 if (!node.location.startsWith('node_modules')) { 719 continue 720 } 721 722 queue.push(async () => { 723 log.silly('inflate', node.location) 724 const { resolved, version, path, name, location, integrity } = node 725 // don't try to hit the registry for linked deps 726 const useResolved = resolved && ( 727 !version || resolved.startsWith('file:') 728 ) 729 const id = useResolved ? resolved : version 730 const spec = npa.resolve(name, id, dirname(path)) 731 const t = `idealTree:inflate:${location}` 732 this.addTracker(t) 733 try { 734 const mani = await pacote.manifest(spec, { 735 ...this.options, 736 resolved: resolved, 737 integrity: integrity, 738 fullMetadata: false, 739 }) 740 node.package = { ...mani, _id: `${mani.name}@${mani.version}` } 741 } catch (er) { 742 const warning = `Could not fetch metadata for ${name}@${id}` 743 log.warn(heading, warning, er) 744 } 745 this.finishTracker(t) 746 }) 747 } 748 await promiseCallLimit(queue) 749 750 // have to re-calc dep flags, because the nodes don't have edges 751 // until their packages get assigned, so everything looks extraneous 752 calcDepFlags(this.idealTree) 753 754 // yes, yes, this isn't the "original" version, but now that it's been 755 // upgraded, we need to make sure we don't do the work to upgrade it 756 // again, since it's now as new as can be. 757 if (!this.options.lockfileVersion && !meta.hiddenLockfile) { 758 meta.originalLockfileVersion = defaultLockfileVersion 759 } 760 this.finishTracker('idealTree:inflate') 761 process.emit('timeEnd', 'idealTree:inflate') 762 } 763 764 // at this point we have a virtual tree with the actual root node's 765 // package deps, which may be partly or entirely incomplete, invalid 766 // or extraneous. 767 #buildDeps () { 768 process.emit('time', 'idealTree:buildDeps') 769 const tree = this.idealTree.target 770 tree.assertRootOverrides() 771 this.#depsQueue.push(tree) 772 // XXX also push anything that depends on a node with a name 773 // in the override list 774 log.silly('idealTree', 'buildDeps') 775 this.addTracker('idealTree', tree.name, '') 776 return this.#buildDepStep() 777 .then(() => process.emit('timeEnd', 'idealTree:buildDeps')) 778 } 779 780 async #buildDepStep () { 781 // removes tracker of previous dependency in the queue 782 if (this.#currentDep) { 783 const { location, name } = this.#currentDep 784 process.emit('timeEnd', `idealTree:${location || '#root'}`) 785 this.finishTracker('idealTree', name, location) 786 this.#currentDep = null 787 } 788 789 if (!this.#depsQueue.length) { 790 return this.#resolveLinks() 791 } 792 793 const node = this.#depsQueue.pop() 794 const bd = node.package.bundleDependencies 795 const hasBundle = bd && Array.isArray(bd) && bd.length 796 const { hasShrinkwrap } = node 797 798 // if the node was already visited, or has since been removed from the 799 // tree, skip over it and process the rest of the queue. If a node has 800 // a shrinkwrap, also skip it, because it's going to get its deps 801 // satisfied by whatever's in that file anyway. 802 if (this.#depsSeen.has(node) || 803 node.root !== this.idealTree || 804 hasShrinkwrap && !this.#complete) { 805 return this.#buildDepStep() 806 } 807 808 this.#depsSeen.add(node) 809 this.#currentDep = node 810 process.emit('time', `idealTree:${node.location || '#root'}`) 811 812 // if we're loading a _complete_ ideal tree, for a --package-lock-only 813 // installation for example, we have to crack open the tarball and 814 // look inside if it has bundle deps or shrinkwraps. note that this is 815 // not necessary during a reification, because we just update the 816 // ideal tree by reading bundles/shrinkwraps in place. 817 // Don't bother if the node is from the actual tree and hasn't 818 // been resolved, because we can't fetch it anyway, could be anything! 819 const crackOpen = this.#complete && 820 node !== this.idealTree && 821 node.resolved && 822 (hasBundle || hasShrinkwrap) 823 if (crackOpen) { 824 const Arborist = this.constructor 825 const opt = { ...this.options } 826 await cacache.tmp.withTmp(this.cache, opt, async path => { 827 await pacote.extract(node.resolved, path, { 828 ...opt, 829 Arborist, 830 resolved: node.resolved, 831 integrity: node.integrity, 832 }) 833 834 if (hasShrinkwrap) { 835 await new Arborist({ ...this.options, path }) 836 .loadVirtual({ root: node }) 837 } 838 839 if (hasBundle) { 840 await new Arborist({ ...this.options, path }) 841 .loadActual({ root: node, ignoreMissing: true }) 842 } 843 }) 844 } 845 846 // if any deps are missing or invalid, then we fetch the manifest for 847 // the thing we want, and build a new dep node from that. 848 // Then, find the ideal placement for that node. The ideal placement 849 // searches from the node's deps (or parent deps in the case of non-root 850 // peer deps), and walks up the tree until it finds the highest spot 851 // where it doesn't cause any conflicts. 852 // 853 // A conflict can be: 854 // - A node by that name already exists at that location. 855 // - The parent has a peer dep on that name 856 // - One of the node's peer deps conflicts at that location, unless the 857 // peer dep is met by a node at that location, which is fine. 858 // 859 // If we create a new node, then build its ideal deps as well. 860 // 861 // Note: this is the same "maximally naive" deduping tree-building 862 // algorithm that npm has used since v3. In a case like this: 863 // 864 // root -> (a@1, b@1||2) 865 // a -> (b@1) 866 // 867 // You'll end up with a tree like this: 868 // 869 // root 870 // +-- a@1 871 // | +-- b@1 872 // +-- b@2 873 // 874 // rather than this, more deduped, but just as correct tree: 875 // 876 // root 877 // +-- a@1 878 // +-- b@1 879 // 880 // Another way to look at it is that this algorithm favors getting higher 881 // version deps at higher levels in the tree, even if that reduces 882 // potential deduplication. 883 // 884 // Set `preferDedupe: true` in the options to replace the shallower 885 // dep if allowed. 886 887 const tasks = [] 888 const peerSource = this.#peerSetSource.get(node) || node 889 for (const edge of this.#problemEdges(node)) { 890 if (edge.peerConflicted) { 891 continue 892 } 893 894 // peerSetSource is only relevant when we have a peerEntryEdge 895 // otherwise we're setting regular non-peer deps as if they have 896 // a virtual root of whatever brought in THIS node. 897 // so we VR the node itself if the edge is not a peer 898 const source = edge.peer ? peerSource : node 899 900 const virtualRoot = this.#virtualRoot(source, true) 901 // reuse virtual root if we already have one, but don't 902 // try to do the override ahead of time, since we MAY be able 903 // to create a more correct tree than the virtual root could. 904 const vrEdge = virtualRoot && virtualRoot.edgesOut.get(edge.name) 905 const vrDep = vrEdge && vrEdge.valid && vrEdge.to 906 // only re-use the virtualRoot if it's a peer edge we're placing. 907 // otherwise, we end up in situations where we override peer deps that 908 // we could have otherwise found homes for. Eg: 909 // xy -> (x, y) 910 // x -> PEER(z@1) 911 // y -> PEER(z@2) 912 // If xy is a dependency, we can resolve this like: 913 // project 914 // +-- xy 915 // | +-- y 916 // | +-- z@2 917 // +-- x 918 // +-- z@1 919 // But if x and y are loaded in the same virtual root, then they will 920 // be forced to agree on a version of z. 921 const required = new Set([edge.from]) 922 const parent = edge.peer ? virtualRoot : null 923 const dep = vrDep && vrDep.satisfies(edge) ? vrDep 924 : await this.#nodeFromEdge(edge, parent, null, required) 925 926 /* istanbul ignore next */ 927 debug(() => { 928 if (!dep) { 929 throw new Error('no dep??') 930 } 931 }) 932 933 tasks.push({ edge, dep }) 934 } 935 936 const placeDeps = tasks.sort((a, b) => localeCompare(a.edge.name, b.edge.name)) 937 938 const promises = [] 939 for (const { edge, dep } of placeDeps) { 940 const pd = new PlaceDep({ 941 edge, 942 dep, 943 944 auditReport: this.auditReport, 945 explicitRequest: this.#explicitRequests.has(edge), 946 force: this.options.force, 947 installLinks: this.installLinks, 948 installStrategy: this.#installStrategy, 949 legacyPeerDeps: this.legacyPeerDeps, 950 preferDedupe: this.#preferDedupe, 951 strictPeerDeps: this.#strictPeerDeps, 952 updateNames: this[_updateNames], 953 }) 954 // placing a dep is actually a tree of placing the dep itself 955 // and all of its peer group that aren't already met by the tree 956 depth({ 957 tree: pd, 958 getChildren: pd => pd.children, 959 visit: pd => { 960 const { placed, edge, canPlace: cpd } = pd 961 // if we didn't place anything, nothing to do here 962 if (!placed) { 963 return 964 } 965 966 // we placed something, that means we changed the tree 967 if (placed.errors.length) { 968 this.#loadFailures.add(placed) 969 } 970 this.#mutateTree = true 971 if (cpd.canPlaceSelf === OK) { 972 for (const edgeIn of placed.edgesIn) { 973 if (edgeIn === edge) { 974 continue 975 } 976 const { from, valid, peerConflicted } = edgeIn 977 if (!peerConflicted && !valid && !this.#depsSeen.has(from)) { 978 this.addTracker('idealTree', from.name, from.location) 979 this.#depsQueue.push(edgeIn.from) 980 } 981 } 982 } else { 983 /* istanbul ignore else - should be only OK or REPLACE here */ 984 if (cpd.canPlaceSelf === REPLACE) { 985 // this may also create some invalid edges, for example if we're 986 // intentionally causing something to get nested which was 987 // previously placed in this location. 988 for (const edgeIn of placed.edgesIn) { 989 if (edgeIn === edge) { 990 continue 991 } 992 993 const { valid, peerConflicted } = edgeIn 994 if (!valid && !peerConflicted) { 995 // if it's already been visited, we have to re-visit 996 // otherwise, just enqueue normally. 997 this.#depsSeen.delete(edgeIn.from) 998 this.#depsQueue.push(edgeIn.from) 999 } 1000 } 1001 } 1002 } 1003 1004 /* istanbul ignore if - should be impossible */ 1005 if (cpd.canPlaceSelf === CONFLICT) { 1006 debug(() => { 1007 const er = new Error('placed with canPlaceSelf=CONFLICT') 1008 throw Object.assign(er, { placeDep: pd }) 1009 }) 1010 return 1011 } 1012 1013 // lastly, also check for the missing deps of the node we placed, 1014 // and any holes created by pruning out conflicted peer sets. 1015 this.#depsQueue.push(placed) 1016 for (const dep of pd.needEvaluation) { 1017 this.#depsSeen.delete(dep) 1018 this.#depsQueue.push(dep) 1019 } 1020 1021 // pre-fetch any problem edges, since we'll need these soon 1022 // if it fails at this point, though, dont' worry because it 1023 // may well be an optional dep that has gone missing. it'll 1024 // fail later anyway. 1025 for (const e of this.#problemEdges(placed)) { 1026 promises.push(() => 1027 this.#fetchManifest(npa.resolve(e.name, e.spec, fromPath(placed, e))) 1028 .catch(er => null) 1029 ) 1030 } 1031 }, 1032 }) 1033 } 1034 1035 for (const { to } of node.edgesOut.values()) { 1036 if (to && to.isLink && to.target) { 1037 this.#linkNodes.add(to) 1038 } 1039 } 1040 1041 await promiseCallLimit(promises) 1042 return this.#buildDepStep() 1043 } 1044 1045 // loads a node from an edge, and then loads its peer deps (and their 1046 // peer deps, on down the line) into a virtual root parent. 1047 async #nodeFromEdge (edge, parent_, secondEdge, required) { 1048 // create a virtual root node with the same deps as the node that 1049 // is requesting this one, so that we can get all the peer deps in 1050 // a context where they're likely to be resolvable. 1051 // Note that the virtual root will also have virtual copies of the 1052 // targets of any child Links, so that they resolve appropriately. 1053 const parent = parent_ || this.#virtualRoot(edge.from) 1054 1055 const spec = npa.resolve(edge.name, edge.spec, edge.from.path) 1056 const first = await this.#nodeFromSpec(edge.name, spec, parent, edge) 1057 1058 // we might have a case where the parent has a peer dependency on 1059 // `foo@*` which resolves to v2, but another dep in the set has a 1060 // peerDependency on `foo@1`. In that case, if we force it to be v2, 1061 // we're unnecessarily triggering an ERESOLVE. 1062 // If we have a second edge to worry about, and it's not satisfied 1063 // by the first node, try a second and see if that satisfies the 1064 // original edge here. 1065 const spec2 = secondEdge && npa.resolve( 1066 edge.name, 1067 secondEdge.spec, 1068 secondEdge.from.path 1069 ) 1070 const second = secondEdge && !secondEdge.valid 1071 ? await this.#nodeFromSpec(edge.name, spec2, parent, secondEdge) 1072 : null 1073 1074 // pick the second one if they're both happy with that, otherwise first 1075 const node = second && edge.valid ? second : first 1076 // ensure the one we want is the one that's placed 1077 node.parent = parent 1078 1079 if (required.has(edge.from) && edge.type !== 'peerOptional' || 1080 secondEdge && ( 1081 required.has(secondEdge.from) && secondEdge.type !== 'peerOptional')) { 1082 required.add(node) 1083 } 1084 1085 // keep track of the thing that caused this node to be included. 1086 const src = parent.sourceReference 1087 this.#peerSetSource.set(node, src) 1088 1089 // do not load the peers along with the set if this is a global top pkg 1090 // otherwise we'll be tempted to put peers as other top-level installed 1091 // things, potentially clobbering what's there already, which is not 1092 // what we want. the missing edges will be picked up on the next pass. 1093 if (this.options.global && edge.from.isProjectRoot) { 1094 return node 1095 } 1096 1097 // otherwise, we have to make sure that our peers can go along with us. 1098 return this.#loadPeerSet(node, required) 1099 } 1100 1101 #virtualRoot (node, reuse = false) { 1102 if (reuse && this.#virtualRoots.has(node)) { 1103 return this.#virtualRoots.get(node) 1104 } 1105 1106 const vr = new Node({ 1107 path: node.realpath, 1108 sourceReference: node, 1109 installLinks: this.installLinks, 1110 legacyPeerDeps: this.legacyPeerDeps, 1111 overrides: node.overrides, 1112 }) 1113 1114 // also need to set up any targets from any link deps, so that 1115 // they are properly reflected in the virtual environment 1116 for (const child of node.children.values()) { 1117 if (child.isLink) { 1118 new Node({ 1119 path: child.realpath, 1120 sourceReference: child.target, 1121 root: vr, 1122 }) 1123 } 1124 } 1125 1126 this.#virtualRoots.set(node, vr) 1127 return vr 1128 } 1129 1130 #problemEdges (node) { 1131 // skip over any bundled deps, they're not our problem. 1132 // Note that this WILL fetch bundled meta-deps which are also dependencies 1133 // but not listed as bundled deps. When reifying, we first unpack any 1134 // nodes that have bundleDependencies, then do a loadActual on them, move 1135 // the nodes into the ideal tree, and then prune. So, fetching those 1136 // possibly-bundled meta-deps at this point doesn't cause any worse 1137 // problems than a few unnecessary packument fetches. 1138 1139 // also skip over any nodes in the tree that failed to load, since those 1140 // will crash the install later on anyway. 1141 const bd = node.isProjectRoot || node.isWorkspace ? null 1142 : node.package.bundleDependencies 1143 const bundled = new Set(bd || []) 1144 1145 const problems = [] 1146 for (const edge of node.edgesOut.values()) { 1147 // If it's included in a bundle, we take whatever is specified. 1148 if (bundled.has(edge.name)) { 1149 continue 1150 } 1151 1152 // If it's already been logged as a load failure, skip it. 1153 if (edge.to && this.#loadFailures.has(edge.to)) { 1154 continue 1155 } 1156 1157 // If it's shrinkwrapped, we use what the shrinkwap wants. 1158 if (edge.to && edge.to.inShrinkwrap) { 1159 continue 1160 } 1161 1162 // If the edge has no destination, that's a problem, unless 1163 // if it's peerOptional and not explicitly requested. 1164 if (!edge.to) { 1165 if (edge.type !== 'peerOptional' || 1166 this.#explicitRequests.has(edge)) { 1167 problems.push(edge) 1168 } 1169 continue 1170 } 1171 1172 // If the edge has an error, there's a problem. 1173 if (!edge.valid) { 1174 problems.push(edge) 1175 continue 1176 } 1177 1178 // If the edge is a workspace, and it's valid, leave it alone 1179 if (edge.to.isWorkspace) { 1180 continue 1181 } 1182 1183 // user explicitly asked to update this package by name, problem 1184 if (this[_updateNames].includes(edge.name)) { 1185 problems.push(edge) 1186 continue 1187 } 1188 1189 // fixing a security vulnerability with this package, problem 1190 if (this.auditReport && this.auditReport.isVulnerable(edge.to)) { 1191 problems.push(edge) 1192 continue 1193 } 1194 1195 // user has explicitly asked to install this package, problem 1196 if (this.#explicitRequests.has(edge)) { 1197 problems.push(edge) 1198 continue 1199 } 1200 } 1201 return problems 1202 } 1203 1204 async #fetchManifest (spec) { 1205 const options = { 1206 ...this.options, 1207 avoid: this.#avoidRange(spec.name), 1208 } 1209 // get the intended spec and stored metadata from yarn.lock file, 1210 // if available and valid. 1211 spec = this.idealTree.meta.checkYarnLock(spec, options) 1212 1213 if (this.#manifests.has(spec.raw)) { 1214 return this.#manifests.get(spec.raw) 1215 } else { 1216 const cleanRawSpec = cleanUrl(spec.rawSpec) 1217 log.silly('fetch manifest', spec.raw.replace(spec.rawSpec, cleanRawSpec)) 1218 const o = { 1219 ...options, 1220 fullMetadata: true, 1221 } 1222 const p = pacote.manifest(spec, o) 1223 .then(({ license, ...mani }) => { 1224 this.#manifests.set(spec.raw, mani) 1225 return mani 1226 }) 1227 this.#manifests.set(spec.raw, p) 1228 return p 1229 } 1230 } 1231 1232 #nodeFromSpec (name, spec, parent, edge) { 1233 // pacote will slap integrity on its options, so we have to clone 1234 // the object so it doesn't get mutated. 1235 // Don't bother to load the manifest for link deps, because the target 1236 // might be within another package that doesn't exist yet. 1237 const { installLinks, legacyPeerDeps } = this 1238 const isWorkspace = this.idealTree.workspaces && this.idealTree.workspaces.has(spec.name) 1239 1240 // spec is a directory, link it unless installLinks is set or it's a workspace 1241 // TODO post arborist refactor, will need to check for installStrategy=linked 1242 if (spec.type === 'directory' && (isWorkspace || !installLinks)) { 1243 return this.#linkFromSpec(name, spec, parent, edge) 1244 } 1245 1246 // if the spec matches a workspace name, then see if the workspace node will 1247 // satisfy the edge. if it does, we return the workspace node to make sure it 1248 // takes priority. 1249 if (isWorkspace) { 1250 const existingNode = this.idealTree.edgesOut.get(spec.name).to 1251 if (existingNode && existingNode.isWorkspace && existingNode.satisfies(edge)) { 1252 return existingNode 1253 } 1254 } 1255 1256 // spec isn't a directory, and either isn't a workspace or the workspace we have 1257 // doesn't satisfy the edge. try to fetch a manifest and build a node from that. 1258 return this.#fetchManifest(spec) 1259 .then(pkg => new Node({ name, pkg, parent, installLinks, legacyPeerDeps }), error => { 1260 error.requiredBy = edge.from.location || '.' 1261 1262 // failed to load the spec, either because of enotarget or 1263 // fetch failure of some other sort. save it so we can verify 1264 // later that it's optional, otherwise the error is fatal. 1265 const n = new Node({ 1266 name, 1267 parent, 1268 error, 1269 installLinks, 1270 legacyPeerDeps, 1271 }) 1272 this.#loadFailures.add(n) 1273 return n 1274 }) 1275 } 1276 1277 #linkFromSpec (name, spec, parent, edge) { 1278 const realpath = spec.fetchSpec 1279 const { installLinks, legacyPeerDeps } = this 1280 return rpj(realpath + '/package.json').catch(() => ({})).then(pkg => { 1281 const link = new Link({ name, parent, realpath, pkg, installLinks, legacyPeerDeps }) 1282 this.#linkNodes.add(link) 1283 return link 1284 }) 1285 } 1286 1287 // load all peer deps and meta-peer deps into the node's parent 1288 // At the end of this, the node's peer-type outward edges are all 1289 // resolved, and so are all of theirs, but other dep types are not. 1290 // We prefer to get peer deps that meet the requiring node's dependency, 1291 // if possible, since that almost certainly works (since that package was 1292 // developed with this set of deps) and will typically be more restrictive. 1293 // Note that the peers in the set can conflict either with each other, 1294 // or with a direct dependency from the virtual root parent! In strict 1295 // mode, this is always an error. In force mode, it never is, and we 1296 // prefer the parent's non-peer dep over a peer dep, or the version that 1297 // gets placed first. In non-strict mode, we behave strictly if the 1298 // virtual root is based on the root project, and allow non-peer parent 1299 // deps to override, but throw if no preference can be determined. 1300 async #loadPeerSet (node, required) { 1301 const peerEdges = [...node.edgesOut.values()] 1302 // we typically only install non-optional peers, but we have to 1303 // factor them into the peerSet so that we can avoid conflicts 1304 .filter(e => e.peer && !(e.valid && e.to)) 1305 .sort(({ name: a }, { name: b }) => localeCompare(a, b)) 1306 1307 for (const edge of peerEdges) { 1308 // already placed this one, and we're happy with it. 1309 if (edge.valid && edge.to) { 1310 continue 1311 } 1312 1313 const parentEdge = node.parent.edgesOut.get(edge.name) 1314 const { isProjectRoot, isWorkspace } = node.parent.sourceReference 1315 const isMine = isProjectRoot || isWorkspace 1316 const conflictOK = this.options.force || !isMine && !this.#strictPeerDeps 1317 1318 if (!edge.to) { 1319 if (!parentEdge) { 1320 // easy, just put the thing there 1321 await this.#nodeFromEdge(edge, node.parent, null, required) 1322 continue 1323 } else { 1324 // if the parent's edge is very broad like >=1, and the edge in 1325 // question is something like 1.x, then we want to get a 1.x, not 1326 // a 2.x. pass along the child edge as an advisory guideline. 1327 // if the parent edge doesn't satisfy the child edge, and the 1328 // child edge doesn't satisfy the parent edge, then we have 1329 // a conflict. this is always a problem in strict mode, never 1330 // in force mode, and a problem in non-strict mode if this isn't 1331 // on behalf of our project. in all such cases, we warn at least. 1332 const dep = await this.#nodeFromEdge( 1333 parentEdge, 1334 node.parent, 1335 edge, 1336 required 1337 ) 1338 1339 // hooray! that worked! 1340 if (edge.valid) { 1341 continue 1342 } 1343 1344 // allow it. either we're overriding, or it's not something 1345 // that will be installed by default anyway, and we'll fail when 1346 // we get to the point where we need to, if we need to. 1347 if (conflictOK || !required.has(dep)) { 1348 edge.peerConflicted = true 1349 continue 1350 } 1351 1352 // problem 1353 this.#failPeerConflict(edge, parentEdge) 1354 } 1355 } 1356 1357 // There is something present already, and we're not happy about it 1358 // See if the thing we WOULD be happy with is also going to satisfy 1359 // the other dependents on the current node. 1360 const current = edge.to 1361 const dep = await this.#nodeFromEdge(edge, null, null, required) 1362 if (dep.canReplace(current)) { 1363 await this.#nodeFromEdge(edge, node.parent, null, required) 1364 continue 1365 } 1366 1367 // at this point we know that there is a dep there, and 1368 // we don't like it. always fail strictly, always allow forcibly or 1369 // in non-strict mode if it's not our fault. don't warn here, because 1370 // we are going to warn again when we place the deps, if we end up 1371 // overriding for something else. If the thing that has this dep 1372 // isn't also required, then there's a good chance we won't need it, 1373 // so allow it for now and let it conflict if it turns out to actually 1374 // be necessary for the installation. 1375 if (conflictOK || !required.has(edge.from)) { 1376 continue 1377 } 1378 1379 // ok, it's the root, or we're in unforced strict mode, so this is bad 1380 this.#failPeerConflict(edge, parentEdge) 1381 } 1382 return node 1383 } 1384 1385 #failPeerConflict (edge, currentEdge) { 1386 const expl = this.#explainPeerConflict(edge, currentEdge) 1387 throw Object.assign(new Error('unable to resolve dependency tree'), expl) 1388 } 1389 1390 #explainPeerConflict (edge, currentEdge) { 1391 const node = edge.from 1392 const curNode = node.resolve(edge.name) 1393 const current = curNode.explain() 1394 return { 1395 code: 'ERESOLVE', 1396 current, 1397 // it SHOULD be impossible to get here without a current node in place, 1398 // but this at least gives us something report on when bugs creep into 1399 // the tree handling logic. 1400 currentEdge: currentEdge ? currentEdge.explain() : null, 1401 edge: edge.explain(), 1402 strictPeerDeps: this.#strictPeerDeps, 1403 force: this.options.force, 1404 } 1405 } 1406 1407 // go through all the links in the this.#linkNodes set 1408 // for each one: 1409 // - if outside the root, ignore it, assume it's fine, it's not our problem 1410 // - if a node in the tree already, assign the target to that node. 1411 // - if a path under an existing node, then assign that as the fsParent, 1412 // and add it to the _depsQueue 1413 // 1414 // call buildDepStep if anything was added to the queue, otherwise we're done 1415 #resolveLinks () { 1416 for (const link of this.#linkNodes) { 1417 this.#linkNodes.delete(link) 1418 1419 // link we never ended up placing, skip it 1420 if (link.root !== this.idealTree) { 1421 continue 1422 } 1423 1424 const tree = this.idealTree.target 1425 const external = !link.target.isDescendantOf(tree) 1426 1427 // outside the root, somebody else's problem, ignore it 1428 if (external && !this.#follow) { 1429 continue 1430 } 1431 1432 // didn't find a parent for it or it has not been seen yet 1433 // so go ahead and process it. 1434 const unseenLink = (link.target.parent || link.target.fsParent) && 1435 !this.#depsSeen.has(link.target) 1436 1437 if (this.#follow && 1438 !link.target.parent && 1439 !link.target.fsParent || 1440 unseenLink) { 1441 this.addTracker('idealTree', link.target.name, link.target.location) 1442 this.#depsQueue.push(link.target) 1443 } 1444 } 1445 1446 if (this.#depsQueue.length) { 1447 return this.#buildDepStep() 1448 } 1449 } 1450 1451 #fixDepFlags () { 1452 process.emit('time', 'idealTree:fixDepFlags') 1453 const metaFromDisk = this.idealTree.meta.loadedFromDisk 1454 const flagsSuspect = this[_flagsSuspect] 1455 const mutateTree = this.#mutateTree 1456 // if the options set prune:false, then we don't prune, but we still 1457 // mark the extraneous items in the tree if we modified it at all. 1458 // If we did no modifications, we just iterate over the extraneous nodes. 1459 // if we started with an empty tree, then the dep flags are already 1460 // all set to true, and there can be nothing extraneous, so there's 1461 // nothing to prune, because we built it from scratch. if we didn't 1462 // add or remove anything, then also nothing to do. 1463 if (metaFromDisk && mutateTree) { 1464 resetDepFlags(this.idealTree) 1465 } 1466 1467 // update all the dev/optional/etc flags in the tree 1468 // either we started with a fresh tree, or we 1469 // reset all the flags to find the extraneous nodes. 1470 // 1471 // if we started from a blank slate, or changed something, then 1472 // the dep flags will be all set to true. 1473 if (!metaFromDisk || mutateTree) { 1474 calcDepFlags(this.idealTree) 1475 } else { 1476 // otherwise just unset all the flags on the root node 1477 // since they will sometimes have the default value 1478 this.idealTree.extraneous = false 1479 this.idealTree.dev = false 1480 this.idealTree.optional = false 1481 this.idealTree.devOptional = false 1482 this.idealTree.peer = false 1483 } 1484 1485 // at this point, any node marked as extraneous should be pruned. 1486 // if we started from a shrinkwrap, and then added/removed something, 1487 // then the tree is suspect. Prune what is marked as extraneous. 1488 // otherwise, don't bother. 1489 const needPrune = metaFromDisk && (mutateTree || flagsSuspect) 1490 if (this.#prune && needPrune) { 1491 this.#idealTreePrune() 1492 for (const node of this.idealTree.inventory.values()) { 1493 if (node.extraneous) { 1494 node.parent = null 1495 } 1496 } 1497 } 1498 1499 process.emit('timeEnd', 'idealTree:fixDepFlags') 1500 } 1501 1502 #idealTreePrune () { 1503 for (const node of this.idealTree.inventory.values()) { 1504 if (node.extraneous) { 1505 node.parent = null 1506 } 1507 } 1508 } 1509 1510 #pruneFailedOptional () { 1511 for (const node of this.#loadFailures) { 1512 if (!node.optional) { 1513 throw node.errors[0] 1514 } 1515 1516 const set = optionalSet(node) 1517 for (const node of set) { 1518 node.parent = null 1519 } 1520 } 1521 } 1522 1523 async prune (options = {}) { 1524 // allow the user to set options on the ctor as well. 1525 // XXX: deprecate separate method options objects. 1526 options = { ...this.options, ...options } 1527 1528 await this.buildIdealTree(options) 1529 1530 this.#idealTreePrune() 1531 1532 if (!this.options.workspacesEnabled) { 1533 const excludeNodes = this.excludeWorkspacesDependencySet(this.idealTree) 1534 for (const node of this.idealTree.inventory.values()) { 1535 if ( 1536 node.parent !== null 1537 && !node.isProjectRoot 1538 && !excludeNodes.has(node) 1539 ) { 1540 this[_addNodeToTrashList](node) 1541 } 1542 } 1543 } 1544 1545 return this.reify(options) 1546 } 1547} 1548