codekingpro/portable-devtools
114k
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 PackageJson = require('@npmcli/package-json')
32const nameFromFolder = require('@npmcli/name-from-folder')
33const npa = require('npm-package-arg')
34const semver = require('semver')
35const util = require('node:util')
36const { getPaths: getBinPaths } = require('bin-links')
37const { log } = require('proc-log')
38const { resolve, relative, dirname, basename } = require('node:path')
39const { walkUp } = require('walk-up-path')
40
41const CaseInsensitiveMap = require('./case-insensitive-map.js')
42const Edge = require('./edge.js')
43const Inventory = require('./inventory.js')
44const OverrideSet = require('./override-set.js')
45const consistentResolve = require('./consistent-resolve.js')
46const debug = require('./debug.js')
47const gatherDepSet = require('./gather-dep-set.js')
48const printableTree = require('./printable.js')
49const querySelectorAll = require('./query-selector-all.js')
50const relpath = require('./relpath.js')
51const treeCheck = require('./tree-check.js')
52
53const _package = Symbol('_package')
54const _parent = Symbol('_parent')
55const _target = Symbol.for('_target')
56const _fsParent = Symbol('_fsParent')
57const _reloadNamedEdges = Symbol('_reloadNamedEdges')
58// overridden by Link class
59const _loadDeps = Symbol.for('Arborist.Node._loadDeps')
60const _refreshLocation = Symbol.for('_refreshLocation')
61const _changePath = Symbol.for('_changePath')
62// used by Link class as well
63const _delistFromMeta = Symbol.for('_delistFromMeta')
64const _explain = Symbol('_explain')
65const _explanation = Symbol('_explanation')
66
67class Node {
68 #global
69 #meta
70 #root
71 #workspaces
72
73 constructor (options) {
74 // NB: path can be null if it's a link target
75 const {
76 children,
77 dev = true,
78 devOptional = true,
79 dummy = false,
80 error,
81 extraneous = true,
82 fsChildren,
83 fsParent,
84 global = false,
85 hasShrinkwrap,
86 inert = false,
87 installLinks = false,
88 integrity,
89 isInStore = false,
90 legacyPeerDeps = false,
91 linksIn,
92 loadOverrides = false,
93 meta,
94 name, // allow setting name explicitly when we haven't set a path yet
95 optional = true,
96 overrides,
97 parent,
98 path,
99 peer = true,
100 realpath,
101 resolved,
102 root,
103 sourceReference = null,
104 } = options
105 // this object gives querySelectorAll somewhere to stash context about a node
106 // while processing a query
107 this.queryContext = {}
108
109 // true if part of a global install
110 this.#global = global
111
112 this.#workspaces = null
113
114 this.errors = error ? [error] : []
115 this.isInStore = isInStore
116
117 // this will usually be null, except when modeling a
118 // package's dependencies in a virtual root.
119 this.sourceReference = sourceReference
120
121 // have to set the internal package ref before assigning the parent, because this.package is read when adding to inventory
122 if (sourceReference) {
123 this[_package] = sourceReference.package
124 } else {
125 // TODO if this came from pacote.manifest we don't have to do this, we can be told to skip this step
126 const pkg = new PackageJson()
127 let content = {}
128 // TODO this is overly guarded. If pkg is not an object we should not allow it at all.
129 if (options.pkg && typeof options.pkg === 'object') {
130 content = options.pkg
131 }
132 pkg.fromContent(content)
133 pkg.syncNormalize()
134 this[_package] = pkg.content
135 }
136
137 this.name = name ||
138 nameFromFolder(path || this.package.name || realpath) ||
139 this.package.name ||
140 null
141
142 // should be equal if not a link
143 this.path = path ? resolve(path) : null
144
145 if (!this.name && (!this.path || this.path !== dirname(this.path))) {
146 throw new TypeError('could not detect node name from path or package')
147 }
148
149 this.realpath = !this.isLink ? this.path : resolve(realpath)
150
151 this.resolved = resolved || null
152 if (!this.resolved) {
153 // note: this *only* works for non-file: deps, so we avoid even
154 // trying here.
155 // file: deps are tracked in package.json will _resolved set to the
156 // full path to the tarball or link target. However, if the package
157 // is checked into git or moved to another location, that's 100% not
158 // portable at all! The _where and _location don't provide much help,
159 // since _location is just where the module ended up in the tree,
160 // and _where can be different than the actual root if it's a
161 // meta-dep deeper in the dependency graph.
162 //
163 // If we don't have the other oldest indicators of legacy npm, then it's
164 // probably what we're getting from pacote, which IS trustworthy.
165 //
166 // Otherwise, hopefully a shrinkwrap will help us out.
167 const resolved = consistentResolve(this.package._resolved)
168 if (resolved && !(/^file:/.test(resolved) && this.package._where)) {
169 this.resolved = resolved
170 }
171 }
172 this.integrity = integrity || this.package._integrity || null
173 this.hasShrinkwrap = hasShrinkwrap || this.package._hasShrinkwrap || false
174 this.installLinks = installLinks
175 this.legacyPeerDeps = legacyPeerDeps
176
177 this.children = new CaseInsensitiveMap()
178 this.fsChildren = new Set()
179 this.inventory = new Inventory()
180 this.tops = new Set()
181 this.linksIn = new Set(linksIn || [])
182
183 // these three are set by an Arborist taking a catalog
184 // after the tree is built. We don't get this along the way,
185 // because they have a tendency to change as new children are
186 // added, especially when they're deduped. Eg, a dev dep may be
187 // a 3-levels-deep dependency of a non-dev dep. If we calc the
188 // flags along the way, then they'll tend to be invalid by the
189 // time we need to look at them.
190 if (!dummy) {
191 this.dev = dev
192 this.optional = optional
193 this.devOptional = devOptional
194 this.peer = peer
195 this.extraneous = extraneous
196 this.dummy = false
197 } else {
198 // true if this is a placeholder for the purpose of serving as a
199 // fsParent to link targets that get their deps resolved outside
200 // the root tree folder.
201 this.dummy = true
202 this.dev = false
203 this.optional = false
204 this.devOptional = false
205 this.peer = false
206 this.extraneous = false
207 }
208
209 this.inert = inert
210
211 this.edgesIn = new Set()
212 this.edgesOut = new CaseInsensitiveMap()
213
214 if (overrides) {
215 this.overrides = overrides
216 } else if (loadOverrides) {
217 const overrides = this.package.overrides || {}
218 if (Object.keys(overrides).length > 0) {
219 this.overrides = new OverrideSet({
220 overrides: this.package.overrides,
221 })
222 }
223 }
224
225 // only relevant for the root and top nodes
226 this.meta = meta
227
228 // Note: this is _slightly_ less efficient for the initial tree
229 // building than it could be, but in exchange, it's a much simpler
230 // algorithm.
231 // If this node has a bunch of children, and those children satisfy
232 // its various deps, then we're going to _first_ create all the
233 // edges, and _then_ assign the children into place, re-resolving
234 // them all in _reloadNamedEdges.
235 // A more efficient, but more complicated, approach would be to
236 // flag this node as being a part of a tree build, so it could
237 // hold off on resolving its deps until its children are in place.
238
239 // call the parent setter
240 // Must be set prior to calling _loadDeps, because top-ness is relevant
241
242 // will also assign root if present on the parent
243 this[_parent] = null
244 this.parent = parent || null
245
246 this[_fsParent] = null
247 this.fsParent = fsParent || null
248
249 // see parent/root setters below.
250 // root is set to parent's root if we have a parent; otherwise, if it's
251 // null, then it's set to the node itself.
252 if (!parent && !fsParent) {
253 this.root = root || null
254 }
255
256 // mostly a convenience for testing, but also a way to create
257 // trees in a more declarative way than setting parent on each
258 if (children) {
259 for (const c of children) {
260 new Node({ ...c, parent: this })
261 }
262 }
263 if (fsChildren) {
264 for (const c of fsChildren) {
265 new Node({ ...c, fsParent: this })
266 }
267 }
268
269 // now load all the dep edges
270 this[_loadDeps]()
271 }
272
273 get meta () {
274 return this.#meta
275 }
276
277 set meta (meta) {
278 this.#meta = meta
279 if (meta) {
280 meta.add(this)
281 }
282 }
283
284 get global () {
285 if (this.#root === this) {
286 return this.#global
287 }
288 return this.#root.global
289 }
290
291 // true for packages installed directly in the global node_modules folder
292 get globalTop () {
293 return this.global && this.parent && this.parent.isProjectRoot
294 }
295
296 get workspaces () {
297 return this.#workspaces
298 }
299
300 set workspaces (workspaces) {
301 // deletes edges if they already exists
302 if (this.#workspaces) {
303 for (const name of this.#workspaces.keys()) {
304 if (!workspaces.has(name)) {
305 this.edgesOut.get(name).detach()
306 }
307 }
308 }
309
310 this.#workspaces = workspaces
311 this.#loadWorkspaces()
312 this[_loadDeps]()
313 }
314
315 get binPaths () {
316 if (!this.parent) {
317 return []
318 }
319
320 return getBinPaths({
321 pkg: this.package,
322 path: this.path,
323 global: this.global,
324 top: this.globalTop,
325 })
326 }
327
328 get hasInstallScript () {
329 const { hasInstallScript, scripts } = this.package
330 const { install, preinstall, postinstall } = scripts || {}
331 return !!(hasInstallScript || install || preinstall || postinstall)
332 }
333
334 get version () {
335 return this.package.version || ''
336 }
337
338 get packageName () {
339 return this.package.name || null
340 }
341
342 get pkgid () {
343 const { name = '', version = '' } = this.package
344 // root package will prefer package name over folder name,
345 // and never be called an alias.
346 const { isProjectRoot } = this
347 const myname = isProjectRoot ? name || this.name
348 : this.name
349 const alias = !isProjectRoot && name && myname !== name ? `npm:${name}@`
350 : ''
351 return `${myname}@${alias}${version}`
352 }
353
354 get overridden () {
355 if (!this.overrides) {
356 return false
357 }
358 if (!this.overrides.value) {
359 return false
360 }
361 if (this.overrides.name !== this.name) {
362 return false
363 }
364
365 // The overrides rule is for a package with this name, but some override rules only apply to specific
366 // versions. To make sure this package was actually overridden, we check whether any edge going in
367 // had the rule applied to it, in which case its overrides set is different than its source node.
368 for (const edge of this.edgesIn) {
369 if (edge.overrides && edge.overrides.name === this.name && edge.overrides.value === this.version) {
370 if (!edge.overrides.isEqual(edge.from.overrides)) {
371 return true
372 }
373 }
374 }
375
376 return false
377 }
378
379 get package () {
380 return this[_package]
381 }
382
383 set package (pkg) {
384 // just detach them all. we could make this _slightly_ more efficient
385 // by only detaching the ones that changed, but we'd still have to walk
386 // them all, and the comparison logic gets a bit tricky. we generally
387 // only do this more than once at the root level, so the resolve() calls
388 // are only one level deep, and there's not much to be saved, anyway.
389 // simpler to just toss them all out.
390 for (const edge of this.edgesOut.values()) {
391 edge.detach()
392 }
393
394 this[_explanation] = null
395 /* istanbul ignore next - should be impossible */
396 if (!pkg || typeof pkg !== 'object') {
397 debug(() => {
398 throw new Error('setting Node.package to non-object')
399 })
400 pkg = {}
401 }
402 this[_package] = pkg
403 this.#loadWorkspaces()
404 this[_loadDeps]()
405 // do a hard reload, since the dependents may now be valid or invalid
406 // as a result of the package change.
407 this.edgesIn.forEach(edge => edge.reload(true))
408 }
409
410 // node.explain(nodes seen already, edge we're trying to satisfy
411 // if edge is not specified, it lists every edge into the node.
412 explain (edge = null, seen = []) {
413 if (this[_explanation]) {
414 return this[_explanation]
415 }
416
417 return this[_explanation] = this[_explain](edge, seen)
418 }
419
420 [_explain] (edge, seen) {
421 if (this.isProjectRoot && !this.sourceReference) {
422 return {
423 location: this.path,
424 }
425 }
426
427 const why = {
428 name: this.isProjectRoot || this.isTop ? this.packageName : this.name,
429 version: this.package.version,
430 }
431 if (this.errors.length || !this.packageName || !this.package.version) {
432 why.errors = this.errors.length ? this.errors : [
433 new Error('invalid package: lacks name and/or version'),
434 ]
435 why.package = this.package
436 }
437
438 if (this.root.sourceReference) {
439 const { name, version } = this.root.package
440 why.whileInstalling = {
441 name,
442 version,
443 path: this.root.sourceReference.path,
444 }
445 }
446
447 if (this.sourceReference) {
448 return this.sourceReference.explain(edge, seen)
449 }
450
451 if (seen.includes(this)) {
452 return why
453 }
454
455 why.location = this.location
456 why.isWorkspace = this.isWorkspace
457
458 // make a new list each time. we can revisit, but not loop.
459 seen = seen.concat(this)
460
461 why.dependents = []
462 if (edge) {
463 why.dependents.push(edge.explain(seen))
464 } else {
465 // ignore invalid edges, since those aren't satisfied by this thing,
466 // and are not keeping it held in this spot anyway.
467 const edges = []
468 for (const edge of this.edgesIn) {
469 if (!edge.valid && !edge.from.isProjectRoot) {
470 continue
471 }
472
473 edges.push(edge)
474 }
475 for (const edge of edges) {
476 why.dependents.push(edge.explain(seen))
477 }
478 }
479
480 if (this.linksIn.size) {
481 why.linksIn = [...this.linksIn].map(link => link[_explain](edge, seen))
482 }
483
484 return why
485 }
486
487 isDescendantOf (node) {
488 for (let p = this; p; p = p.resolveParent) {
489 if (p === node) {
490 return true
491 }
492 }
493 return false
494 }
495
496 shouldOmit (omitSet) {
497 if (!omitSet.size) {
498 return false
499 }
500
501 const { top } = this
502
503 // if the top is not the root or workspace then we do not want to omit it
504 if (!top.isProjectRoot && !top.isWorkspace) {
505 return false
506 }
507
508 // omit node if the dep type matches any omit flags that were set
509 return (
510 this.peer && omitSet.has('peer') ||
511 this.dev && omitSet.has('dev') ||
512 this.optional && omitSet.has('optional') ||
513 this.devOptional && omitSet.has('optional') && omitSet.has('dev')
514 )
515 }
516
517 getBundler (path = []) {
518 // made a cycle, definitely not bundled!
519 if (path.includes(this)) {
520 return null
521 }
522
523 path.push(this)
524
525 const parent = this[_parent]
526 if (!parent) {
527 return null
528 }
529
530 const pBundler = parent.getBundler(path)
531 if (pBundler) {
532 return pBundler
533 }
534
535 const ppkg = parent.package
536 const bd = ppkg && ppkg.bundleDependencies
537 // explicit bundling
538 if (Array.isArray(bd) && bd.includes(this.name)) {
539 return parent
540 }
541
542 // deps that are deduped up to the bundling level are bundled.
543 // however, if they get their dep met further up than that,
544 // then they are not bundled. Ie, installing a package with
545 // unmet bundled deps will not cause your deps to be bundled.
546 for (const edge of this.edgesIn) {
547 const eBundler = edge.from.getBundler(path)
548 if (!eBundler) {
549 continue
550 }
551
552 if (eBundler === parent) {
553 return eBundler
554 }
555 }
556
557 return null
558 }
559
560 get inBundle () {
561 return !!this.getBundler()
562 }
563
564 // when reifying, if a package is technically in a bundleDependencies list,
565 // but that list is the root project, we still have to install it. This
566 // getter returns true if it's in a dependency's bundle list, not the root's.
567 get inDepBundle () {
568 const bundler = this.getBundler()
569 return !!bundler && bundler !== this.root
570 }
571
572 get isWorkspace () {
573 if (this.isProjectRoot) {
574 return false
575 }
576 const { root } = this
577 const { type, to } = root.edgesOut.get(this.packageName) || {}
578 return type === 'workspace' && to && (to.target === this || to === this)
579 }
580
581 get isRoot () {
582 return this === this.root
583 }
584
585 get isProjectRoot () {
586 // only treat as project root if it's the actual link that is the root, or the target of the root link, but NOT if it's another link to the same root that happens to be somewhere else.
587 return this === this.root || this === this.root.target
588 }
589
590 get isRegistryDependency () {
591 if (this.edgesIn.size === 0) {
592 return false
593 }
594 for (const edge of this.edgesIn) {
595 if (!npa(edge.spec).registry) {
596 return false
597 }
598 }
599 return true
600 }
601
602 * ancestry () {
603 for (let anc = this; anc; anc = anc.resolveParent) {
604 yield anc
605 }
606 }
607
608 set root (root) {
609 // setting to null means this is the new root
610 // should only ever be one step
611 while (root && root.root !== root) {
612 root = root.root
613 }
614
615 root = root || this
616
617 // delete from current root inventory
618 this[_delistFromMeta]()
619
620 // can't set the root (yet) if there's no way to determine location
621 // this allows us to do new Node({...}) and then set the root later.
622 // just make the assignment so we don't lose it, and move on.
623 if (!this.path || !root.realpath || !root.path) {
624 this.#root = root
625 return
626 }
627
628 // temporarily become a root node
629 this.#root = this
630
631 // break all linksIn, we're going to re-set them if needed later
632 for (const link of this.linksIn) {
633 link[_target] = null
634 this.linksIn.delete(link)
635 }
636
637 // temporarily break this link as well, we'll re-set if possible later
638 const { target } = this
639 if (this.isLink) {
640 if (target) {
641 target.linksIn.delete(this)
642 if (target.root === this) {
643 target[_delistFromMeta]()
644 }
645 }
646 this[_target] = null
647 }
648
649 // if this is part of a cascading root set, then don't do this bit
650 // but if the parent/fsParent is in a different set, we have to break
651 // that reference before proceeding
652 if (this.parent && this.parent.root !== root) {
653 this.parent.children.delete(this.name)
654 this[_parent] = null
655 }
656 if (this.fsParent && this.fsParent.root !== root) {
657 this.fsParent.fsChildren.delete(this)
658 this[_fsParent] = null
659 }
660
661 if (root === this) {
662 this[_refreshLocation]()
663 } else {
664 // setting to some different node.
665 const loc = relpath(root.realpath, this.path)
666 const current = root.inventory.get(loc)
667
668 // clobber whatever is there now
669 if (current) {
670 current.root = null
671 }
672
673 this.#root = root
674 // set this.location and add to inventory
675 this[_refreshLocation]()
676
677 // try to find our parent/fsParent in the new root inventory
678 for (const p of walkUp(dirname(this.path))) {
679 if (p === this.path) {
680 continue
681 }
682 const ploc = relpath(root.realpath, p)
683 const parent = root.inventory.get(ploc)
684 if (parent) {
685 /* istanbul ignore next - impossible */
686 if (parent.isLink) {
687 debug(() => {
688 throw Object.assign(new Error('assigning parentage to link'), {
689 path: this.path,
690 parent: parent.path,
691 parentReal: parent.realpath,
692 })
693 })
694 continue
695 }
696 const childLoc = `${ploc}${ploc ? '/' : ''}node_modules/${this.name}`
697 const isParent = this.location === childLoc
698 if (isParent) {
699 const oldChild = parent.children.get(this.name)
700 if (oldChild && oldChild !== this) {
701 oldChild.root = null
702 }
703 if (this.parent) {
704 this.parent.children.delete(this.name)
705 this.parent[_reloadNamedEdges](this.name)
706 }
707 parent.children.set(this.name, this)
708 this[_parent] = parent
709 // don't do it for links, because they don't have a target yet
710 // we'll hit them up a bit later on.
711 if (!this.isLink) {
712 parent[_reloadNamedEdges](this.name)
713 }
714 } else {
715 /* istanbul ignore if - should be impossible, since we break
716 * all fsParent/child relationships when moving? */
717 if (this.fsParent) {
718 this.fsParent.fsChildren.delete(this)
719 }
720 parent.fsChildren.add(this)
721 this[_fsParent] = parent
722 }
723 break
724 }
725 }
726
727 // if it doesn't have a parent, it's a top node
728 if (!this.parent) {
729 root.tops.add(this)
730 } else {
731 root.tops.delete(this)
732 }
733
734 // assign parentage for any nodes that need to have this as a parent
735 // this can happen when we have a node at nm/a/nm/b added *before*
736 // the node at nm/a, which might have the root node as a fsParent.
737 // we can't rely on the public setter here, because it calls into
738 // this function to set up these references!
739 // check dirname so that /foo isn't treated as the fsparent of /foo-bar
740 const nmloc = `${this.location}${this.location ? '/' : ''}node_modules/`
741 // only walk top nodes, since anything else already has a parent.
742 for (const child of root.tops) {
743 const isChild = child.location === nmloc + child.name
744 const isFsChild =
745 dirname(child.path).startsWith(this.path) &&
746 child !== this &&
747 !child.parent &&
748 (
749 !child.fsParent ||
750 child.fsParent === this ||
751 dirname(this.path).startsWith(child.fsParent.path)
752 )
753
754 if (!isChild && !isFsChild) {
755 continue
756 }
757
758 // set up the internal parentage links
759 if (this.isLink) {
760 child.root = null
761 } else {
762 // can't possibly have a parent, because it's in tops
763 if (child.fsParent) {
764 child.fsParent.fsChildren.delete(child)
765 }
766 child[_fsParent] = null
767 if (isChild) {
768 this.children.set(child.name, child)
769 child[_parent] = this
770 root.tops.delete(child)
771 } else {
772 this.fsChildren.add(child)
773 child[_fsParent] = this
774 }
775 }
776 }
777
778 // look for any nodes with the same realpath. either they're links
779 // to that realpath, or a thing at that realpath if we're adding a link
780 // (if we're adding a regular node, we already deleted the old one)
781 for (const node of root.inventory.query('realpath', this.realpath)) {
782 if (node === this) {
783 continue
784 }
785
786 /* istanbul ignore next - should be impossible */
787 debug(() => {
788 if (node.root !== root) {
789 throw new Error('inventory contains node from other root')
790 }
791 })
792
793 if (this.isLink) {
794 const target = node.target
795 this[_target] = target
796 this[_package] = target.package
797 target.linksIn.add(this)
798 // reload edges here, because now we have a target
799 if (this.parent) {
800 this.parent[_reloadNamedEdges](this.name)
801 }
802 break
803 } else {
804 /* istanbul ignore else - should be impossible */
805 if (node.isLink) {
806 node[_target] = this
807 node[_package] = this.package
808 this.linksIn.add(node)
809 if (node.parent) {
810 node.parent[_reloadNamedEdges](node.name)
811 }
812 } else {
813 debug(() => {
814 throw Object.assign(new Error('duplicate node in root setter'), {
815 path: this.path,
816 realpath: this.realpath,
817 root: root.realpath,
818 })
819 })
820 }
821 }
822 }
823 }
824
825 // reload all edgesIn where the root doesn't match, so we don't have
826 // cross-tree dependency graphs
827 for (const edge of this.edgesIn) {
828 if (edge.from.root !== root) {
829 edge.reload()
830 }
831 }
832 // reload all edgesOut where root doesn't match, or is missing, since
833 // it might not be missing in the new tree
834 for (const edge of this.edgesOut.values()) {
835 if (!edge.to || edge.to.root !== root) {
836 edge.reload()
837 }
838 }
839
840 // now make sure our family comes along for the ride!
841 const family = new Set([
842 ...this.fsChildren,
843 ...this.children.values(),
844 ...this.inventory.values(),
845 ].filter(n => n !== this))
846
847 for (const child of family) {
848 if (child.root !== root) {
849 child[_delistFromMeta]()
850 child[_parent] = null
851 this.children.delete(child.name)
852 child[_fsParent] = null
853 this.fsChildren.delete(child)
854 for (const l of child.linksIn) {
855 l[_target] = null
856 child.linksIn.delete(l)
857 }
858 }
859 }
860 for (const child of family) {
861 if (child.root !== root) {
862 child.root = root
863 }
864 }
865
866 // if we had a target, and didn't find one in the new root, then bring
867 // it over as well, but only if we're setting the link into a new root,
868 // as we don't want to lose the target any time we remove a link.
869 if (this.isLink && target && !this.target && root !== this) {
870 target.root = root
871 }
872
873 // tree should always be valid upon root setter completion.
874 treeCheck(this)
875 if (this !== root) {
876 treeCheck(root)
877 }
878 }
879
880 get root () {
881 return this.#root || this
882 }
883
884 #loadWorkspaces () {
885 if (!this.#workspaces) {
886 return
887 }
888
889 for (const [name, path] of this.#workspaces.entries()) {
890 new Edge({ from: this, name, spec: `file:${path}`, type: 'workspace' })
891 }
892 }
893
894 [_loadDeps] () {
895 // Caveat! Order is relevant!
896 // Packages in optionalDependencies are optional.
897 // Packages in both deps and devDeps are required.
898 // Note the subtle breaking change from v6: it is no longer possible
899 // to have a different spec for a devDep than production dep.
900
901 // Linked targets that are disconnected from the tree are tops,
902 // but don't have a 'path' field, only a 'realpath', because we
903 // don't know their canonical location. We don't need their devDeps.
904 const pd = this.package.peerDependencies
905 const ad = this.package.acceptDependencies || {}
906 if (pd && typeof pd === 'object' && !this.legacyPeerDeps) {
907 const pm = this.package.peerDependenciesMeta || {}
908 const peerDependencies = {}
909 const peerOptional = {}
910 for (const [name, dep] of Object.entries(pd)) {
911 if (pm[name]?.optional) {
912 peerOptional[name] = dep
913 } else {
914 peerDependencies[name] = dep
915 }
916 }
917 this.#loadDepType(peerDependencies, 'peer', ad)
918 this.#loadDepType(peerOptional, 'peerOptional', ad)
919 }
920
921 this.#loadDepType(this.package.dependencies, 'prod', ad)
922 this.#loadDepType(this.package.optionalDependencies, 'optional', ad)
923
924 const { globalTop, isTop, path, sourceReference } = this
925 const {
926 globalTop: srcGlobalTop,
927 isTop: srcTop,
928 path: srcPath,
929 } = sourceReference || {}
930 const thisDev = isTop && !globalTop && path
931 const srcDev = !sourceReference || srcTop && !srcGlobalTop && srcPath
932 if (thisDev && srcDev) {
933 this.#loadDepType(this.package.devDependencies, 'dev', ad)
934 }
935 }
936
937 #loadDepType (deps, type, ad) {
938 // Because of the order in which _loadDeps runs, we always want to
939 // prioritize a new edge over an existing one
940 for (const [name, spec] of Object.entries(deps || {})) {
941 const current = this.edgesOut.get(name)
942 if (!current || current.type !== 'workspace') {
943 new Edge({ from: this, name, spec, accept: ad[name], type })
944 }
945 }
946 }
947
948 get fsParent () {
949 // in debug setter prevents fsParent from being this
950 return this[_fsParent]
951 }
952
953 set fsParent (fsParent) {
954 if (!fsParent) {
955 if (this[_fsParent]) {
956 this.root = null
957 }
958 return
959 }
960
961 debug(() => {
962 if (fsParent === this) {
963 throw new Error('setting node to its own fsParent')
964 }
965
966 if (fsParent.realpath === this.realpath) {
967 throw new Error('setting fsParent to same path')
968 }
969
970 // the initial set MUST be an actual walk-up from the realpath
971 // subsequent sets will re-root on the new fsParent's path.
972 if (!this[_fsParent] && this.realpath.indexOf(fsParent.realpath) !== 0) {
973 throw Object.assign(new Error('setting fsParent improperly'), {
974 path: this.path,
975 realpath: this.realpath,
976 fsParent: {
977 path: fsParent.path,
978 realpath: fsParent.realpath,
979 },
980 })
981 }
982 })
983
984 if (fsParent.isLink) {
985 fsParent = fsParent.target
986 }
987
988 // setting a thing to its own fsParent is not normal, but no-op for safety
989 if (this === fsParent || fsParent.realpath === this.realpath) {
990 return
991 }
992
993 // nothing to do
994 if (this[_fsParent] === fsParent) {
995 return
996 }
997
998 const oldFsParent = this[_fsParent]
999 const newPath = !oldFsParent ? this.path
1000 : resolve(fsParent.path, relative(oldFsParent.path, this.path))
1001 const nmPath = resolve(fsParent.path, 'node_modules', this.name)
1002
1003 // this is actually the parent, set that instead
1004 if (newPath === nmPath) {
1005 this.parent = fsParent
1006 return
1007 }
1008
1009 const pathChange = newPath !== this.path
1010
1011 // remove from old parent/fsParent
1012 const oldParent = this.parent
1013 const oldName = this.name
1014 if (this.parent) {
1015 this.parent.children.delete(this.name)
1016 this[_parent] = null
1017 }
1018 if (this.fsParent) {
1019 this.fsParent.fsChildren.delete(this)
1020 this[_fsParent] = null
1021 }
1022
1023 // update this.path/realpath for this and all children/fsChildren
1024 if (pathChange) {
1025 this[_changePath](newPath)
1026 }
1027
1028 if (oldParent) {
1029 oldParent[_reloadNamedEdges](oldName)
1030 }
1031
1032 // clobbers anything at that path, resets all appropriate references
1033 this.root = fsParent.root
1034 }
1035
1036 // is it safe to replace one node with another? check the edges to
1037 // make sure no one will get upset. Note that the node might end up
1038 // having its own unmet dependencies, if the new node has new deps.
1039 // Note that there are cases where Arborist will opt to insert a node
1040 // into the tree even though this function returns false! This is
1041 // necessary when a root dependency is added or updated, or when a
1042 // root dependency brings peer deps along with it. In that case, we
1043 // will go ahead and create the invalid state, and then try to resolve
1044 // it with more tree construction, because it's a user request.
1045 canReplaceWith (node, ignorePeers) {
1046 if (node.name !== this.name) {
1047 return false
1048 }
1049
1050 if (node.packageName !== this.packageName) {
1051 return false
1052 }
1053
1054 // If this node has no dependencies, then it's irrelevant to check the override
1055 // rules of the replacement node.
1056 if (this.edgesOut.size) {
1057 // XXX need to check for two root nodes?
1058 if (node.overrides) {
1059 if (!node.overrides.isEqual(this.overrides)) {
1060 return false
1061 }
1062 } else {
1063 if (this.overrides) {
1064 return false
1065 }
1066 }
1067 }
1068
1069 ignorePeers = new Set(ignorePeers)
1070
1071 // gather up all the deps of this node and that are only depended
1072 // upon by deps of this node. those ones don't count, since
1073 // they'll be replaced if this node is replaced anyway.
1074 const depSet = gatherDepSet([this], e => e.to !== this && e.valid)
1075
1076 for (const edge of this.edgesIn) {
1077 // when replacing peer sets, we need to be able to replace the entire
1078 // peer group, which means we ignore incoming edges from other peers
1079 // within the replacement set.
1080 if (!this.isTop &&
1081 edge.from.parent === this.parent &&
1082 edge.peer &&
1083 ignorePeers.has(edge.from.name)) {
1084 continue
1085 }
1086
1087 // only care about edges that don't originate from this node
1088 if (!depSet.has(edge.from) && !edge.satisfiedBy(node)) {
1089 return false
1090 }
1091 }
1092
1093 return true
1094 }
1095
1096 canReplace (node, ignorePeers) {
1097 return node.canReplaceWith(this, ignorePeers)
1098 }
1099
1100 // return true if it's safe to remove this node, because anything that
1101 // is depending on it would be fine with the thing that they would resolve
1102 // to if it was removed, or nothing is depending on it in the first place.
1103 canDedupe (preferDedupe = false, explicitRequest = false) {
1104 // not allowed to mess with shrinkwraps or bundles
1105 if (this.inDepBundle || this.inShrinkwrap) {
1106 return false
1107 }
1108
1109 // it's a top level pkg, or a dep of one
1110 if (!this.resolveParent || !this.resolveParent.resolveParent) {
1111 return false
1112 }
1113
1114 // no one wants it, remove it
1115 if (this.edgesIn.size === 0) {
1116 return true
1117 }
1118
1119 const other = this.resolveParent.resolveParent.resolve(this.name)
1120
1121 // nothing else, need this one
1122 if (!other) {
1123 return false
1124 }
1125
1126 // if it's the same thing, then always fine to remove
1127 if (other.matches(this)) {
1128 return true
1129 }
1130
1131 // if the other thing can't replace this, then skip it
1132 if (!other.canReplace(this)) {
1133 return false
1134 }
1135
1136 // if we prefer dedupe, or if the version is equal, take the other
1137 if (preferDedupe || semver.eq(other.version, this.version)) {
1138 return true
1139 }
1140
1141 // if our current version isn't the result of an override, then prefer to take the greater version
1142 if (!this.overridden && semver.gt(other.version, this.version)) {
1143 return true
1144 }
1145
1146 // if the other version was an explicit request, then prefer to take the other version
1147 if (explicitRequest) {
1148 return true
1149 }
1150
1151 return false
1152 }
1153
1154 satisfies (requested) {
1155 if (requested instanceof Edge) {
1156 return this.name === requested.name && requested.satisfiedBy(this)
1157 }
1158
1159 const parsed = npa(requested)
1160 const { name = this.name, rawSpec: spec } = parsed
1161 return this.name === name && this.satisfies(new Edge({
1162 from: new Node({ path: this.root.realpath }),
1163 type: 'prod',
1164 name,
1165 spec,
1166 }))
1167 }
1168
1169 matches (node) {
1170 // if the nodes are literally the same object, obviously a match.
1171 if (node === this) {
1172 return true
1173 }
1174
1175 // if the names don't match, they're different things, even if
1176 // the package contents are identical.
1177 if (node.name !== this.name) {
1178 return false
1179 }
1180
1181 // if they're links, they match if the targets match
1182 if (this.isLink) {
1183 return node.isLink && this.target.matches(node.target)
1184 }
1185
1186 // if they're two project root nodes, they're different if the paths differ
1187 if (this.isProjectRoot && node.isProjectRoot) {
1188 return this.path === node.path
1189 }
1190
1191 // if the integrity matches, then they're the same.
1192 if (this.integrity && node.integrity) {
1193 return this.integrity === node.integrity
1194 }
1195
1196 // if no integrity, check resolved
1197 if (this.resolved && node.resolved) {
1198 return this.resolved === node.resolved
1199 }
1200
