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