• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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