Skip to content

Commit cd0199f

Browse files
committed
update algorithm for json output
1 parent 6c55568 commit cd0199f

File tree

2 files changed

+100
-131
lines changed

2 files changed

+100
-131
lines changed

lib/commands/ls.js

Lines changed: 86 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const { resolve, relative, sep } = require('node:path')
22
const archy = require('archy')
33
const npa = require('npm-package-arg')
4-
const { output, log } = require('proc-log')
4+
const { output } = require('proc-log')
55
const ArboristWorkspaceCmd = require('../arborist-cmd.js')
66
const localeCompare = require('@isaacs/string-locale-compare')('en')
77

@@ -19,72 +19,6 @@ const _problems = Symbol('problems')
1919
const _required = Symbol('required')
2020
const _type = Symbol('type')
2121

22-
function bfs (tree, visit, getChildren) {
23-
const queue = [tree]
24-
let finalResult = null
25-
const seenNodes = new Map()
26-
const problems = new Set()
27-
28-
seenNodes.set(tree.path, tree)
29-
while (queue.length > 0) {
30-
const node = queue.shift()
31-
32-
const result = visit(node, problems)
33-
if (!finalResult) {
34-
finalResult = result
35-
}
36-
queue.push(...getChildren(node, result, seenNodes))
37-
}
38-
return [finalResult, seenNodes, problems]
39-
}
40-
41-
const deepCopyNodes = (nodes) => {
42-
return nodes.map(child => {
43-
const copiedChild = { ...child }
44-
if (child.nodes && Array.isArray(child.nodes)) {
45-
copiedChild.nodes = deepCopyNodes(child.nodes)
46-
}
47-
return copiedChild
48-
})
49-
}
50-
51-
function dfs (
52-
node,
53-
getChildren,
54-
visit,
55-
seenNodes = new Map(),
56-
problems = new Set(),
57-
cache = new Map(),
58-
encounterCount = new Map()
59-
) {
60-
// Track the number of encounters for the current node
61-
// Why because we want to start storing after the node is identified as a deduped edge
62-
const count = (encounterCount.get(node.path) || 0) + 1
63-
encounterCount.set(node.path, count)
64-
65-
// Start caching only after the third encounter
66-
if (cache.has(node.path)) {
67-
return [cache.get(node.path), seenNodes, problems]
68-
}
69-
70-
const result = visit(node, problems)
71-
if (count > 1) {
72-
cache.set(node.path, result)
73-
}
74-
75-
// Get children of current node
76-
const children = getChildren(node, result, seenNodes)
77-
78-
// Recurse on each child
79-
for (const child of children) {
80-
const [cResult] = dfs(child, getChildren, visit, seenNodes, problems, cache, encounterCount)
81-
result[_include] = result[_include] || cResult[_include]
82-
cResult[_include] && result.nodes.push(cResult)
83-
}
84-
85-
return [result, seenNodes, problems]
86-
}
87-
8822
class LS extends ArboristWorkspaceCmd {
8923
static description = 'List installed packages'
9024
static name = 'ls'
@@ -170,15 +104,20 @@ class LS extends ArboristWorkspaceCmd {
170104
return true
171105
}
172106

173-
const getChildren = (node, nodeResult, seenNodes) => {
107+
const getChildren = (node, nodeResult, seenNodes, traversePathMap) => {
174108
const seenPaths = new Set()
175109
const workspace = node.isWorkspace
176110
const currentDepth = workspace ? 0 : node[_depth]
177111
const target = (node.target)?.edgesOut
178112

113+
const traversePath = [...(traversePathMap.get(nodeResult[_parent]) || [])]
114+
const isCircular = traversePath?.includes(node.realpath + '-' + node.pkgid)
115+
traversePath.push(node.realpath + '-' + node.pkgid)
116+
traversePathMap.set(nodeResult, traversePath)
117+
179118
const shouldSkipChildren =
180-
(currentDepth > depthToPrint)
181-
|| nodeResult?.isCircular
119+
(currentDepth > depthToPrint) || !nodeResult
120+
|| isCircular
182121

183122
return (shouldSkipChildren || !target)
184123
? []
@@ -215,12 +154,6 @@ class LS extends ArboristWorkspaceCmd {
215154
}
216155
}
217156

218-
item.traversePath = (node[_parent]?.traversePath || []).concat([node.realpath + '-' + node.pkgid])
219-
220-
if (node[_parent]?.traversePath?.includes(node.realpath + '-' + node.pkgid)) {
221-
item.isCircular = true
222-
}
223-
224157
// return a promise so we don't blow the stack
225158
return item
226159
}
@@ -232,7 +165,23 @@ class LS extends ArboristWorkspaceCmd {
232165

233166
// add root node of tree to list of seenNodes
234167

235-
const [result, seenNodes, problems] = dfs(tree, getChildren, visit)
168+
const seenNodes = new Map()
169+
const problems = new Set()
170+
const cache = new Map()
171+
const traversePathMap = new Map()
172+
173+
seenNodes.set(tree.path, tree)
174+
175+
const result = exploreDependencyGraph(
176+
tree,
177+
getChildren,
178+
visit,
179+
{ json, parseable },
180+
seenNodes,
181+
problems,
182+
cache,
183+
traversePathMap
184+
)
236185

237186
// handle the special case of a broken package.json in the root folder
238187
const [rootError] = tree.errors.filter(e =>
@@ -286,6 +235,61 @@ class LS extends ArboristWorkspaceCmd {
286235

287236
module.exports = LS
288237

238+
const exploreDependencyGraph = (
239+
node,
240+
getChildren,
241+
visit,
242+
{ json, parseable },
243+
seenNodes,
244+
problems,
245+
cache,
246+
traversePathMap,
247+
encounterCount = new Map()
248+
) => {
249+
// Track the number of encounters for the current node
250+
// Why because we want to start storing/caching after the node is identified as a deduped edge
251+
const count = node.path ? (encounterCount.get(node.path) || 0) + 1 : 0
252+
node.path && encounterCount.set(node.path, count)
253+
254+
if (node.path && cache.has(node.path)) {
255+
return cache.get(node.path)
256+
}
257+
258+
const currentNodeResult = visit(node, problems)
259+
if (count > 2) {
260+
cache.set(node.path, currentNodeResult)
261+
}
262+
263+
// Get children of current node
264+
const children = getChildren(node, currentNodeResult, seenNodes, traversePathMap)
265+
266+
// Recurse on each child
267+
for (const child of children) {
268+
const childResult = exploreDependencyGraph(
269+
child,
270+
getChildren,
271+
visit,
272+
{ json, parseable },
273+
seenNodes,
274+
problems,
275+
cache,
276+
traversePathMap,
277+
encounterCount
278+
)
279+
currentNodeResult[_include] = currentNodeResult[_include] || childResult[_include]
280+
if (childResult[_include] && !parseable) {
281+
if (json) {
282+
currentNodeResult.dependencies = currentNodeResult.dependencies || {}
283+
currentNodeResult.dependencies[childResult[_name]] = childResult
284+
} else {
285+
currentNodeResult.nodes.push(childResult)
286+
}
287+
}
288+
}
289+
290+
return currentNodeResult
291+
}
292+
289293
const isGitNode = (node) => {
290294
if (!node.resolved) {
291295
return
@@ -323,26 +327,6 @@ const getProblems = (node, { global }) => {
323327
return problems
324328
}
325329

326-
// annotates _parent and _include metadata into the resulting
327-
// item obj allowing for filtering out results during output
328-
const augmentItemWithIncludeMetadata = (node, item) => {
329-
// item[_parent] = node[_parent]
330-
// item[_include] = node[_include]
331-
332-
// append current item to its parent.nodes which is the
333-
// structure expected by archy in order to print tree
334-
// if (node[_include]) {
335-
// // includes all ancestors of included node
336-
// let p = node[_parent]
337-
// while (p) {
338-
// p[_include] = true
339-
// p = p[_parent]
340-
// }
341-
// }
342-
343-
return item
344-
}
345-
346330
const getHumanOutputItem = (node, { args, chalk, global, long }) => {
347331
const { pkgid, path } = node
348332
const workspacePkgId = chalk.blueBright(pkgid)
@@ -406,7 +390,7 @@ const getHumanOutputItem = (node, { args, chalk, global, long }) => {
406390
}
407391

408392
const getJsonOutputItem = (node, { global, long }) => {
409-
const item = {}
393+
const item = { [_include]: node[_include] }
410394

411395
if (node.version) {
412396
item.version = node.version
@@ -463,7 +447,7 @@ const getJsonOutputItem = (node, { global, long }) => {
463447
item.problems = [...node[_problems]]
464448
}
465449

466-
return augmentItemWithIncludeMetadata(node, item)
450+
return item
467451
}
468452

469453
const filterByEdgesTypes = ({ link, omit }) => (edge) => {
@@ -571,9 +555,9 @@ const augmentNodesWithMetadata = ({
571555
return node
572556
}
573557

574-
const sortAlphabetically = ({ pkgid: a }, { pkgid: b }) => localeCompare(b, a)
558+
const sortAlphabetically = ({ pkgid: a }, { pkgid: b }) => localeCompare(a, b)
575559

576-
const humanOutput = ({ chalk, result, seenItems, unicode }) => {
560+
const humanOutput = ({ chalk, result, unicode }) => {
577561
// we need to traverse the entire tree in order to determine which items
578562
// should be included (since a nested transitive included dep will make it
579563
// so that all its ancestors should be displayed)
@@ -592,7 +576,7 @@ const humanOutput = ({ chalk, result, seenItems, unicode }) => {
592576
return chalk.reset(archyOutput)
593577
}
594578

595-
const jsonOutput = ({ path, problems, result, rootError, seenItems }) => {
579+
const jsonOutput = ({ path, problems, result, rootError }) => {
596580
if (problems.size) {
597581
result.problems = [...problems]
598582
}
@@ -609,19 +593,6 @@ const jsonOutput = ({ path, problems, result, rootError, seenItems }) => {
609593
// should be included (since a nested transitive included dep will make it
610594
// so that all its ancestors should be displayed)
611595
// here is where we put items in their expected place for json output
612-
for (const item of seenItems) {
613-
delete item.traversePath
614-
delete item.isCircular
615-
// append current item to its parent item.dependencies obj in order
616-
// to provide a json object structure that represents the installed tree
617-
if (item[_include] && item[_parent]) {
618-
if (!item[_parent].dependencies) {
619-
item[_parent].dependencies = {}
620-
}
621-
622-
item[_parent].dependencies[item[_name]] = item
623-
}
624-
}
625596

626597
return result
627598
}

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

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ exports[`test/lib/commands/ls.js TAP ls --parseable --long > should output tree
149149
{CWD}/prefix/node_modules/peer-dep:peer-dep@1.0.0
150150
{CWD}/prefix/node_modules/prod-dep:prod-dep@1.0.0
151151
{CWD}/prefix/node_modules/foo:foo@1.0.0
152-
{CWD}/prefix/node_modules/prod-dep/node_modules/dog:dog@2.0.0
153152
{CWD}/prefix/node_modules/dog:dog@1.0.0
153+
{CWD}/prefix/node_modules/prod-dep/node_modules/dog:dog@2.0.0
154154
`
155155

156156
exports[`test/lib/commands/ls.js TAP ls --parseable --long missing/invalid/extraneous > should output parseable result containing EXTRANEOUS/INVALID labels 1`] = `
@@ -169,8 +169,8 @@ exports[`test/lib/commands/ls.js TAP ls --parseable --long print symlink target
169169
{CWD}/prefix/node_modules/peer-dep:peer-dep@1.0.0
170170
{CWD}/prefix/node_modules/prod-dep:prod-dep@1.0.0
171171
{CWD}/prefix/node_modules/foo:foo@1.0.0
172-
{CWD}/prefix/node_modules/prod-dep/node_modules/dog:dog@2.0.0
173172
{CWD}/prefix/node_modules/dog:dog@1.0.0
173+
{CWD}/prefix/node_modules/prod-dep/node_modules/dog:dog@2.0.0
174174
`
175175

176176
exports[`test/lib/commands/ls.js TAP ls --parseable --long with extraneous deps > should output long parseable output with extraneous info 1`] = `
@@ -267,8 +267,8 @@ exports[`test/lib/commands/ls.js TAP ls --parseable unmet optional dep > should
267267
{CWD}/prefix/node_modules/peer-dep
268268
{CWD}/prefix/node_modules/prod-dep
269269
{CWD}/prefix/node_modules/foo
270-
{CWD}/prefix/node_modules/prod-dep/node_modules/dog
271270
{CWD}/prefix/node_modules/dog
271+
{CWD}/prefix/node_modules/prod-dep/node_modules/dog
272272
`
273273

274274
exports[`test/lib/commands/ls.js TAP ls --parseable unmet peer dep > should output parseable signaling missing peer dep in problems 1`] = `
@@ -279,8 +279,8 @@ exports[`test/lib/commands/ls.js TAP ls --parseable unmet peer dep > should outp
279279
{CWD}/prefix/node_modules/peer-dep
280280
{CWD}/prefix/node_modules/prod-dep
281281
{CWD}/prefix/node_modules/foo
282-
{CWD}/prefix/node_modules/prod-dep/node_modules/dog
283282
{CWD}/prefix/node_modules/dog
283+
{CWD}/prefix/node_modules/prod-dep/node_modules/dog
284284
`
285285

286286
exports[`test/lib/commands/ls.js TAP ls --parseable using aliases > should output tree containing aliases 1`] = `
@@ -467,12 +467,12 @@ workspaces-tree@1.0.0 {CWD}/prefix
467467
| +-- baz@1.0.0
468468
| +-- c@1.0.0
469469
| \`-- d@1.0.0 deduped -> ./d
470-
| \`-- foo@1.1.1 deduped
471-
| \`-- bar@1.0.0 deduped
470+
| \`-- foo@1.1.1
471+
| \`-- bar@1.0.0
472472
+-- b@1.0.0 -> ./b
473473
+-- d@1.0.0 -> ./d
474-
| \`-- foo@1.1.1
475-
| \`-- bar@1.0.0
474+
| \`-- foo@1.1.1 deduped
475+
| \`-- bar@1.0.0 deduped
476476
+-- e@1.0.0 -> ./group/e
477477
+-- f@1.0.0 -> ./group/f
478478
\`-- pacote@1.0.0
@@ -483,12 +483,12 @@ workspaces-tree@1.0.0 {CWD}/prefix
483483
+-- a@1.0.0 -> ./a
484484
| +-- c@1.0.0
485485
| \`-- d@1.0.0 deduped -> ./d
486-
| \`-- foo@1.1.1 deduped
487-
| \`-- bar@1.0.0 deduped
486+
| \`-- foo@1.1.1
487+
| \`-- bar@1.0.0
488488
+-- b@1.0.0 -> ./b
489489
+-- d@1.0.0 -> ./d
490-
| \`-- foo@1.1.1
491-
| \`-- bar@1.0.0
490+
| \`-- foo@1.1.1 deduped
491+
| \`-- bar@1.0.0 deduped
492492
+-- e@1.0.0 -> ./group/e
493493
+-- f@1.0.0 -> ./group/f
494494
\`-- pacote@1.0.0
@@ -672,8 +672,6 @@ test-npm-ls@1.0.0 {CWD}/prefix
672672
+-- chai@1.0.0 extraneous
673673
| \`-- dog@1.0.0 deduped invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat, "2.x" from node_modules/chai
674674
| \`-- cat@1.0.0 deduped invalid: "^2.0.0" from the root project
675-
| \`-- dog@1.0.0 deduped invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat, "2.x" from node_modules/chai, "^2.0.0" from node_modules/cat, "^2.0.0" from node_modules/cat
676-
\`-- dog@1.0.0 invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat, "2.x" from node_modules/chai
677-
\`-- cat@1.0.0 deduped invalid: "^2.0.0" from the root project
678-
\`-- dog@1.0.0 deduped invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat, "2.x" from node_modules/chai, "^2.0.0" from node_modules/cat
675+
| \`-- dog@1.0.0 deduped invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat, "2.x" from node_modules/chai, "^2.0.0" from node_modules/cat
676+
\`-- dog@1.0.0 deduped invalid: "^1.2.3" from the root project, "^2.0.0" from node_modules/cat, "2.x" from node_modules/chai, "^2.0.0" from node_modules/cat
679677
`

0 commit comments

Comments
 (0)