diff --git a/node_modules/@npmcli/arborist/README.md b/node_modules/@npmcli/arborist/README.md index a760156b823a5..c8efd37b74741 100644 --- a/node_modules/@npmcli/arborist/README.md +++ b/node_modules/@npmcli/arborist/README.md @@ -46,8 +46,6 @@ const arb = new Arborist({ // rather unusual pattern: '//registry.foo.com:token': 'blahblahblah', '//basic.auth.only.foo.com:_auth': 'aXNhYWNzOm5vdCBteSByZWFsIHBhc3N3b3Jk', - - // in fact, ANY config can be scoped to a registry... '//registry.foo.com:always-auth': true, }) 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 fac0623d2933a..ee3f38a2f178f 100644 --- a/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js +++ b/node_modules/@npmcli/arborist/lib/arborist/build-ideal-tree.js @@ -6,6 +6,7 @@ const cacache = require('cacache') const semver = require('semver') const pickManifest = require('npm-pick-manifest') const promiseCallLimit = require('promise-call-limit') +const getPeerSet = require('../peer-set.js') const fromPath = require('../from-path.js') const calcDepFlags = require('../calc-dep-flags.js') @@ -36,6 +37,7 @@ const _depsQueue = Symbol('depsQueue') const _currentDep = Symbol('currentDep') const _updateAll = Symbol('updateAll') const _mutateTree = Symbol('mutateTree') +const _flagsSuspect = Symbol.for('flagsSuspect') const _prune = Symbol('prune') const _preferDedupe = Symbol('preferDedupe') const _legacyBundling = Symbol('legacyBundling') @@ -76,6 +78,9 @@ const _globalRootNode = Symbol('globalRootNode') const _isVulnerable = Symbol.for('isVulnerable') const _usePackageLock = Symbol.for('usePackageLock') +// used for the ERESOLVE error to show the last peer conflict encountered +const _peerConflict = Symbol('peerConflict') + // used by Reify mixin const _force = Symbol.for('force') const _explicitRequests = Symbol.for('explicitRequests') @@ -122,6 +127,7 @@ module.exports = cls => class IdealTreeBuilder extends cls { this[_loadFailures] = new Set() this[_linkNodes] = new Set() this[_manifests] = new Map() + this[_peerConflict] = null } get explicitRequests () { @@ -661,7 +667,7 @@ This is a one-time fix-up, please be patient... const { legacyPeerDeps } = this parent = parent || new Node({ path: '/virtual-root', - pkg: edge.from.package, + sourceReference: edge.from, legacyPeerDeps, }) @@ -700,18 +706,24 @@ This is a one-time fix-up, please be patient... node.isRoot && this[_explicitRequests].has(edge.name))) } - [_fetchManifest] (spec) { + async [_fetchManifest] (spec) { + const options = { + ...this.options, + avoid: this[_avoidRange](spec.name), + } + // get the intended spec and stored metadata from yarn.lock file, + // if available and valid. + spec = this.idealTree.meta.checkYarnLock(spec, options) + if (this[_manifests].has(spec.raw)) return this[_manifests].get(spec.raw) else { - const options = { - ...this.options, - avoid: this[_avoidRange](spec.name), - } - // get the intended spec and stored metadata from yarn.lock file, - // if available and valid. - spec = this.idealTree.meta.checkYarnLock(spec, options) + this.log.silly('fetch manifest', spec.raw) const p = pacote.manifest(spec, options) + .then(mani => { + this[_manifests].set(spec.raw, mani) + return mani + }) this[_manifests].set(spec.raw, p) return p } @@ -728,6 +740,7 @@ This is a one-time fix-up, please be patient... : this[_fetchManifest](spec) .then(pkg => new Node({ name, pkg, parent, legacyPeerDeps }), error => { error.requiredBy = edge.from.location || '.' + // failed to load the spec, either because of enotarget or // fetch failure of some other sort. save it so we can verify // later that it's optional, otherwise the error is fatal. @@ -787,16 +800,25 @@ This is a one-time fix-up, please be patient... let target let canPlace = null + let warnPeer = false for (let check = start; check; check = check.resolveParent) { const cp = this[_canPlaceDep](dep, check, edge, peerEntryEdge) - this.log.silly('placeDep', check.location, `${edge.name}@${edge.spec}`, cp, `for: ${node.package._id}`) // anything other than a conflict is fine to proceed with if (cp !== CONFLICT) { canPlace = cp target = check - } else + } else { + if (check === start) { + // if it's a peer dep, and the first place we're putting it conflicts + // because the node has a direct dependency on the pkg in question, + // then we treat that as an override when --force is applied, and + // just warn about it. + const checkEdge = check.edgesOut.get(edge.name) + warnPeer = check === start && edge.peer && checkEdge + } break + } // nest packages like npm v1 and v2 // very disk-inefficient @@ -810,23 +832,41 @@ This is a one-time fix-up, please be patient... } if (!target) { - const current = node.resolve(edge.name) - throw Object.assign(new Error('unable to resolve dependency tree'), { + const curNode = node.resolve(edge.name) + const pc = this[_peerConflict] || { peer: null, current: null } + // we'll only get one of these + const current = curNode ? curNode.explain() : pc.current + const peerConflict = pc.peer + const expl = { code: 'ERESOLVE', - package: edge.name, - spec: edge.spec, + dep: dep.explain(edge), + current, + peerConflict, + fixWithForce: edge.peer && !!warnPeer, type: edge.type, - requiredBy: node.package._id, - location: node.path, - ...(!current ? {} : { - current: { - pkgid: current.package._id, - location: current.location, - }, - }) - }) + isPeer: edge.peer, + } + + if (this[_force] && expl.fixWithForce) { + this.log.warn('ERESOLVE', 'overriding peer dependency', expl) + return [] + } else { + const er = new Error('unable to resolve dependency tree') + throw Object.assign(er, expl) + } } + this.log.silly( + 'placeDep', + target.location || 'ROOT', + `${edge.name}@${edge.spec}`, + canPlace, + `for: ${node.package._id || node.location}` + ) + + // it worked, so we clearly have no peer conflicts at this point. + this[_peerConflict] = null + // Can only get KEEP here if the original edge was valid, // and we're checking for an update but it's already up to date. if (canPlace === KEEP) { @@ -854,6 +894,15 @@ This is a one-time fix-up, please be patient... } dep.replace(oldChild) this[_pruneForReplacement](dep, oldDeps) + // this may also create some invalid edges, for example if we're + // intentionally causing something to get nested which was previously + // placed in this location. + for (const edge of dep.edgesIn) { + if (edge.invalid) { + this[_depsQueue].push(edge.from) + this[_depsSeen].delete(edge.from) + } + } } else dep.parent = target @@ -876,7 +925,6 @@ This is a one-time fix-up, please be patient... // in case we just made some duplicates that can be removed, // prune anything deeper in the tree that can be replaced by this if (this.idealTree) { - for (const node of this.idealTree.inventory.query('name', dep.name)) { if (node !== dep && node.isDescendantOf(target) && @@ -908,14 +956,25 @@ This is a one-time fix-up, please be patient... // note that dep has now been removed from the virtualRoot set // by virtue of being placed in the target's node_modules. if (virtualRoot) { + const peers = [] + // double loop so that we don't yank things out and then fail to find + // them in the virtualRoot's children. for (const peerEdge of dep.edgesOut.values()) { + // XXX needs some rework if (peerEdge.peer && !peerEdge.valid) { const peer = virtualRoot.children.get(peerEdge.name) - const peerPlaced = this[_placeDep]( - peer, dep, peerEdge, peerEntryEdge || edge) - placed.push(...peerPlaced) + || /* istanbul ignore next - should be impossible */ peerEdge.to + /* istanbul ignore else - should be impossible */ + if (peer) + peers.push([peer, peerEdge]) } } + + for (const [peer, peerEdge] of peers) { + const peerPlaced = this[_placeDep]( + peer, dep, peerEdge, peerEntryEdge || edge) + placed.push(...peerPlaced) + } } return placed @@ -1004,7 +1063,46 @@ This is a one-time fix-up, please be patient... // last try, if we prefer deduplication over novelty, check to see if // this (older) dep can satisfy the needs of the less nested instance if (this[_preferDedupe] && current.canReplaceWith(dep)) { - return this[_canPlacePeers](dep, target, edge, REPLACE, peerEntryEdge) + const res = this[_canPlacePeers](dep, target, edge, REPLACE, peerEntryEdge) + /* istanbul ignore else - It's extremely rare that a replaceable + * node would be a conflict, if the current one wasn't a conflict, + * but it is theoretically possible if peer deps are pinned. In + * that case we treat it like any other conflict, and keep trying */ + if (res !== CONFLICT) + return res + } + + // if this is a peer dep, AND target is the resolveParent of the edge, + // then this is the only place it can go. If the current node is not + // a non-peer dependency of this specific target, then it can replace + // and dupe it deeper in the tree. If the current node is a peer dep + // in a set that is a non-peer dep of a deeper target, then replace + // the whole peer set and the module bringing it in, and add the + // dependent to the queue for re-evaluation. + if (edge.peer && target === edge.from.resolveParent && !peerEntryEdge) { + const peerSet = getPeerSet(current) + for (const p of peerSet) { + // if any have a non-peer dep from the target, or a peer dep if + // the target is root, then we can't safely replace. + for (const edge of p.edgesIn) { + if (edge.peer) { + // root deps take precedence always. + // other peer deps on this node are irrelevant though. + if (edge.from.isRoot) { + return CONFLICT + } + continue + } + // note that we MAY resolve this conflict by using the target's + // conflicting dep on the peer, if --force is set. + if (edge.from === target) { + return CONFLICT + } + } + } + // all peers could be nested deeper in the tree, so replace + // adding to the queue will happen later when we scan dep's edgesIn + return REPLACE } // no agreement could be reached :( @@ -1068,8 +1166,14 @@ This is a one-time fix-up, please be patient... being cautious */ if (peerEdge) { const canPlacePeer = this[_canPlaceDep](peer, target, peerEdge, edge) - if (canPlacePeer === CONFLICT) + if (canPlacePeer === CONFLICT) { + const current = target.resolve(peer.name) + this[_peerConflict] = { + peer: peer.explain(peerEdge), + current: current && current.explain(), + } return CONFLICT + } } } } @@ -1137,6 +1241,8 @@ This is a one-time fix-up, please be patient... [_fixDepFlags] () { process.emit('time', 'idealTree:fixDepFlags') const metaFromDisk = this.idealTree.meta.loadedFromDisk + const flagsSuspect = this[_flagsSuspect] + const mutateTree = this[_mutateTree] // if the options set prune:false, then we don't prune, but we still // mark the extraneous items in the tree if we modified it at all. // If we did no modifications, we just iterate over the extraneous nodes. @@ -1144,7 +1250,7 @@ This is a one-time fix-up, please be patient... // all set to true, and there can be nothing extraneous, so there's // nothing to prune, because we built it from scratch. if we didn't // add or remove anything, then also nothing to do. - if (metaFromDisk && this[_mutateTree]) + if (metaFromDisk && mutateTree) this[_resetDepFlags]() // update all the dev/optional/etc flags in the tree @@ -1153,7 +1259,7 @@ This is a one-time fix-up, please be patient... // // if we started from a blank slate, or changed something, then // the dep flags will be all set to true. - if (!metaFromDisk || this[_mutateTree]) + if (!metaFromDisk || mutateTree) calcDepFlags(this.idealTree) else { // otherwise just unset all the flags on the root node @@ -1169,9 +1275,9 @@ This is a one-time fix-up, please be patient... // if we started from a shrinkwrap, and then added/removed something, // then the tree is suspect. Prune what is marked as extraneous. // otherwise, don't bother. - if (this[_prune] && metaFromDisk && this[_mutateTree]) { + const needPrune = metaFromDisk && (mutateTree || flagsSuspect) + if (this[_prune] && needPrune) this[_idealTreePrune]() - } process.emit('timeEnd', 'idealTree:fixDepFlags') } diff --git a/node_modules/@npmcli/arborist/lib/arborist/load-virtual.js b/node_modules/@npmcli/arborist/lib/arborist/load-virtual.js index 435619e321ce5..c11b38ca6be1c 100644 --- a/node_modules/@npmcli/arborist/lib/arborist/load-virtual.js +++ b/node_modules/@npmcli/arborist/lib/arborist/load-virtual.js @@ -10,6 +10,7 @@ const Shrinkwrap = require('../shrinkwrap.js') const Node = require('../node.js') const Link = require('../link.js') const relpath = require('../relpath.js') +const calcDepFlags = require('../calc-dep-flags.js') const rpj = require('read-package-json-fast') const loadFromShrinkwrap = Symbol('loadFromShrinkwrap') @@ -20,6 +21,12 @@ const loadRoot = Symbol('loadRoot') const loadNode = Symbol('loadVirtualNode') const loadLink = Symbol('loadVirtualLink') const loadWorkspaces = Symbol('loadWorkspaces') +const flagsSuspect = Symbol.for('flagsSuspect') +const reCalcDepFlags = Symbol('reCalcDepFlags') +const checkRootEdges = Symbol('checkRootEdges') + +const depsToEdges = (type, deps) => + Object.entries(deps).map(d => [type, ...d]) module.exports = cls => class VirtualLoader extends cls { constructor (options) { @@ -27,6 +34,7 @@ module.exports = cls => class VirtualLoader extends cls { // the virtual tree we load from a shrinkwrap this.virtualTree = options.virtualTree + this[flagsSuspect] = false } // public method @@ -71,14 +79,86 @@ module.exports = cls => class VirtualLoader extends cls { root.optional = false root.devOptional = false root.peer = false + this[checkRootEdges](s, root) s.add(root) this.virtualTree = root const {links, nodes} = this[resolveNodes](s, root) await this[resolveLinks](links, nodes) this[assignParentage](nodes) + if (this[flagsSuspect]) + this[reCalcDepFlags]() return root } + [reCalcDepFlags] () { + // reset all dep flags + for (const node of this.virtualTree.inventory.values()) { + node.extraneous = true + node.dev = true + node.optional = true + node.devOptional = true + node.peer = true + } + calcDepFlags(this.virtualTree, true) + } + + // check the lockfile deps, and see if they match. if they do not + // then we have to reset dep flags at the end. for example, if the + // user manually edits their package.json file, then we need to know + // that the idealTree is no longer entirely trustworthy. + [checkRootEdges] (s, root) { + // loaded virtually from tree, no chance of being out of sync + // ancient lockfiles are critically damaged by this process, + // so we need to just hope for the best in those cases. + if (!s.loadedFromDisk || s.ancientLockfile) + return + + const lock = s.get('') + const prod = lock.dependencies || {} + const dev = lock.devDependencies || {} + const optional = lock.optionalDependencies || {} + const peer = lock.peerDependencies || {} + const peerOptional = {} + if (lock.peerDependenciesMeta) { + for (const [name, meta] of Object.entries(lock.peerDependenciesMeta)) { + if (meta.optional && peer[name] !== undefined) { + peerOptional[name] = peer[name] + delete peer[name] + } + } + } + for (const name of Object.keys(optional)) { + delete prod[name] + } + + const lockEdges = [ + ...depsToEdges('prod', prod), + ...depsToEdges('dev', dev), + ...depsToEdges('optional', optional), + ...depsToEdges('peer', peer), + ...depsToEdges('peerOptional', peerOptional), + ].sort(([atype, aname], [btype, bname]) => + atype.localeCompare(btype) || aname.localeCompare(bname)) + + const rootEdges = [...root.edgesOut.values()] + .map(e => [e.type, e.name, e.spec]) + .sort(([atype, aname], [btype, bname]) => + atype.localeCompare(btype) || aname.localeCompare(bname)) + + if (rootEdges.length !== lockEdges.length) { + // something added or removed + return this[flagsSuspect] = true + } + + for (let i = 0; i < lockEdges.length; i++) { + if (rootEdges[i][0] !== lockEdges[i][0] || + rootEdges[i][1] !== lockEdges[i][1] || + rootEdges[i][2] !== lockEdges[i][2]) { + return this[flagsSuspect] = true + } + } + } + // separate out link metadatas, and create Node objects for nodes [resolveNodes] (s, root) { const links = new Map() @@ -186,7 +266,7 @@ module.exports = cls => class VirtualLoader extends cls { [loadWorkspaces] (node, s) { const workspaces = mapWorkspaces.virtual({ cwd: node.path, - lockfile: s.data + lockfile: s.data, }) if (workspaces.size) node.workspaces = workspaces diff --git a/node_modules/@npmcli/arborist/lib/node.js b/node_modules/@npmcli/arborist/lib/node.js index f598479365e06..4bd4320fbed41 100644 --- a/node_modules/@npmcli/arborist/lib/node.js +++ b/node_modules/@npmcli/arborist/lib/node.js @@ -58,6 +58,9 @@ const _refreshPath = Symbol('_refreshPath') const _delistFromMeta = Symbol('_delistFromMeta') const _global = Symbol.for('global') const _workspaces = Symbol('_workspaces') +const _explain = Symbol('_explain') +const _explainEdge = Symbol('_explainEdge') +const _explanation = Symbol('_explanation') const relpath = require('./relpath.js') const consistentResolve = require('./consistent-resolve.js') @@ -89,6 +92,7 @@ class Node { peer = true, global = false, dummy = false, + sourceReference = null, } = options // true if part of a global install @@ -97,7 +101,13 @@ class Node { this[_workspaces] = null this.errors = error ? [error] : [] - const pkg = normalize(options.pkg || {}) + + // this will usually be null, except when modeling a + // package's dependencies in a virtual root. + this.sourceReference = sourceReference + + const pkg = sourceReference ? sourceReference.package + : normalize(options.pkg || {}) this.name = name || nameFromFolder(path || pkg.name || realpath) || @@ -230,11 +240,11 @@ class Node { return this.global && this.parent.isRoot } - get workspaces() { + get workspaces () { return this[_workspaces] } - set workspaces(workspaces) { + set workspaces (workspaces) { // deletes edges if they already exists if (this[_workspaces]) for (const [name, path] of this[_workspaces].entries()) { @@ -291,6 +301,7 @@ class Node { edge.detach() } + this[_explanation] = null this[_package] = pkg this[_loadWorkspaces]() this[_loadDeps]() @@ -299,6 +310,90 @@ class Node { this.edgesIn.forEach(edge => edge.reload(true)) } + // node.explain(nodes seen already, edge we're trying to satisfy + // if edge is not specified, it lists every edge into the node. + explain (edge = null, seen = []) { + if (this[_explanation]) + return this[_explanation] + + return this[_explanation] = this[_explain](edge, seen) + } + + [_explain] (edge, seen) { + if (this.isRoot && !this.sourceReference) { + return { + location: this.path + } + } + + const why = { + name: this.isRoot ? this.package.name : this.name, + version: this.package.version, + } + if (this.errors.length || !this.package.name || !this.package.version) { + why.errors = this.errors.length ? this.errors : [ + new Error('invalid package: lacks name and/or version') + ] + why.package = this.package + } + + if (this.root.sourceReference) { + const {name, version} = this.root.package + why.whileInstalling = { + name, + version, + } + if (edge) + this[_explainEdge](edge, seen) + } + + if (this.sourceReference) + return this.sourceReference.explain(edge, seen) + + if (seen.includes(this)) + return why + + why.location = this.location + + // make a new list each time. we can revisit, but not loop. + seen = seen.concat(this) + + why.dependents = [] + if (edge) { + why.dependents.push(this[_explainEdge](edge, seen)) + } else { + // if we have an edge from the root, just show that, and stop there + // no need to go deeper, because it doesn't provide much more value. + const edges = [] + for (const edge of this.edgesIn) { + if (!edge.valid && !edge.from.isRoot) + continue + + if (edge.from.isRoot) { + edges.length = 0 + edges.push(edge) + break + } + + edges.push(edge) + } + for (const edge of edges) { + why.dependents.push(this[_explainEdge](edge, seen)) + } + } + return why + } + + // return the edge data, and an explanation of how that edge came to be here + [_explainEdge] (edge, seen) { + return { + type: edge.type, + spec: edge.spec, + ...(edge.error ? { error: edge.error } : {}), + from: edge.from.explain(null, seen), + } + } + isDescendantOf (node) { for (let p = this; p; p = p.parent) { if (p === node) @@ -495,6 +590,7 @@ class Node { // called when we find that we have an fsParent which could account // for some missing edges which are actually fine and not missing at all. [_reloadEdges] (filter) { + this[_explanation] = null this.edgesOut.forEach(edge => filter(edge) && edge.reload()) this.fsChildren.forEach(c => c[_reloadEdges](filter)) this.children.forEach(c => c[_reloadEdges](filter)) diff --git a/node_modules/@npmcli/arborist/lib/peer-set.js b/node_modules/@npmcli/arborist/lib/peer-set.js new file mode 100644 index 0000000000000..727814e1de3f0 --- /dev/null +++ b/node_modules/@npmcli/arborist/lib/peer-set.js @@ -0,0 +1,25 @@ +// when we have to dupe a set of peer dependencies deeper into the tree in +// order to make room for a dep that would otherwise conflict, we use +// this to get the set of all deps that have to be checked to ensure +// nothing is locking them into the current location. +// +// this is different in its semantics from an "optional set" (ie, the nodes +// that should be removed if an optional dep fails), because in this case, +// we specifically intend to include deps in the peer set that have +// dependants outside the set. +const peerSet = node => { + const set = new Set([node]) + for (const node of set) { + for (const edge of node.edgesOut.values()) { + if (edge.valid && edge.peer && edge.to) + set.add(edge.to) + } + for (const edge of node.edgesIn) { + if (edge.valid && edge.peer) + set.add(edge.from) + } + } + return set +} + +module.exports = peerSet diff --git a/node_modules/@npmcli/arborist/package.json b/node_modules/@npmcli/arborist/package.json index d09953ababb34..153edb7415d58 100644 --- a/node_modules/@npmcli/arborist/package.json +++ b/node_modules/@npmcli/arborist/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/arborist", - "version": "0.0.19", + "version": "0.0.20", "description": "Manage node_modules trees", "dependencies": { "@npmcli/installed-package-contents": "^1.0.5", @@ -22,8 +22,9 @@ "read-package-json-fast": "^1.2.1", "readdir-scoped-modules": "^1.1.0", "semver": "^7.1.2", - "treeverse": "^1.0.1", - "walk-up-path": "^1.0.0" + "treeverse": "^1.0.4", + "walk-up-path": "^1.0.0", + "json-parse-even-better-errors": "^2.3.1" }, "devDependencies": { "minify-registry-metadata": "^2.1.0", @@ -57,6 +58,6 @@ ], "coverage-map": "map.js", "esm": false, - "timeout": "60" + "timeout": "120" } } diff --git a/node_modules/treeverse/README.md b/node_modules/treeverse/README.md index 485685af922e6..ce08381dae25b 100644 --- a/node_modules/treeverse/README.md +++ b/node_modules/treeverse/README.md @@ -110,3 +110,20 @@ to the result of visiting (and leaving) the top node in the tree. * `filter` - Filter out child nodes from the traversal. Note that this filters the entire branch of the tree, not just that one node. That is, children of filtered nodes are not traversed either. + +## STACK DEPTH WARNING + +When a `leave` method is specified, then recursion is used, because +maintaining state otherwise is challenging. This means that using `leave` +with a synchronous depth first traversal of very deeply nested trees will +result in stack overflow errors. + +To avoid this, either make one or more of the functions async, or do all of +the work in the `visit` method. + +Breadth-first traversal always uses a loop, and is stack-safe. + +It is _possible_ to implement depth first traversal with a leave method +using a loop rather than recursion, but maintaining the `leave(node, +[children])` API surface would be challenging, and is not implemented at +this time. diff --git a/node_modules/treeverse/lib/breadth.js b/node_modules/treeverse/lib/breadth.js index d2f8539bdabbe..56c02ec627c8c 100644 --- a/node_modules/treeverse/lib/breadth.js +++ b/node_modules/treeverse/lib/breadth.js @@ -11,18 +11,28 @@ const breadth = ({ visit, filter = () => true, - seen = new Map(), getChildren, tree, - queue = [], }) => { + const queue = [] + const seen = new Map() + + const next = () => { + while (queue.length) { + const node = queue.shift() + const res = visitNode(node) + if (isPromise(res)) { + return res.then(() => next()) + } + } + return seen.get(tree) + } const visitNode = (tree) => { if (seen.has(tree)) return seen.get(tree) seen.set(tree, null) - const res = visit ? visit(tree) : tree if (isPromise(res)) { const fullResult = res.then(res => { @@ -42,22 +52,13 @@ const breadth = ({ return isPromise(kids) ? kids.then(processKids) : processKids(kids) } - const processKids = kids => { + const processKids = (kids) => { kids = (kids || []).filter(filter) queue.push(...kids) - return next() - } - - const next = () => { - while (queue.length) { - const res = visitNode(queue.shift()) - if (isPromise(res)) - return res.then(() => next()) - } - return seen.get(tree) } - return visitNode(tree) + queue.push(tree) + return next() } const isPromise = p => p && typeof p.then === 'function' diff --git a/node_modules/treeverse/lib/depth-descent.js b/node_modules/treeverse/lib/depth-descent.js new file mode 100644 index 0000000000000..8ac3af014ebf9 --- /dev/null +++ b/node_modules/treeverse/lib/depth-descent.js @@ -0,0 +1,87 @@ +// Perform a depth-first walk of a tree, ONLY doing the descent (visit) +// +// This uses a stack rather than recursion, so that it can handle deeply +// nested trees without call stack overflows. (My kingdom for proper TCO!) +// +// This is only used for cases where leave() is not specified. +// +// a +// +-- b +// | +-- 1 +// | +-- 2 +// +-- c +// +-- 3 +// +-- 4 +// +// Expect: +// visit a +// visit b +// visit 1 +// visit 2 +// visit c +// visit 3 +// visit 4 +// +// stack.push(tree) +// while stack not empty +// pop T from stack +// VISIT(T) +// get children C of T +// push each C onto stack + +const depth = ({ + visit, + filter, + getChildren, + tree, +}) => { + const stack = [] + const seen = new Map() + + const next = () => { + while (stack.length) { + const node = stack.pop() + const res = visitNode(node) + if (isPromise(res)) { + return res.then(() => next()) + } + } + return seen.get(tree) + } + + const visitNode = (tree) => { + if (seen.has(tree)) + return seen.get(tree) + + seen.set(tree, null) + const res = visit ? visit(tree) : tree + if (isPromise(res)) { + const fullResult = res.then(res => { + seen.set(tree, res) + return kidNodes(tree) + }) + seen.set(tree, fullResult) + return fullResult + } else { + seen.set(tree, res) + return kidNodes(tree) + } + } + + const kidNodes = (tree) => { + const kids = getChildren(tree, seen.get(tree)) + return isPromise(kids) ? kids.then(processKids) : processKids(kids) + } + + const processKids = (kids) => { + kids = (kids || []).filter(filter) + stack.push(...kids) + } + + stack.push(tree) + return next() +} + +const isPromise = p => p && typeof p.then === 'function' + +module.exports = depth diff --git a/node_modules/treeverse/lib/depth.js b/node_modules/treeverse/lib/depth.js index 763b0931dac6e..dbab1c28a2d15 100644 --- a/node_modules/treeverse/lib/depth.js +++ b/node_modules/treeverse/lib/depth.js @@ -14,6 +14,7 @@ // If either visit or leave return a Promise for any node, then the // walk returns a Promise. +const depthDescent = require('./depth-descent.js') const depth = ({ visit, leave, @@ -22,6 +23,9 @@ const depth = ({ getChildren, tree, }) => { + if (!leave) + return depthDescent({ visit, filter, getChildren, tree }) + if (seen.has(tree)) return seen.get(tree) @@ -56,8 +60,6 @@ const depth = ({ } const leaveNode = kids => { - if (!leave) - return seen.get(tree) const res = leave(seen.get(tree), kids) seen.set(tree, res) // if it's a promise at this point, the caller deals with it diff --git a/node_modules/treeverse/package.json b/node_modules/treeverse/package.json index 490f2e23cb91b..337194cfde970 100644 --- a/node_modules/treeverse/package.json +++ b/node_modules/treeverse/package.json @@ -1,6 +1,6 @@ { "name": "treeverse", - "version": "1.0.3", + "version": "1.0.4", "description": "Walk any kind of tree structure depth- or breadth-first. Supports promises and advanced map-reduce operations with a very small API.", "author": "Isaac Z. Schlueter (https://izs.me)", "license": "ISC", diff --git a/package-lock.json b/package-lock.json index 6974829d03e0b..0756cae06bb74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,7 +88,7 @@ ], "license": "Artistic-2.0", "dependencies": { - "@npmcli/arborist": "^0.0.19", + "@npmcli/arborist": "^0.0.20", "@npmcli/ci-detect": "^1.2.0", "@npmcli/config": "^1.1.7", "@npmcli/run-script": "^1.5.0", @@ -397,9 +397,9 @@ "dev": true }, "node_modules/@npmcli/arborist": { - "version": "0.0.19", - "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-0.0.19.tgz", - "integrity": "sha512-Y8JY1FnE3HoCZDBTXN1TIqMoGnywV9l0L21cBZQPx+MEAJoF87gfhg56RAwEICUD8wca6X1v4/A2fqPPMGzscw==", + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-0.0.20.tgz", + "integrity": "sha512-MklbWx9KFkMgi6gdzyzNMyWOX+f+OEwebyRkOSxTyYFfgLtqU+SlmrlhQ0mgF4jODZ70YkR5lfAvF0T5V57XVw==", "inBundle": true, "dependencies": { "@npmcli/installed-package-contents": "^1.0.5", @@ -409,6 +409,7 @@ "bin-links": "^2.1.2", "cacache": "^15.0.3", "common-ancestor-path": "^1.0.1", + "json-parse-even-better-errors": "^2.3.1", "json-stringify-nice": "^1.1.1", "mkdirp-infer-owner": "^2.0.0", "npm-install-checks": "^4.0.0", @@ -421,7 +422,7 @@ "read-package-json-fast": "^1.2.1", "readdir-scoped-modules": "^1.1.0", "semver": "^7.1.2", - "treeverse": "^1.0.1", + "treeverse": "^1.0.4", "walk-up-path": "^1.0.0" } }, @@ -8618,9 +8619,9 @@ } }, "node_modules/treeverse": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-1.0.3.tgz", - "integrity": "sha512-vE1An8RHapGybKZq+iD3tLvtZy3qboEWDh9wFTjVS5B+BUEJrK1rHjJBVokmv8uLSD6/dhvSDSDX6oY4XVnVvg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-1.0.4.tgz", + "integrity": "sha512-whw60l7r+8ZU8Tu/Uc2yxtc4ZTZbR/PF3u1IPNKGQ6p8EICLb3Z2lAgoqw9bqYd8IkgnsaOcLzYHFckjqNsf0g==", "inBundle": true }, "node_modules/trivial-deferred": { @@ -9459,9 +9460,9 @@ "dev": true }, "@npmcli/arborist": { - "version": "0.0.19", - "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-0.0.19.tgz", - "integrity": "sha512-Y8JY1FnE3HoCZDBTXN1TIqMoGnywV9l0L21cBZQPx+MEAJoF87gfhg56RAwEICUD8wca6X1v4/A2fqPPMGzscw==", + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-0.0.20.tgz", + "integrity": "sha512-MklbWx9KFkMgi6gdzyzNMyWOX+f+OEwebyRkOSxTyYFfgLtqU+SlmrlhQ0mgF4jODZ70YkR5lfAvF0T5V57XVw==", "requires": { "@npmcli/installed-package-contents": "^1.0.5", "@npmcli/map-workspaces": "0.0.0-pre.1", @@ -9470,6 +9471,7 @@ "bin-links": "^2.1.2", "cacache": "^15.0.3", "common-ancestor-path": "^1.0.1", + "json-parse-even-better-errors": "^2.3.1", "json-stringify-nice": "^1.1.1", "mkdirp-infer-owner": "^2.0.0", "npm-install-checks": "^4.0.0", @@ -9482,7 +9484,7 @@ "read-package-json-fast": "^1.2.1", "readdir-scoped-modules": "^1.1.0", "semver": "^7.1.2", - "treeverse": "^1.0.1", + "treeverse": "^1.0.4", "walk-up-path": "^1.0.0" } }, @@ -15743,9 +15745,9 @@ } }, "treeverse": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-1.0.3.tgz", - "integrity": "sha512-vE1An8RHapGybKZq+iD3tLvtZy3qboEWDh9wFTjVS5B+BUEJrK1rHjJBVokmv8uLSD6/dhvSDSDX6oY4XVnVvg==" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-1.0.4.tgz", + "integrity": "sha512-whw60l7r+8ZU8Tu/Uc2yxtc4ZTZbR/PF3u1IPNKGQ6p8EICLb3Z2lAgoqw9bqYd8IkgnsaOcLzYHFckjqNsf0g==" }, "trivial-deferred": { "version": "1.0.1", diff --git a/package.json b/package.json index 30e4221da6309..f732c40e62a36 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@npmcli/arborist": "^0.0.19", + "@npmcli/arborist": "^0.0.20", "@npmcli/ci-detect": "^1.2.0", "@npmcli/run-script": "^1.5.0", "abbrev": "~1.1.1",