diff --git a/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js b/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js index aa37acfe52d31..899d92ca937cc 100644 --- a/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js +++ b/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js @@ -379,6 +379,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { optional: false, global: this[_global], legacyPeerDeps: this.legacyPeerDeps, + loadOverrides: true, }) if (root.isLink) { root.target = new Node({ @@ -676,6 +677,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { // calls rather than walking over everything in the tree. const set = this.idealTree.inventory .filter(n => this[_shouldUpdateNode](n)) + // XXX add any invalid edgesOut to the queue for (const node of set) { for (const edge of node.edgesIn) { this.addTracker('idealTree', edge.from.name, edge.from.location) @@ -772,7 +774,10 @@ This is a one-time fix-up, please be patient... [_buildDeps] () { process.emit('time', 'idealTree:buildDeps') const tree = this.idealTree.target + tree.assertRootOverrides() this[_depsQueue].push(tree) + // XXX also push anything that depends on a node with a name + // in the override list this.log.silly('idealTree', 'buildDeps') this.addTracker('idealTree', tree.name, '') return this[_buildDepStep]() @@ -1112,6 +1117,7 @@ This is a one-time fix-up, please be patient... path: node.realpath, sourceReference: node, legacyPeerDeps: this.legacyPeerDeps, + overrides: node.overrides, }) // also need to set up any targets from any link deps, so that diff --git a/node_modules/@npmcli/arborist/lib/arborist/load-actual.js b/node_modules/@npmcli/arborist/lib/arborist/load-actual.js index a232bf32b32d0..0d260858d81c6 100644 --- a/node_modules/@npmcli/arborist/lib/arborist/load-actual.js +++ b/node_modules/@npmcli/arborist/lib/arborist/load-actual.js @@ -127,6 +127,7 @@ module.exports = cls => class ActualLoader extends cls { realpath: real, pkg: {}, global, + loadOverrides: true, }) return this[_loadActualActually]({ root, ignoreMissing, global }) } @@ -135,8 +136,11 @@ module.exports = cls => class ActualLoader extends cls { this[_actualTree] = await this[_loadFSNode]({ path: this.path, real: await realpath(this.path, this[_rpcache], this[_stcache]), + loadOverrides: true, }) + this[_actualTree].assertRootOverrides() + // Note: hidden lockfile will be rejected if it's not the latest thing // in the folder, or if any of the entries in the hidden lockfile are // missing. @@ -236,13 +240,26 @@ module.exports = cls => class ActualLoader extends cls { this[_actualTree] = root } - [_loadFSNode] ({ path, parent, real, root }) { + [_loadFSNode] ({ path, parent, real, root, loadOverrides }) { if (!real) { return realpath(path, this[_rpcache], this[_stcache]) .then( - real => this[_loadFSNode]({ path, parent, real, root }), + real => this[_loadFSNode]({ + path, + parent, + real, + root, + loadOverrides, + }), // if realpath fails, just provide a dummy error node - error => new Node({ error, path, realpath: path, parent, root }) + error => new Node({ + error, + path, + realpath: path, + parent, + root, + loadOverrides, + }) ) } @@ -271,6 +288,7 @@ module.exports = cls => class ActualLoader extends cls { error, parent, root, + loadOverrides, }) }) .then(node => { diff --git a/node_modules/@npmcli/arborist/lib/arborist/load-virtual.js b/node_modules/@npmcli/arborist/lib/arborist/load-virtual.js index 7761380e9f71f..4d65e3da6f683 100644 --- a/node_modules/@npmcli/arborist/lib/arborist/load-virtual.js +++ b/node_modules/@npmcli/arborist/lib/arborist/load-virtual.js @@ -72,6 +72,7 @@ module.exports = cls => class VirtualLoader extends cls { this[rootOptionProvided] = options.root await this[loadFromShrinkwrap](s, root) + root.assertRootOverrides() return treeCheck(this.virtualTree) } diff --git a/node_modules/@npmcli/arborist/lib/edge.js b/node_modules/@npmcli/arborist/lib/edge.js index 1881001ef143e..87439e7645366 100644 --- a/node_modules/@npmcli/arborist/lib/edge.js +++ b/node_modules/@npmcli/arborist/lib/edge.js @@ -29,6 +29,7 @@ class ArboristEdge {} const printableEdge = (edge) => { const edgeFrom = edge.from && edge.from.location const edgeTo = edge.to && edge.to.location + const override = edge.overrides && edge.overrides.value return Object.assign(new ArboristEdge(), { name: edge.name, @@ -38,12 +39,13 @@ const printableEdge = (edge) => { ...(edgeTo ? { to: edgeTo } : {}), ...(edge.error ? { error: edge.error } : {}), ...(edge.peerConflicted ? { peerConflicted: true } : {}), + ...(override ? { overridden: override } : {}), }) } class Edge { constructor (options) { - const { type, name, spec, accept, from } = options + const { type, name, spec, accept, from, overrides } = options if (typeof spec !== 'string') { throw new TypeError('must provide string spec') @@ -55,6 +57,10 @@ class Edge { this[_spec] = spec + if (overrides !== undefined) { + this.overrides = overrides + } + if (accept !== undefined) { if (typeof accept !== 'string') { throw new TypeError('accept field must be a string if provided') @@ -82,8 +88,11 @@ class Edge { } satisfiedBy (node) { - return node.name === this.name && - depValid(node, this.spec, this.accept, this.from) + if (node.name !== this.name) { + return false + } + + return depValid(node, this.spec, this.accept, this.from) } explain (seen = []) { @@ -101,6 +110,10 @@ class Edge { type: this.type, name: this.name, spec: this.spec, + ...(this.rawSpec !== this.spec ? { + rawSpec: this.rawSpec, + overridden: true, + } : {}), ...(bundled ? { bundled } : {}), ...(error ? { error } : {}), ...(from ? { from: from.explain(null, seen) } : {}), @@ -143,7 +156,28 @@ class Edge { return this[_name] } + get rawSpec () { + return this[_spec] + } + get spec () { + if (this.overrides && this.overrides.value && this.overrides.name === this.name) { + if (this.overrides.value.startsWith('$')) { + const ref = this.overrides.value.slice(1) + const pkg = this.from.root.package + const overrideSpec = (pkg.devDependencies && pkg.devDependencies[ref]) || + (pkg.optionalDependencies && pkg.optionalDependencies[ref]) || + (pkg.dependencies && pkg.dependencies[ref]) || + (pkg.peerDependencies && pkg.peerDependencies[ref]) + + if (overrideSpec) { + return overrideSpec + } + + throw new Error(`Unable to resolve reference ${this.overrides.value}`) + } + return this.overrides.value + } return this[_spec] } @@ -213,6 +247,7 @@ class Edge { if (node.edgesOut.has(this.name)) { node.edgesOut.get(this.name).detach() } + node.addEdgeOut(this) this.reload() } diff --git a/node_modules/@npmcli/arborist/lib/node.js b/node_modules/@npmcli/arborist/lib/node.js index d311b6a837817..45c288bcf6cf7 100644 --- a/node_modules/@npmcli/arborist/lib/node.js +++ b/node_modules/@npmcli/arborist/lib/node.js @@ -32,6 +32,7 @@ const semver = require('semver') const nameFromFolder = require('@npmcli/name-from-folder') const Edge = require('./edge.js') const Inventory = require('./inventory.js') +const OverrideSet = require('./override-set.js') const { normalize } = require('read-package-json-fast') const { getPaths: getBinPaths } = require('bin-links') const npa = require('npm-package-arg') @@ -88,6 +89,8 @@ class Node { legacyPeerDeps = false, linksIn, hasShrinkwrap, + overrides, + loadOverrides = false, extraneous = true, dev = true, optional = true, @@ -190,6 +193,17 @@ class Node { // because this.package is read when adding to inventory this[_package] = pkg && typeof pkg === 'object' ? pkg : {} + if (overrides) { + this.overrides = overrides + } else if (loadOverrides) { + const overrides = this[_package].overrides || {} + if (Object.keys(overrides).length > 0) { + this.overrides = new OverrideSet({ + overrides: this[_package].overrides, + }) + } + } + // only relevant for the root and top nodes this.meta = meta @@ -963,6 +977,11 @@ class Node { return false } + // XXX need to check for two root nodes? + if (node.overrides !== this.overrides) { + return false + } + ignorePeers = new Set(ignorePeers) // gather up all the deps of this node and that are only depended @@ -1208,6 +1227,10 @@ class Node { this[_changePath](newPath) } + if (parent.overrides) { + this.overrides = parent.overrides.getNodeRule(this) + } + // clobbers anything at that path, resets all appropriate references this.root = parent.root } @@ -1279,11 +1302,33 @@ class Node { } } + assertRootOverrides () { + if (!this.isProjectRoot || !this.overrides) { + return + } + + for (const edge of this.edgesOut.values()) { + // if these differ an override has been applied, those are not allowed + // for top level dependencies so throw an error + if (edge.spec !== edge.rawSpec && !edge.spec.startsWith('$')) { + throw Object.assign(new Error(`Override for ${edge.name}@${edge.rawSpec} conflicts with direct dependency`), { code: 'EOVERRIDE' }) + } + } + } + addEdgeOut (edge) { + if (this.overrides) { + edge.overrides = this.overrides.getEdgeRule(edge) + } + this.edgesOut.set(edge.name, edge) } addEdgeIn (edge) { + if (edge.overrides) { + this.overrides = edge.overrides + } + this.edgesIn.add(edge) // try to get metadata from the yarn.lock file diff --git a/node_modules/@npmcli/arborist/lib/override-set.js b/node_modules/@npmcli/arborist/lib/override-set.js new file mode 100644 index 0000000000000..e2e04e03e911e --- /dev/null +++ b/node_modules/@npmcli/arborist/lib/override-set.js @@ -0,0 +1,123 @@ +const npa = require('npm-package-arg') +const semver = require('semver') + +class OverrideSet { + constructor ({ overrides, key, parent }) { + this.parent = parent + this.children = new Map() + + if (typeof overrides === 'string') { + overrides = { '.': overrides } + } + + // change a literal empty string to * so we can use truthiness checks on + // the value property later + if (overrides['.'] === '') { + overrides['.'] = '*' + } + + if (parent) { + const spec = npa(key) + if (!spec.name) { + throw new Error(`Override without name: ${key}`) + } + + this.name = spec.name + spec.name = '' + this.key = key + this.keySpec = spec.rawSpec === '' ? '' : spec.toString() + this.value = overrides['.'] || this.keySpec + } + + for (const [key, childOverrides] of Object.entries(overrides)) { + if (key === '.') { + continue + } + + const child = new OverrideSet({ + parent: this, + key, + overrides: childOverrides, + }) + + this.children.set(child.key, child) + } + } + + getEdgeRule (edge) { + for (const rule of this.ruleset.values()) { + if (rule.name !== edge.name) { + continue + } + + if (rule.keySpec === '' || + semver.intersects(edge.spec, rule.keySpec)) { + return rule + } + } + + return this + } + + getNodeRule (node) { + for (const rule of this.ruleset.values()) { + if (rule.name !== node.name) { + continue + } + + if (rule.keySpec === '' || + semver.satisfies(node.version, rule.keySpec) || + semver.satisfies(node.version, rule.value)) { + return rule + } + } + + return this + } + + getMatchingRule (node) { + for (const rule of this.ruleset.values()) { + if (rule.name !== node.name) { + continue + } + + if (rule.keySpec === '' || + semver.satisfies(node.version, rule.keySpec) || + semver.satisfies(node.version, rule.value)) { + return rule + } + } + + return null + } + + * ancestry () { + for (let ancestor = this; ancestor; ancestor = ancestor.parent) { + yield ancestor + } + } + + get isRoot () { + return !this.parent + } + + get ruleset () { + const ruleset = new Map() + + for (const override of this.ancestry()) { + for (const kid of override.children.values()) { + if (!ruleset.has(kid.key)) { + ruleset.set(kid.key, kid) + } + } + + if (!override.isRoot && !ruleset.has(override.key)) { + ruleset.set(override.key, override) + } + } + + return ruleset + } +} + +module.exports = OverrideSet diff --git a/node_modules/@npmcli/arborist/lib/place-dep.js b/node_modules/@npmcli/arborist/lib/place-dep.js index be735d5fc1c4b..c0cbe91fe3667 100644 --- a/node_modules/@npmcli/arborist/lib/place-dep.js +++ b/node_modules/@npmcli/arborist/lib/place-dep.js @@ -295,6 +295,7 @@ class PlaceDep { integrity: dep.integrity, legacyPeerDeps: this.legacyPeerDeps, error: dep.errors[0], + ...(dep.overrides ? { overrides: dep.overrides } : {}), ...(dep.isLink ? { target: dep.target, realpath: dep.realpath } : {}), }) diff --git a/node_modules/@npmcli/arborist/lib/printable.js b/node_modules/@npmcli/arborist/lib/printable.js index 018e569b1d2f1..7c8d52a4207aa 100644 --- a/node_modules/@npmcli/arborist/lib/printable.js +++ b/node_modules/@npmcli/arborist/lib/printable.js @@ -1,6 +1,5 @@ // helper function to output a clearer visualization // of the current node and its descendents - const localeCompare = require('@isaacs/string-locale-compare')('en') const util = require('util') const relpath = require('./relpath.js') @@ -65,6 +64,11 @@ class ArboristNode { this.errors = tree.errors.map(treeError) } + if (tree.overrides) { + this.overrides = new Map([...tree.overrides.ruleset.values()] + .map((override) => [override.key, override.value])) + } + // edgesOut sorted by name if (tree.edgesOut.size) { this.edgesOut = new Map([...tree.edgesOut.entries()] @@ -126,7 +130,10 @@ class Edge { constructor (edge) { this.type = edge.type this.name = edge.name - this.spec = edge.spec || '*' + this.spec = edge.rawSpec || '*' + if (edge.rawSpec !== edge.spec) { + this.override = edge.spec + } if (edge.error) { this.error = edge.error } @@ -145,6 +152,8 @@ class EdgeOut extends Edge { [util.inspect.custom] () { return `{ ${this.type} ${this.name}@${this.spec}${ + this.override ? ` overridden:${this.override}` : '' + }${ this.to ? ' -> ' + this.to : '' }${ this.error ? ' ' + this.error : '' diff --git a/node_modules/@npmcli/arborist/package.json b/node_modules/@npmcli/arborist/package.json index 34d38572d38d7..12fede6857d65 100644 --- a/node_modules/@npmcli/arborist/package.json +++ b/node_modules/@npmcli/arborist/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/arborist", - "version": "4.0.5", + "version": "4.1.0", "description": "Manage node_modules trees", "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", @@ -37,10 +37,11 @@ "walk-up-path": "^1.0.0" }, "devDependencies": { - "@npmcli/template-oss": "^2.3.0", + "@npmcli/template-oss": "^2.3.1", "benchmark": "^2.1.4", "chalk": "^4.1.0", "minify-registry-metadata": "^2.1.0", + "nock": "^13.2.0", "tap": "^15.1.2", "tcompare": "^5.0.6" }, @@ -93,7 +94,7 @@ "engines": { "node": "^12.13.0 || ^14.15.0 || >=16" }, - "templateVersion": "2.3.0", + "templateVersion": "2.3.1", "eslintIgnore": [ "test/fixtures/", "!test/fixtures/*.js" diff --git a/package-lock.json b/package-lock.json index b7fc90a9c6f55..b6df88dfee279 100644 --- a/package-lock.json +++ b/package-lock.json @@ -87,7 +87,7 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^4.0.5", + "@npmcli/arborist": "^4.1.0", "@npmcli/ci-detect": "^1.4.0", "@npmcli/config": "^2.3.2", "@npmcli/map-workspaces": "^2.0.0", @@ -801,9 +801,9 @@ } }, "node_modules/@npmcli/arborist": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-4.0.5.tgz", - "integrity": "sha512-WR2cqxzjsvmHJ9sKCdqBYG/qeiAXB9ev1iq1W2Rry7LxeJ7eDtTr4mOWe/TBvp6xFzevGecQc2YEWwExTuLZLg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-4.1.0.tgz", + "integrity": "sha512-bkaOqCuTUtpVOe1vaAP7TUihu64wIbnSDpsbqBJUsGFTLYXbjKwi6xj8Zx5cfHkM3nqyeEEbPYlGkt0TXjKrUg==", "inBundle": true, "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", @@ -10664,9 +10664,9 @@ "dev": true }, "@npmcli/arborist": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-4.0.5.tgz", - "integrity": "sha512-WR2cqxzjsvmHJ9sKCdqBYG/qeiAXB9ev1iq1W2Rry7LxeJ7eDtTr4mOWe/TBvp6xFzevGecQc2YEWwExTuLZLg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-4.1.0.tgz", + "integrity": "sha512-bkaOqCuTUtpVOe1vaAP7TUihu64wIbnSDpsbqBJUsGFTLYXbjKwi6xj8Zx5cfHkM3nqyeEEbPYlGkt0TXjKrUg==", "requires": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/installed-package-contents": "^1.0.7", diff --git a/package.json b/package.json index f9ba8cd3c801b..73199f1795d42 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ }, "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^4.0.5", + "@npmcli/arborist": "^4.1.0", "@npmcli/ci-detect": "^1.4.0", "@npmcli/config": "^2.3.2", "@npmcli/map-workspaces": "^2.0.0",