1
1
const { resolve, relative, sep } = require ( 'node:path' )
2
2
const archy = require ( 'archy' )
3
- const { breadth } = require ( 'treeverse' )
4
3
const npa = require ( 'npm-package-arg' )
5
4
const { output } = require ( 'proc-log' )
6
5
const ArboristWorkspaceCmd = require ( '../arborist-cmd.js' )
@@ -62,6 +61,12 @@ class LS extends ArboristWorkspaceCmd {
62
61
63
62
const path = global ? resolve ( this . npm . globalDir , '..' ) : this . npm . prefix
64
63
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
+
65
70
const Arborist = require ( '@npmcli/arborist' )
66
71
67
72
const arb = new Arborist ( {
@@ -105,34 +110,23 @@ class LS extends ArboristWorkspaceCmd {
105
110
return true
106
111
}
107
112
108
- const seenItems = new Set ( )
109
113
const seenNodes = new Map ( )
110
114
const problems = new Set ( )
111
115
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 ,
127
118
getChildren ( node , nodeResult ) {
128
119
const seenPaths = new Set ( )
129
120
const workspace = node . isWorkspace
130
121
const currentDepth = workspace ? 0 : node [ _depth ]
122
+ const target = ( node . target ) ?. edgesOut
123
+
131
124
const shouldSkipChildren =
132
- ! ( node instanceof Arborist . Node ) || ( currentDepth > depthToPrint )
133
- return ( shouldSkipChildren )
125
+ ( currentDepth > depthToPrint ) || ! nodeResult
126
+
127
+ return ( shouldSkipChildren || ! target )
<
2364
tr class="diff-line-row">134
128
? [ ]
135
- : [ ...( node . target ) . edgesOut . values ( ) ]
129
+ : [ ...target . values ( ) ]
136
130
. filter ( filterBySelectedWorkspaces )
137
131
. filter ( currentDepth === 0 ? filterByEdgesTypes ( {
138
132
link,
@@ -148,15 +142,25 @@ class LS extends ArboristWorkspaceCmd {
148
142
seenNodes,
149
143
} ) )
150
144
} ,
151
- // visit each `node` of the `tree`, returning an `item` - these are
152
- // the elements that will be used to build the final output
153
145
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
+
154
153
node [ _problems ] = getProblems ( node , { global } )
155
154
156
155
const item = json
157
156
? getJsonOutputItem ( node , { global, long } )
158
157
: parseable
159
- ? null
158
+ ? {
159
+ pkgid : node . pkgid ,
160
+ path : node . path ,
161
+ [ _dedupe ] : node [ _dedupe ] ,
162
+ [ _parent ] : node [ _parent ] ,
163
+ }
160
164
: getHumanOutputItem ( node , { args, chalk, global, long } )
161
165
162
166
// loop through list of node problems to add them to global list
@@ -165,24 +169,22 @@ class LS extends ArboristWorkspaceCmd {
165
169
problems . add ( problem )
166
170
}
167
171
}
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
173
173
} ,
174
+ opts : { json, parseable } ,
175
+ seenNodes,
174
176
} )
175
177
176
178
// handle the special case of a broken package.json in the root folder
177
179
const [ rootError ] = tree . errors . filter ( e =>
178
180
e . code === 'EJSONPARSE' && e . path === resolve ( path , 'package.json' ) )
179
181
180
182
if ( json ) {
181
- output . buffer ( jsonOutput ( { path, problems, result, rootError, seenItems } ) )
183
+ output . buffer ( jsonOutput ( { path, problems, result, rootError } ) )
182
184
} else {
183
185
output . standard ( parseable
184
186
? parseableOutput ( { seenNodes, global, long } )
185
- : humanOutput ( { chalk, result, seenItems , unicode } )
187
+ : humanOutput ( { chalk, result, unicode } )
186
188
)
187
189
}
188
190
@@ -225,6 +227,70 @@ class LS extends ArboristWorkspaceCmd {
225
227
226
228
module . exports = LS
227
229
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 child
A851
Result = 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
+
228
294
const isGitNode = ( node ) => {
229
295
if ( ! node . resolved ) {
230
296
return
@@ -262,26 +328,6 @@ const getProblems = (node, { global }) => {
262
328
return problems
263
329
}
264
330
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
-
285
331
const getHumanOutputItem = ( node , { args, chalk, global, long } ) => {
286
332
const { pkgid, path } = node
287
333
const workspacePkgId = chalk . blueBright ( pkgid )
@@ -339,13 +385,13 @@ const getHumanOutputItem = (node, { args, chalk, global, long }) => {
339
385
) +
340
386
( isGitNode ( node ) ? ` (${ node . resolved } )` : '' ) +
341
387
( node . isLink ? ` -> ${ relativePrefix } ${ targetLocation } ` : '' ) +
342
- ( long ? `\n${ node . package . description || '' } ` : '' )
388
+ ( long ? `\n${ node . package ? .description || '' } ` : '' )
343
389
344
- return augmentItemWithIncludeMetadata ( node , { label, nodes : [ ] } )
390
+ return ( { label, nodes : [ ] , [ _include ] : node [ _include ] , [ _parent ] : node [ _parent ] } )
345
391
}
346
392
347
393
const getJsonOutputItem = ( node , { global, long } ) => {
348
- const item = { }
394
+ const item = { [ _include ] : node [ _include ] , [ _parent ] : node [ _parent ] }
349
395
350
396
if ( node . version ) {
351
397
item . version = node . version
@@ -402,7 +448,7 @@ const getJsonOutputItem = (node, { global, long }) => {
402
448
item . problems = [ ...node [ _problems ] ]
403
449
}
404
450
405
- return augmentItemWithIncludeMetadata ( node , item )
451
+ return item
406
452
}
407
453
408
454
const filterByEdgesTypes = ( { link, omit } ) => ( edge ) => {
@@ -467,27 +513,24 @@ const augmentNodesWithMetadata = ({
467
513
// revisit that node in tree traversal logic, so we make it so that
468
514
// we have a diff obj for deduped nodes:
469
515
if ( seenNodes . has ( node . path ) ) {
470
- const { realpath, root } = node
471
- const targetLocation = root ? relative ( root . realpath , realpath )
472
- : node . targetLocation
473
516
node = {
474
517
name : node . name ,
475
518
version : node . version ,
476
519
pkgid : node . pkgid ,
520
+ target : node . target ,
521
+ edgesOut : node . edgesOut ,
522
+ children : node . children ,
477
523
package : node . package ,
478
524
path : node . path ,
479
525
isLink : node . isLink ,
480
526
realpath : node . realpath ,
481
- targetLocation,
527
+ targetLocation : node . targetLocation ,
482
528
[ _type ] : node [ _type ] ,
483
529
[ _invalid ] : node [ _invalid ] ,
484
530
[ _missing ] : node [ _missing ] ,
485
531
// if it's missing, it's not deduped, it's just missing
486
532
[ _dedupe ] : ! node [ _missing ] ,
487
533
}
488
- } else {
489
- // keeps track of already seen nodes in order to check for dedupes
490
- seenNodes . set ( node . path , node )
491
534
}
492
535
493
536
// _parent is going to be a ref to a treeverse-visited node (returned from
@@ -499,7 +542,11 @@ const augmentNodesWithMetadata = ({
499
542
// _filteredBy is used to apply extra color info to the item that
500
543
// was used in args in order to filter
501
544
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
+ } )
503
550
// _depth keeps track of how many levels deep tree traversal currently is
504
551
// so that we can `npm ls --depth=1`
505
552
node [ _depth ] = currentDepth + 1
@@ -509,17 +556,7 @@ const augmentNodesWithMetadata = ({
509
556
510
557
const sortAlphabetically = ( { pkgid : a } , { pkgid : b } ) => localeCompare ( a , b )
511
558
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 } ) => {
523
560
if ( ! result . nodes . length ) {
524
561
result . nodes = [ '(empty)' ]
525
562
}
@@ -528,7 +565,7 @@ const humanOutput = ({ chalk, result, seenItems, unicode }) => {
528
565
return chalk . reset ( archyOutput )
529
566
}
530
567
531
- const jsonOutput = ( { path, problems, result, rootError, seenItems } ) => {
568
+ const jsonOutput = ( { path, problems, result, rootError } ) => {
532
569
if ( problems . size ) {
533
570
result . problems = [ ...problems ]
534
571
}
@@ -541,22 +578,6 @@ const jsonOutput = ({ path, problems, result, rootError, seenItems }) => {
541
578
result . invalid = true
542
579
}
543
580
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
-
560
581
return result
561
582
}
562
583
0 commit comments