1+ import { type ParsedLockFile , traverse , type ParsedDependency } from 'lockparse' ;
2+
13function getLsCommand (
24 lockfilePath : string ,
35 packageName : string
@@ -17,20 +19,104 @@ function getLsCommand(
1719 return undefined ;
1820}
1921
22+ function getParentPath (
23+ node : ParsedDependency ,
24+ parentMap : WeakMap < ParsedDependency , ParsedDependency > | undefined
25+ ) : string [ ] {
26+ const parentPath : string [ ] = [ ] ;
27+ if ( ! parentMap ) {
28+ return parentPath ;
29+ }
30+ let currentParent = parentMap . get ( node ) ;
31+ while ( currentParent ) {
32+ parentPath . push ( `${ currentParent . name } @${ currentParent . version } ` ) ;
33+ currentParent = parentMap . get ( currentParent ) ;
34+ }
35+ return parentPath ;
36+ }
37+
38+ function computeParentPaths (
39+ lockfile : ParsedLockFile ,
40+ duplicateDependencyNames : Set < string > ,
41+ dependencyMap : Map < string , Set < string > >
42+ ) : Map < string , string > {
43+ const parentPaths = new Map < string , string > ( ) ;
44+
45+ const visitorFn = (
46+ node : ParsedDependency ,
47+ _parent : ParsedDependency | null ,
48+ parentMap ?: WeakMap < ParsedDependency , ParsedDependency >
49+ ) => {
50+ if ( ! duplicateDependencyNames . has ( node . name ) ) {
51+ return ;
52+ }
53+ const versionSet = dependencyMap . get ( node . name ) ;
54+ if ( ! versionSet ) {
55+ return ;
56+ }
57+ const parentPath = getParentPath ( node , parentMap ) ;
58+ parentPaths . set ( `${ node . name } @${ node . version } ` , parentPath . join ( ' -> ' ) ) ;
59+ } ;
60+ const visitor = {
61+ dependency : visitorFn ,
62+ devDependency : visitorFn ,
63+ peerDependency : visitorFn ,
64+ optionalDependency : visitorFn
65+ } ;
66+ for ( const pkg of lockfile . packages ) {
67+ visitorFn ( pkg , null ) ;
68+ traverse ( pkg , visitor ) ;
69+ }
70+
71+ return parentPaths ;
72+ }
73+
2074export function scanForDuplicates (
2175 messages : string [ ] ,
2276 threshold : number ,
2377 dependencyMap : Map < string , Set < string > > ,
24- lockfilePath : string
78+ lockfilePath : string ,
79+ lockfile : ParsedLockFile
2580) : void {
2681 const duplicateRows : string [ ] = [ ] ;
82+ const duplicateDependencyNames = new Set < string > ( ) ;
83+
2784 for ( const [ packageName , currentVersionSet ] of dependencyMap ) {
2885 if ( currentVersionSet . size > threshold ) {
29- const versions = Array . from ( currentVersionSet ) . sort ( ) ;
30- duplicateRows . push (
31- `| ${ packageName } | ${ currentVersionSet . size } versions | ${ versions . join ( ', ' ) } |`
32- ) ;
86+ duplicateDependencyNames . add ( packageName ) ;
87+ }
88+ }
89+
90+ if ( duplicateDependencyNames . size === 0 ) {
91+ return ;
92+ }
93+
94+ const parentPaths = computeParentPaths (
95+ lockfile ,
96+ duplicateDependencyNames ,
97+ dependencyMap
98+ ) ;
99+
100+ for ( const name of duplicateDependencyNames ) {
101+ const versionSet = dependencyMap . get ( name ) ;
102+ if ( ! versionSet ) {
103+ continue ;
104+ }
105+ const versions = Array . from ( versionSet ) . sort ( ) ;
106+
107+ // Build collapsible details showing where each version comes from
108+ const detailsLines : string [ ] = [ ] ;
109+ for ( const version of versions ) {
110+ const pathKey = `${ name } @${ version } ` ;
111+ const path = parentPaths . get ( pathKey ) ;
112+ const pathDisplay = path || '(root)' ;
113+ detailsLines . push ( `**${ version } **: ${ pathDisplay } ` ) ;
33114 }
115+
116+ const detailsContent = detailsLines . join ( ' \n' ) ;
117+ const collapsibleSection = `<details><summary>${ versionSet . size } version${ versionSet . size > 1 ? 's' : '' } </summary>\n\n${ detailsContent } \n\n</details>` ;
118+
119+ duplicateRows . push ( `| ${ name } | ${ collapsibleSection } |` ) ;
34120 }
35121
36122 if ( duplicateRows . length > 0 ) {
@@ -41,8 +127,8 @@ export function scanForDuplicates(
41127 messages . push (
42128 `## ⚠️ Duplicate Dependencies (threshold: ${ threshold } )
43129
44- | 📦 Package | 🔢 Version Count | 📋 Versions |
45- | --- | --- | --- |
130+ | 📦 Package | 📋 Versions |
131+ | --- | --- |
46132${ duplicateRows . join ( '\n' ) } ${ helpMessage } `
47133 ) ;
48134 }
0 commit comments