11const { resolve, relative, sep } = require ( 'node:path' )
22const archy = require ( 'archy' )
3- const { breadth } = require ( 'treeverse' )
43const npa = require ( 'npm-package-arg' )
54const { output } = require ( 'proc-log' )
65const 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
226228module . 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+
228294const 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-
285331const 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
347393const 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
408454const 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
510557const 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