diff options
author | isaacs <i@izs.me> | 2021-04-01 21:42:32 +0300 |
---|---|---|
committer | isaacs <i@izs.me> | 2021-04-01 21:42:34 +0300 |
commit | fb095a708a1f930bbd0195446ac611b82bfeff14 (patch) | |
tree | 500d004ec2ee414cbd07a08c2bdcd1d0678f770b /node_modules/@npmcli | |
parent | 7b177e43f3bfb558bcd8723cdb2166a3df19647a (diff) |
@npmcli/arborist@2.3.0
* [#2896](https://github.com/npm/cli/issues/2896) Provide currentEdge in
ERESOLVE if known, and address self-linking edge case.
* Add/remove dependencies to/from workspaces when set, not root project
* Only reify the portions of the dependency graph identified by the
`workspace` configuration value.
* Do not recursively `chown` the project root path.
Diffstat (limited to 'node_modules/@npmcli')
-rw-r--r-- | node_modules/@npmcli/arborist/bin/lib/options.js | 8 | ||||
-rw-r--r-- | node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js | 123 | ||||
-rw-r--r-- | node_modules/@npmcli/arborist/lib/arborist/load-actual.js | 30 | ||||
-rw-r--r-- | node_modules/@npmcli/arborist/lib/arborist/reify.js | 143 | ||||
-rw-r--r-- | node_modules/@npmcli/arborist/lib/diff.js | 69 | ||||
-rw-r--r-- | node_modules/@npmcli/arborist/lib/link.js | 20 | ||||
-rw-r--r-- | node_modules/@npmcli/arborist/lib/node.js | 7 | ||||
-rw-r--r-- | node_modules/@npmcli/arborist/lib/printable.js | 9 | ||||
-rw-r--r-- | node_modules/@npmcli/arborist/lib/tree-check.js | 47 | ||||
-rw-r--r-- | node_modules/@npmcli/arborist/package.json | 4 |
10 files changed, 390 insertions, 70 deletions
diff --git a/node_modules/@npmcli/arborist/bin/lib/options.js b/node_modules/@npmcli/arborist/bin/lib/options.js index bf8e08ec2..a1b671962 100644 --- a/node_modules/@npmcli/arborist/bin/lib/options.js +++ b/node_modules/@npmcli/arborist/bin/lib/options.js @@ -33,7 +33,13 @@ for (const arg of process.argv.slice(2)) { options.omit.push(arg.substr('--omit='.length)) } else if (/^--before=/.test(arg)) options.before = new Date(arg.substr('--before='.length)) - else if (/^--[^=]+=/.test(arg)) { + else if (/^-w.+/.test(arg)) { + options.workspaces = options.workspaces || [] + options.workspaces.push(arg.replace(/^-w/, '')) + } else if (/^--workspace=/.test(arg)) { + options.workspaces = options.workspaces || [] + options.workspaces.push(arg.replace(/^--workspace=/, '')) + } else if (/^--[^=]+=/.test(arg)) { const [key, ...v] = arg.replace(/^--/, '').split('=') const val = v.join('=') options[key] = val === 'false' ? false : val === 'true' ? true : val 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 f7e5b7e32..f836fc04d 100644 --- a/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js +++ b/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js @@ -44,12 +44,14 @@ const _currentDep = Symbol('currentDep') const _updateAll = Symbol('updateAll') const _mutateTree = Symbol('mutateTree') const _flagsSuspect = Symbol.for('flagsSuspect') +const _workspaces = Symbol.for('workspaces') const _prune = Symbol('prune') const _preferDedupe = Symbol('preferDedupe') const _legacyBundling = Symbol('legacyBundling') const _parseSettings = Symbol('parseSettings') const _initTree = Symbol('initTree') const _applyUserRequests = Symbol('applyUserRequests') +const _applyUserRequestsToNode = Symbol('applyUserRequestsToNode') const _inflateAncientLockfile = Symbol('inflateAncientLockfile') const _buildDeps = Symbol('buildDeps') const _buildDepStep = Symbol('buildDepStep') @@ -109,7 +111,7 @@ const _peerSetSource = Symbol.for('peerSetSource') // used by Reify mixin const _force = Symbol.for('force') -const _explicitRequests = Symbol.for('explicitRequests') +const _explicitRequests = Symbol('explicitRequests') const _global = Symbol.for('global') const _idealTreePrune = Symbol.for('idealTreePrune') @@ -130,8 +132,10 @@ module.exports = cls => class IdealTreeBuilder extends cls { force = false, packageLock = true, strictPeerDeps = false, + workspaces = [], } = options + this[_workspaces] = workspaces || [] this[_force] = !!force this[_strictPeerDeps] = !!strictPeerDeps @@ -143,6 +147,9 @@ module.exports = cls => class IdealTreeBuilder extends cls { this[_globalStyle] = this[_global] || globalStyle this[_follow] = !!follow + if (this[_workspaces].length && this[_global]) + throw new Error('Cannot operate on workspaces in global mode') + this[_explicitRequests] = new Set() this[_preferDedupe] = false this[_legacyBundling] = false @@ -157,6 +164,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { this[_manifests] = new Map() this[_peerConflict] = null this[_edgesOverridden] = new Set() + this[_resolvedAdd] = [] // a map of each module in a peer set to the thing that depended on // that set of peers in the first place. Use a WeakMap so that we @@ -204,8 +212,8 @@ module.exports = cls => class IdealTreeBuilder extends cls { try { await this[_initTree]() - await this[_applyUserRequests](options) await this[_inflateAncientLockfile]() + await this[_applyUserRequests](options) await this[_buildDeps]() await this[_fixDepFlags]() await this[_pruneFailedOptional]() @@ -266,6 +274,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { this[_preferDedupe] = !!options.preferDedupe this[_legacyBundling] = !!options.legacyBundling this[_updateNames] = update.names + this[_updateAll] = update.all // we prune by default unless explicitly set to boolean false this[_prune] = options.prune !== false @@ -387,6 +396,42 @@ module.exports = cls => class IdealTreeBuilder extends cls { async [_applyUserRequests] (options) { process.emit('time', 'idealTree:userRequests') const tree = this.idealTree.target || this.idealTree + + if (!this[_workspaces].length) { + return this[_applyUserRequestsToNode](tree, options).then(() => + process.emit('timeEnd', 'idealTree:userRequests')) + } + + const wsMap = tree.workspaces + if (!wsMap) { + this.log.warn('idealTree', 'Workspace filter set, but no workspaces present') + return + } + + const promises = [] + for (const name of this[_workspaces]) { + const path = wsMap.get(name) + if (!path) { + this.log.warn('idealTree', `Workspace ${name} in filter set, but not in workspaces`) + continue + } + const loc = relpath(tree.realpath, path) + const node = tree.inventory.get(loc) + + /* istanbul ignore if - should be impossible */ + if (!node) { + this.log.warn('idealTree', `Workspace ${name} in filter set, but no workspace folder present`) + continue + } + + promises.push(this[_applyUserRequestsToNode](node, options)) + } + + return Promise.all(promises).then(() => + process.emit('timeEnd', 'idealTree:userRequests')) + } + + async [_applyUserRequestsToNode] (tree, options) { // If we have a list of package names to update, and we know it's // going to update them wherever they are, add any paths into those // named nodes to the buildIdealTree queue. @@ -395,38 +440,49 @@ module.exports = cls => class IdealTreeBuilder extends cls { // global updates only update the globalTop nodes, but we need to know // that they're there, and not reinstall the world unnecessarily. + const globalExplicitUpdateNames = [] if (this[_global] && (this[_updateAll] || this[_updateNames].length)) { const nm = resolve(this.path, 'node_modules') for (const name of await readdir(nm).catch(() => [])) { - if (this[_updateNames].includes(name)) - this[_explicitRequests].add(name) tree.package.dependencies = tree.package.dependencies || {} - if (this[_updateAll] || this[_updateNames].includes(name)) + const updateName = this[_updateNames].includes(name) + if (this[_updateAll] || updateName) { + if (updateName) + globalExplicitUpdateNames.push(name) tree.package.dependencies[name] = '*' + } } } if (this.auditReport && this.auditReport.size > 0) this[_queueVulnDependents](options) - if (options.rm && options.rm.length) { - addRmPkgDeps.rm(tree.package, options.rm) - for (const name of options.rm) - this[_explicitRequests].add(name) + const { add, rm } = options + + if (rm && rm.length) { + addRmPkgDeps.rm(tree.package, rm) + for (const name of rm) + this[_explicitRequests].add({ from: tree, name, action: 'DELETE' }) } - if (options.add) - await this[_add](options) + if (add && add.length) + await this[_add](tree, options) - // triggers a refresh of all edgesOut - if (options.add && options.add.length || options.rm && options.rm.length || this[_global]) + // triggers a refresh of all edgesOut. this has to be done BEFORE + // adding the edges to explicitRequests, because the package setter + // resets all edgesOut. + if (add && add.length || rm && rm.length || this[_global]) tree.package = tree.package - process.emit('timeEnd', 'idealTree:userRequests') + + for (const spec of this[_resolvedAdd]) + this[_explicitRequests].add(tree.edgesOut.get(spec.name)) + for (const name of globalExplicitUpdateNames) + this[_explicitRequests].add(tree.edgesOut.get(name)) } // This returns a promise because we might not have the name yet, // and need to call pacote.manifest to find the name. - [_add] ({add, saveType = null, saveBundle = false}) { + [_add] (tree, {add, saveType = null, saveBundle = false}) { // get the name for each of the specs in the list. // ie, doing `foo@bar` we just return foo // but if it's a url or git, we don't know the name until we @@ -438,10 +494,9 @@ module.exports = cls => class IdealTreeBuilder extends cls { .then(add => this[_updateFilePath](add)) .then(add => this[_followSymlinkPath](add)) })).then(add => { - this[_resolvedAdd] = add + this[_resolvedAdd].push(...add) // now add is a list of spec objects with names. // find a home for each of them! - const tree = this.idealTree.target || this.idealTree addRmPkgDeps.add({ pkg: tree.package, add, @@ -449,8 +504,6 @@ module.exports = cls => class IdealTreeBuilder extends cls { saveType, path: this.path, }) - for (const spec of add) - this[_explicitRequests].add(spec.name) }) } @@ -991,7 +1044,7 @@ This is a one-time fix-up, please be patient... // if it's peerOptional and not explicitly requested. if (!edge.to) { return edge.type !== 'peerOptional' || - this[_explicitRequests].has(edge.name) + this[_explicitRequests].has(edge) } // If the edge has an error, there's a problem. @@ -1007,7 +1060,7 @@ This is a one-time fix-up, please be patient... return true // If the user has explicitly asked to install this package, it's a problem. - if (node.isProjectRoot && this[_explicitRequests].has(edge.name)) + if (node.isProjectRoot && this[_explicitRequests].has(edge)) return true // No problems! @@ -1131,7 +1184,7 @@ This is a one-time fix-up, please be patient... continue // problem - this[_failPeerConflict](edge) + this[_failPeerConflict](edge, parentEdge) } } @@ -1147,17 +1200,17 @@ This is a one-time fix-up, please be patient... continue // ok, it's the root, or we're in unforced strict mode, so this is bad - this[_failPeerConflict](edge) + this[_failPeerConflict](edge, parentEdge) } return node } - [_failPeerConflict] (edge) { - const expl = this[_explainPeerConflict](edge) + [_failPeerConflict] (edge, currentEdge) { + const expl = this[_explainPeerConflict](edge, currentEdge) throw Object.assign(new Error('unable to resolve dependency tree'), expl) } - [_explainPeerConflict] (edge) { + [_explainPeerConflict] (edge, currentEdge) { const node = edge.from const curNode = node.resolve(edge.name) const pc = this[_peerConflict] || { peer: null, current: null } @@ -1166,6 +1219,10 @@ This is a one-time fix-up, please be patient... return { code: 'ERESOLVE', current, + // it SHOULD be impossible to get here without a current node in place, + // but this at least gives us something report on when bugs creep into + // the tree handling logic. + currentEdge: currentEdge ? currentEdge.explain() : null, edge: edge.explain(), peerConflict, strictPeerDeps: this[_strictPeerDeps], @@ -1190,7 +1247,7 @@ This is a one-time fix-up, please be patient... [_placeDep] (dep, node, edge, peerEntryEdge = null, peerPath = []) { if (edge.to && !edge.error && - !this[_explicitRequests].has(edge.name) && + !this[_explicitRequests].has(edge) && !this[_updateNames].includes(edge.name) && !this[_isVulnerable](edge.to)) return [] @@ -1480,9 +1537,15 @@ This is a one-time fix-up, please be patient... if (target.children.has(edge.name)) { const current = target.children.get(edge.name) - // same thing = keep - if (dep.matches(current)) - return KEEP + // same thing = keep, UNLESS the current doesn't satisfy and new + // one does satisfy. This can happen if it's a link to a matching target + // at a different location, which satisfies a version dep, but not a + // file: dep. If neither of them satisfy, then we can replace it, + // because presumably it's better for a peer or something. + if (dep.matches(current)) { + if (current.satisfies(edge) || !dep.satisfies(edge)) + return KEEP + } const { version: curVer } = current const { version: newVer } = dep diff --git a/node_modules/@npmcli/arborist/lib/arborist/load-actual.js b/node_modules/@npmcli/arborist/lib/arborist/load-actual.js index 49e76e265..d9e7fb46d 100644 --- a/node_modules/@npmcli/arborist/lib/arborist/load-actual.js +++ b/node_modules/@npmcli/arborist/lib/arborist/load-actual.js @@ -32,6 +32,7 @@ const _loadActual = Symbol('loadActual') const _loadActualVirtually = Symbol('loadActualVirtually') const _loadActualActually = Symbol('loadActualActually') const _loadWorkspaces = Symbol.for('loadWorkspaces') +const _loadWorkspaceTargets = Symbol('loadWorkspaceTargets') const _actualTreePromise = Symbol('actualTreePromise') const _actualTree = Symbol('actualTree') const _transplant = Symbol('transplant') @@ -150,18 +151,22 @@ module.exports = cls => class ActualLoader extends cls { await new this.constructor({...this.options}).loadVirtual({ root: this[_actualTree], }) + await this[_loadWorkspaces](this[_actualTree]) + if (this[_actualTree].workspaces && this[_actualTree].workspaces.size) + calcDepFlags(this[_actualTree], !root) this[_transplant](root) return this[_actualTree] } async [_loadActualActually] ({ root, ignoreMissing, global }) { await this[_loadFSTree](this[_actualTree]) + await this[_loadWorkspaces](this[_actualTree]) + await this[_loadWorkspaceTargets](this[_actualTree]) if (!ignoreMissing) await this[_findMissingEdges]() this[_findFSParents]() this[_transplant](root) - await this[_loadWorkspaces](this[_actualTree]) if (global) { // need to depend on the children, or else all of them // will end up being flagged as extraneous, since the @@ -178,16 +183,37 @@ module.exports = cls => class ActualLoader extends cls { return this[_actualTree] } + // if there are workspace targets without Link nodes created, load + // the targets, so that we know what they are. + async [_loadWorkspaceTargets] (tree) { + if (!tree.workspaces || !tree.workspaces.size) + return + + const promises = [] + for (const path of tree.workspaces.values()) { + if (!this[_cache].has(path)) { + const p = this[_loadFSNode]({ path, root: this[_actualTree] }) + .then(node => this[_loadFSTree](node)) + promises.push(p) + } + } + await Promise.all(promises) + } + [_transplant] (root) { if (!root || root === this[_actualTree]) return + this[_actualTree][_changePath](root.path) for (const node of this[_actualTree].children.values()) { if (!this[_transplantFilter](node)) - node.parent = null + node.root = null } root.replace(this[_actualTree]) + for (const node of this[_actualTree].fsChildren) + node.root = this[_transplantFilter](node) ? root : null + this[_actualTree] = root } diff --git a/node_modules/@npmcli/arborist/lib/arborist/reify.js b/node_modules/@npmcli/arborist/lib/arborist/reify.js index 000804552..aaaa3d61c 100644 --- a/node_modules/@npmcli/arborist/lib/arborist/reify.js +++ b/node_modules/@npmcli/arborist/lib/arborist/reify.js @@ -14,6 +14,7 @@ const fs = require('fs') const {promisify} = require('util') const symlink = promisify(fs.symlink) const mkdirp = require('mkdirp-infer-owner') +const justMkdirp = require('mkdirp') const moveFile = require('@npmcli/move-file') const rimraf = promisify(require('rimraf')) const packageContents = require('@npmcli/installed-package-contents') @@ -26,6 +27,7 @@ const retirePath = require('../retire-path.js') const promiseAllRejectLate = require('promise-all-reject-late') const optionalSet = require('../optional-set.js') const updateRootPackageJson = require('../update-root-package-json.js') +const calcDepFlags = require('../calc-dep-flags.js') const _retiredPaths = Symbol('retiredPaths') const _retiredUnchanged = Symbol('retiredUnchanged') @@ -36,6 +38,8 @@ const _retireShallowNodes = Symbol.for('retireShallowNodes') const _getBundlesByDepth = Symbol('getBundlesByDepth') const _registryResolved = Symbol('registryResolved') const _addNodeToTrashList = Symbol('addNodeToTrashList') +const _workspaces = Symbol.for('workspaces') + // shared by rebuild mixin const _trashList = Symbol.for('trashList') const _handleOptionalFailure = Symbol.for('handleOptionalFailure') @@ -82,7 +86,6 @@ const _global = Symbol.for('global') // defined by Ideal mixin const _pruneBundledMetadeps = Symbol.for('pruneBundledMetadeps') -const _explicitRequests = Symbol.for('explicitRequests') const _resolvedAdd = Symbol.for('resolvedAdd') const _usePackageLock = Symbol.for('usePackageLock') const _formatPackageLock = Symbol.for('formatPackageLock') @@ -146,7 +149,10 @@ module.exports = cls => class Reifier extends cls { if (this[_packageLockOnly] || this[_dryRun]) return - await mkdirp(resolve(this.path)) + // we do NOT want to set ownership on this folder, especially + // recursively, because it can have other side effects to do that + // in a project directory. We just want to make it if it's missing. + await justMkdirp(resolve(this.path)) } async [_reifyPackages] () { @@ -237,9 +243,25 @@ module.exports = cls => class Reifier extends cls { const actualOpt = this[_global] ? { ignoreMissing: true, global: true, - filter: (node, kid) => - this[_explicitRequests].size === 0 || !node.isProjectRoot ? true - : (this.idealTree.edgesOut.has(kid) || this[_explicitRequests].has(kid)), + filter: (node, kid) => { + // if it's not the project root, and we have no explicit requests, + // then we're already into a nested dep, so we keep it + if (this.explicitRequests.size === 0 || !node.isProjectRoot) + return true + + // if we added it as an edgeOut, then we want it + if (this.idealTree.edgesOut.has(kid)) + return true + + // if it's an explicit request, then we want it + const hasExplicit = [...this.explicitRequests] + .some(edge => edge.name === kid) + if (hasExplicit) + return true + + // ignore the rest of the global install folder + return false + }, } : { ignoreMissing: true } if (!this[_global]) { @@ -266,9 +288,35 @@ module.exports = cls => class Reifier extends cls { // to just invalidate the parts that changed, but avoid walking the // whole tree again. + const filterNodes = [] + if (this[_global] && this.explicitRequests.size) { + const idealTree = this.idealTree.target || this.idealTree + const actualTree = this.actualTree.target || this.actualTree + // we ONLY are allowed to make changes in the global top-level + // children where there's an explicit request. + for (const { name } of this.explicitRequests) { + const ideal = idealTree.children.get(name) + if (ideal) + filterNodes.push(ideal) + const actual = actualTree.children.get(name) + if (actual) + filterNodes.push(actual) + } + } else { + for (const ws of this[_workspaces]) { + const ideal = this.idealTree.children.get(ws) + if (ideal) + filterNodes.push(ideal) + const actual = this.actualTree.children.get(ws) + if (actual) + filterNodes.push(actual) + } + } + // find all the nodes that need to change between the actual // and ideal trees. this.diff = Diff.calculate({ + filterNodes, actual: this.actualTree, ideal: this.idealTree, }) @@ -886,7 +934,7 @@ module.exports = cls => class Reifier extends cls { // to things like git repos and tarball file/urls. However, if the // user requested 'foo@', and we have a foo@file:../foo, then we should // end up saving the spec we actually used, not whatever they gave us. - if (this[_resolvedAdd]) { + if (this[_resolvedAdd].length) { const root = this.idealTree const pkg = root.package for (const { name } of this[_resolvedAdd]) { @@ -966,20 +1014,85 @@ module.exports = cls => class Reifier extends cls { return meta.save(saveOpt) } - [_copyIdealToActual] () { + async [_copyIdealToActual] () { + // clean up any trash that is still in the tree + for (const path of this[_trashList]) { + const loc = relpath(this.idealTree.realpath, path) + const node = this.idealTree.inventory.get(loc) + if (node && node.root === this.idealTree) + node.parent = null + } + + // if we filtered to only certain nodes, then anything ELSE needs + // to be untouched in the resulting actual tree, even if it differs + // in the idealTree. Copy over anything that was in the actual and + // was not changed, delete anything in the ideal and not actual. + // Then we move the entire idealTree over to this.actualTree, and + // save the hidden lockfile. + if (this.diff && this.diff.filterSet.size) { + const { filterSet } = this.diff + const seen = new Set() + for (const [loc, ideal] of this.idealTree.inventory.entries()) { + if (seen.has(loc)) + continue + seen.add(loc) + + // if it's an ideal node from the filter set, then skip it + // because we already made whatever changes were necessary + if (filterSet.has(ideal)) + continue + + // otherwise, if it's not in the actualTree, then it's not a thing + // that we actually added. And if it IS in the actualTree, then + // it's something that we left untouched, so we need to record + // that. + const actual = this.actualTree.inventory.get(loc) + if (!actual) + ideal.root = null + else { + if ([...actual.linksIn].some(link => filterSet.has(link))) { + seen.add(actual.location) + continue + } + const { realpath, isLink } = actual + if (isLink && ideal.isLink && ideal.realpath === realpath) + continue + else + actual.root = this.idealTree + } + } + + // now find any actual nodes that may not be present in the ideal + // tree, but were left behind by virtue of not being in the filter + for (const [loc, actual] of this.actualTree.inventory.entries()) { + if (seen.has(loc)) + continue + seen.add(loc) + if (filterSet.has(actual)) + continue + actual.root = this.idealTree + } + + // prune out any tops that lack a linkIn + for (const top of this.idealTree.tops) { + if (top.linksIn.size === 0) + top.root = null + } + + // need to calculate dep flags, since nodes may have been marked + // as extraneous or otherwise incorrect during transit. + calcDepFlags(this.idealTree) + } + // save the ideal's meta as a hidden lockfile after we actualize it this.idealTree.meta.filename = - this.path + '/node_modules/.package-lock.json' + this.idealTree.realpath + '/node_modules/.package-lock.json' this.idealTree.meta.hiddenLockfile = true + this.actualTree = this.idealTree this.idealTree = null - for (const path of this[_trashList]) { - const loc = relpath(this.path, path) - const node = this.actualTree.inventory.get(loc) - if (node && node.root === this.actualTree) - node.parent = null - } - return !this[_global] && this.actualTree.meta.save() + if (!this[_global]) + await this.actualTree.meta.save() } } diff --git a/node_modules/@npmcli/arborist/lib/diff.js b/node_modules/@npmcli/arborist/lib/diff.js index ada67f816..84a8bae41 100644 --- a/node_modules/@npmcli/arborist/lib/diff.js +++ b/node_modules/@npmcli/arborist/lib/diff.js @@ -11,7 +11,8 @@ const {existsSync} = require('fs') const ssri = require('ssri') class Diff { - constructor ({actual, ideal}) { + constructor ({actual, ideal, filterSet}) { + this.filterSet = filterSet this.children = [] this.actual = actual this.ideal = ideal @@ -29,9 +30,54 @@ class Diff { this.removed = [] } - static calculate ({actual, ideal}) { + static calculate ({actual, ideal, filterNodes = []}) { + // if there's a filterNode, then: + // - get the path from the root to the filterNode. The root or + // root.target should have an edge either to the filterNode or + // a link to the filterNode. If not, abort. Add the path to the + // filterSet. + // - Add set of Nodes depended on by the filterNode to filterSet. + // - Anything outside of that set should be ignored by getChildren + const filterSet = new Set() + for (const filterNode of filterNodes) { + const { root } = filterNode + if (root !== ideal && root !== actual) + throw new Error('invalid filterNode: outside idealTree/actualTree') + const { target } = root + const rootTarget = target || root + const edge = [...rootTarget.edgesOut.values()].filter(e => { + return e.to && (e.to === filterNode || e.to.target === filterNode) + })[0] + filterSet.add(root) + filterSet.add(rootTarget) + filterSet.add(ideal) + filterSet.add(actual) + if (edge && edge.to) { + filterSet.add(edge.to) + if (edge.to.target) + filterSet.add(edge.to.target) + } + filterSet.add(filterNode) + + depth({ + tree: filterNode, + visit: node => filterSet.add(node), + getChildren: node => { + node = node.target || node + const loc = node.location + const idealNode = ideal.inventory.get(loc) + const ideals = !idealNode ? [] + : [...idealNode.edgesOut.values()].filter(e => e.to).map(e => e.to) + const actualNode = actual.inventory.get(loc) + const actuals = !actualNode ? [] + : [...actualNode.edgesOut.values()].filter(e => e.to).map(e => e.to) + return ideals.concat(actuals) + }, + }) + } + return depth({ - tree: new Diff({actual, ideal}), + tree: new Diff({actual, ideal, filterSet}), getChildren, leave, }) @@ -89,20 +135,20 @@ const allChildren = node => { // to create the diff tree const getChildren = diff => { const children = [] - const {unchanged, removed} = diff + const {actual, ideal, unchanged, removed, filterSet} = diff // Note: we DON'T diff fsChildren themselves, because they are either // included in the package contents, or part of some other project, and // will never appear in legacy shrinkwraps anyway. but we _do_ include the // child nodes of fsChildren, because those are nodes that we are typically // responsible for installing. - const actualKids = allChildren(diff.actual) - const idealKids = allChildren(diff.ideal) + const actualKids = allChildren(actual) + const idealKids = allChildren(ideal) const paths = new Set([...actualKids.keys(), ...idealKids.keys()]) for (const path of paths) { const actual = actualKids.get(path) const ideal = idealKids.get(path) - diffNode(actual, ideal, children, unchanged, removed) + diffNode(actual, ideal, children, unchanged, removed, filterSet) } if (diff.leaves && !children.length) @@ -111,7 +157,10 @@ const getChildren = diff => { return children } -const diffNode = (actual, ideal, children, unchanged, removed) => { +const diffNode = (actual, ideal, children, unchanged, removed, filterSet) => { + if (filterSet.size && !(filterSet.has(ideal) || filterSet.has(actual))) + return + const action = getAction({actual, ideal}) // if it's a match, then get its children @@ -119,7 +168,7 @@ const diffNode = (actual, ideal, children, unchanged, removed) => { if (action) { if (action === 'REMOVE') removed.push(actual) - children.push(new Diff({actual, ideal})) + children.push(new Diff({actual, ideal, filterSet})) } else { unchanged.push(ideal) // !*! Weird dirty hack warning !*! @@ -150,7 +199,7 @@ const diffNode = (actual, ideal, children, unchanged, removed) => { for (const node of bundledChildren) node.parent = ideal } - children.push(...getChildren({actual, ideal, unchanged, removed})) + children.push(...getChildren({actual, ideal, unchanged, removed, filterSet})) } } diff --git a/node_modules/@npmcli/arborist/lib/link.js b/node_modules/@npmcli/arborist/lib/link.js index 2394c6e41..4d15428d8 100644 --- a/node_modules/@npmcli/arborist/lib/link.js +++ b/node_modules/@npmcli/arborist/lib/link.js @@ -23,13 +23,19 @@ class Link extends Node { : null), }) - this.target = target || new Node({ - ...options, - path: realpath, - parent: null, - fsParent: null, - root: this.root, - }) + if (target) + this.target = target + else if (this.realpath === this.root.path) + this.target = this.root + else { + this.target = new Node({ + ...options, + path: realpath, + parent: null, + fsParent: null, + root: this.root, + }) + } } get version () { diff --git a/node_modules/@npmcli/arborist/lib/node.js b/node_modules/@npmcli/arborist/lib/node.js index fa39bed5e..197804e0c 100644 --- a/node_modules/@npmcli/arborist/lib/node.js +++ b/node_modules/@npmcli/arborist/lib/node.js @@ -685,6 +685,7 @@ class Node { ...this.children.values(), ...this.inventory.values(), ].filter(n => n !== this)) + for (const child of family) { if (child.root !== root) { child[_delistFromMeta]() @@ -704,12 +705,14 @@ class Node { } // if we had a target, and didn't find one in the new root, then bring - // it over as well. - if (this.isLink && target && !this.target) + // it over as well, but only if we're setting the link into a new root, + // as we don't want to lose the target any time we remove a link. + if (this.isLink && target && !this.target && root !== this) target.root = root // tree should always be valid upon root setter completion. treeCheck(this) + treeCheck(root) } get root () { diff --git a/node_modules/@npmcli/arborist/lib/printable.js b/node_modules/@npmcli/arborist/lib/printable.js index 588121dbc..169984fcf 100644 --- a/node_modules/@npmcli/arborist/lib/printable.js +++ b/node_modules/@npmcli/arborist/lib/printable.js @@ -2,6 +2,7 @@ // of the current node and its descendents const util = require('util') +const relpath = require('./relpath.js') class ArboristNode { constructor (tree, path) { @@ -47,6 +48,11 @@ class ArboristNode { .map(edge => new EdgeIn(edge))) } + if (tree.workspaces && tree.workspaces.size) { + this.workspaces = new Map([...tree.workspaces.entries()] + .map(([name, path]) => [name, relpath(tree.root.realpath, path)])) + } + // fsChildren sorted by path if (tree.fsChildren.size) { this.fsChildren = new Set([...tree.fsChildren] @@ -126,6 +132,9 @@ class EdgeIn extends Edge { } const printableTree = (tree, path = []) => { + if (!tree) + return tree + const Cls = tree.isLink ? ArboristLink : tree.sourceReference ? ArboristVirtualNode : ArboristNode diff --git a/node_modules/@npmcli/arborist/lib/tree-check.js b/node_modules/@npmcli/arborist/lib/tree-check.js index 00b43296f..a7e8d9c01 100644 --- a/node_modules/@npmcli/arborist/lib/tree-check.js +++ b/node_modules/@npmcli/arborist/lib/tree-check.js @@ -1,6 +1,8 @@ const debug = require('./debug.js') const checkTree = (tree, checkUnreachable = true) => { + const log = [['START TREE CHECK', tree.path]] + // this can only happen in tests where we have a "tree" object // that isn't actually a tree. if (!tree.root || !tree.root.inventory) @@ -9,8 +11,21 @@ const checkTree = (tree, checkUnreachable = true) => { const { inventory } = tree.root const seen = new Set() const check = (node, via = tree, viaType = 'self') => { + log.push([ + 'CHECK', + node && node.location, + via && via.location, + viaType, + 'seen=' + seen.has(node), + 'promise=' + !!(node && node.then), + 'root=' + !!(node && node.isRoot), + ]) + if (!node || seen.has(node) || node.then) return + + seen.add(node) + if (node.isRoot && node !== tree.root) { throw Object.assign(new Error('double root'), { node: node.path, @@ -19,6 +34,7 @@ const checkTree = (tree, checkUnreachable = true) => { root: tree.root.path, via: via.path, viaType, + log, }) } @@ -31,6 +47,7 @@ const checkTree = (tree, checkUnreachable = true) => { via: via.path, viaType, otherRoot: node.root && node.root.path, + log, }) } @@ -43,6 +60,7 @@ const checkTree = (tree, checkUnreachable = true) => { viaType, inventory: [...node.inventory.values()].map(node => [node.path, node.location]), + log, }) } @@ -53,6 +71,7 @@ const checkTree = (tree, checkUnreachable = true) => { root: tree.root.path, via: via.path, viaType, + log, }) } @@ -65,14 +84,38 @@ const checkTree = (tree, checkUnreachable = true) => { via: via.path, viaType, devEdges: devEdges.map(e => [e.type, e.name, e.spec, e.error]), + log, + }) + } + + if (node.path === tree.root.path && node !== tree.root) { + throw Object.assign(new Error('node with same path as root'), { + node: node.path, + tree: tree.path, + root: tree.root.path, + via: via.path, + viaType, + log, + }) + } + + if (!node.isLink && node.path !== node.realpath) { + throw Object.assign(new Error('non-link with mismatched path/realpath'), { + node: node.path, + tree: tree.path, + realpath: node.realpath, + root: tree.root.path, + via: via.path, + viaType, + log, }) } const { parent, fsParent, target } = node - seen.add(node) check(parent, node, 'parent') check(fsParent, node, 'fsParent') check(target, node, 'target') + log.push(['CHILDREN', node.location, ...node.children.keys()]) for (const kid of node.children.values()) check(kid, node, 'children') for (const kid of node.fsChildren) @@ -81,6 +124,7 @@ const checkTree = (tree, checkUnreachable = true) => { check(link, node, 'linksIn') for (const top of node.tops) check(top, node, 'tops') + log.push(['DONE', node.location]) } check(tree) if (checkUnreachable) { @@ -92,6 +136,7 @@ const checkTree = (tree, checkUnreachable = true) => { location: node.location, root: tree.root.path, tree: tree.path, + log, }) } } diff --git a/node_modules/@npmcli/arborist/package.json b/node_modules/@npmcli/arborist/package.json index e745be2c7..d08102ce0 100644 --- a/node_modules/@npmcli/arborist/package.json +++ b/node_modules/@npmcli/arborist/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/arborist", - "version": "2.2.9", + "version": "2.3.0", "description": "Manage node_modules trees", "dependencies": { "@npmcli/installed-package-contents": "^1.0.7", @@ -14,7 +14,7 @@ "cacache": "^15.0.3", "common-ancestor-path": "^1.0.1", "json-parse-even-better-errors": "^2.3.1", - "json-stringify-nice": "^1.1.1", + "json-stringify-nice": "^1.1.2", "mkdirp-infer-owner": "^2.0.0", "npm-install-checks": "^4.0.0", "npm-package-arg": "^8.1.0", |