Skip to content

Commit 1a088fb

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

File tree

2 files changed

+100
-72
lines changed

2 files changed

+100
-72
lines changed

lib/commands/ls.js

Lines changed: 93 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('includeWorkspaceRoot')
6262

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

@@ -246,81 +246,105 @@ 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+
// Cache deduped nodes
295+
// if (currentNode[_dedupe]) {
296+
// cache.set(currentNode.path, currentNodeResult)
297+
// }
298+
299+
// Get children if not circular/shouldn't skip
300+
let children = []
301+
if (!isCircular && !shouldSkipChildren) {
302+
children = getChildren(currentNode, wsNodes, configs)
303+
}
304+
305+
// Push children onto stack
306+
for (let i = children.length - 1; i >= 0; i--) {
307+
const child = children[i]
308+
child[_depth] = currentDepth + 1
309+
stack.push({
310+
node: child,
311+
parent: currentNodeResult,
312+
parentTraversePath: currentPath,
313+
state: 'not-processed',
314+
})
315+
}
316+
// since this node is processed
317+
current.state = 'processed' // next state will be 'processed'
318+
current.currentNodeResult = currentNodeResult
319+
current.children = children
320+
}
321+
if (state === 'processed') {
322+
// Collect child results
323+
const { currentNodeResult, children } = current
324+
325+
for (const child of children) {
326+
const childResult = results.get(child)
327+
currentNodeResult[_include] = currentNodeResult[_include] || childResult[_include]
328+
if (childResult[_include] && !parseable) {
329+
if (json) {
330+
currentNodeResult.dependencies = currentNodeResult.dependencies || {}
331+
currentNodeResult.dependencies[childResult[_name]] = childResult
332+
} else {
333+
currentNodeResult.nodes.push(childResult)
334+
}
335+
}
336+
}
337+
338+
results.set(currentNode, currentNodeResult)
339+
if (currentNode[_dedupe]) {
340+
cache.set(currentNode.path, currentNodeResult)
319341
}
342+
stack.pop()
320343
}
321344
}
322345

323-
return currentNodeResult
346+
// Return the result for the root node
347+
return results.get(node)
324348
}
325349

326350
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)