• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// mixin implementing the reify method
2const onExit = require('../signal-handling.js')
3const pacote = require('pacote')
4const AuditReport = require('../audit-report.js')
5const { subset, intersects } = require('semver')
6const npa = require('npm-package-arg')
7const semver = require('semver')
8const debug = require('../debug.js')
9const { walkUp } = require('walk-up-path')
10const log = require('proc-log')
11const hgi = require('hosted-git-info')
12const rpj = require('read-package-json-fast')
13
14const { dirname, resolve, relative, join } = require('path')
15const { depth: dfwalk } = require('treeverse')
16const {
17  lstat,
18  mkdir,
19  rm,
20  symlink,
21} = require('fs/promises')
22const { moveFile } = require('@npmcli/fs')
23const PackageJson = require('@npmcli/package-json')
24const packageContents = require('@npmcli/installed-package-contents')
25const runScript = require('@npmcli/run-script')
26const { checkEngine, checkPlatform } = require('npm-install-checks')
27
28const treeCheck = require('../tree-check.js')
29const relpath = require('../relpath.js')
30const Diff = require('../diff.js')
31const retirePath = require('../retire-path.js')
32const promiseAllRejectLate = require('promise-all-reject-late')
33const { callLimit: promiseCallLimit } = require('promise-call-limit')
34const optionalSet = require('../optional-set.js')
35const calcDepFlags = require('../calc-dep-flags.js')
36const { saveTypeMap, hasSubKey } = require('../add-rm-pkg-deps.js')
37
38const Shrinkwrap = require('../shrinkwrap.js')
39const { defaultLockfileVersion } = Shrinkwrap
40
41const _retiredPaths = Symbol('retiredPaths')
42const _retiredUnchanged = Symbol('retiredUnchanged')
43const _sparseTreeDirs = Symbol('sparseTreeDirs')
44const _sparseTreeRoots = Symbol('sparseTreeRoots')
45const _savePrefix = Symbol('savePrefix')
46const _retireShallowNodes = Symbol.for('retireShallowNodes')
47const _getBundlesByDepth = Symbol('getBundlesByDepth')
48const _registryResolved = Symbol('registryResolved')
49const _addNodeToTrashList = Symbol.for('addNodeToTrashList')
50
51// shared by rebuild mixin
52const _trashList = Symbol.for('trashList')
53const _handleOptionalFailure = Symbol.for('handleOptionalFailure')
54const _loadTrees = Symbol.for('loadTrees')
55
56// shared symbols for swapping out when testing
57const _diffTrees = Symbol.for('diffTrees')
58const _createSparseTree = Symbol.for('createSparseTree')
59const _loadShrinkwrapsAndUpdateTrees = Symbol.for('loadShrinkwrapsAndUpdateTrees')
60const _shrinkwrapInflated = Symbol('shrinkwrapInflated')
61const _bundleUnpacked = Symbol('bundleUnpacked')
62const _bundleMissing = Symbol('bundleMissing')
63const _reifyNode = Symbol.for('reifyNode')
64const _extractOrLink = Symbol('extractOrLink')
65const _updateAll = Symbol.for('updateAll')
66const _updateNames = Symbol.for('updateNames')
67// defined by rebuild mixin
68const _checkBins = Symbol.for('checkBins')
69const _symlink = Symbol('symlink')
70const _warnDeprecated = Symbol('warnDeprecated')
71const _loadBundlesAndUpdateTrees = Symbol.for('loadBundlesAndUpdateTrees')
72const _submitQuickAudit = Symbol('submitQuickAudit')
73const _unpackNewModules = Symbol.for('unpackNewModules')
74const _moveContents = Symbol.for('moveContents')
75const _moveBackRetiredUnchanged = Symbol.for('moveBackRetiredUnchanged')
76const _build = Symbol.for('build')
77const _removeTrash = Symbol.for('removeTrash')
78const _renamePath = Symbol.for('renamePath')
79const _rollbackRetireShallowNodes = Symbol.for('rollbackRetireShallowNodes')
80const _rollbackCreateSparseTree = Symbol.for('rollbackCreateSparseTree')
81const _rollbackMoveBackRetiredUnchanged = Symbol.for('rollbackMoveBackRetiredUnchanged')
82const _saveIdealTree = Symbol.for('saveIdealTree')
83const _copyIdealToActual = Symbol('copyIdealToActual')
84const _addOmitsToTrashList = Symbol('addOmitsToTrashList')
85const _packageLockOnly = Symbol('packageLockOnly')
86const _dryRun = Symbol('dryRun')
87const _validateNodeModules = Symbol('validateNodeModules')
88const _nmValidated = Symbol('nmValidated')
89const _validatePath = Symbol('validatePath')
90const _reifyPackages = Symbol.for('reifyPackages')
91
92const _omitDev = Symbol('omitDev')
93const _omitOptional = Symbol('omitOptional')
94const _omitPeer = Symbol('omitPeer')
95
96const _pruneBundledMetadeps = Symbol('pruneBundledMetadeps')
97
98// defined by Ideal mixin
99const _resolvedAdd = Symbol.for('resolvedAdd')
100const _usePackageLock = Symbol.for('usePackageLock')
101const _formatPackageLock = Symbol.for('formatPackageLock')
102
103const _createIsolatedTree = Symbol.for('createIsolatedTree')
104
105module.exports = cls => class Reifier extends cls {
106  constructor (options) {
107    super(options)
108
109    const {
110      savePrefix = '^',
111      packageLockOnly = false,
112      dryRun = false,
113      formatPackageLock = true,
114    } = options
115
116    this[_dryRun] = !!dryRun
117    this[_packageLockOnly] = !!packageLockOnly
118    this[_savePrefix] = savePrefix
119    this[_formatPackageLock] = !!formatPackageLock
120
121    this.diff = null
122    this[_retiredPaths] = {}
123    this[_shrinkwrapInflated] = new Set()
124    this[_retiredUnchanged] = {}
125    this[_sparseTreeDirs] = new Set()
126    this[_sparseTreeRoots] = new Set()
127    this[_trashList] = new Set()
128    // the nodes we unpack to read their bundles
129    this[_bundleUnpacked] = new Set()
130    // child nodes we'd EXPECT to be included in a bundle, but aren't
131    this[_bundleMissing] = new Set()
132    this[_nmValidated] = new Set()
133  }
134
135  // public method
136  async reify (options = {}) {
137    const linked = (options.installStrategy || this.options.installStrategy) === 'linked'
138
139    if (this[_packageLockOnly] && this.options.global) {
140      const er = new Error('cannot generate lockfile for global packages')
141      er.code = 'ESHRINKWRAPGLOBAL'
142      throw er
143    }
144
145    const omit = new Set(options.omit || [])
146    this[_omitDev] = omit.has('dev')
147    this[_omitOptional] = omit.has('optional')
148    this[_omitPeer] = omit.has('peer')
149
150    // start tracker block
151    this.addTracker('reify')
152    process.emit('time', 'reify')
153    await this[_validatePath]()
154    await this[_loadTrees](options)
155
156    const oldTree = this.idealTree
157    if (linked) {
158      // swap out the tree with the isolated tree
159      // this is currently technical debt which will be resolved in a refactor
160      // of Node/Link trees
161      log.warn('reify', 'The "linked" install strategy is EXPERIMENTAL and may contain bugs.')
162      this.idealTree = await this[_createIsolatedTree](this.idealTree)
163    }
164    await this[_diffTrees]()
165    await this[_reifyPackages]()
166    if (linked) {
167      // swap back in the idealTree
168      // so that the lockfile is preserved
169      this.idealTree = oldTree
170    }
171    await this[_saveIdealTree](options)
172    await this[_copyIdealToActual]()
173    // This is a very bad pattern and I can't wait to stop doing it
174    this.auditReport = await this.auditReport
175
176    this.finishTracker('reify')
177    process.emit('timeEnd', 'reify')
178    return treeCheck(this.actualTree)
179  }
180
181  async [_validatePath] () {
182    // don't create missing dirs on dry runs
183    if (this[_packageLockOnly] || this[_dryRun]) {
184      return
185    }
186
187    // we do NOT want to set ownership on this folder, especially
188    // recursively, because it can have other side effects to do that
189    // in a project directory.  We just want to make it if it's missing.
190    await mkdir(resolve(this.path), { recursive: true })
191
192    // do not allow the top-level node_modules to be a symlink
193    await this[_validateNodeModules](resolve(this.path, 'node_modules'))
194  }
195
196  async [_reifyPackages] () {
197    // we don't submit the audit report or write to disk on dry runs
198    if (this[_dryRun]) {
199      return
200    }
201
202    if (this[_packageLockOnly]) {
203      // we already have the complete tree, so just audit it now,
204      // and that's all we have to do here.
205      return this[_submitQuickAudit]()
206    }
207
208    // ok, we're about to start touching the fs.  need to roll back
209    // if we get an early termination.
210    let reifyTerminated = null
211    const removeHandler = onExit(({ signal }) => {
212      // only call once.  if signal hits twice, we just terminate
213      removeHandler()
214      reifyTerminated = Object.assign(new Error('process terminated'), {
215        signal,
216      })
217      return false
218    })
219
220    // [rollbackfn, [...actions]]
221    // after each step, if the process was terminated, execute the rollback
222    // note that each rollback *also* calls the previous one when it's
223    // finished, and then the first one throws the error, so we only need
224    // a new rollback step when we have a new thing that must be done to
225    // revert the install.
226    const steps = [
227      [_rollbackRetireShallowNodes, [
228        _retireShallowNodes,
229      ]],
230      [_rollbackCreateSparseTree, [
231        _createSparseTree,
232        _addOmitsToTrashList,
233        _loadShrinkwrapsAndUpdateTrees,
234        _loadBundlesAndUpdateTrees,
235        _submitQuickAudit,
236        _unpackNewModules,
237      ]],
238      [_rollbackMoveBackRetiredUnchanged, [
239        _moveBackRetiredUnchanged,
240        _build,
241      ]],
242    ]
243    for (const [rollback, actions] of steps) {
244      for (const action of actions) {
245        try {
246          await this[action]()
247          if (reifyTerminated) {
248            throw reifyTerminated
249          }
250        } catch (er) {
251          await this[rollback](er)
252          /* istanbul ignore next - rollback throws, should never hit this */
253          throw er
254        }
255      }
256    }
257
258    // no rollback for this one, just exit with the error, since the
259    // install completed and can't be safely recovered at this point.
260    await this[_removeTrash]()
261    if (reifyTerminated) {
262      throw reifyTerminated
263    }
264
265    // done modifying the file system, no need to keep listening for sigs
266    removeHandler()
267  }
268
269  // when doing a local install, we load everything and figure it all out.
270  // when doing a global install, we *only* care about the explicit requests.
271  [_loadTrees] (options) {
272    process.emit('time', 'reify:loadTrees')
273    const bitOpt = {
274      ...options,
275      complete: this[_packageLockOnly] || this[_dryRun],
276    }
277
278    // if we're only writing a package lock, then it doesn't matter what's here
279    if (this[_packageLockOnly]) {
280      return this.buildIdealTree(bitOpt)
281        .then(() => process.emit('timeEnd', 'reify:loadTrees'))
282    }
283
284    const actualOpt = this.options.global ? {
285      ignoreMissing: true,
286      global: true,
287      filter: (node, kid) => {
288        // if it's not the project root, and we have no explicit requests,
289        // then we're already into a nested dep, so we keep it
290        if (this.explicitRequests.size === 0 || !node.isProjectRoot) {
291          return true
292        }
293
294        // if we added it as an edgeOut, then we want it
295        if (this.idealTree.edgesOut.has(kid)) {
296          return true
297        }
298
299        // if it's an explicit request, then we want it
300        const hasExplicit = [...this.explicitRequests]
301          .some(edge => edge.name === kid)
302        if (hasExplicit) {
303          return true
304        }
305
306        // ignore the rest of the global install folder
307        return false
308      },
309    } : { ignoreMissing: true }
310
311    if (!this.options.global) {
312      return Promise.all([
313        this.loadActual(actualOpt),
314        this.buildIdealTree(bitOpt),
315      ]).then(() => process.emit('timeEnd', 'reify:loadTrees'))
316    }
317
318    // the global install space tends to have a lot of stuff in it.  don't
319    // load all of it, just what we care about.  we won't be saving a
320    // hidden lockfile in there anyway.  Note that we have to load ideal
321    // BEFORE loading actual, so that the actualOpt can use the
322    // explicitRequests which is set during buildIdealTree
323    return this.buildIdealTree(bitOpt)
324      .then(() => this.loadActual(actualOpt))
325      .then(() => process.emit('timeEnd', 'reify:loadTrees'))
326  }
327
328  [_diffTrees] () {
329    if (this[_packageLockOnly]) {
330      return
331    }
332
333    process.emit('time', 'reify:diffTrees')
334    // XXX if we have an existing diff already, there should be a way
335    // to just invalidate the parts that changed, but avoid walking the
336    // whole tree again.
337
338    const includeWorkspaces = this.options.workspacesEnabled
339    const includeRootDeps = !includeWorkspaces
340      || this.options.includeWorkspaceRoot && this.options.workspaces.length > 0
341
342    const filterNodes = []
343    if (this.options.global && this.explicitRequests.size) {
344      const idealTree = this.idealTree.target
345      const actualTree = this.actualTree.target
346      // we ONLY are allowed to make changes in the global top-level
347      // children where there's an explicit request.
348      for (const { name } of this.explicitRequests) {
349        const ideal = idealTree.children.get(name)
350        if (ideal) {
351          filterNodes.push(ideal)
352        }
353        const actual = actualTree.children.get(name)
354        if (actual) {
355          filterNodes.push(actual)
356        }
357      }
358    } else {
359      if (includeWorkspaces) {
360        // add all ws nodes to filterNodes
361        for (const ws of this.options.workspaces) {
362          const ideal = this.idealTree.children.get(ws)
363          if (ideal) {
364            filterNodes.push(ideal)
365          }
366          const actual = this.actualTree.children.get(ws)
367          if (actual) {
368            filterNodes.push(actual)
369          }
370        }
371      }
372      if (includeRootDeps) {
373        // add all non-workspace nodes to filterNodes
374        for (const tree of [this.idealTree, this.actualTree]) {
375          for (const { type, to } of tree.edgesOut.values()) {
376            if (type !== 'workspace' && to) {
377              filterNodes.push(to)
378            }
379          }
380        }
381      }
382    }
383
384    // find all the nodes that need to change between the actual
385    // and ideal trees.
386    this.diff = Diff.calculate({
387      shrinkwrapInflated: this[_shrinkwrapInflated],
388      filterNodes,
389      actual: this.actualTree,
390      ideal: this.idealTree,
391    })
392
393    // we don't have to add 'removed' folders to the trashlist, because
394    // they'll be moved aside to a retirement folder, and then the retired
395    // folder will be deleted at the end.  This is important when we have
396    // a folder like FOO being "removed" in favor of a folder like "foo",
397    // because if we remove node_modules/FOO on case-insensitive systems,
398    // it will remove the dep that we *want* at node_modules/foo.
399
400    process.emit('timeEnd', 'reify:diffTrees')
401  }
402
403  // add the node and all its bins to the list of things to be
404  // removed later on in the process.  optionally, also mark them
405  // as a retired paths, so that we move them out of the way and
406  // replace them when rolling back on failure.
407  [_addNodeToTrashList] (node, retire = false) {
408    const paths = [node.path, ...node.binPaths]
409    const moves = this[_retiredPaths]
410    log.silly('reify', 'mark', retire ? 'retired' : 'deleted', paths)
411    for (const path of paths) {
412      if (retire) {
413        const retired = retirePath(path)
414        moves[path] = retired
415        this[_trashList].add(retired)
416      } else {
417        this[_trashList].add(path)
418      }
419    }
420  }
421
422  // move aside the shallowest nodes in the tree that have to be
423  // changed or removed, so that we can rollback if necessary.
424  [_retireShallowNodes] () {
425    process.emit('time', 'reify:retireShallow')
426    const moves = this[_retiredPaths] = {}
427    for (const diff of this.diff.children) {
428      if (diff.action === 'CHANGE' || diff.action === 'REMOVE') {
429        // we'll have to clean these up at the end, so add them to the list
430        this[_addNodeToTrashList](diff.actual, true)
431      }
432    }
433    log.silly('reify', 'moves', moves)
434    const movePromises = Object.entries(moves)
435      .map(([from, to]) => this[_renamePath](from, to))
436    return promiseAllRejectLate(movePromises)
437      .then(() => process.emit('timeEnd', 'reify:retireShallow'))
438  }
439
440  [_renamePath] (from, to, didMkdirp = false) {
441    return moveFile(from, to)
442      .catch(er => {
443        // Occasionally an expected bin file might not exist in the package,
444        // or a shim/symlink might have been moved aside.  If we've already
445        // handled the most common cause of ENOENT (dir doesn't exist yet),
446        // then just ignore any ENOENT.
447        if (er.code === 'ENOENT') {
448          return didMkdirp ? null : mkdir(dirname(to), { recursive: true }).then(() =>
449            this[_renamePath](from, to, true))
450        } else if (er.code === 'EEXIST') {
451          return rm(to, { recursive: true, force: true }).then(() => moveFile(from, to))
452        } else {
453          throw er
454        }
455      })
456  }
457
458  [_rollbackRetireShallowNodes] (er) {
459    process.emit('time', 'reify:rollback:retireShallow')
460    const moves = this[_retiredPaths]
461    const movePromises = Object.entries(moves)
462      .map(([from, to]) => this[_renamePath](to, from))
463    return promiseAllRejectLate(movePromises)
464      // ignore subsequent rollback errors
465      .catch(er => {})
466      .then(() => process.emit('timeEnd', 'reify:rollback:retireShallow'))
467      .then(() => {
468        throw er
469      })
470  }
471
472  // adding to the trash list will skip reifying, and delete them
473  // if they are currently in the tree and otherwise untouched.
474  [_addOmitsToTrashList] () {
475    if (!this[_omitDev] && !this[_omitOptional] && !this[_omitPeer]) {
476      return
477    }
478
479    process.emit('time', 'reify:trashOmits')
480
481    for (const node of this.idealTree.inventory.values()) {
482      const { top } = node
483
484      // if the top is not the root or workspace then we do not want to omit it
485      if (!top.isProjectRoot && !top.isWorkspace) {
486        continue
487      }
488
489      // if a diff filter has been created, then we do not omit the node if the
490      // top node is not in that set
491      if (this.diff?.filterSet?.size && !this.diff.filterSet.has(top)) {
492        continue
493      }
494
495      // omit node if the dep type matches any omit flags that were set
496      if (
497        node.peer && this[_omitPeer] ||
498        node.dev && this[_omitDev] ||
499        node.optional && this[_omitOptional] ||
500        node.devOptional && this[_omitOptional] && this[_omitDev]
501      ) {
502        this[_addNodeToTrashList](node)
503      }
504    }
505
506    process.emit('timeEnd', 'reify:trashOmits')
507  }
508
509  [_createSparseTree] () {
510    process.emit('time', 'reify:createSparse')
511    // if we call this fn again, we look for the previous list
512    // so that we can avoid making the same directory multiple times
513    const leaves = this.diff.leaves
514      .filter(diff => {
515        return (diff.action === 'ADD' || diff.action === 'CHANGE') &&
516          !this[_sparseTreeDirs].has(diff.ideal.path) &&
517          !diff.ideal.isLink
518      })
519      .map(diff => diff.ideal)
520
521    // we check this in parallel, so guard against multiple attempts to
522    // retire the same path at the same time.
523    const dirsChecked = new Set()
524    return promiseAllRejectLate(leaves.map(async node => {
525      for (const d of walkUp(node.path)) {
526        if (d === node.top.path) {
527          break
528        }
529        if (dirsChecked.has(d)) {
530          continue
531        }
532        dirsChecked.add(d)
533        const st = await lstat(d).catch(er => null)
534        // this can happen if we have a link to a package with a name
535        // that the filesystem treats as if it is the same thing.
536        // would be nice to have conditional istanbul ignores here...
537        /* istanbul ignore next - defense in depth */
538        if (st && !st.isDirectory()) {
539          const retired = retirePath(d)
540          this[_retiredPaths][d] = retired
541          this[_trashList].add(retired)
542          await this[_renamePath](d, retired)
543        }
544      }
545      this[_sparseTreeDirs].add(node.path)
546      const made = await mkdir(node.path, { recursive: true })
547      // if the directory already exists, made will be undefined. if that's the case
548      // we don't want to remove it because we aren't the ones who created it so we
549      // omit it from the _sparseTreeRoots
550      if (made) {
551        this[_sparseTreeRoots].add(made)
552      }
553    }))
554      .then(() => process.emit('timeEnd', 'reify:createSparse'))
555  }
556
557  [_rollbackCreateSparseTree] (er) {
558    process.emit('time', 'reify:rollback:createSparse')
559    // cut the roots of the sparse tree that were created, not the leaves
560    const roots = this[_sparseTreeRoots]
561    // also delete the moves that we retired, so that we can move them back
562    const failures = []
563    const targets = [...roots, ...Object.keys(this[_retiredPaths])]
564    const unlinks = targets
565      .map(path => rm(path, { recursive: true, force: true }).catch(er => failures.push([path, er])))
566    return promiseAllRejectLate(unlinks).then(() => {
567      // eslint-disable-next-line promise/always-return
568      if (failures.length) {
569        log.warn('cleanup', 'Failed to remove some directories', failures)
570      }
571    })
572      .then(() => process.emit('timeEnd', 'reify:rollback:createSparse'))
573      .then(() => this[_rollbackRetireShallowNodes](er))
574  }
575
576  // shrinkwrap nodes define their dependency branches with a file, so
577  // we need to unpack them, read that shrinkwrap file, and then update
578  // the tree by calling loadVirtual with the node as the root.
579  [_loadShrinkwrapsAndUpdateTrees] () {
580    const seen = this[_shrinkwrapInflated]
581    const shrinkwraps = this.diff.leaves
582      .filter(d => (d.action === 'CHANGE' || d.action === 'ADD' || !d.action) &&
583        d.ideal.hasShrinkwrap && !seen.has(d.ideal) &&
584        !this[_trashList].has(d.ideal.path))
585
586    if (!shrinkwraps.length) {
587      return
588    }
589
590    process.emit('time', 'reify:loadShrinkwraps')
591
592    const Arborist = this.constructor
593    return promiseAllRejectLate(shrinkwraps.map(diff => {
594      const node = diff.ideal
595      seen.add(node)
596      return diff.action ? this[_reifyNode](node) : node
597    }))
598      .then(nodes => promiseAllRejectLate(nodes.map(node => new Arborist({
599        ...this.options,
600        path: node.path,
601      }).loadVirtual({ root: node }))))
602      // reload the diff and sparse tree because the ideal tree changed
603      .then(() => this[_diffTrees]())
604      .then(() => this[_createSparseTree]())
605      .then(() => this[_addOmitsToTrashList]())
606      .then(() => this[_loadShrinkwrapsAndUpdateTrees]())
607      .then(() => process.emit('timeEnd', 'reify:loadShrinkwraps'))
608  }
609
610  // create a symlink for Links, extract for Nodes
611  // return the node object, since we usually want that
612  // handle optional dep failures here
613  // If node is in trash list, skip it
614  // If reifying fails, and the node is optional, add it and its optionalSet
615  // to the trash list
616  // Always return the node.
617  [_reifyNode] (node) {
618    if (this[_trashList].has(node.path)) {
619      return node
620    }
621
622    const timer = `reifyNode:${node.location}`
623    process.emit('time', timer)
624    this.addTracker('reify', node.name, node.location)
625
626    const { npmVersion, nodeVersion, cpu, os, libc } = this.options
627    const p = Promise.resolve().then(async () => {
628      // when we reify an optional node, check the engine and platform
629      // first. be sure to ignore the --force and --engine-strict flags,
630      // since we always want to skip any optional packages we can't install.
631      // these checks throwing will result in a rollback and removal
632      // of the mismatches
633      // eslint-disable-next-line promise/always-return
634      if (node.optional) {
635        checkEngine(node.package, npmVersion, nodeVersion, false)
636        checkPlatform(node.package, false, { cpu, os, libc })
637      }
638      await this[_checkBins](node)
639      await this[_extractOrLink](node)
640      await this[_warnDeprecated](node)
641    })
642
643    return this[_handleOptionalFailure](node, p)
644      .then(() => {
645        this.finishTracker('reify', node.name, node.location)
646        process.emit('timeEnd', timer)
647        return node
648      })
649  }
650
651  // do not allow node_modules to be a symlink
652  async [_validateNodeModules] (nm) {
653    if (this.options.force || this[_nmValidated].has(nm)) {
654      return
655    }
656    const st = await lstat(nm).catch(() => null)
657    if (!st || st.isDirectory()) {
658      this[_nmValidated].add(nm)
659      return
660    }
661    log.warn('reify', 'Removing non-directory', nm)
662    await rm(nm, { recursive: true, force: true })
663  }
664
665  async [_extractOrLink] (node) {
666    const nm = resolve(node.parent.path, 'node_modules')
667    await this[_validateNodeModules](nm)
668
669    if (!node.isLink) {
670      // in normal cases, node.resolved should *always* be set by now.
671      // however, it is possible when a lockfile is damaged, or very old,
672      // or in some other race condition bugs in npm v6, that a previously
673      // bundled dependency will have just a version, but no resolved value,
674      // and no 'bundled: true' setting.
675      // Do the best with what we have, or else remove it from the tree
676      // entirely, since we can't possibly reify it.
677      let res = null
678      if (node.resolved) {
679        const registryResolved = this[_registryResolved](node.resolved)
680        if (registryResolved) {
681          res = `${node.name}@${registryResolved}`
682        }
683      } else if (node.package.name && node.version) {
684        res = `${node.package.name}@${node.version}`
685      }
686
687      // no idea what this thing is.  remove it from the tree.
688      if (!res) {
689        const warning = 'invalid or damaged lockfile detected\n' +
690          'please re-try this operation once it completes\n' +
691          'so that the damage can be corrected, or perform\n' +
692          'a fresh install with no lockfile if the problem persists.'
693        log.warn('reify', warning)
694        log.verbose('reify', 'unrecognized node in tree', node.path)
695        node.parent = null
696        node.fsParent = null
697        this[_addNodeToTrashList](node)
698        return
699      }
700      await debug(async () => {
701        const st = await lstat(node.path).catch(e => null)
702        if (st && !st.isDirectory()) {
703          debug.log('unpacking into a non-directory', node)
704          throw Object.assign(new Error('ENOTDIR: not a directory'), {
705            code: 'ENOTDIR',
706            path: node.path,
707          })
708        }
709      })
710      await pacote.extract(res, node.path, {
711        ...this.options,
712        resolved: node.resolved,
713        integrity: node.integrity,
714      })
715      // store nodes don't use Node class so node.package doesn't get updated
716      if (node.isInStore) {
717        const pkg = await rpj(join(node.path, 'package.json'))
718        node.package.scripts = pkg.scripts
719      }
720      return
721    }
722
723    // node.isLink
724    await rm(node.path, { recursive: true, force: true })
725    await this[_symlink](node)
726  }
727
728  async [_symlink] (node) {
729    const dir = dirname(node.path)
730    const target = node.realpath
731    const rel = relative(dir, target)
732    await mkdir(dir, { recursive: true })
733    return symlink(rel, node.path, 'junction')
734  }
735
736  [_warnDeprecated] (node) {
737    const { _id, deprecated } = node.package
738    if (deprecated) {
739      log.warn('deprecated', `${_id}: ${deprecated}`)
740    }
741  }
742
743  // if the node is optional, then the failure of the promise is nonfatal
744  // just add it and its optional set to the trash list.
745  [_handleOptionalFailure] (node, p) {
746    return (node.optional ? p.catch(er => {
747      const set = optionalSet(node)
748      for (node of set) {
749        log.verbose('reify', 'failed optional dependency', node.path)
750        this[_addNodeToTrashList](node)
751      }
752    }) : p).then(() => node)
753  }
754
755  [_registryResolved] (resolved) {
756    // the default registry url is a magic value meaning "the currently
757    // configured registry".
758    // `resolved` must never be falsey.
759    //
760    // XXX: use a magic string that isn't also a valid value, like
761    // ${REGISTRY} or something.  This has to be threaded through the
762    // Shrinkwrap and Node classes carefully, so for now, just treat
763    // the default reg as the magical animal that it has been.
764    const resolvedURL = hgi.parseUrl(resolved)
765
766    if (!resolvedURL) {
767      // if we could not parse the url at all then returning nothing
768      // here means it will get removed from the tree in the next step
769      return
770    }
771
772    if ((this.options.replaceRegistryHost === resolvedURL.hostname)
773      || this.options.replaceRegistryHost === 'always') {
774      // this.registry always has a trailing slash
775      return `${this.registry.slice(0, -1)}${resolvedURL.pathname}${resolvedURL.searchParams}`
776    }
777
778    return resolved
779  }
780
781  // bundles are *sort of* like shrinkwraps, in that the branch is defined
782  // by the contents of the package.  however, in their case, rather than
783  // shipping a virtual tree that must be reified, they ship an entire
784  // reified actual tree that must be unpacked and not modified.
785  [_loadBundlesAndUpdateTrees] (
786    depth = 0, bundlesByDepth = this[_getBundlesByDepth]()
787  ) {
788    if (depth === 0) {
789      process.emit('time', 'reify:loadBundles')
790    }
791
792    const maxBundleDepth = bundlesByDepth.get('maxBundleDepth')
793    if (depth > maxBundleDepth) {
794      // if we did something, then prune the tree and update the diffs
795      if (maxBundleDepth !== -1) {
796        this[_pruneBundledMetadeps](bundlesByDepth)
797        this[_diffTrees]()
798      }
799      process.emit('timeEnd', 'reify:loadBundles')
800      return
801    }
802
803    // skip any that have since been removed from the tree, eg by a
804    // shallower bundle overwriting them with a bundled meta-dep.
805    const set = (bundlesByDepth.get(depth) || [])
806      .filter(node => node.root === this.idealTree &&
807        node.target !== node.root &&
808        !this[_trashList].has(node.path))
809
810    if (!set.length) {
811      return this[_loadBundlesAndUpdateTrees](depth + 1, bundlesByDepth)
812    }
813
814    // extract all the nodes with bundles
815    return promiseCallLimit(set.map(node => {
816      return () => {
817        this[_bundleUnpacked].add(node)
818        return this[_reifyNode](node)
819      }
820    }), { rejectLate: true })
821    // then load their unpacked children and move into the ideal tree
822      .then(nodes =>
823        promiseAllRejectLate(nodes.map(async node => {
824          const arb = new this.constructor({
825            ...this.options,
826            path: node.path,
827          })
828          const notTransplanted = new Set(node.children.keys())
829          await arb.loadActual({
830            root: node,
831            // don't transplant any sparse folders we created
832            // loadActual will set node.package to {} for empty directories
833            // if by chance there are some empty folders in the node_modules
834            // tree for some other reason, then ok, ignore those too.
835            transplantFilter: node => {
836              if (node.package._id) {
837                // it's actually in the bundle if it gets transplanted
838                notTransplanted.delete(node.name)
839                return true
840              } else {
841                return false
842              }
843            },
844          })
845          for (const name of notTransplanted) {
846            this[_bundleMissing].add(node.children.get(name))
847          }
848        })))
849    // move onto the next level of bundled items
850      .then(() => this[_loadBundlesAndUpdateTrees](depth + 1, bundlesByDepth))
851  }
852
853  [_getBundlesByDepth] () {
854    const bundlesByDepth = new Map()
855    let maxBundleDepth = -1
856    dfwalk({
857      tree: this.diff,
858      visit: diff => {
859        const node = diff.ideal
860        if (!node) {
861          return
862        }
863        if (node.isProjectRoot) {
864          return
865        }
866
867        const { bundleDependencies } = node.package
868        if (bundleDependencies && bundleDependencies.length) {
869          maxBundleDepth = Math.max(maxBundleDepth, node.depth)
870          if (!bundlesByDepth.has(node.depth)) {
871            bundlesByDepth.set(node.depth, [node])
872          } else {
873            bundlesByDepth.get(node.depth).push(node)
874          }
875        }
876      },
877      getChildren: diff => diff.children,
878    })
879
880    bundlesByDepth.set('maxBundleDepth', maxBundleDepth)
881    return bundlesByDepth
882  }
883
884  // https://github.com/npm/cli/issues/1597#issuecomment-667639545
885  [_pruneBundledMetadeps] (bundlesByDepth) {
886    const bundleShadowed = new Set()
887
888    // Example dep graph:
889    // root -> (a, c)
890    // a -> BUNDLE(b)
891    // b -> c
892    // c -> b
893    //
894    // package tree:
895    // root
896    // +-- a
897    // |   +-- b(1)
898    // |   +-- c(1)
899    // +-- b(2)
900    // +-- c(2)
901    // 1. mark everything that's shadowed by anything in the bundle.  This
902    //    marks b(2) and c(2).
903    // 2. anything with edgesIn from outside the set, mark not-extraneous,
904    //    remove from set.  This unmarks c(2).
905    // 3. continue until no change
906    // 4. remove everything in the set from the tree.  b(2) is pruned
907
908    // create the list of nodes shadowed by children of bundlers
909    for (const bundles of bundlesByDepth.values()) {
910      // skip the 'maxBundleDepth' item
911      if (!Array.isArray(bundles)) {
912        continue
913      }
914      for (const node of bundles) {
915        for (const name of node.children.keys()) {
916          const shadow = node.parent.resolve(name)
917          if (!shadow) {
918            continue
919          }
920          bundleShadowed.add(shadow)
921          shadow.extraneous = true
922        }
923      }
924    }
925
926    // lib -> (a@1.x) BUNDLE(a@1.2.3 (b@1.2.3))
927    // a@1.2.3 -> (b@1.2.3)
928    // a@1.3.0 -> (b@2)
929    // b@1.2.3 -> ()
930    // b@2 -> (c@2)
931    //
932    // root
933    // +-- lib
934    // |   +-- a@1.2.3
935    // |   +-- b@1.2.3
936    // +-- b@2 <-- shadowed, now extraneous
937    // +-- c@2 <-- also shadowed, because only dependent is shadowed
938    for (const shadow of bundleShadowed) {
939      for (const shadDep of shadow.edgesOut.values()) {
940        /* istanbul ignore else - pretty unusual situation, just being
941         * defensive here. Would mean that a bundled dep has a dependency
942         * that is unmet. which, weird, but if you bundle it, we take
943         * whatever you put there and assume the publisher knows best. */
944        if (shadDep.to) {
945          bundleShadowed.add(shadDep.to)
946          shadDep.to.extraneous = true
947        }
948      }
949    }
950
951    let changed
952    do {
953      changed = false
954      for (const shadow of bundleShadowed) {
955        for (const edge of shadow.edgesIn) {
956          if (!bundleShadowed.has(edge.from)) {
957            shadow.extraneous = false
958            bundleShadowed.delete(shadow)
959            changed = true
960            break
961          }
962        }
963      }
964    } while (changed)
965
966    for (const shadow of bundleShadowed) {
967      this[_addNodeToTrashList](shadow)
968      shadow.root = null
969    }
970  }
971
972  async [_submitQuickAudit] () {
973    if (this.options.audit === false) {
974      this.auditReport = null
975      return
976    }
977
978    // we submit the quick audit at this point in the process, as soon as
979    // we have all the deps resolved, so that it can overlap with the other
980    // actions as much as possible.  Stash the promise, which we resolve
981    // before finishing the reify() and returning the tree.  Thus, we do
982    // NOT return the promise, as the intent is for this to run in parallel
983    // with the reification, and be resolved at a later time.
984    process.emit('time', 'reify:audit')
985    const options = { ...this.options }
986    const tree = this.idealTree
987
988    // if we're operating on a workspace, only audit the workspace deps
989    if (this.options.workspaces.length) {
990      options.filterSet = this.workspaceDependencySet(
991        tree,
992        this.options.workspaces,
993        this.options.includeWorkspaceRoot
994      )
995    }
996
997    this.auditReport = AuditReport.load(tree, options).then(res => {
998      process.emit('timeEnd', 'reify:audit')
999      return res
1000    })
1001  }
1002
1003  // ok!  actually unpack stuff into their target locations!
1004  // The sparse tree has already been created, so we walk the diff
1005  // kicking off each unpack job.  If any fail, we rm the sparse
1006  // tree entirely and try to put everything back where it was.
1007  [_unpackNewModules] () {
1008    process.emit('time', 'reify:unpack')
1009    const unpacks = []
1010    dfwalk({
1011      tree: this.diff,
1012      visit: diff => {
1013        // no unpacking if we don't want to change this thing
1014        if (diff.action !== 'CHANGE' && diff.action !== 'ADD') {
1015          return
1016        }
1017
1018        const node = diff.ideal
1019        const bd = this[_bundleUnpacked].has(node)
1020        const sw = this[_shrinkwrapInflated].has(node)
1021        const bundleMissing = this[_bundleMissing].has(node)
1022
1023        // check whether we still need to unpack this one.
1024        // test the inDepBundle last, since that's potentially a tree walk.
1025        const doUnpack = node && // can't unpack if removed!
1026          // root node already exists
1027          !node.isRoot &&
1028          // already unpacked to read bundle
1029          !bd &&
1030          // already unpacked to read sw
1031          !sw &&
1032          // already unpacked by another dep's bundle
1033          (bundleMissing || !node.inDepBundle)
1034
1035        if (doUnpack) {
1036          unpacks.push(this[_reifyNode](node))
1037        }
1038      },
1039      getChildren: diff => diff.children,
1040    })
1041    return promiseAllRejectLate(unpacks)
1042      .then(() => process.emit('timeEnd', 'reify:unpack'))
1043  }
1044
1045  // This is the part where we move back the unchanging nodes that were
1046  // the children of a node that did change.  If this fails, the rollback
1047  // is a three-step process.  First, we try to move the retired unchanged
1048  // nodes BACK to their retirement folders, then delete the sparse tree,
1049  // then move everything out of retirement.
1050  [_moveBackRetiredUnchanged] () {
1051    // get a list of all unchanging children of any shallow retired nodes
1052    // if they are not the ancestor of any node in the diff set, then the
1053    // directory won't already exist, so just rename it over.
1054    // This is sort of an inverse diff tree, of all the nodes where
1055    // the actualTree and idealTree _don't_ differ, starting from the
1056    // shallowest nodes that we moved aside in the first place.
1057    process.emit('time', 'reify:unretire')
1058    const moves = this[_retiredPaths]
1059    this[_retiredUnchanged] = {}
1060    return promiseAllRejectLate(this.diff.children.map(diff => {
1061      // skip if nothing was retired
1062      if (diff.action !== 'CHANGE' && diff.action !== 'REMOVE') {
1063        return
1064      }
1065
1066      const { path: realFolder } = diff.actual
1067      const retireFolder = moves[realFolder]
1068      /* istanbul ignore next - should be impossible */
1069      debug(() => {
1070        if (!retireFolder) {
1071          const er = new Error('trying to un-retire but not retired')
1072          throw Object.assign(er, {
1073            realFolder,
1074            retireFolder,
1075            actual: diff.actual,
1076            ideal: diff.ideal,
1077            action: diff.action,
1078          })
1079        }
1080      })
1081
1082      this[_retiredUnchanged][retireFolder] = []
1083      return promiseAllRejectLate(diff.unchanged.map(node => {
1084        // no need to roll back links, since we'll just delete them anyway
1085        if (node.isLink) {
1086          return mkdir(dirname(node.path), { recursive: true, force: true })
1087            .then(() => this[_reifyNode](node))
1088        }
1089
1090        // will have been moved/unpacked along with bundler
1091        if (node.inDepBundle && !this[_bundleMissing].has(node)) {
1092          return
1093        }
1094
1095        this[_retiredUnchanged][retireFolder].push(node)
1096
1097        const rel = relative(realFolder, node.path)
1098        const fromPath = resolve(retireFolder, rel)
1099        // if it has bundleDependencies, then make node_modules.  otherwise
1100        // skip it.
1101        const bd = node.package.bundleDependencies
1102        const dir = bd && bd.length ? node.path + '/node_modules' : node.path
1103        return mkdir(dir, { recursive: true }).then(() => this[_moveContents](node, fromPath))
1104      }))
1105    }))
1106      .then(() => process.emit('timeEnd', 'reify:unretire'))
1107  }
1108
1109  // move the contents from the fromPath to the node.path
1110  [_moveContents] (node, fromPath) {
1111    return packageContents({
1112      path: fromPath,
1113      depth: 1,
1114      packageJsonCache: new Map([[fromPath + '/package.json', node.package]]),
1115    }).then(res => promiseAllRejectLate(res.map(path => {
1116      const rel = relative(fromPath, path)
1117      const to = resolve(node.path, rel)
1118      return this[_renamePath](path, to)
1119    })))
1120  }
1121
1122  [_rollbackMoveBackRetiredUnchanged] (er) {
1123    const moves = this[_retiredPaths]
1124    // flip the mapping around to go back
1125    const realFolders = new Map(Object.entries(moves).map(([k, v]) => [v, k]))
1126    const promises = Object.entries(this[_retiredUnchanged])
1127      .map(([retireFolder, nodes]) => promiseAllRejectLate(nodes.map(node => {
1128        const realFolder = realFolders.get(retireFolder)
1129        const rel = relative(realFolder, node.path)
1130        const fromPath = resolve(retireFolder, rel)
1131        return this[_moveContents]({ ...node, path: fromPath }, node.path)
1132      })))
1133    return promiseAllRejectLate(promises)
1134      .then(() => this[_rollbackCreateSparseTree](er))
1135  }
1136
1137  [_build] () {
1138    process.emit('time', 'reify:build')
1139
1140    // for all the things being installed, run their appropriate scripts
1141    // run in tip->root order, so as to be more likely to build a node's
1142    // deps before attempting to build it itself
1143    const nodes = []
1144    dfwalk({
1145      tree: this.diff,
1146      leave: diff => {
1147        if (!diff.ideal.isProjectRoot) {
1148          nodes.push(diff.ideal)
1149        }
1150      },
1151      // process adds before changes, ignore removals
1152      getChildren: diff => diff && diff.children,
1153      filter: diff => diff.action === 'ADD' || diff.action === 'CHANGE',
1154    })
1155
1156    // pick up link nodes from the unchanged list as we want to run their
1157    // scripts in every install despite of having a diff status change
1158    for (const node of this.diff.unchanged) {
1159      const tree = node.root.target
1160
1161      // skip links that only live within node_modules as they are most
1162      // likely managed by packages we installed, we only want to rebuild
1163      // unchanged links we directly manage
1164      const linkedFromRoot = node.parent === tree || node.target.fsTop === tree
1165      if (node.isLink && linkedFromRoot) {
1166        nodes.push(node)
1167      }
1168    }
1169
1170    return this.rebuild({ nodes, handleOptionalFailure: true })
1171      .then(() => process.emit('timeEnd', 'reify:build'))
1172  }
1173
1174  // the tree is pretty much built now, so it's cleanup time.
1175  // remove the retired folders, and any deleted nodes
1176  // If this fails, there isn't much we can do but tell the user about it.
1177  // Thankfully, it's pretty unlikely that it'll fail, since rm is a node builtin.
1178  async [_removeTrash] () {
1179    process.emit('time', 'reify:trash')
1180    const promises = []
1181    const failures = []
1182    const _rm = path => rm(path, { recursive: true, force: true }).catch(er => failures.push([path, er]))
1183
1184    for (const path of this[_trashList]) {
1185      promises.push(_rm(path))
1186    }
1187
1188    await promiseAllRejectLate(promises)
1189    if (failures.length) {
1190      log.warn('cleanup', 'Failed to remove some directories', failures)
1191    }
1192    process.emit('timeEnd', 'reify:trash')
1193  }
1194
1195  // last but not least, we save the ideal tree metadata to the package-lock
1196  // or shrinkwrap file, and any additions or removals to package.json
1197  async [_saveIdealTree] (options) {
1198    // the ideal tree is actualized now, hooray!
1199    // it still contains all the references to optional nodes that were removed
1200    // for install failures.  Those still end up in the shrinkwrap, so we
1201    // save it first, then prune out the optional trash, and then return it.
1202
1203    const save = !(options.save === false)
1204
1205    // we check for updates in order to make sure we run save ideal tree
1206    // even though save=false since we want `npm update` to be able to
1207    // write to package-lock files by default
1208    const hasUpdates = this[_updateAll] || this[_updateNames].length
1209
1210    // we're going to completely skip save ideal tree in case of a global or
1211    // dry-run install and also if the save option is set to false, EXCEPT for
1212    // update since the expected behavior for npm7+ is for update to
1213    // NOT save to package.json, we make that exception since we still want
1214    // saveIdealTree to be able to write the lockfile by default.
1215    const saveIdealTree = !(
1216      (!save && !hasUpdates)
1217      || this.options.global
1218      || this[_dryRun]
1219    )
1220
1221    if (!saveIdealTree) {
1222      return false
1223    }
1224
1225    process.emit('time', 'reify:save')
1226
1227    const updatedTrees = new Set()
1228    const updateNodes = nodes => {
1229      for (const { name, tree: addTree } of nodes) {
1230        // addTree either the root, or a workspace
1231        const edge = addTree.edgesOut.get(name)
1232        const pkg = addTree.package
1233        const req = npa.resolve(name, edge.spec, addTree.realpath)
1234        const { rawSpec, subSpec } = req
1235
1236        const spec = subSpec ? subSpec.rawSpec : rawSpec
1237        const child = edge.to
1238
1239        // if we tried to install an optional dep, but it was a version
1240        // that we couldn't resolve, this MAY be missing.  if we haven't
1241        // blown up by now, it's because it was not a problem, though, so
1242        // just move on.
1243        if (!child || !addTree.isTop) {
1244          continue
1245        }
1246
1247        let newSpec
1248        // True if the dependency is getting installed from a local file path
1249        // In this case it is not possible to do the normal version comparisons
1250        // as the new version will be a file path
1251        const isLocalDep = req.type === 'directory' || req.type === 'file'
1252        if (req.registry) {
1253          const version = child.version
1254          const prefixRange = version ? this[_savePrefix] + version : '*'
1255          // if we installed a range, then we save the range specified
1256          // if it is not a subset of the ^x.y.z.  eg, installing a range
1257          // of `1.x <1.2.3` will not be saved as `^1.2.0`, because that
1258          // would allow versions outside the requested range.  Tags and
1259          // specific versions save with the save-prefix.
1260          const isRange = (subSpec || req).type === 'range'
1261
1262          let range = spec
1263          if (
1264            !isRange ||
1265            spec === '*' ||
1266            subset(prefixRange, spec, { loose: true })
1267          ) {
1268            range = prefixRange
1269          }
1270
1271          const pname = child.packageName
1272          const alias = name !== pname
1273          newSpec = alias ? `npm:${pname}@${range}` : range
1274        } else if (req.hosted) {
1275          // save the git+https url if it has auth, otherwise shortcut
1276          const h = req.hosted
1277          const opt = { noCommittish: false }
1278          if (h.https && h.auth) {
1279            newSpec = `git+${h.https(opt)}`
1280          } else {
1281            newSpec = h.shortcut(opt)
1282          }
1283        } else if (isLocalDep) {
1284          // when finding workspace nodes, make sure that
1285          // we save them using their version instead of
1286          // using their relative path
1287          if (edge.type === 'workspace') {
1288            const { version } = edge.to.target
1289            const prefixRange = version ? this[_savePrefix] + version : '*'
1290            newSpec = prefixRange
1291          } else {
1292            // save the relative path in package.json
1293            // Normally saveSpec is updated with the proper relative
1294            // path already, but it's possible to specify a full absolute
1295            // path initially, in which case we can end up with the wrong
1296            // thing, so just get the ultimate fetchSpec and relativize it.
1297            const p = req.fetchSpec.replace(/^file:/, '')
1298            const rel = relpath(addTree.realpath, p).replace(/#/g, '%23')
1299            newSpec = `file:${rel}`
1300          }
1301        } else {
1302          newSpec = req.saveSpec
1303        }
1304
1305        if (options.saveType) {
1306          const depType = saveTypeMap.get(options.saveType)
1307          pkg[depType][name] = newSpec
1308          // rpj will have moved it here if it was in both
1309          // if it is empty it will be deleted later
1310          if (options.saveType === 'prod' && pkg.optionalDependencies) {
1311            delete pkg.optionalDependencies[name]
1312          }
1313        } else {
1314          if (hasSubKey(pkg, 'dependencies', name)) {
1315            pkg.dependencies[name] = newSpec
1316          }
1317
1318          if (hasSubKey(pkg, 'devDependencies', name)) {
1319            pkg.devDependencies[name] = newSpec
1320            // don't update peer or optional if we don't have to
1321            if (hasSubKey(pkg, 'peerDependencies', name) && (isLocalDep || !intersects(newSpec, pkg.peerDependencies[name]))) {
1322              pkg.peerDependencies[name] = newSpec
1323            }
1324
1325            if (hasSubKey(pkg, 'optionalDependencies', name) && (isLocalDep || !intersects(newSpec, pkg.optionalDependencies[name]))) {
1326              pkg.optionalDependencies[name] = newSpec
1327            }
1328          } else {
1329            if (hasSubKey(pkg, 'peerDependencies', name)) {
1330              pkg.peerDependencies[name] = newSpec
1331            }
1332
1333            if (hasSubKey(pkg, 'optionalDependencies', name)) {
1334              pkg.optionalDependencies[name] = newSpec
1335            }
1336          }
1337        }
1338
1339        updatedTrees.add(addTree)
1340      }
1341    }
1342
1343    // Returns true if any of the edges from this node has a semver
1344    // range definition that is an exact match to the version installed
1345    // e.g: should return true if for a given an installed version 1.0.0,
1346    // range is either =1.0.0 or 1.0.0
1347    const exactVersion = node => {
1348      for (const edge of node.edgesIn) {
1349        try {
1350          if (semver.subset(edge.spec, node.version)) {
1351            return false
1352          }
1353        } catch {
1354          // ignore errors
1355        }
1356      }
1357      return true
1358    }
1359
1360    // helper that retrieves an array of nodes that were
1361    // potentially updated during the reify process, in order
1362    // to limit the number of nodes to check and update, only
1363    // select nodes from the inventory that are direct deps
1364    // of a given package.json (project root or a workspace)
1365    // and in ase of using a list of `names`, restrict nodes
1366    // to only names that are found in this list
1367    const retrieveUpdatedNodes = names => {
1368      const filterDirectDependencies = node =>
1369        !node.isRoot && node.resolveParent && node.resolveParent.isRoot
1370        && (!names || names.includes(node.name))
1371        && exactVersion(node) // skip update for exact ranges
1372
1373      const directDeps = this.idealTree.inventory
1374        .filter(filterDirectDependencies)
1375
1376      // traverses the list of direct dependencies and collect all nodes
1377      // to be updated, since any of them might have changed during reify
1378      const nodes = []
1379      for (const node of directDeps) {
1380        for (const edgeIn of node.edgesIn) {
1381          nodes.push({
1382            name: node.name,
1383            tree: edgeIn.from.target,
1384          })
1385        }
1386      }
1387      return nodes
1388    }
1389
1390    if (save) {
1391      // when using update all alongside with save, we'll make
1392      // sure to refresh every dependency of the root idealTree
1393      if (this[_updateAll]) {
1394        const nodes = retrieveUpdatedNodes()
1395        updateNodes(nodes)
1396      } else {
1397        // resolvedAdd is the list of user add requests, but with names added
1398        // to things like git repos and tarball file/urls.  However, if the
1399        // user requested 'foo@', and we have a foo@file:../foo, then we should
1400        // end up saving the spec we actually used, not whatever they gave us.
1401        if (this[_resolvedAdd].length) {
1402          updateNodes(this[_resolvedAdd])
1403        }
1404
1405        // if updating given dependencies by name, restrict the list of
1406        // nodes to check to only those currently in _updateNames
1407        if (this[_updateNames].length) {
1408          const nodes = retrieveUpdatedNodes(this[_updateNames])
1409          updateNodes(nodes)
1410        }
1411
1412        // grab any from explicitRequests that had deps removed
1413        for (const { from: tree } of this.explicitRequests) {
1414          updatedTrees.add(tree)
1415        }
1416      }
1417    }
1418
1419    if (save) {
1420      for (const tree of updatedTrees) {
1421        // refresh the edges so they have the correct specs
1422        tree.package = tree.package
1423        const pkgJson = await PackageJson.load(tree.path, { create: true })
1424        const {
1425          dependencies = {},
1426          devDependencies = {},
1427          optionalDependencies = {},
1428          peerDependencies = {},
1429          // bundleDependencies is not required by PackageJson like the other
1430          // fields here PackageJson also doesn't omit an empty array for this
1431          // field so defaulting this to an empty array would add that field to
1432          // every package.json file.
1433          bundleDependencies,
1434        } = tree.package
1435
1436        pkgJson.update({
1437          dependencies,
1438          devDependencies,
1439          optionalDependencies,
1440          peerDependencies,
1441          bundleDependencies,
1442        })
1443        await pkgJson.save()
1444      }
1445    }
1446
1447    // before now edge specs could be changing, affecting the `requires` field
1448    // in the package lock, so we hold off saving to the very last action
1449    if (this[_usePackageLock]) {
1450      // preserve indentation, if possible
1451      let format = this.idealTree.package[Symbol.for('indent')]
1452      if (format === undefined) {
1453        format = '  '
1454      }
1455
1456      // TODO this ignores options.save
1457      await this.idealTree.meta.save({
1458        format: (this[_formatPackageLock] && format) ? format
1459        : this[_formatPackageLock],
1460      })
1461    }
1462
1463    process.emit('timeEnd', 'reify:save')
1464    return true
1465  }
1466
1467  async [_copyIdealToActual] () {
1468    // clean up any trash that is still in the tree
1469    for (const path of this[_trashList]) {
1470      const loc = relpath(this.idealTree.realpath, path)
1471      const node = this.idealTree.inventory.get(loc)
1472      if (node && node.root === this.idealTree) {
1473        node.parent = null
1474      }
1475    }
1476
1477    // if we filtered to only certain nodes, then anything ELSE needs
1478    // to be untouched in the resulting actual tree, even if it differs
1479    // in the idealTree.  Copy over anything that was in the actual and
1480    // was not changed, delete anything in the ideal and not actual.
1481    // Then we move the entire idealTree over to this.actualTree, and
1482    // save the hidden lockfile.
1483    if (this.diff && this.diff.filterSet.size) {
1484      const reroot = new Set()
1485
1486      const { filterSet } = this.diff
1487      const seen = new Set()
1488      for (const [loc, ideal] of this.idealTree.inventory.entries()) {
1489        seen.add(loc)
1490
1491        // if it's an ideal node from the filter set, then skip it
1492        // because we already made whatever changes were necessary
1493        if (filterSet.has(ideal)) {
1494          continue
1495        }
1496
1497        // otherwise, if it's not in the actualTree, then it's not a thing
1498        // that we actually added.  And if it IS in the actualTree, then
1499        // it's something that we left untouched, so we need to record
1500        // that.
1501        const actual = this.actualTree.inventory.get(loc)
1502        if (!actual) {
1503          ideal.root = null
1504        } else {
1505          if ([...actual.linksIn].some(link => filterSet.has(link))) {
1506            seen.add(actual.location)
1507            continue
1508          }
1509          const { realpath, isLink } = actual
1510          if (isLink && ideal.isLink && ideal.realpath === realpath) {
1511            continue
1512          } else {
1513            reroot.add(actual)
1514          }
1515        }
1516      }
1517
1518      // now find any actual nodes that may not be present in the ideal
1519      // tree, but were left behind by virtue of not being in the filter
1520      for (const [loc, actual] of this.actualTree.inventory.entries()) {
1521        if (seen.has(loc)) {
1522          continue
1523        }
1524        seen.add(loc)
1525
1526        // we know that this is something that ISN'T in the idealTree,
1527        // or else we will have addressed it in the previous loop.
1528        // If it's in the filterSet, that means we intentionally removed
1529        // it, so nothing to do here.
1530        if (filterSet.has(actual)) {
1531          continue
1532        }
1533
1534        reroot.add(actual)
1535      }
1536
1537      // go through the rerooted actual nodes, and move them over.
1538      for (const actual of reroot) {
1539        actual.root = this.idealTree
1540      }
1541
1542      // prune out any tops that lack a linkIn, they are no longer relevant.
1543      for (const top of this.idealTree.tops) {
1544        if (top.linksIn.size === 0) {
1545          top.root = null
1546        }
1547      }
1548
1549      // need to calculate dep flags, since nodes may have been marked
1550      // as extraneous or otherwise incorrect during transit.
1551      calcDepFlags(this.idealTree)
1552    }
1553
1554    // save the ideal's meta as a hidden lockfile after we actualize it
1555    this.idealTree.meta.filename =
1556      this.idealTree.realpath + '/node_modules/.package-lock.json'
1557    this.idealTree.meta.hiddenLockfile = true
1558    this.idealTree.meta.lockfileVersion = defaultLockfileVersion
1559
1560    this.actualTree = this.idealTree
1561    this.idealTree = null
1562
1563    if (!this.options.global) {
1564      await this.actualTree.meta.save()
1565      const ignoreScripts = !!this.options.ignoreScripts
1566      // if we aren't doing a dry run or ignoring scripts and we actually made changes to the dep
1567      // tree, then run the dependencies scripts
1568      if (!this[_dryRun] && !ignoreScripts && this.diff && this.diff.children.length) {
1569        const { path, package: pkg } = this.actualTree.target
1570        const stdio = this.options.foregroundScripts ? 'inherit' : 'pipe'
1571        const { scripts = {} } = pkg
1572        for (const event of ['predependencies', 'dependencies', 'postdependencies']) {
1573          if (Object.prototype.hasOwnProperty.call(scripts, event)) {
1574            const timer = `reify:run:${event}`
1575            process.emit('time', timer)
1576            log.info('run', pkg._id, event, scripts[event])
1577            await runScript({
1578              event,
1579              path,
1580              pkg,
1581              stdio,
1582              scriptShell: this.options.scriptShell,
1583            })
1584            process.emit('timeEnd', timer)
1585          }
1586        }
1587      }
1588    }
1589  }
1590
1591  async dedupe (options = {}) {
1592    // allow the user to set options on the ctor as well.
1593    // XXX: deprecate separate method options objects.
1594    options = { ...this.options, ...options }
1595    const tree = await this.loadVirtual().catch(() => this.loadActual())
1596    const names = []
1597    for (const name of tree.inventory.query('name')) {
1598      if (tree.inventory.query('name', name).size > 1) {
1599        names.push(name)
1600      }
1601    }
1602    return this.reify({
1603      ...options,
1604      preferDedupe: true,
1605      update: { names },
1606    })
1607  }
1608}
1609