Skip to content

Commit dd5fac8

Browse files
committed
fix: ls to display all deduped nodes
1 parent dda6f87 commit dd5fac8

File tree

2 files changed

+153
-125
lines changed

2 files changed

+153
-125
lines changed

lib/commands/ls.js

Lines changed: 111 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
const { resolve, relative, sep } = require('node:path')
22
const archy = require('archy')
3-
const { breadth } = require('treeverse')
43
const npa = require('npm-package-arg')
54
const { output } = require('proc-log')
65
const ArboristWorkspaceCmd = require('../arborist-cmd.js')
@@ -62,6 +61,12 @@ class LS extends ArboristWorkspaceCmd {
6261

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

64+
// defines special handling of printed depth when filtering with args
65+
const filterDefaultDepth = depth === null ? Infinity : depth
66+
const depthToPrint = (all || args.length)
67+
? filterDefaultDepth
68+
: (depth || 0)
69+
6570
const Arborist = require('@npmcli/arborist')
6671

6772
const arb = new Arborist({
@@ -105,34 +110,23 @@ class LS extends ArboristWorkspaceCmd {
105110
return true
106111
}
107112

108-
const seenItems = new Set()
109113
const seenNodes = new Map()
110114
const problems = new Set()
111115

112-
// defines special handling of printed depth when filtering with args
113-
const filterDefaultDepth = depth === null ? Infinity : depth
114-
const depthToPrint = (all || args.length)
115-
? filterDefaultDepth
116-
: (depth || 0)
117-
118-
// add root node of tree to list of seenNodes
119-
seenNodes.set(tree.path, tree)
120-
121-
// tree traversal happens here, using treeverse.breadth
122-
const result = await breadth({
123-
tree,
124-
// recursive method, `node` is going to be the current elem (starting from
125-
// the `tree` obj) that was just visited in the `visit` method below
126-
// `nodeResult` is going to be the returned `item` from `visit`
116+
const result = exploreDependencyGraph({
117+
node: tree,
127118
getChildren (node, nodeResult) {
128119
const seenPaths = new Set()
129120
const workspace = node.isWorkspace
130121
const currentDepth = workspace ? 0 : node[_depth]
122+
const target = (node.target)?.edgesOut
123+
131124
const shouldSkipChildren =
132-
!(node instanceof Arborist.Node) || (currentDepth > depthToPrint)
133-
return (shouldSkipChildren)
125+
(currentDepth > depthToPrint) || !nodeResult
126+
127+
return (shouldSkipChildren || !target)
134128
? []
135-
: [...(node.target).edgesOut.values()]
129+
: [...target.values()]
136130
.filter(filterBySelectedWorkspaces)
137131
.filter(currentDepth === 0 ? filterByEdgesTypes({
138132
link,
@@ -148,15 +142,25 @@ class LS extends ArboristWorkspaceCmd {
148142
seenNodes,
149143
}))
150144
},
151-
// visit each `node` of the `tree`, returning an `item` - these are
152-
// the elements that will be used to build the final output
153145
visit (node) {
146+
// add to seenNodes as soon as we visit and not when the children are calculated in previous call
147+
if (!seenNodes.has(node.path)) {
148+
seenNodes.set(node.path, node)
149+
} else {
150+
node[_dedupe] = !node[_missing]
151+
}
152+
154153
node[_problems] = getProblems(node, { global })
155154

156155
const item = json
157156
? getJsonOutputItem(node, { global, long })
158157
: parseable
159-
? null
158+
? {
159+
pkgid: node.pkgid,
160+
path: node.path,
161+
[_dedupe]: node[_dedupe],
162+
[_parent]: node[_parent],
163+
}
160164
: getHumanOutputItem(node, { args, chalk, global, long })
161165

162166
// loop through list of node problems to add them to global list
@@ -165,24 +169,22 @@ class LS extends ArboristWorkspaceCmd {
165169
problems.add(problem)
166170
}
167171
}
168-
169-
seenItems.add(item)
170-
171-
// return a promise so we don't blow the stack
172-
return Promise.resolve(item)
172+
return item
173173
},
174+
opts: { json, parseable },
175+
seenNodes,
174176
})
175177

176178
// handle the special case of a broken package.json in the root folder
177179
const [rootError] = tree.errors.filter(e =>
178180
e.code === 'EJSONPARSE' && e.path === resolve(path, 'package.json'))
179181

180182
if (json) {
181-
output.buffer(jsonOutput({ path, problems, result, rootError, seenItems }))
183+
output.buffer(jsonOutput({ path, problems, result, rootError }))
182184
} else {
183185
output.standard(parseable
184186
? parseableOutput({ seenNodes, global, long })
185-
: humanOutput({ chalk, result, seenItems, unicode })
187+
: humanOutput({ chalk, result, unicode })
186188
)
187189
}
188190

@@ -225,6 +227,70 @@ class LS extends ArboristWorkspaceCmd {
225227

226228
module.exports = LS
227229

230+
const exploreDependencyGraph = ({
231+
node,
232+
getChildren,
233+
visit,
234+
opts,
235+
seenNodes,
236+
cache = new Map(),
237+
traversePathMap = new Map(),
238+
}) => {
239+
const { json, parseable } = opts
240+
241+
// cahce is for already visited nodes results
242+
// if the node is already seen, we can return it from cache
243+
if (cache.has(node.path)) {
244+
return cache.get(node.path)
245+
}
246+
247+
const currentNodeResult = visit(node)
248+
249+
// how the this node is explored
250+
// so if the explored path contains this node again then it's a cycle
251+
// and we don't want to explore it again
252+
const traversePath = [...(traversePathMap.get(currentNodeResult[_parent]) || [])]
253+
const isCircular = traversePath?.includes(node.pkgid)
254+
traversePath.push(node.pkgid)
255+
traversePathMap.set(currentNodeResult, traversePath)
256+
257+
// we want to start using cache after node is identified as a deduped
258+
if (node[_dedupe]) {
259+
cache.set(node.path, currentNodeResult)
260+
}
261+
262+
// Get children of current node
263+
const children = isCircular
264+
? []
265+
: getChildren(node, currentNodeResult)
266+
267+
// Recurse on each child node
268+
for (const child of children) {
269+
const childResult = exploreDependencyGraph({
270+
node: child,
271+
getChildren,
272+
visit,
273+
opts,
274+
seenNodes,
275+
cache,
276+
traversePathMap,
277+
})
278+
// include current node if any of its children are included
279+
currentNodeResult[_include] = currentNodeResult[_include] || childResult[_include]
280+
281+
if (childResult[_include] && !parseable) {
282+
if (json) {
283+
currentNodeResult.dependencies = currentNodeResult.dependencies || {}
284+
currentNodeResult.dependencies[childResult[_name]] = childResult
285+
} else {
286+
currentNodeResult.nodes.push(childResult)
287+
}
288+
}
289+
}
290+
291+
return currentNodeResult
292+
}
293+
228294
const isGitNode = (node) => {
229295
if (!node.resolved) {
230296
return
@@ -262,26 +328,6 @@ const getProblems = (node, { global }) => {
262328
return problems
263329
}
264330

265-
// annotates _parent and _include metadata into the resulting
266-
// item obj allowing for filtering out results during output
267-
const augmentItemWithIncludeMetadata = (node, item) => {
268-
item[_parent] = node[_parent]
269-
item[_include] = node[_include]
270-
271-
// append current item to its parent.nodes which is the
272-
// structure expected by archy in order to print tree
273-
if (node[_include]) {
274-
// includes all ancestors of included node
275-
let p = node[_parent]
276-
while (p) {
277-
p[_include] = true
278-
p = p[_parent]
279-
}
280-
}
281-
282-
return item
283-
}
284-
285331
const getHumanOutputItem = (node, { args, chalk, global, long }) => {
286332
const { pkgid, path } = node
287333
const workspacePkgId = chalk.blueBright(pkgid)
@@ -339,13 +385,13 @@ const getHumanOutputItem = (node, { args, chalk, global, long }) => {
339385
) +
340386
(isGitNode(node) ? ` (${node.resolved})` : '') +
341387
(node.isLink ? ` -> ${relativePrefix}${targetLocation}` : '') +
342-
(long ? `\n${node.package.description || ''}` : '')
388+
(long ? `\n${node.package?.description || ''}` : '')
343389

344-
return augmentItemWithIncludeMetadata(node, { label, nodes: [] })
390+
return ({ label, nodes: [], [_include]: node[_include], [_parent]: node[_parent] })
345391
}
346392

347393
const getJsonOutputItem = (node, { global, long }) => {
348-
const item = {}
394+
const item = { [_include]: node[_include], [_parent]: node[_parent] }
349395

350396
if (node.version) {
351397
item.version = node.version
@@ -402,7 +448,7 @@ const getJsonOutputItem = (node, { global, long }) => {
402448
item.problems = [...node[_problems]]
403449
}
404450

405-
return augmentItemWithIncludeMetadata(node, item)
451+
return item
406452
}
407453

408454
const filterByEdgesTypes = ({ link, omit }) => (edge) => {
@@ -467,27 +513,24 @@ const augmentNodesWithMetadata = ({
467513
// revisit that node in tree traversal logic, so we make it so that
468514
// we have a diff obj for deduped nodes:
469515
if (seenNodes.has(node.path)) {
470-
const { realpath, root } = node
471-
const targetLocation = root ? relative(root.realpath, realpath)
472-
: node.targetLocation
473516
node = {
474517
name: node.name,
475518
version: node.version,
476519
pkgid: node.pkgid,
520+
target: node.target,
521+
edgesOut: node.edgesOut,
522+
children: node.children,
477523
package: node.package,
478524
path: node.path,
479525
isLink: node.isLink,
480526
realpath: node.realpath,
481-
targetLocation,
527+
targetLocation: node.targetLocation,
482528
[_type]: node[_type],
483529
[_invalid]: node[_invalid],
484530
[_missing]: node[_missing],
485531
// if it's missing, it's not deduped, it's just missing
486532
[_dedupe]: !node[_missing],
487533
}
488-
} else {
489-
// keeps track of already seen nodes in order to check for dedupes
490-
seenNodes.set(node.path, node)
491534
}
492535

493536
// _parent is going to be a ref to a treeverse-visited node (returned from
@@ -499,7 +542,11 @@ const augmentNodesWithMetadata = ({
499542
// _filteredBy is used to apply extra color info to the item that
500543
// was used in args in order to filter
501544
node[_filteredBy] = node[_include] =
502-
filterByPositionalArgs(args, { node: seenNodes.get(node.path) })
545+
filterByPositionalArgs(args, {
546+
node: seenNodes.has(node.path)
547+
? seenNodes.get(node.path)
548+
: node,
549+
})
503550
// _depth keeps track of how many levels deep tree traversal currently is
504551
// so that we can `npm ls --depth=1`
505552
node[_depth] = currentDepth + 1
@@ -509,17 +556,7 @@ const augmentNodesWithMetadata = ({
509556

510557
const sortAlphabetically = ({ pkgid: a }, { pkgid: b }) => localeCompare(a, b)
511558

512-
const humanOutput = ({ chalk, result, seenItems, unicode }) => {
513-
// we need to traverse the entire tree in order to determine which items
514-
// should be included (since a nested transitive included dep will make it
515-
// so that all its ancestors should be displayed)
516-
// here is where we put items in their expected place for archy output
517-
for (const item of seenItems) {
518-
if (item[_include] && item[_parent]) {
519-
item[_parent].nodes.push(item)
520-
}
521-
}
522-
559+
const humanOutput = ({ chalk, result, unicode }) => {
523560
if (!result.nodes.length) {
524561
result.nodes = ['(empty)']
525562
}
@@ -528,7 +565,7 @@ const humanOutput = ({ chalk, result, seenItems, unicode }) => {
528565
return chalk.reset(archyOutput)
529566
}
530567

531-
const jsonOutput = ({ path, problems, result, rootError, seenItems }) => {
568+
const jsonOutput = ({ path, problems, result, rootError }) => {
532569
if (problems.size) {
533570
result.problems = [...problems]
534571
}
@@ -541,22 +578,6 @@ const jsonOutput = ({ path, problems, result, rootError, seenItems }) => {
541578
result.invalid = true
542579
}
543580

544-
// we need to traverse the entire tree in order to determine which items
545-
// should be included (since a nested transitive included dep will make it
546-
// so that all its ancestors should be displayed)
547-
// here is where we put items in their expected place for json output
548-
for (const item of seenItems) {
549-
// append current item to its parent item.dependencies obj in order
550-
// to provide a json object structure that represents the installed tree
551-
if (item[_include] && item[_parent]) {
552-
if (!item[_parent].dependencies) {
553-
item[_parent].dependencies = {}
554-
}
555-
556-
item[_parent].dependencies[item[_name]] = item
557-
}
558-
}
559-
560581
return result
561582
}
562583

0 commit comments

Comments
 (0)