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 c1f18af7e43dc..9328b8043bd4e 100644 --- a/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js +++ b/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js @@ -314,10 +314,11 @@ module.exports = cls => class IdealTreeBuilder extends cls { .then(async root => { if (!this[_updateAll] && !this[_global] && !root.meta.loadedFromDisk) { await new this.constructor(this.options).loadActual({ root }) + const tree = root.target || root // even though we didn't load it from a package-lock.json FILE, // we still loaded it "from disk", meaning we have to reset // dep flags before assuming that any mutations were reflected. - if (root.children.size) + if (tree.children.size) root.meta.loadedFromDisk = true } return root @@ -332,20 +333,28 @@ module.exports = cls => class IdealTreeBuilder extends cls { }) } - [_globalRootNode] () { - const root = this[_rootNodeFromPackage]({ dependencies: {} }) + async [_globalRootNode] () { + const root = await this[_rootNodeFromPackage]({ dependencies: {} }) // this is a gross kludge to handle the fact that we don't save // metadata on the root node in global installs, because the "root" // node is something like /usr/local/lib. const meta = new Shrinkwrap({ path: this.path }) meta.reset() root.meta = meta - return Promise.resolve(root) + return root } - [_rootNodeFromPackage] (pkg) { - return new Node({ + async [_rootNodeFromPackage] (pkg) { + // if the path doesn't exist, then we explode at this point. Note that + // this is not a problem for reify(), since it creates the root path + // before ever loading trees. + // TODO: make buildIdealTree() and loadActual handle a missing root path, + // or a symlink to a missing target, and let reify() create it as needed. + const real = await realpath(this.path, this[_rpcache], this[_stcache]) + const Cls = real === this.path ? Node : Link + const root = new Cls({ path: this.path, + realpath: real, pkg, extraneous: false, dev: false, @@ -355,12 +364,29 @@ module.exports = cls => class IdealTreeBuilder extends cls { global: this[_global], legacyPeerDeps: this.legacyPeerDeps, }) + if (root.isLink) { + root.target = new Node({ + path: real, + realpath: real, + pkg, + extraneous: false, + dev: false, + devOptional: false, + peer: false, + optional: false, + global: this[_global], + legacyPeerDeps: this.legacyPeerDeps, + root, + }) + } + return root } // process the add/rm requests by modifying the root node, and the // update.names request by queueing nodes dependent on those named. async [_applyUserRequests] (options) { process.emit('time', 'idealTree:userRequests') + const tree = this.idealTree.target || this.idealTree // 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. @@ -373,7 +399,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { const nm = resolve(this.path, 'node_modules') for (const name of await readdir(nm)) { if (this[_updateAll] || this[_updateNames].includes(name)) - this.idealTree.package.dependencies[name] = '*' + tree.package.dependencies[name] = '*' } } @@ -381,7 +407,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { this[_queueVulnDependents](options) if (options.rm && options.rm.length) { - addRmPkgDeps.rm(this.idealTree.package, options.rm) + addRmPkgDeps.rm(tree.package, options.rm) for (const name of options.rm) this[_explicitRequests].add(name) } @@ -391,7 +417,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { // triggers a refresh of all edgesOut if (options.add && options.add.length || options.rm && options.rm.length) - this.idealTree.package = this.idealTree.package + tree.package = tree.package process.emit('timeEnd', 'idealTree:userRequests') } @@ -410,8 +436,9 @@ module.exports = cls => class IdealTreeBuilder extends cls { this[_resolvedAdd] = 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: this.idealTree.package, + pkg: tree.package, add, saveBundle, saveType, @@ -514,7 +541,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { fixAvailable, } = topVuln for (const node of topNodes) { - if (node !== this.idealTree) { + if (node !== this.idealTree && node !== this.idealTree.target) { // not something we're going to fix, sorry. have to cd into // that directory and fix it yourself. this.log.warn('audit', 'Manual fix required in linked project ' + @@ -606,7 +633,7 @@ This is a one-time fix-up, please be patient... this.addTracker('idealTree:inflate') const queue = [] for (const node of inventory.values()) { - if (node.isRoot) + if (node.isProjectRoot) continue queue.push(async () => { @@ -646,11 +673,12 @@ This is a one-time fix-up, please be patient... // at this point we have a virtual tree with the actual root node's // package deps, which may be partly or entirely incomplete, invalid // or extraneous. - [_buildDeps] (node) { + [_buildDeps] () { process.emit('time', 'idealTree:buildDeps') - this[_depsQueue].push(this.idealTree) + const tree = this.idealTree.target || this.idealTree + this[_depsQueue].push(tree) this.log.silly('idealTree', 'buildDeps') - this.addTracker('idealTree', this.idealTree.name, '') + this.addTracker('idealTree', tree.name, '') return this[_buildDepStep]() .then(() => process.emit('timeEnd', 'idealTree:buildDeps')) } @@ -835,7 +863,7 @@ This is a one-time fix-up, please be patient... // loads a node from an edge, and then loads its peer deps (and their // peer deps, on down the line) into a virtual root parent. - [_nodeFromEdge] (edge, parent_) { + async [_nodeFromEdge] (edge, parent_, secondEdge = null) { // create a virtual root node with the same deps as the node that // is requesting this one, so that we can get all the peer deps in // a context where they're likely to be resolvable. @@ -843,22 +871,43 @@ This is a one-time fix-up, please be patient... const realParent = edge.peer ? edge.from.resolveParent : edge.from const spec = npa.resolve(edge.name, edge.spec, edge.from.path) - return this[_nodeFromSpec](edge.name, spec, parent, edge) - .then(node => { - // handle otherwise unresolvable dependency nesting loops by - // creating a symbolic link - // a1 -> b1 -> a2 -> b2 -> a1 -> ... - // instead of nesting forever, when the loop occurs, create - // a symbolic link to the earlier instance - for (let p = edge.from.resolveParent; p; p = p.resolveParent) { - if (p.matches(node) && !p.isRoot) - return new Link({ parent: realParent, target: p }) - } - // keep track of the thing that caused this node to be included. - const src = parent.sourceReference - this[_peerSetSource].set(node, src) - return this[_loadPeerSet](node) - }) + const first = await this[_nodeFromSpec](edge.name, spec, parent, edge) + + // we might have a case where the parent has a peer dependency on + // `foo@*` which resolves to v2, but another dep in the set has a + // peerDependency on `foo@1`. In that case, if we force it to be v2, + // we're unnecessarily triggering an ERESOLVE. + // If we have a second edge to worry about, and it's not satisfied + // by the first node, try a second and see if that satisfies the + // original edge here. + const spec2 = secondEdge && npa.resolve( + edge.name, + secondEdge.spec, + secondEdge.from.path + ) + const second = secondEdge && !secondEdge.valid + ? await this[_nodeFromSpec](edge.name, spec2, parent, secondEdge) + : null + + // pick the second one if they're both happy with that, otherwise first + const node = second && edge.valid ? second : first + // ensure the one we want is the one that's placed + node.parent = parent + + // handle otherwise unresolvable dependency nesting loops by + // creating a symbolic link + // a1 -> b1 -> a2 -> b2 -> a1 -> ... + // instead of nesting forever, when the loop occurs, create + // a symbolic link to the earlier instance + for (let p = edge.from.resolveParent; p; p = p.resolveParent) { + if (p.matches(node) && !p.isTop) + return new Link({ parent: realParent, target: p }) + } + + // keep track of the thing that caused this node to be included. + const src = parent.sourceReference + this[_peerSetSource].set(node, src) + return this[_loadPeerSet](node) } [_virtualRoot] (node, reuse = false) { @@ -886,7 +935,7 @@ This is a one-time fix-up, please be patient... // also skip over any nodes in the tree that failed to load, since those // will crash the install later on anyway. - const bd = node.isRoot ? null : node.package.bundleDependencies + const bd = node.isProjectRoot ? null : node.package.bundleDependencies const bundled = new Set(bd || []) return [...node.edgesOut.values()] @@ -923,7 +972,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.isRoot && this[_explicitRequests].has(edge.name)) + if (node.isProjectRoot && this[_explicitRequests].has(edge.name)) return true // No problems! @@ -1005,30 +1054,34 @@ This is a one-time fix-up, please be patient... // deps to override, but throw if no preference can be determined. async [_loadPeerSet] (node) { const peerEdges = [...node.edgesOut.values()] - // we only care about peers here, and don't install peerOptionals - .filter(e => e.peer && !e.valid && !e.optional) + // we typically only install non-optional peers, but we have to + // factor them into the peerSet so that we can avoid conflicts + .filter(e => e.peer && !(e.valid && e.to)) .sort(({name: a}, {name: b}) => a.localeCompare(b)) for (const edge of peerEdges) { // already placed this one, and we're happy with it. - if (edge.valid) + if (edge.valid && edge.to) continue const parentEdge = node.parent.edgesOut.get(edge.name) - const {isRoot, isWorkspace} = node.parent.sourceReference - const isMine = isRoot || isWorkspace - if (edge.missing) { + const {isProjectRoot, isWorkspace} = node.parent.sourceReference + const isMine = isProjectRoot || isWorkspace + if (!edge.to) { if (!parentEdge) { // easy, just put the thing there await this[_nodeFromEdge](edge, node.parent) continue } else { - // try to put the parent's preference, and make sure that satisfies. - // if so, we're good. - // if it does not, then we have a problem in strict mode, no problem + // if the parent's edge is very broad like >=1, and the edge in + // question is something like 1.x, then we want to get a 1.x, not + // a 2.x. pass along the child edge as an advisory guideline. + // if the parent edge doesn't satisfy the child edge, and the + // child edge doesn't satisfy the parent edge, then we have + // a conflict. this is always a problem in strict mode, never // in force mode, and a problem in non-strict mode if this isn't - // on behalf of the root node. In all such cases, we warn at least. - await this[_nodeFromEdge](parentEdge, node.parent) + // on behalf of our project. in all such cases, we warn at least. + await this[_nodeFromEdge](parentEdge, node.parent, edge) // hooray! that worked! if (edge.valid) @@ -1037,8 +1090,9 @@ This is a one-time fix-up, please be patient... // allow it if (this[_force] || !isMine && !this[_strictPeerDeps]) continue - else - this[_failPeerConflict](edge) + + // problem + this[_failPeerConflict](edge) } } @@ -1101,7 +1155,7 @@ This is a one-time fix-up, please be patient... // top nodes should still get peer deps from their fsParent if possible, // and only install locally if there's no other option, eg for a link // outside of the project root, or for a conflicted dep. - const start = edge.peer && !node.isRoot ? node.resolveParent || node + const start = edge.peer && !node.isProjectRoot ? node.resolveParent || node : node let target @@ -1137,7 +1191,8 @@ This is a one-time fix-up, please be patient... // when installing globally, or just in global style, we never place // deps above the first level. - if (this[_globalStyle] && check.resolveParent === this.idealTree) + const tree = this.idealTree && this.idealTree.target || this.idealTree + if (this[_globalStyle] && check.resolveParent === tree) break } @@ -1350,8 +1405,8 @@ This is a one-time fix-up, please be patient... // depends on a, and it has a conflict, it's our problem. So, the root // (or whatever is bringing in a) becomes the "effective source" for // the purposes of this calculation. - const { isRoot, isWorkspace } = isSource ? target : source || {} - const isMine = isRoot || isWorkspace + const { isProjectRoot, isWorkspace } = isSource ? target : source || {} + const isMine = isProjectRoot || isWorkspace // Useful testing thingie right here. // peerEntryEdge should *always* be a non-peer dependency, or a peer diff --git a/node_modules/@npmcli/arborist/lib/arborist/reify.js b/node_modules/@npmcli/arborist/lib/arborist/reify.js index d916b49c22c01..19c7fa384de51 100644 --- a/node_modules/@npmcli/arborist/lib/arborist/reify.js +++ b/node_modules/@npmcli/arborist/lib/arborist/reify.js @@ -1,5 +1,6 @@ // mixin implementing the reify method +const onExit = require('../signal-handling.js') const pacote = require('pacote') const rpj = require('read-package-json-fast') const { updateDepSpec } = require('../dep-spec.js') @@ -27,8 +28,9 @@ const updateRootPackageJson = require('../update-root-package-json.js') const _retiredPaths = Symbol('retiredPaths') const _retiredUnchanged = Symbol('retiredUnchanged') const _sparseTreeDirs = Symbol('sparseTreeDirs') +const _sparseTreeRoots = Symbol('sparseTreeRoots') const _savePrefix = Symbol('savePrefix') -const _retireShallowNodes = Symbol('retireShallowNodes') +const _retireShallowNodes = Symbol.for('retireShallowNodes') const _getBundlesByDepth = Symbol('getBundlesByDepth') const _registryResolved = Symbol('registryResolved') const _addNodeToTrashList = Symbol('addNodeToTrashList') @@ -54,7 +56,7 @@ const _awaitQuickAudit = Symbol('awaitQuickAudit') const _unpackNewModules = Symbol.for('unpackNewModules') const _moveContents = Symbol.for('moveContents') const _moveBackRetiredUnchanged = Symbol.for('moveBackRetiredUnchanged') -const _build = Symbol('build') +const _build = Symbol.for('build') const _removeTrash = Symbol.for('removeTrash') const _renamePath = Symbol.for('renamePath') const _rollbackRetireShallowNodes = Symbol.for('rollbackRetireShallowNodes') @@ -102,6 +104,7 @@ module.exports = cls => class Reifier extends cls { this[_retiredPaths] = {} this[_retiredUnchanged] = {} this[_sparseTreeDirs] = new Set() + this[_sparseTreeRoots] = new Set() this[_trashList] = new Set() } @@ -153,16 +156,63 @@ module.exports = cls => class Reifier extends cls { return this[_submitQuickAudit]() } - await this[_retireShallowNodes]() - await this[_createSparseTree]() - await this[_addOmitsToTrashList]() - await this[_loadShrinkwrapsAndUpdateTrees]() - await this[_loadBundlesAndUpdateTrees]() - await this[_submitQuickAudit]() - await this[_unpackNewModules]() - await this[_moveBackRetiredUnchanged]() - await this[_build]() + // ok, we're about to start touching the fs. need to roll back + // if we get an early termination. + let reifyTerminated = null + const removeHandler = onExit(({signal}) => { + // only call once. if signal hits twice, we just terminate + removeHandler() + reifyTerminated = Object.assign(new Error('process terminated'), { + signal, + }) + return false + }) + + // [rollbackfn, [...actions]] + // after each step, if the process was terminated, execute the rollback + // note that each rollback *also* calls the previous one when it's + // finished, and then the first one throws the error, so we only need + // a new rollback step when we have a new thing that must be done to + // revert the install. + const steps = [ + [_rollbackRetireShallowNodes, [ + _retireShallowNodes, + ]], + [_rollbackCreateSparseTree, [ + _createSparseTree, + _addOmitsToTrashList, + _loadShrinkwrapsAndUpdateTrees, + _loadBundlesAndUpdateTrees, + _submitQuickAudit, + _unpackNewModules, + ]], + [_rollbackMoveBackRetiredUnchanged, [ + _moveBackRetiredUnchanged, + _build, + ]], + ] + for (const [rollback, actions] of steps) { + for (const action of actions) { + try { + await this[action]() + if (reifyTerminated) + throw reifyTerminated + } catch (er) { + await this[rollback](er) + /* istanbul ignore next - rollback throws, should never hit this */ + throw er + } + } + } + + // no rollback for this one, just exit with the error, since the + // install completed and can't be safely recovered at this point. await this[_removeTrash]() + if (reifyTerminated) + throw reifyTerminated + + // done modifying the file system, no need to keep listening for sigs + removeHandler() } // when doing a local install, we load everything and figure it all out. @@ -183,8 +233,9 @@ module.exports = cls => class Reifier extends cls { const actualOpt = this[_global] ? { ignoreMissing: true, global: true, - filter: (node, kid) => !node.isRoot ? true - : (node.edgesOut.has(kid) || this[_explicitRequests].has(kid)), + filter: (node, kid) => !node.isRoot && node !== node.root.target + ? true + : (node.edgesOut.has(kid) || this[_explicitRequests].has(kid)), } : { ignoreMissing: true } if (!this[_global]) { @@ -260,7 +311,6 @@ module.exports = cls => class Reifier extends cls { const movePromises = Object.entries(moves) .map(([from, to]) => this[_renamePath](from, to)) return promiseAllRejectLate(movePromises) - .catch(er => this[_rollbackRetireShallowNodes](er)) .then(() => process.emit('timeEnd', 'reify:retireShallow')) } @@ -326,18 +376,22 @@ module.exports = cls => class Reifier extends cls { .map(diff => diff.ideal.path) return promiseAllRejectLate(dirs.map(d => mkdirp(d))) - .then(() => dirs.forEach(dir => this[_sparseTreeDirs].add(dir))) + .then(made => { + made.forEach(made => this[_sparseTreeRoots].add(made)) + dirs.forEach(dir => this[_sparseTreeDirs].add(dir)) + }) .then(() => process.emit('timeEnd', 'reify:createSparse')) - .catch(er => this[_rollbackCreateSparseTree](er)) } [_rollbackCreateSparseTree] (er) { process.emit('time', 'reify:rollback:createSparse') - // cut the roots of the sparse tree, not the leaves - const moves = this[_retiredPaths] + // cut the roots of the sparse tree that were created, not the leaves + const roots = this[_sparseTreeRoots] + // also delete the moves that we retired, so that we can move them back const failures = [] - const unlinks = Object.entries(moves) - .map(([from, to]) => rimraf(from).catch(er => failures.push([from, er]))) + const targets = [...roots, ...Object.keys(this[_retiredPaths])] + const unlinks = targets + .map(path => rimraf(path).catch(er => failures.push([path, er]))) return promiseAllRejectLate(unlinks) .then(() => { if (failures.length) @@ -375,7 +429,6 @@ module.exports = cls => class Reifier extends cls { .then(() => this[_diffTrees]()) .then(() => this[_createSparseTree]()) .then(() => process.emit('timeEnd', 'reify:loadShrinkwraps')) - .catch(er => this[_rollbackCreateSparseTree](er)) } // create a symlink for Links, extract for Nodes @@ -543,7 +596,6 @@ module.exports = cls => class Reifier extends cls { })))) // move onto the next level of bundled items .then(() => this[_loadBundlesAndUpdateTrees](depth + 1, bundlesByDepth)) - .catch(er => this[_rollbackCreateSparseTree](er)) } [_getBundlesByDepth] () { @@ -677,7 +729,6 @@ module.exports = cls => class Reifier extends cls { }) return promiseAllRejectLate(unpacks) .then(() => process.emit('timeEnd', 'reify:unpack')) - .catch(er => this[_rollbackCreateSparseTree](er)) } // This is the part where we move back the unchanging nodes that were @@ -720,7 +771,6 @@ module.exports = cls => class Reifier extends cls { })) })) .then(() => process.emit('timeEnd', 'reify:unretire')) - .catch(er => this[_rollbackMoveBackRetiredUnchanged](er)) } // move the contents from the fromPath to the node.path @@ -771,7 +821,6 @@ module.exports = cls => class Reifier extends cls { return this.rebuild({ nodes, handleOptionalFailure: true }) .then(() => process.emit('timeEnd', 'reify:build')) - .catch(er => this[_rollbackMoveBackRetiredUnchanged](er)) } // the tree is pretty much built now, so it's cleanup time. diff --git a/node_modules/@npmcli/arborist/lib/node.js b/node_modules/@npmcli/arborist/lib/node.js index 396bcb58a2de9..01147b9d48da8 100644 --- a/node_modules/@npmcli/arborist/lib/node.js +++ b/node_modules/@npmcli/arborist/lib/node.js @@ -64,70 +64,7 @@ const _meta = Symbol('_meta') const relpath = require('./relpath.js') const consistentResolve = require('./consistent-resolve.js') -// helper function to output a clearer visualization -// of the current node and its descendents -class ArboristNode {} - -const printableTree = (tree, path = []) => - (path.includes(tree) ? { location: tree.location } : (path.push(tree), Object.assign(new ArboristNode(), { - name: tree.name, - ...(tree.package && tree.package.version - ? { version: tree.package.version } - : {}), - location: tree.location, - path: tree.path, - realpath: tree.realpath, - ...(tree.isLink ? { target: printableTree(tree.target, path) } : {}), - ...(tree.resolved != null ? { resolved: tree.resolved } : {}), - ...(tree.extraneous ? { extraneous: true } : { - ...(tree.dev ? { dev: true } : {}), - ...(tree.optional ? { optional: true } : {}), - ...(tree.devOptional && !tree.dev && !tree.optional - ? { devOptional: true } : {}), - ...(tree.peer ? { peer: true } : {}), - }), - ...(tree.inBundle ? { bundled: true } : {}), - // handle top-level tree error - ...(tree.error - ? { - error: { - code: tree.error.code, - ...(tree.error.path - ? { path: tree.error.path } - : {}), - }, - } : {}), - // handle errors for each node - ...(tree.errors && tree.errors.length - ? { - errors: tree.errors.map(error => ({ - code: error.code, - ...(error.path - ? { path: error.path } - : {}), - })), - } : {}), - ...(tree.edgesIn && tree.edgesIn.size ? { - edgesIn: new Set([...tree.edgesIn] - .sort((a, b) => a.from.location.localeCompare(b.from.location))), - } : {}), - ...(tree.edgesOut && tree.edgesOut.size ? { - edgesOut: new Map([...tree.edgesOut.entries()] - .sort((a, b) => a[0].localeCompare(b[0]))), - } : {}), - ...(tree.fsChildren && tree.fsChildren.size ? { - fsChildren: new Set([...tree.fsChildren] - .sort((a, b) => a.path.localeCompare(b.path)) - .map(tree => printableTree(tree, path))), - } : {}), - ...(tree.target || !tree.children || !tree.children.size - ? {} - : { - children: new Map([...tree.children.entries()] - .sort((a, b) => a[0].localeCompare(b[0])) - .map(([name, tree]) => [name, printableTree(tree, path)])), - }), - }))) +const printableTree = require('./printable.js') class Node { constructor (options) { @@ -527,6 +464,10 @@ class Node { return this === this.root } + get isProjectRoot () { + return this === this.root || this === this.root.target + } + set root (root) { // setting to null means this is the new root // should only ever be one step diff --git a/node_modules/@npmcli/arborist/lib/printable.js b/node_modules/@npmcli/arborist/lib/printable.js new file mode 100644 index 0000000000000..fb73c7c2bc434 --- /dev/null +++ b/node_modules/@npmcli/arborist/lib/printable.js @@ -0,0 +1,129 @@ +// helper function to output a clearer visualization +// of the current node and its descendents + +const util = require('util') + +class ArboristNode { + constructor (tree, path) { + this.name = tree.name + if (tree.package.name && tree.package.name !== this.name) + this.packageName = tree.package.name + if (tree.version) + this.version = tree.version + this.location = tree.location + this.path = tree.path + if (tree.realpath !== this.path) + this.realpath = tree.realpath + if (tree.resolved !== null) + this.resolved = tree.resolved + if (tree.extraneous) + this.extraneous = true + if (tree.dev) + this.dev = true + if (tree.optional) + this.optional = true + if (tree.devOptional && !tree.dev && !tree.optional) + this.devOptional = true + if (tree.peer) + this.peer = true + if (tree.inBundle) + this.bundled = true + if (tree.error) + this.error = treeError(tree.error) + if (tree.errors && tree.errors.length) + this.errors = tree.errors.map(treeError) + + // edgesOut sorted by name + if (tree.edgesOut.size) { + this.edgesOut = new Map([...tree.edgesOut.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([name, edge]) => [name, new EdgeOut(edge)])) + } + + // edgesIn sorted by location + if (tree.edgesIn.size) { + this.edgesIn = new Set([...tree.edgesIn] + .sort((a, b) => a.from.location.localeCompare(b.from.location)) + .map(edge => new EdgeIn(edge))) + } + + // fsChildren sorted by path + if (tree.fsChildren.size) { + this.fsChildren = new Set([...tree.fsChildren] + .sort(({path: a}, {path: b}) => a.localeCompare(b)) + .map(tree => printableTree(tree, path))) + } + + // children sorted by name + if (tree.children.size) { + this.children = new Map([...tree.children.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([name, tree]) => [name, printableTree(tree, path)])) + } + } +} + +class ArboristLink extends ArboristNode { + constructor (tree, path) { + super(tree, path) + this.target = printableTree(tree.target, path) + } +} + +const treeError = ({code, path}) => ({ + code, + ...(path ? { path } : {}), +}) + +// print out edges without dumping the full node all over again +// this base class will toJSON as a plain old object, but the +// util.inspect() output will be a bit cleaner +class Edge { + constructor (edge) { + this.type = edge.type + this.name = edge.name + this.spec = edge.spec || '*' + if (edge.error) + this.error = edge.error + } +} + +// don't care about 'from' for edges out +class EdgeOut extends Edge { + constructor (edge) { + super(edge) + this.to = edge.to && edge.to.location + } + + [util.inspect.custom] () { + return `{ ${this.type} ${this.name}@${this.spec}${ + this.to ? ' -> ' + this.to : '' + }${ + this.error ? ' ' + this.error : '' + } }` + } +} + +// don't care about 'to' for edges in +class EdgeIn extends Edge { + constructor (edge) { + super(edge) + this.from = edge.from && edge.from.location + } + + [util.inspect.custom] () { + return `{ ${this.from || '""'} ${this.type} ${this.name}@${this.spec}${ + this.error ? ' ' + this.error : '' + } }` + } +} + +const printableTree = (tree, path = []) => { + if (path.includes(tree)) + return { location: tree.location } + path.push(tree) + const Cls = tree.isLink ? ArboristLink : ArboristNode + return new Cls(tree, path) +} + +module.exports = printableTree diff --git a/node_modules/@npmcli/arborist/lib/signal-handling.js b/node_modules/@npmcli/arborist/lib/signal-handling.js new file mode 100644 index 0000000000000..1051cd593970a --- /dev/null +++ b/node_modules/@npmcli/arborist/lib/signal-handling.js @@ -0,0 +1,67 @@ +const signals = require('./signals.js') + +// for testing, expose the process being used +module.exports = Object.assign(fn => setup(fn), { process }) + +// do all of this in a setup function so that we can call it +// multiple times for multiple reifies that might be going on. +// Otherwise, Arborist.reify() is a global action, which is a +// new constraint we'd be adding with this behavior. +const setup = fn => { + const { process } = module.exports + + const sigListeners = { loaded: false } + + const unload = () => { + if (!sigListeners.loaded) + return + for (const sig of signals) { + try { + process.removeListener(sig, sigListeners[sig]) + } catch (er) {} + } + process.removeListener('beforeExit', onBeforeExit) + sigListeners.loaded = false + } + + const onBeforeExit = () => { + // this trick ensures that we exit with the same signal we caught + // Ie, if you press ^C and npm gets a SIGINT, we'll do the rollback + // and then exit with a SIGINT signal once we've removed the handler. + // The timeout is there because signals are asynchronous, so we need + // the process to NOT exit on its own, which means we have to have + // something keeping the event loop looping. Hence this hack. + unload() + process.kill(process.pid, signalReceived) + setTimeout(() => {}, 500) + } + + let signalReceived = null + const listener = (sig, fn) => () => { + signalReceived = sig + + // if we exit normally, but caught a signal which would have been fatal, + // then re-send it once we're done with whatever cleanup we have to do. + unload() + if (process.listeners(sig).length < 1) + process.once('beforeExit', onBeforeExit) + + fn({ signal: sig }) + } + + // do the actual loading here + for (const sig of signals) { + sigListeners[sig] = listener(sig, fn) + const max = process.getMaxListeners() + try { + // if we call this a bunch of times, avoid triggering the warning + const { length } = process.listeners(sig) + if (length >= max) + process.setMaxListeners(length + 1) + process.on(sig, sigListeners[sig]) + } catch (er) {} + } + sigListeners.loaded = true + + return unload +} diff --git a/node_modules/@npmcli/arborist/lib/signals.js b/node_modules/@npmcli/arborist/lib/signals.js new file mode 100644 index 0000000000000..8dcd585c4c065 --- /dev/null +++ b/node_modules/@npmcli/arborist/lib/signals.js @@ -0,0 +1,58 @@ +// copied from signal-exit + +// This is not the set of all possible signals. +// +// It IS, however, the set of all signals that trigger +// an exit on either Linux or BSD systems. Linux is a +// superset of the signal names supported on BSD, and +// the unknown signals just fail to register, so we can +// catch that easily enough. +// +// Don't bother with SIGKILL. It's uncatchable, which +// means that we can't fire any callbacks anyway. +// +// If a user does happen to register a handler on a non- +// fatal signal like SIGWINCH or something, and then +// exit, it'll end up firing `process.emit('exit')`, so +// the handler will be fired anyway. +// +// SIGBUS, SIGFPE, SIGSEGV and SIGILL, when not raised +// artificially, inherently leave the process in a +// state from which it is not safe to try and enter JS +// listeners. + +const platform = global.__ARBORIST_FAKE_PLATFORM__ || process.platform + +module.exports = [ + 'SIGABRT', + 'SIGALRM', + 'SIGHUP', + 'SIGINT', + 'SIGTERM', +] + +if (platform !== 'win32') { + module.exports.push( + 'SIGVTALRM', + 'SIGXCPU', + 'SIGXFSZ', + 'SIGUSR2', + 'SIGTRAP', + 'SIGSYS', + 'SIGQUIT', + 'SIGIOT' + // should detect profiler and enable/disable accordingly. + // see #21 + // 'SIGPROF' + ) +} + +if (platform === 'linux') { + module.exports.push( + 'SIGIO', + 'SIGPOLL', + 'SIGPWR', + 'SIGSTKFLT', + 'SIGUNUSED' + ) +} diff --git a/node_modules/@npmcli/arborist/package.json b/node_modules/@npmcli/arborist/package.json index fafd1fb0f865f..bf0de29939182 100644 --- a/node_modules/@npmcli/arborist/package.json +++ b/node_modules/@npmcli/arborist/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/arborist", - "version": "2.0.6", + "version": "2.1.0", "description": "Manage node_modules trees", "dependencies": { "@npmcli/installed-package-contents": "^1.0.5", diff --git a/package-lock.json b/package-lock.json index 233dee00b1f2c..6b8750e3102d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -358,7 +358,7 @@ ], "license": "Artistic-2.0", "dependencies": { - "@npmcli/arborist": "^2.0.6", + "@npmcli/arborist": "^2.1.0", "@npmcli/ci-detect": "^1.2.0", "@npmcli/config": "^1.2.8", "@npmcli/run-script": "^1.8.1", @@ -687,9 +687,9 @@ } }, "node_modules/@npmcli/arborist": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.0.6.tgz", - "integrity": "sha512-3VF6rr3TlGABVZHksblQCcG+aXvsND+pdkUc7vKsKyvY5DB1b6QxXUHwJTPTZz7hKvFM5GQPewp8OxMUdMDMRQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.1.0.tgz", + "integrity": "sha512-ltBA6olA04/Gt1KJ2YTE5V0Bxi2U4to7psst6JFlRHBfqxE6LiHKbqqiIRXB5qmW0c+26LOR9ocH+NxKjddX8w==", "inBundle": true, "dependencies": { "@npmcli/installed-package-contents": "^1.0.5", @@ -9924,9 +9924,9 @@ } }, "@npmcli/arborist": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.0.6.tgz", - "integrity": "sha512-3VF6rr3TlGABVZHksblQCcG+aXvsND+pdkUc7vKsKyvY5DB1b6QxXUHwJTPTZz7hKvFM5GQPewp8OxMUdMDMRQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.1.0.tgz", + "integrity": "sha512-ltBA6olA04/Gt1KJ2YTE5V0Bxi2U4to7psst6JFlRHBfqxE6LiHKbqqiIRXB5qmW0c+26LOR9ocH+NxKjddX8w==", "requires": { "@npmcli/installed-package-contents": "^1.0.5", "@npmcli/map-workspaces": "^1.0.1", diff --git a/package.json b/package.json index 11dc23dcefa7d..25b5e6c8c77c6 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@npmcli/arborist": "^2.0.6", + "@npmcli/arborist": "^2.1.0", "@npmcli/ci-detect": "^1.2.0", "@npmcli/config": "^1.2.8", "@npmcli/run-script": "^1.8.1",