• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// inventory, path, realpath, root, and parent
2//
3// node.root is a reference to the root module in the tree (ie, typically the
4// cwd project folder)
5//
6// node.location is the /-delimited path from the root module to the node.  In
7// the case of link targets that may be outside of the root's package tree,
8// this can include some number of /../ path segments.  The location of the
9// root module is always '.'.  node.location thus never contains drive letters
10// or absolute paths, and is portable within a given project, suitable for
11// inclusion in lockfiles and metadata.
12//
13// node.path is the path to the place where this node lives on disk.  It is
14// system-specific and absolute.
15//
16// node.realpath is the path to where the module actually resides on disk.  In
17// the case of non-link nodes, node.realpath is equivalent to node.path.  In
18// the case of link nodes, it is equivalent to node.target.path.
19//
20// Setting node.parent will set the node's root to the parent's root, as well
21// as updating edgesIn and edgesOut to reload dependency resolutions as needed,
22// and setting node.path to parent.path/node_modules/name.
23//
24// node.inventory is a Map of name to a Set() of all the nodes under a given
25// root by that name.  It's empty for non-root nodes, and changing the root
26// reference will remove it from the old root's inventory and add it to the new
27// one.  This map is useful for cases like `npm update foo` or `npm ls foo`
28// where we need to quickly find all instances of a given package name within a
29// tree.
30
31const semver = require('semver')
32const nameFromFolder = require('@npmcli/name-from-folder')
33const Edge = require('./edge.js')
34const Inventory = require('./inventory.js')
35const OverrideSet = require('./override-set.js')
36const { normalize } = require('read-package-json-fast')
37const { getPaths: getBinPaths } = require('bin-links')
38const npa = require('npm-package-arg')
39const debug = require('./debug.js')
40const gatherDepSet = require('./gather-dep-set.js')
41const treeCheck = require('./tree-check.js')
42const { walkUp } = require('walk-up-path')
43
44const { resolve, relative, dirname, basename } = require('path')
45const util = require('util')
46const _package = Symbol('_package')
47const _parent = Symbol('_parent')
48const _target = Symbol.for('_target')
49const _fsParent = Symbol('_fsParent')
50const _reloadNamedEdges = Symbol('_reloadNamedEdges')
51// overridden by Link class
52const _loadDeps = Symbol.for('Arborist.Node._loadDeps')
53const _refreshLocation = Symbol.for('_refreshLocation')
54const _changePath = Symbol.for('_changePath')
55// used by Link class as well
56const _delistFromMeta = Symbol.for('_delistFromMeta')
57const _explain = Symbol('_explain')
58const _explanation = Symbol('_explanation')
59
60const relpath = require('./relpath.js')
61const consistentResolve = require('./consistent-resolve.js')
62
63const printableTree = require('./printable.js')
64const CaseInsensitiveMap = require('./case-insensitive-map.js')
65
66const querySelectorAll = require('./query-selector-all.js')
67
68class Node {
69  #global
70  #meta
71  #root
72  #workspaces
73
74  constructor (options) {
75    // NB: path can be null if it's a link target
76    const {
77      root,
78      path,
79      realpath,
80      parent,
81      error,
82      meta,
83      fsParent,
84      resolved,
85      integrity,
86      // allow setting name explicitly when we haven't set a path yet
87      name,
88      children,
89      fsChildren,
90      installLinks = false,
91      legacyPeerDeps = false,
92      linksIn,
93      isInStore = false,
94      hasShrinkwrap,
95      overrides,
96      loadOverrides = false,
97      extraneous = true,
98      dev = true,
99      optional = true,
100      devOptional = true,
101      peer = true,
102      global = false,
103      dummy = false,
104      sourceReference = null,
105    } = options
106    // this object gives querySelectorAll somewhere to stash context about a node
107    // while processing a query
108    this.queryContext = {}
109
110    // true if part of a global install
111    this.#global = global
112
113    this.#workspaces = null
114
115    this.errors = error ? [error] : []
116    this.isInStore = isInStore
117
118    // this will usually be null, except when modeling a
119    // package's dependencies in a virtual root.
120    this.sourceReference = sourceReference
121
122    const pkg = sourceReference ? sourceReference.package
123      : normalize(options.pkg || {})
124
125    this.name = name ||
126      nameFromFolder(path || pkg.name || realpath) ||
127      pkg.name ||
128      null
129
130    // should be equal if not a link
131    this.path = path ? resolve(path) : null
132
133    if (!this.name && (!this.path || this.path !== dirname(this.path))) {
134      throw new TypeError('could not detect node name from path or package')
135    }
136
137    this.realpath = !this.isLink ? this.path : resolve(realpath)
138
139    this.resolved = resolved || null
140    if (!this.resolved) {
141      // note: this *only* works for non-file: deps, so we avoid even
142      // trying here.
143      // file: deps are tracked in package.json will _resolved set to the
144      // full path to the tarball or link target.  However, if the package
145      // is checked into git or moved to another location, that's 100% not
146      // portable at all!  The _where and _location don't provide much help,
147      // since _location is just where the module ended up in the tree,
148      // and _where can be different than the actual root if it's a
149      // meta-dep deeper in the dependency graph.
150      //
151      // If we don't have the other oldest indicators of legacy npm, then it's
152      // probably what we're getting from pacote, which IS trustworthy.
153      //
154      // Otherwise, hopefully a shrinkwrap will help us out.
155      const resolved = consistentResolve(pkg._resolved)
156      if (resolved && !(/^file:/.test(resolved) && pkg._where)) {
157        this.resolved = resolved
158      }
159    }
160    this.integrity = integrity || pkg._integrity || null
161    this.hasShrinkwrap = hasShrinkwrap || pkg._hasShrinkwrap || false
162    this.installLinks = installLinks
163    this.legacyPeerDeps = legacyPeerDeps
164
165    this.children = new CaseInsensitiveMap()
166    this.fsChildren = new Set()
167    this.inventory = new Inventory()
168    this.tops = new Set()
169    this.linksIn = new Set(linksIn || [])
170
171    // these three are set by an Arborist taking a catalog
172    // after the tree is built.  We don't get this along the way,
173    // because they have a tendency to change as new children are
174    // added, especially when they're deduped.  Eg, a dev dep may be
175    // a 3-levels-deep dependency of a non-dev dep.  If we calc the
176    // flags along the way, then they'll tend to be invalid  by the
177    // time we need to look at them.
178    if (!dummy) {
179      this.dev = dev
180      this.optional = optional
181      this.devOptional = devOptional
182      this.peer = peer
183      this.extraneous = extraneous
184      this.dummy = false
185    } else {
186      // true if this is a placeholder for the purpose of serving as a
187      // fsParent to link targets that get their deps resolved outside
188      // the root tree folder.
189      this.dummy = true
190      this.dev = false
191      this.optional = false
192      this.devOptional = false
193      this.peer = false
194      this.extraneous = false
195    }
196
197    this.edgesIn = new Set()
198    this.edgesOut = new CaseInsensitiveMap()
199
200    // have to set the internal package ref before assigning the parent,
201    // because this.package is read when adding to inventory
202    this[_package] = pkg && typeof pkg === 'object' ? pkg : {}
203
204    if (overrides) {
205      this.overrides = overrides
206    } else if (loadOverrides) {
207      const overrides = this[_package].overrides || {}
208      if (Object.keys(overrides).length > 0) {
209        this.overrides = new OverrideSet({
210          overrides: this[_package].overrides,
211        })
212      }
213    }
214
215    // only relevant for the root and top nodes
216    this.meta = meta
217
218    // Note: this is _slightly_ less efficient for the initial tree
219    // building than it could be, but in exchange, it's a much simpler
220    // algorithm.
221    // If this node has a bunch of children, and those children satisfy
222    // its various deps, then we're going to _first_ create all the
223    // edges, and _then_ assign the children into place, re-resolving
224    // them all in _reloadNamedEdges.
225    // A more efficient, but more complicated, approach would be to
226    // flag this node as being a part of a tree build, so it could
227    // hold off on resolving its deps until its children are in place.
228
229    // call the parent setter
230    // Must be set prior to calling _loadDeps, because top-ness is relevant
231
232    // will also assign root if present on the parent
233    this[_parent] = null
234    this.parent = parent || null
235
236    this[_fsParent] = null
237    this.fsParent = fsParent || null
238
239    // see parent/root setters below.
240    // root is set to parent's root if we have a parent, otherwise if it's
241    // null, then it's set to the node itself.
242    if (!parent && !fsParent) {
243      this.root = root || null
244    }
245
246    // mostly a convenience for testing, but also a way to create
247    // trees in a more declarative way than setting parent on each
248    if (children) {
249      for (const c of children) {
250        new Node({ ...c, parent: this })
251      }
252    }
253    if (fsChildren) {
254      for (const c of fsChildren) {
255        new Node({ ...c, fsParent: this })
256      }
257    }
258
259    // now load all the dep edges
260    this[_loadDeps]()
261  }
262
263  get meta () {
264    return this.#meta
265  }
266
267  set meta (meta) {
268    this.#meta = meta
269    if (meta) {
270      meta.add(this)
271    }
272  }
273
274  get global () {
275    if (this.#root === this) {
276      return this.#global
277    }
278    return this.#root.global
279  }
280
281  // true for packages installed directly in the global node_modules folder
282  get globalTop () {
283    return this.global && this.parent && this.parent.isProjectRoot
284  }
285
286  get workspaces () {
287    return this.#workspaces
288  }
289
290  set workspaces (workspaces) {
291    // deletes edges if they already exists
292    if (this.#workspaces) {
293      for (const name of this.#workspaces.keys()) {
294        if (!workspaces.has(name)) {
295          this.edgesOut.get(name).detach()
296        }
297      }
298    }
299
300    this.#workspaces = workspaces
301    this.#loadWorkspaces()
302    this[_loadDeps]()
303  }
304
305  get binPaths () {
306    if (!this.parent) {
307      return []
308    }
309
310    return getBinPaths({
311      pkg: this[_package],
312      path: this.path,
313      global: this.global,
314      top: this.globalTop,
315    })
316  }
317
318  get hasInstallScript () {
319    const { hasInstallScript, scripts } = this.package
320    const { install, preinstall, postinstall } = scripts || {}
321    return !!(hasInstallScript || install || preinstall || postinstall)
322  }
323
324  get version () {
325    return this[_package].version || ''
326  }
327
328  get packageName () {
329    return this[_package].name || null
330  }
331
332  get pkgid () {
333    const { name = '', version = '' } = this.package
334    // root package will prefer package name over folder name,
335    // and never be called an alias.
336    const { isProjectRoot } = this
337    const myname = isProjectRoot ? name || this.name
338      : this.name
339    const alias = !isProjectRoot && name && myname !== name ? `npm:${name}@`
340      : ''
341    return `${myname}@${alias}${version}`
342  }
343
344  get overridden () {
345    return !!(this.overrides && this.overrides.value && this.overrides.name === this.name)
346  }
347
348  get package () {
349    return this[_package]
350  }
351
352  set package (pkg) {
353    // just detach them all.  we could make this _slightly_ more efficient
354    // by only detaching the ones that changed, but we'd still have to walk
355    // them all, and the comparison logic gets a bit tricky.  we generally
356    // only do this more than once at the root level, so the resolve() calls
357    // are only one level deep, and there's not much to be saved, anyway.
358    // simpler to just toss them all out.
359    for (const edge of this.edgesOut.values()) {
360      edge.detach()
361    }
362
363    this[_explanation] = null
364    /* istanbul ignore next - should be impossible */
365    if (!pkg || typeof pkg !== 'object') {
366      debug(() => {
367        throw new Error('setting Node.package to non-object')
368      })
369      pkg = {}
370    }
371    this[_package] = pkg
372    this.#loadWorkspaces()
373    this[_loadDeps]()
374    // do a hard reload, since the dependents may now be valid or invalid
375    // as a result of the package change.
376    this.edgesIn.forEach(edge => edge.reload(true))
377  }
378
379  // node.explain(nodes seen already, edge we're trying to satisfy
380  // if edge is not specified, it lists every edge into the node.
381  explain (edge = null, seen = []) {
382    if (this[_explanation]) {
383      return this[_explanation]
384    }
385
386    return this[_explanation] = this[_explain](edge, seen)
387  }
388
389  [_explain] (edge, seen) {
390    if (this.isProjectRoot && !this.sourceReference) {
391      return {
392        location: this.path,
393      }
394    }
395
396    const why = {
397      name: this.isProjectRoot || this.isTop ? this.packageName : this.name,
398      version: this.package.version,
399    }
400    if (this.errors.length || !this.packageName || !this.package.version) {
401      why.errors = this.errors.length ? this.errors : [
402        new Error('invalid package: lacks name and/or version'),
403      ]
404      why.package = this.package
405    }
406
407    if (this.root.sourceReference) {
408      const { name, version } = this.root.package
409      why.whileInstalling = {
410        name,
411        version,
412        path: this.root.sourceReference.path,
413      }
414    }
415
416    if (this.sourceReference) {
417      return this.sourceReference.explain(edge, seen)
418    }
419
420    if (seen.includes(this)) {
421      return why
422    }
423
424    why.location = this.location
425    why.isWorkspace = this.isWorkspace
426
427    // make a new list each time.  we can revisit, but not loop.
428    seen = seen.concat(this)
429
430    why.dependents = []
431    if (edge) {
432      why.dependents.push(edge.explain(seen))
433    } else {
434      // ignore invalid edges, since those aren't satisfied by this thing,
435      // and are not keeping it held in this spot anyway.
436      const edges = []
437      for (const edge of this.edgesIn) {
438        if (!edge.valid && !edge.from.isProjectRoot) {
439          continue
440        }
441
442        edges.push(edge)
443      }
444      for (const edge of edges) {
445        why.dependents.push(edge.explain(seen))
446      }
447    }
448
449    if (this.linksIn.size) {
450      why.linksIn = [...this.linksIn].map(link => link[_explain](edge, seen))
451    }
452
453    return why
454  }
455
456  isDescendantOf (node) {
457    for (let p = this; p; p = p.resolveParent) {
458      if (p === node) {
459        return true
460      }
461    }
462    return false
463  }
464
465  getBundler (path = []) {
466    // made a cycle, definitely not bundled!
467    if (path.includes(this)) {
468      return null
469    }
470
471    path.push(this)
472
473    const parent = this[_parent]
474    if (!parent) {
475      return null
476    }
477
478    const pBundler = parent.getBundler(path)
479    if (pBundler) {
480      return pBundler
481    }
482
483    const ppkg = parent.package
484    const bd = ppkg && ppkg.bundleDependencies
485    // explicit bundling
486    if (Array.isArray(bd) && bd.includes(this.name)) {
487      return parent
488    }
489
490    // deps that are deduped up to the bundling level are bundled.
491    // however, if they get their dep met further up than that,
492    // then they are not bundled.  Ie, installing a package with
493    // unmet bundled deps will not cause your deps to be bundled.
494    for (const edge of this.edgesIn) {
495      const eBundler = edge.from.getBundler(path)
496      if (!eBundler) {
497        continue
498      }
499
500      if (eBundler === parent) {
501        return eBundler
502      }
503    }
504
505    return null
506  }
507
508  get inBundle () {
509    return !!this.getBundler()
510  }
511
512  // when reifying, if a package is technically in a bundleDependencies list,
513  // but that list is the root project, we still have to install it.  This
514  // getter returns true if it's in a dependency's bundle list, not the root's.
515  get inDepBundle () {
516    const bundler = this.getBundler()
517    return !!bundler && bundler !== this.root
518  }
519
520  get isWorkspace () {
521    if (this.isProjectRoot) {
522      return false
523    }
524    const { root } = this
525    const { type, to } = root.edgesOut.get(this.packageName) || {}
526    return type === 'workspace' && to && (to.target === this || to === this)
527  }
528
529  get isRoot () {
530    return this === this.root
531  }
532
533  get isProjectRoot () {
534    // only treat as project root if it's the actual link that is the root,
535    // or the target of the root link, but NOT if it's another link to the
536    // same root that happens to be somewhere else.
537    return this === this.root || this === this.root.target
538  }
539
540  get isRegistryDependency () {
541    if (this.edgesIn.size === 0) {
542      return false
543    }
544    for (const edge of this.edgesIn) {
545      if (!npa(edge.spec).registry) {
546        return false
547      }
548    }
549    return true
550  }
551
552  * ancestry () {
553    for (let anc = this; anc; anc = anc.resolveParent) {
554      yield anc
555    }
556  }
557
558  set root (root) {
559    // setting to null means this is the new root
560    // should only ever be one step
561    while (root && root.root !== root) {
562      root = root.root
563    }
564
565    root = root || this
566
567    // delete from current root inventory
568    this[_delistFromMeta]()
569
570    // can't set the root (yet) if there's no way to determine location
571    // this allows us to do new Node({...}) and then set the root later.
572    // just make the assignment so we don't lose it, and move on.
573    if (!this.path || !root.realpath || !root.path) {
574      this.#root = root
575      return
576    }
577
578    // temporarily become a root node
579    this.#root = this
580
581    // break all linksIn, we're going to re-set them if needed later
582    for (const link of this.linksIn) {
583      link[_target] = null
584      this.linksIn.delete(link)
585    }
586
587    // temporarily break this link as well, we'll re-set if possible later
588    const { target } = this
589    if (this.isLink) {
590      if (target) {
591        target.linksIn.delete(this)
592        if (target.root === this) {
593          target[_delistFromMeta]()
594        }
595      }
596      this[_target] = null
597    }
598
599    // if this is part of a cascading root set, then don't do this bit
600    // but if the parent/fsParent is in a different set, we have to break
601    // that reference before proceeding
602    if (this.parent && this.parent.root !== root) {
603      this.parent.children.delete(this.name)
604      this[_parent] = null
605    }
606    if (this.fsParent && this.fsParent.root !== root) {
607      this.fsParent.fsChildren.delete(this)
608      this[_fsParent] = null
609    }
610
611    if (root === this) {
612      this[_refreshLocation]()
613    } else {
614      // setting to some different node.
615      const loc = relpath(root.realpath, this.path)
616      const current = root.inventory.get(loc)
617
618      // clobber whatever is there now
619      if (current) {
620        current.root = null
621      }
622
623      this.#root = root
624      // set this.location and add to inventory
625      this[_refreshLocation]()
626
627      // try to find our parent/fsParent in the new root inventory
628      for (const p of walkUp(dirname(this.path))) {
629        if (p === this.path) {
630          continue
631        }
632        const ploc = relpath(root.realpath, p)
633        const parent = root.inventory.get(ploc)
634        if (parent) {
635          /* istanbul ignore next - impossible */
636          if (parent.isLink) {
637            debug(() => {
638              throw Object.assign(new Error('assigning parentage to link'), {
639                path: this.path,
640                parent: parent.path,
641                parentReal: parent.realpath,
642              })
643            })
644            continue
645          }
646          const childLoc = `${ploc}${ploc ? '/' : ''}node_modules/${this.name}`
647          const isParent = this.location === childLoc
648          if (isParent) {
649            const oldChild = parent.children.get(this.name)
650            if (oldChild && oldChild !== this) {
651              oldChild.root = null
652            }
653            if (this.parent) {
654              this.parent.children.delete(this.name)
655              this.parent[_reloadNamedEdges](this.name)
656            }
657            parent.children.set(this.name, this)
658            this[_parent] = parent
659            // don't do it for links, because they don't have a target yet
660            // we'll hit them up a bit later on.
661            if (!this.isLink) {
662              parent[_reloadNamedEdges](this.name)
663            }
664          } else {
665            /* istanbul ignore if - should be impossible, since we break
666             * all fsParent/child relationships when moving? */
667            if (this.fsParent) {
668              this.fsParent.fsChildren.delete(this)
669            }
670            parent.fsChildren.add(this)
671            this[_fsParent] = parent
672          }
673          break
674        }
675      }
676
677      // if it doesn't have a parent, it's a top node
678      if (!this.parent) {
679        root.tops.add(this)
680      } else {
681        root.tops.delete(this)
682      }
683
684      // assign parentage for any nodes that need to have this as a parent
685      // this can happen when we have a node at nm/a/nm/b added *before*
686      // the node at nm/a, which might have the root node as a fsParent.
687      // we can't rely on the public setter here, because it calls into
688      // this function to set up these references!
689      // check dirname so that /foo isn't treated as the fsparent of /foo-bar
690      const nmloc = `${this.location}${this.location ? '/' : ''}node_modules/`
691      // only walk top nodes, since anything else already has a parent.
692      for (const child of root.tops) {
693        const isChild = child.location === nmloc + child.name
694        const isFsChild =
695          dirname(child.path).startsWith(this.path) &&
696          child !== this &&
697          !child.parent &&
698          (
699            !child.fsParent ||
700            child.fsParent === this ||
701            dirname(this.path).startsWith(child.fsParent.path)
702          )
703
704        if (!isChild && !isFsChild) {
705          continue
706        }
707
708        // set up the internal parentage links
709        if (this.isLink) {
710          child.root = null
711        } else {
712          // can't possibly have a parent, because it's in tops
713          if (child.fsParent) {
714            child.fsParent.fsChildren.delete(child)
715          }
716          child[_fsParent] = null
717          if (isChild) {
718            this.children.set(child.name, child)
719            child[_parent] = this
720            root.tops.delete(child)
721          } else {
722            this.fsChildren.add(child)
723            child[_fsParent] = this
724          }
725        }
726      }
727
728      // look for any nodes with the same realpath.  either they're links
729      // to that realpath, or a thing at that realpath if we're adding a link
730      // (if we're adding a regular node, we already deleted the old one)
731      for (const node of root.inventory.query('realpath', this.realpath)) {
732        if (node === this) {
733          continue
734        }
735
736        /* istanbul ignore next - should be impossible */
737        debug(() => {
738          if (node.root !== root) {
739            throw new Error('inventory contains node from other root')
740          }
741        })
742
743        if (this.isLink) {
744          const target = node.target
745          this[_target] = target
746          this[_package] = target.package
747          target.linksIn.add(this)
748          // reload edges here, because now we have a target
749          if (this.parent) {
750            this.parent[_reloadNamedEdges](this.name)
751          }
752          break
753        } else {
754          /* istanbul ignore else - should be impossible */
755          if (node.isLink) {
756            node[_target] = this
757            node[_package] = this.package
758            this.linksIn.add(node)
759            if (node.parent) {
760              node.parent[_reloadNamedEdges](node.name)
761            }
762          } else {
763            debug(() => {
764              throw Object.assign(new Error('duplicate node in root setter'), {
765                path: this.path,
766                realpath: this.realpath,
767                root: root.realpath,
768              })
769            })
770          }
771        }
772      }
773    }
774
775    // reload all edgesIn where the root doesn't match, so we don't have
776    // cross-tree dependency graphs
777    for (const edge of this.edgesIn) {
778      if (edge.from.root !== root) {
779        edge.reload()
780      }
781    }
782    // reload all edgesOut where root doens't match, or is missing, since
783    // it might not be missing in the new tree
784    for (const edge of this.edgesOut.values()) {
785      if (!edge.to || edge.to.root !== root) {
786        edge.reload()
787      }
788    }
789
790    // now make sure our family comes along for the ride!
791    const family = new Set([
792      ...this.fsChildren,
793      ...this.children.values(),
794      ...this.inventory.values(),
795    ].filter(n => n !== this))
796
797    for (const child of family) {
798      if (child.root !== root) {
799        child[_delistFromMeta]()
800        child[_parent] = null
801        this.children.delete(child.name)
802        child[_fsParent] = null
803        this.fsChildren.delete(child)
804        for (const l of child.linksIn) {
805          l[_target] = null
806          child.linksIn.delete(l)
807        }
808      }
809    }
810    for (const child of family) {
811      if (child.root !== root) {
812        child.root = root
813      }
814    }
815
816    // if we had a target, and didn't find one in the new root, then bring
817    // it over as well, but only if we're setting the link into a new root,
818    // as we don't want to lose the target any time we remove a link.
819    if (this.isLink && target && !this.target && root !== this) {
820      target.root = root
821    }
822
823    if (!this.overrides && this.parent && this.parent.overrides) {
824      this.overrides = this.parent.overrides.getNodeRule(this)
825    }
826    // tree should always be valid upon root setter completion.
827    treeCheck(this)
828    if (this !== root) {
829      treeCheck(root)
830    }
831  }
832
833  get root () {
834    return this.#root || this
835  }
836
837  #loadWorkspaces () {
838    if (!this.#workspaces) {
839      return
840    }
841
842    for (const [name, path] of this.#workspaces.entries()) {
843      new Edge({ from: this, name, spec: `file:${path.replace(/#/g, '%23')}`, type: 'workspace' })
844    }
845  }
846
847  [_loadDeps] () {
848    // Caveat!  Order is relevant!
849    // Packages in optionalDependencies are optional.
850    // Packages in both deps and devDeps are required.
851    // Note the subtle breaking change from v6: it is no longer possible
852    // to have a different spec for a devDep than production dep.
853
854    // Linked targets that are disconnected from the tree are tops,
855    // but don't have a 'path' field, only a 'realpath', because we
856    // don't know their canonical location. We don't need their devDeps.
857    const pd = this.package.peerDependencies
858    const ad = this.package.acceptDependencies || {}
859    if (pd && typeof pd === 'object' && !this.legacyPeerDeps) {
860      const pm = this.package.peerDependenciesMeta || {}
861      const peerDependencies = {}
862      const peerOptional = {}
863      for (const [name, dep] of Object.entries(pd)) {
864        if (pm[name]?.optional) {
865          peerOptional[name] = dep
866        } else {
867          peerDependencies[name] = dep
868        }
869      }
870      this.#loadDepType(peerDependencies, 'peer', ad)
871      this.#loadDepType(peerOptional, 'peerOptional', ad)
872    }
873
874    this.#loadDepType(this.package.dependencies, 'prod', ad)
875    this.#loadDepType(this.package.optionalDependencies, 'optional', ad)
876
877    const { globalTop, isTop, path, sourceReference } = this
878    const {
879      globalTop: srcGlobalTop,
880      isTop: srcTop,
881      path: srcPath,
882    } = sourceReference || {}
883    const thisDev = isTop && !globalTop && path
884    const srcDev = !sourceReference || srcTop && !srcGlobalTop && srcPath
885    if (thisDev && srcDev) {
886      this.#loadDepType(this.package.devDependencies, 'dev', ad)
887    }
888  }
889
890  #loadDepType (deps, type, ad) {
891    // Because of the order in which _loadDeps runs, we always want to
892    // prioritize a new edge over an existing one
893    for (const [name, spec] of Object.entries(deps || {})) {
894      const current = this.edgesOut.get(name)
895      if (!current || current.type !== 'workspace') {
896        new Edge({ from: this, name, spec, accept: ad[name], type })
897      }
898    }
899  }
900
901  get fsParent () {
902    // in debug setter prevents fsParent from being this
903    return this[_fsParent]
904  }
905
906  set fsParent (fsParent) {
907    if (!fsParent) {
908      if (this[_fsParent]) {
909        this.root = null
910      }
911      return
912    }
913
914    debug(() => {
915      if (fsParent === this) {
916        throw new Error('setting node to its own fsParent')
917      }
918
919      if (fsParent.realpath === this.realpath) {
920        throw new Error('setting fsParent to same path')
921      }
922
923      // the initial set MUST be an actual walk-up from the realpath
924      // subsequent sets will re-root on the new fsParent's path.
925      if (!this[_fsParent] && this.realpath.indexOf(fsParent.realpath) !== 0) {
926        throw Object.assign(new Error('setting fsParent improperly'), {
927          path: this.path,
928          realpath: this.realpath,
929          fsParent: {
930            path: fsParent.path,
931            realpath: fsParent.realpath,
932          },
933        })
934      }
935    })
936
937    if (fsParent.isLink) {
938      fsParent = fsParent.target
939    }
940
941    // setting a thing to its own fsParent is not normal, but no-op for safety
942    if (this === fsParent || fsParent.realpath === this.realpath) {
943      return
944    }
945
946    // nothing to do
947    if (this[_fsParent] === fsParent) {
948      return
949    }
950
951    const oldFsParent = this[_fsParent]
952    const newPath = !oldFsParent ? this.path
953      : resolve(fsParent.path, relative(oldFsParent.path, this.path))
954    const nmPath = resolve(fsParent.path, 'node_modules', this.name)
955
956    // this is actually the parent, set that instead
957    if (newPath === nmPath) {
958      this.parent = fsParent
959      return
960    }
961
962    const pathChange = newPath !== this.path
963
964    // remove from old parent/fsParent
965    const oldParent = this.parent
966    const oldName = this.name
967    if (this.parent) {
968      this.parent.children.delete(this.name)
969      this[_parent] = null
970    }
971    if (this.fsParent) {
972      this.fsParent.fsChildren.delete(this)
973      this[_fsParent] = null
974    }
975
976    // update this.path/realpath for this and all children/fsChildren
977    if (pathChange) {
978      this[_changePath](newPath)
979    }
980
981    if (oldParent) {
982      oldParent[_reloadNamedEdges](oldName)
983    }
984
985    // clobbers anything at that path, resets all appropriate references
986    this.root = fsParent.root
987  }
988
989  // is it safe to replace one node with another?  check the edges to
990  // make sure no one will get upset.  Note that the node might end up
991  // having its own unmet dependencies, if the new node has new deps.
992  // Note that there are cases where Arborist will opt to insert a node
993  // into the tree even though this function returns false!  This is
994  // necessary when a root dependency is added or updated, or when a
995  // root dependency brings peer deps along with it.  In that case, we
996  // will go ahead and create the invalid state, and then try to resolve
997  // it with more tree construction, because it's a user request.
998  canReplaceWith (node, ignorePeers) {
999    if (node.name !== this.name) {
1000      return false
1001    }
1002
1003    if (node.packageName !== this.packageName) {
1004      return false
1005    }
1006
1007    // XXX need to check for two root nodes?
1008    if (node.overrides !== this.overrides) {
1009      return false
1010    }
1011    ignorePeers = new Set(ignorePeers)
1012
1013    // gather up all the deps of this node and that are only depended
1014    // upon by deps of this node.  those ones don't count, since
1015    // they'll be replaced if this node is replaced anyway.
1016    const depSet = gatherDepSet([this], e => e.to !== this && e.valid)
1017
1018    for (const edge of this.edgesIn) {
1019      // when replacing peer sets, we need to be able to replace the entire
1020      // peer group, which means we ignore incoming edges from other peers
1021      // within the replacement set.
1022      if (!this.isTop &&
1023        edge.from.parent === this.parent &&
1024        edge.peer &&
1025        ignorePeers.has(edge.from.name)) {
1026        continue
1027      }
1028
1029      // only care about edges that don't originate from this node
1030      if (!depSet.has(edge.from) && !edge.satisfiedBy(node)) {
1031        return false
1032      }
1033    }
1034
1035    return true
1036  }
1037
1038  canReplace (node, ignorePeers) {
1039    return node.canReplaceWith(this, ignorePeers)
1040  }
1041
1042  // return true if it's safe to remove this node, because anything that
1043  // is depending on it would be fine with the thing that they would resolve
1044  // to if it was removed, or nothing is depending on it in the first place.
1045  canDedupe (preferDedupe = false) {
1046    // not allowed to mess with shrinkwraps or bundles
1047    if (this.inDepBundle || this.inShrinkwrap) {
1048      return false
1049    }
1050
1051    // it's a top level pkg, or a dep of one
1052    if (!this.resolveParent || !this.resolveParent.resolveParent) {
1053      return false
1054    }
1055
1056    // no one wants it, remove it
1057    if (this.edgesIn.size === 0) {
1058      return true
1059    }
1060
1061    const other = this.resolveParent.resolveParent.resolve(this.name)
1062
1063    // nothing else, need this one
1064    if (!other) {
1065      return false
1066    }
1067
1068    // if it's the same thing, then always fine to remove
1069    if (other.matches(this)) {
1070      return true
1071    }
1072
1073    // if the other thing can't replace this, then skip it
1074    if (!other.canReplace(this)) {
1075      return false
1076    }
1077
1078    // if we prefer dedupe, or if the version is greater/equal, take the other
1079    if (preferDedupe || semver.gte(other.version, this.version)) {
1080      return true
1081    }
1082
1083    return false
1084  }
1085
1086  satisfies (requested) {
1087    if (requested instanceof Edge) {
1088      return this.name === requested.name && requested.satisfiedBy(this)
1089    }
1090
1091    const parsed = npa(requested)
1092    const { name = this.name, rawSpec: spec } = parsed
1093    return this.name === name && this.satisfies(new Edge({
1094      from: new Node({ path: this.root.realpath }),
1095      type: 'prod',
1096      name,
1097      spec,
1098    }))
1099  }
1100
1101  matches (node) {
1102    // if the nodes are literally the same object, obviously a match.
1103    if (node === this) {
1104      return true
1105    }
1106
1107    // if the names don't match, they're different things, even if
1108    // the package contents are identical.
1109    if (node.name !== this.name) {
1110      return false
1111    }
1112
1113    // if they're links, they match if the targets match
1114    if (this.isLink) {
1115      return node.isLink && this.target.matches(node.target)
1116    }
1117
1118    // if they're two project root nodes, they're different if the paths differ
1119    if (this.isProjectRoot && node.isProjectRoot) {
1120      return this.path === node.path
1121    }
1122
1123    // if the integrity matches, then they're the same.
1124    if (this.integrity && node.integrity) {
1125      return this.integrity === node.integrity
1126    }
1127
1128    // if no integrity, check resolved
1129    if (this.resolved && node.resolved) {
1130      return this.resolved === node.resolved
1131    }
1132
1133    // if no resolved, check both package name and version
1134    // otherwise, conclude that they are different things
1135    return this.packageName && node.packageName &&
1136      this.packageName === node.packageName &&
1137      this.version && node.version &&
1138      this.version === node.version
1139  }
1140
1141  // replace this node with the supplied argument
1142  // Useful when mutating an ideal tree, so we can avoid having to call
1143  // the parent/root setters more than necessary.
1144  replaceWith (node) {
1145    node.replace(this)
1146  }
1147
1148  replace (node) {
1149    this[_delistFromMeta]()
1150
1151    // if the name matches, but is not identical, we are intending to clobber
1152    // something case-insensitively, so merely setting name and path won't
1153    // have the desired effect.  just set the path so it'll collide in the
1154    // parent's children map, and leave it at that.
1155    if (node.parent?.children.get(this.name) === node) {
1156      this.path = resolve(node.parent.path, 'node_modules', this.name)
1157    } else {
1158      this.path = node.path
1159      this.name = node.name
1160    }
1161
1162    if (!this.isLink) {
1163      this.realpath = this.path
1164    }
1165    this[_refreshLocation]()
1166
1167    // keep children when a node replaces another
1168    if (!this.isLink) {
1169      for (const kid of node.children.values()) {
1170        kid.parent = this
1171      }
1172      if (node.isLink && node.target) {
1173        node.target.root = null
1174      }
1175    }
1176
1177    if (!node.isRoot) {
1178      this.root = node.root
1179    }
1180
1181    treeCheck(this)
1182  }
1183
1184  get inShrinkwrap () {
1185    return this.parent &&
1186      (this.parent.hasShrinkwrap || this.parent.inShrinkwrap)
1187  }
1188
1189  get parent () {
1190    // setter prevents _parent from being this
1191    return this[_parent]
1192  }
1193
1194  // This setter keeps everything in order when we move a node from
1195  // one point in a logical tree to another.  Edges get reloaded,
1196  // metadata updated, etc.  It's also called when we *replace* a node
1197  // with another by the same name (eg, to update or dedupe).
1198  // This does a couple of walks out on the node_modules tree, recursing
1199  // into child nodes.  However, as setting the parent is typically done
1200  // with nodes that don't have have many children, and (deduped) package
1201  // trees tend to be broad rather than deep, it's not that bad.
1202  // The only walk that starts from the parent rather than this node is
1203  // limited by edge name.
1204  set parent (parent) {
1205    // when setting to null, just remove it from the tree entirely
1206    if (!parent) {
1207      // but only delete it if we actually had a parent in the first place
1208      // otherwise it's just setting to null when it's already null
1209      if (this[_parent]) {
1210        this.root = null
1211      }
1212      return
1213    }
1214
1215    if (parent.isLink) {
1216      parent = parent.target
1217    }
1218
1219    // setting a thing to its own parent is not normal, but no-op for safety
1220    if (this === parent) {
1221      return
1222    }
1223
1224    const oldParent = this[_parent]
1225
1226    // nothing to do
1227    if (oldParent === parent) {
1228      return
1229    }
1230
1231    // ok now we know something is actually changing, and parent is not a link
1232    const newPath = resolve(parent.path, 'node_modules', this.name)
1233    const pathChange = newPath !== this.path
1234
1235    // remove from old parent/fsParent
1236    if (oldParent) {
1237      oldParent.children.delete(this.name)
1238      this[_parent] = null
1239    }
1240    if (this.fsParent) {
1241      this.fsParent.fsChildren.delete(this)
1242      this[_fsParent] = null
1243    }
1244
1245    // update this.path/realpath for this and all children/fsChildren
1246    if (pathChange) {
1247      this[_changePath](newPath)
1248    }
1249
1250    if (parent.overrides) {
1251      this.overrides = parent.overrides.getNodeRule(this)
1252    }
1253
1254    // clobbers anything at that path, resets all appropriate references
1255    this.root = parent.root
1256  }
1257
1258  // Call this before changing path or updating the _root reference.
1259  // Removes the node from its root the metadata and inventory.
1260  [_delistFromMeta] () {
1261    const root = this.root
1262    if (!root.realpath || !this.path) {
1263      return
1264    }
1265    root.inventory.delete(this)
1266    root.tops.delete(this)
1267    if (root.meta) {
1268      root.meta.delete(this.path)
1269    }
1270    /* istanbul ignore next - should be impossible */
1271    debug(() => {
1272      if ([...root.inventory.values()].includes(this)) {
1273        throw new Error('failed to delist')
1274      }
1275    })
1276  }
1277
1278  // update this.path/realpath and the paths of all children/fsChildren
1279  [_changePath] (newPath) {
1280    // have to de-list before changing paths
1281    this[_delistFromMeta]()
1282    const oldPath = this.path
1283    this.path = newPath
1284    const namePattern = /(?:^|\/|\\)node_modules[\\/](@[^/\\]+[\\/][^\\/]+|[^\\/]+)$/
1285    const nameChange = newPath.match(namePattern)
1286    if (nameChange && this.name !== nameChange[1]) {
1287      this.name = nameChange[1].replace(/\\/g, '/')
1288    }
1289
1290    // if we move a link target, update link realpaths
1291    if (!this.isLink) {
1292      this.realpath = newPath
1293      for (const link of this.linksIn) {
1294        link[_delistFromMeta]()
1295        link.realpath = newPath
1296        link[_refreshLocation]()
1297      }
1298    }
1299    // if we move /x to /y, then a module at /x/a/b becomes /y/a/b
1300    for (const child of this.fsChildren) {
1301      child[_changePath](resolve(newPath, relative(oldPath, child.path)))
1302    }
1303    for (const [name, child] of this.children.entries()) {
1304      child[_changePath](resolve(newPath, 'node_modules', name))
1305    }
1306
1307    this[_refreshLocation]()
1308  }
1309
1310  // Called whenever the root/parent is changed.
1311  // NB: need to remove from former root's meta/inventory and then update
1312  // this.path BEFORE calling this method!
1313  [_refreshLocation] () {
1314    const root = this.root
1315    const loc = relpath(root.realpath, this.path)
1316
1317    this.location = loc
1318
1319    root.inventory.add(this)
1320    if (root.meta) {
1321      root.meta.add(this)
1322    }
1323  }
1324
1325  assertRootOverrides () {
1326    if (!this.isProjectRoot || !this.overrides) {
1327      return
1328    }
1329
1330    for (const edge of this.edgesOut.values()) {
1331      // if these differ an override has been applied, those are not allowed
1332      // for top level dependencies so throw an error
1333      if (edge.spec !== edge.rawSpec && !edge.spec.startsWith('$')) {
1334        throw Object.assign(new Error(`Override for ${edge.name}@${edge.rawSpec} conflicts with direct dependency`), { code: 'EOVERRIDE' })
1335      }
1336    }
1337  }
1338
1339  addEdgeOut (edge) {
1340    if (this.overrides) {
1341      edge.overrides = this.overrides.getEdgeRule(edge)
1342    }
1343
1344    this.edgesOut.set(edge.name, edge)
1345  }
1346
1347  addEdgeIn (edge) {
1348    if (edge.overrides) {
1349      this.overrides = edge.overrides
1350    }
1351
1352    this.edgesIn.add(edge)
1353
1354    // try to get metadata from the yarn.lock file
1355    if (this.root.meta) {
1356      this.root.meta.addEdge(edge)
1357    }
1358  }
1359
1360  [_reloadNamedEdges] (name, rootLoc = this.location) {
1361    const edge = this.edgesOut.get(name)
1362    // if we don't have an edge, do nothing, but keep descending
1363    const rootLocResolved = edge && edge.to &&
1364      edge.to.location === `${rootLoc}/node_modules/${edge.name}`
1365    const sameResolved = edge && this.resolve(name) === edge.to
1366    const recheck = rootLocResolved || !sameResolved
1367    if (edge && recheck) {
1368      edge.reload(true)
1369    }
1370    for (const c of this.children.values()) {
1371      c[_reloadNamedEdges](name, rootLoc)
1372    }
1373
1374    for (const c of this.fsChildren) {
1375      c[_reloadNamedEdges](name, rootLoc)
1376    }
1377  }
1378
1379  get isLink () {
1380    return false
1381  }
1382
1383  get target () {
1384    return this
1385  }
1386
1387  set target (n) {
1388    debug(() => {
1389      throw Object.assign(new Error('cannot set target on non-Link Nodes'), {
1390        path: this.path,
1391      })
1392    })
1393  }
1394
1395  get depth () {
1396    if (this.isTop) {
1397      return 0
1398    }
1399    return this.parent.depth + 1
1400  }
1401
1402  get isTop () {
1403    return !this.parent || this.globalTop
1404  }
1405
1406  get top () {
1407    if (this.isTop) {
1408      return this
1409    }
1410    return this.parent.top
1411  }
1412
1413  get isFsTop () {
1414    return !this.fsParent
1415  }
1416
1417  get fsTop () {
1418    if (this.isFsTop) {
1419      return this
1420    }
1421    return this.fsParent.fsTop
1422  }
1423
1424  get resolveParent () {
1425    return this.parent || this.fsParent
1426  }
1427
1428  resolve (name) {
1429    /* istanbul ignore next - should be impossible,
1430     * but I keep doing this mistake in tests */
1431    debug(() => {
1432      if (typeof name !== 'string' || !name) {
1433        throw new Error('non-string passed to Node.resolve')
1434      }
1435    })
1436    const mine = this.children.get(name)
1437    if (mine) {
1438      return mine
1439    }
1440    const resolveParent = this.resolveParent
1441    if (resolveParent) {
1442      return resolveParent.resolve(name)
1443    }
1444    return null
1445  }
1446
1447  inNodeModules () {
1448    const rp = this.realpath
1449    const name = this.name
1450    const scoped = name.charAt(0) === '@'
1451    const d = dirname(rp)
1452    const nm = scoped ? dirname(d) : d
1453    const dir = dirname(nm)
1454    const base = scoped ? `${basename(d)}/${basename(rp)}` : basename(rp)
1455    return base === name && basename(nm) === 'node_modules' ? dir : false
1456  }
1457
1458  // maybe accept both string value or array of strings
1459  // seems to be what dom API does
1460  querySelectorAll (query, opts) {
1461    return querySelectorAll(this, query, opts)
1462  }
1463
1464  toJSON () {
1465    return printableTree(this)
1466  }
1467
1468  [util.inspect.custom] () {
1469    return this.toJSON()
1470  }
1471}
1472
1473module.exports = Node
1474