Skip to content

Commit 4440111

Browse files
author
Milan Meva
committed
refactor recursion to loop
1 parent 9a229f5 commit 4440111

File tree

2 files changed

+95
-72
lines changed

2 files changed

+95
-72
lines changed

lib/commands/ls.js

Lines changed: 88 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class LS extends ArboristWorkspaceCmd {
5858
const unicode = this.npm.config.get('unicode')
5959
const packageLockOnly = this.npm.config.get('package-lock-only')
6060
const workspacesEnabled = this.npm.flatOptions.workspacesEnabled
61-
const includeWorkspaceRoot = this.npm.flatOptions.includeWorkspaceRoot
61+
const includeWorkspaceRoot = this.npm.config.get('include-workspace-root')
6262

6363
const path = global ? resolve(this.npm.globalDir, '..') : this.npm.prefix
6464

@@ -246,81 +246,100 @@ const exploreDependencyGraph = ({
246246
configs,
247247
seenNodes,
248248
problems,
249-
cache = new Map(),
250-
traversePathMap = new Map(),
251249
}) => {
252250
const { json, parseable, depthToPrint, args } = configs
253251

254-
// cahce is for already visited nodes results
255-
// if the node is already seen, we can return it from cache
256-
if (cache.has(node.path)) {
257-
return cache.get(node.path)
258-
}
259-
260-
const currentNodeResult = visit(node, seenNodes, problems, configs)
261-
262-
// how the this node is explored
263-
// so if the explored path contains this node again then it's a cycle
264-
// and we don't want to explore it again
265-
// Track the path of pkgids to detect cycles efficiently
266-
const parentTraversePath = traversePathMap.get(currentNodeResult[_parent]) || []
267-
const isCircular = parentTraversePath.includes(node.pkgid)
268-
const currentPath = [...parentTraversePath, node.pkgid]
269-
traversePathMap.set(currentNodeResult, currentPath)
270-
271-
// we want to start using cache after node is identified as a deduped
272-
if (node[_dedupe]) {
273-
cache.set(node.path, currentNodeResult)
274-
}
275-
276-
const currentDepth = node.isWorkspace ? 0 : node[_depth]
277-
const shouldSkipChildren =
278-
(currentDepth > depthToPrint)
279-
280-
// Get children of current node
281-
const children = isCircular || shouldSkipChildren || !currentNodeResult
282-
? []
283-
: getChildren(node, wsNodes, configs)
284-
285-
// Recurse on each child node
286-
for (const child of children) {
287-
// _parent is going to be a ref to a traversed node (returned from
288-
// getHumanOutputItem, getJsonOutputItem, etc) so that we have an easy
289-
// shortcut to place new nodes in their right place during tree traversal
290-
child[_parent] = currentNodeResult
291-
// _include is the property that allow us to filter based on position args
292-
// e.g: `npm ls foo`, `npm ls simple-output@2`
293-
// _filteredBy is used to apply extra color info to the item that
294-
// was used in args in order to filter
295-
child[_filteredBy] = child[_include] =
296-
filterByPositionalArgs(args, { node: child })
297-
// _depth keeps track of how many levels deep tree traversal currently is
298-
// so that we can `npm ls --depth=1`
299-
child[_depth] = currentDepth + 1
300-
301-
const childResult = exploreDependencyGraph({
302-
node: child,
303-
wsNodes,
304-
configs,
305-
seenNodes,
306-
problems,
307-
cache,
308-
traversePathMap,
309-
})
310-
// include current node if any of its children are included
311-
currentNodeResult[_include] = currentNodeResult[_include] || childResult[_include]
312-
313-
if (childResult[_include] && !parseable) {
314-
if (json) {
315-
currentNodeResult.dependencies = currentNodeResult.dependencies || {}
316-
currentNodeResult.dependencies[childResult[_name]] = childResult
317-
} else {
318-
currentNodeResult.nodes.push(childResult)
252+
// Stack for iterative traversal
253+
const stack = []
254+
// Map to store results for each node
255+
const results = new WeakMap()
256+
257+
// Cache for deduped nodes
258+
const cache = new Map()
259+
260+
// Initialize stack with the root node
261+
stack.push({
262+
node,
263+
parent: null,
264+
parentTraversePath: [],
265+
state: 'not-processed', // 'enter' or 'exit'
266+
})
267+
268+
while (stack.length > 0) {
269+
const current = stack[stack.length - 1]
270+
const { node: currentNode, parentTraversePath, state, parent } = current
271+
272+
const currentDepth = currentNode.isWorkspace ? 0 : currentNode[_depth]
273+
const shouldSkipChildren = currentDepth > depthToPrint
274+
275+
currentNode[_parent] = parent
276+
currentNode[_filteredBy] = currentNode[_include] =
277+
filterByPositionalArgs(args, { node: currentNode })
278+
279+
// Cycle detection
280+
const isCircular = parentTraversePath.includes(currentNode.path)
281+
const currentPath = [...parentTraversePath, currentNode.path]
282+
283+
if (state === 'not-processed') {
284+
// Check cache
285+
if (cache.has(currentNode.path)) {
286+
results.set(currentNode, cache.get(currentNode.path))
287+
stack.pop()
288+
continue
289+
}
290+
291+
// Visit node
292+
const currentNodeResult = visit(currentNode, seenNodes, problems, configs)
293+
294+
// Get children if not circular/shouldn't skip
295+
let children = []
296+
if (!isCircular && !shouldSkipChildren) {
297+
children = getChildren(currentNode, wsNodes, configs)
298+
}
299+
300+
// Push children onto stack
301+
for (let i = children.length - 1; i >= 0; i--) {
302+
const child = children[i]
303+
child[_depth] = currentDepth + 1
304+
stack.push({
305+
node: child,
306+
parent: currentNodeResult,
307+
parentTraversePath: currentPath,
308+
state: 'not-processed',
309+
})
310+
}
311+
// since this node is processed
312+
current.state = 'processed' // next state will be 'processed'
313+
current.currentNodeResult = currentNodeResult
314+
current.children = children
315+
}
316+
if (state === 'processed') {
317+
// Collect child results
318+
const { currentNodeResult, children } = current
319+
320+
for (const child of children) {
321+
const childResult = results.get(child)
322+
currentNodeResult[_include] = currentNodeResult[_include] || childResult[_include]
323+
if (childResult[_include] && !parseable) {
324+
if (json) {
325+
currentNodeResult.dependencies = currentNodeResult.dependencies || {}
326+
currentNodeResult.dependencies[childResult[_name]] = childResult
327+
} else {
328+
currentNodeResult.nodes.push(childResult)
329+
}
330+
}
331+
}
332+
333+
results.set(currentNode, currentNodeResult)
334+
if (currentNode[_dedupe]) {
335+
cache.set(currentNode.path, currentNodeResult)
319336
}
337+
stack.pop()
320338
}
321339
}
322340

323-
return currentNodeResult
341+
// Return the result for the root node
342+
return results.get(node)
324343
}
325344

326345
const isGitNode = (node) => {

tap-snapshots/test/lib/commands/ls.js.test.cjs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ [email protected] {CWD}/prefix
526526
exports[`test/lib/commands/ls.js TAP ls missing package.json > should output tree missing name/version of top-level package 1`] = `
527527
{CWD}/prefix
528528
+-- [email protected] extraneous
529-
+-- [email protected] extraneous
529+
+-- [email protected] deduped extraneous
530530
\`-- [email protected] extraneous
531531
\`-- [email protected] deduped extraneous
532532
`
@@ -672,7 +672,11 @@ [email protected] {CWD}/prefix
672672
| \`-- [email protected] deduped invalid: "^2.0.0" from the root project
673673
+-- [email protected] extraneous
674674
| \`-- [email protected] deduped invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat, "2.x" from node_modules/chai
675-
| \`-- [email protected] deduped invalid: "^2.0.0" from the root project
675+
| \`-- [email protected] invalid: "^2.0.0" from the root project
676+
| \`-- [email protected] invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat
677+
| \`-- [email protected] deduped invalid: "^2.0.0" from the root project
676678
\`-- [email protected] deduped invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat, "2.x" from node_modules/chai
677-
\`-- [email protected] deduped invalid: "^2.0.0" from the root project
679+
\`-- [email protected] invalid: "^2.0.0" from the root project
680+
\`-- [email protected] invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat
681+
\`-- [email protected] deduped invalid: "^2.0.0" from the root project
678682
`

0 commit comments

Comments
 (0)