11import { hoist , type HoisterTree , type HoisterResult } from "./hoist"
22import * as path from "path"
33import * as fs from "fs"
4- import { NodeModuleInfo , DependencyTree , DependencyGraph } from "./types"
4+ import type { NodeModuleInfo , DependencyTree , DependencyGraph , Dependency } from "./types"
55import { exec , log } from "builder-util"
66
7- export abstract class NodeModulesCollector {
8- private nodeModules : NodeModuleInfo [ ]
9- protected dependencyPathMap : Map < string , string >
10- protected allDependencies : Map < string , DependencyTree > = new Map ( )
7+ export abstract class NodeModulesCollector < T extends Dependency < T , OptionalsType > , OptionalsType > {
8+ private nodeModules : NodeModuleInfo [ ] = [ ]
9+ protected dependencyPathMap : Map < string , string > = new Map ( )
10+ protected allDependencies : Map < string , T > = new Map ( )
1111
12- constructor ( private readonly rootDir : string ) {
13- this . dependencyPathMap = new Map ( )
14- this . nodeModules = [ ]
12+ constructor ( private readonly rootDir : string ) { }
13+
14+ public async getNodeModules ( ) : Promise < NodeModuleInfo [ ] > {
15+ const tree : T = await this . getDependenciesTree ( )
16+ const realTree : T = this . getTreeFromWorkspaces ( tree )
17+ const parsedTree : Dependency < T , OptionalsType > = this . extractRelevantData ( realTree )
18+
19+ this . collectAllDependencies ( parsedTree )
20+
21+ const productionTree : DependencyTree = this . extractProductionDependencyTree ( parsedTree )
22+ const dependencyGraph : DependencyGraph = this . convertToDependencyGraph ( productionTree )
23+
24+ const hoisterResult : HoisterResult = hoist ( this . transToHoisterTree ( dependencyGraph ) , { check : true } )
25+ this . _getNodeModules ( hoisterResult . dependencies , this . nodeModules )
26+
27+ return this . nodeModules
1528 }
1629
17- private transToHoisterTree ( obj : DependencyGraph , key : string = `.` , nodes : Map < string , HoisterTree > = new Map ( ) ) : HoisterTree {
18- let node = nodes . get ( key )
19- const name = key . match ( / @ ? [ ^ @ ] + / ) ! [ 0 ]
20- if ( ! node ) {
21- node = {
22- name,
23- identName : name ,
24- reference : key . match ( / @ ? [ ^ @ ] + @ ? ( .+ ) ? / ) ! [ 1 ] || `` ,
25- dependencies : new Set < HoisterTree > ( ) ,
26- peerNames : new Set < string > ( [ ] ) ,
27- }
28- nodes . set ( key , node )
30+ protected abstract getCommand ( ) : string
31+ protected abstract getArgs ( ) : string [ ]
32+ protected abstract parseDependenciesTree ( jsonBlob : string ) : T
33+ protected abstract extractProductionDependencyTree ( tree : Dependency < T , OptionalsType > ) : DependencyTree
2934
30- for ( const dep of ( obj [ key ] || { } ) . dependencies || [ ] ) {
31- node . dependencies . add ( this . transToHoisterTree ( obj , dep , nodes ) )
32- }
35+ protected async getDependenciesTree ( ) : Promise < T > {
36+ const command = this . getCommand ( )
37+ const args = this . getArgs ( )
38+ const dependencies = await exec ( command , args , {
39+ cwd : this . rootDir ,
40+ shell : true ,
41+ } )
42+ return this . parseDependenciesTree ( dependencies )
43+ }
44+
45+ protected extractRelevantData ( npmTree : T ) : Dependency < T , OptionalsType > {
46+ // Do not use `...npmTree` as we are explicitly extracting the data we need
47+ const { name, version, path, workspaces, dependencies } = npmTree
48+ const tree : Dependency < T , OptionalsType > = {
49+ name,
50+ version,
51+ path,
52+ workspaces,
53+ // DFS extract subtree
54+ dependencies : this . extractInternal ( dependencies ) ,
3355 }
34- return node
56+
57+ return tree
3558 }
3659
37- protected resolvePath ( filePath : string ) {
60+ protected extractInternal ( deps : T [ "dependencies" ] ) : T [ "dependencies" ] {
61+ return deps && Object . keys ( deps ) . length > 0
62+ ? Object . entries ( deps ) . reduce ( ( accum , [ packageName , depObjectOrVersionString ] ) => {
63+ return {
64+ ...accum ,
65+ [ packageName ] :
66+ typeof depObjectOrVersionString === "object" && Object . keys ( depObjectOrVersionString ) . length > 0
67+ ? this . extractRelevantData ( depObjectOrVersionString )
68+ : depObjectOrVersionString ,
69+ }
70+ } , { } )
71+ : undefined
72+ }
73+
74+ protected resolvePath ( filePath : string ) : string {
3875 try {
3976 const stats = fs . lstatSync ( filePath )
4077 if ( stats . isSymbolicLink ( ) ) {
@@ -48,70 +85,89 @@ export abstract class NodeModulesCollector {
4885 }
4986 }
5087
51- private convertToDependencyGraph ( tree : DependencyTree ) : DependencyGraph {
52- const result : DependencyGraph = { "." : { } }
53-
54- const flatten = ( node : DependencyTree , parentKey = "." ) => {
55- const dependencies = node . dependencies || { }
56-
57- for ( const [ key , value ] of Object . entries ( dependencies ) ) {
58- // Skip empty dependencies(like some optionalDependencies)
59- if ( Object . keys ( value ) . length === 0 ) {
60- continue
61- }
62- const version = value . version || ""
63- const newKey = `${ key } @${ version } `
64- this . dependencyPathMap . set ( newKey , path . normalize ( this . resolvePath ( value . path ) ) )
65- if ( ! result [ parentKey ] ?. dependencies ) {
66- result [ parentKey ] = { dependencies : [ ] }
67- }
68- result [ parentKey ] . dependencies ! . push ( newKey )
69-
70- if ( node . __circularDependencyDetected ) {
71- continue
72- }
73- flatten ( value , newKey )
88+ private convertToDependencyGraph ( tree : DependencyTree , parentKey = "." ) : DependencyGraph {
89+ return Object . entries ( tree . dependencies || { } ) . reduce < DependencyGraph > ( ( acc , curr ) => {
90+ const [ packageName , dependencies ] = curr
91+ // Skip empty dependencies (like some optionalDependencies)
92+ if ( Object . keys ( dependencies ) . length === 0 ) {
93+ return acc
94+ }
95+ const version = dependencies . version || ""
96+ const newKey = `${ packageName } @${ version } `
97+ if ( ! dependencies . path ) {
98+ log . error (
99+ {
100+ packageName,
101+ data : dependencies ,
102+ parentModule : tree . name ,
103+ parentVersion : tree . version ,
104+ } ,
105+ "dependency path is undefined"
106+ )
107+ throw new Error ( "unable to parse `path` during `tree.dependencies` reduce" )
108+ }
109+ // Map dependency details: name, version and path to the dependency tree
110+ this . dependencyPathMap . set ( newKey , path . normalize ( this . resolvePath ( dependencies . path ) ) )
111+ if ( ! acc [ parentKey ] ) {
112+ acc [ parentKey ] = { dependencies : [ ] }
113+ }
114+ acc [ parentKey ] . dependencies . push ( newKey )
115+ if ( tree . implicitDependenciesInjected ) {
116+ log . debug (
117+ {
118+ dependency : packageName ,
119+ version,
120+ path : dependencies . path ,
121+ parentModule : tree . name ,
122+ parentVersion : tree . version ,
123+ } ,
124+ "converted implicit dependency"
125+ )
126+ return acc
74127 }
75- }
76128
77- flatten ( tree )
78- return result
129+ return { ... acc , ... this . convertToDependencyGraph ( dependencies , newKey ) }
130+ } , { } )
79131 }
80132
81- getAllDependencies ( tree : DependencyTree ) {
82- const dependencies = tree . dependencies || { }
83- for ( const [ key , value ] of Object . entries ( dependencies ) ) {
84- if ( value . dependencies && Object . keys ( value . dependencies ) . length > 0 ) {
133+ private collectAllDependencies ( tree : Dependency < T , OptionalsType > ) {
134+ for ( const [ key , value ] of Object . entries ( tree . dependencies || { } ) ) {
135+ if ( Object . keys ( value . dependencies ?? { } ) . length > 0 ) {
85136 this . allDependencies . set ( `${ key } @${ value . version } ` , value )
86- this . getAllDependencies ( value )
137+ this . collectAllDependencies ( value )
87138 }
88139 }
89140 }
90141
91- abstract getCommand ( ) : string
92- abstract getArgs ( ) : string [ ]
93- abstract removeNonProductionDependencie ( tree : DependencyTree ) : void
142+ private getTreeFromWorkspaces ( tree : T ) : T {
143+ if ( tree . workspaces && tree . dependencies ) {
144+ for ( const [ key , value ] of Object . entries ( tree . dependencies ) ) {
145+ if ( this . rootDir . endsWith ( path . normalize ( key ) ) ) {
146+ return value
147+ }
148+ }
149+ }
150+ return tree
151+ }
94152
95- protected async getDependenciesTree ( ) : Promise < DependencyTree > {
96- const command = this . getCommand ( )
97- const args = this . getArgs ( )
98- const dependencies = await exec ( command , args , {
99- cwd : this . rootDir ,
100- shell : true ,
101- } )
102- const dependencyTree : DependencyTree | DependencyTree [ ] = JSON . parse ( dependencies )
153+ private transToHoisterTree ( obj : DependencyGraph , key : string = `.` , nodes : Map < string , HoisterTree > = new Map ( ) ) : HoisterTree {
154+ let node = nodes . get ( key )
155+ const name = key . match ( / @ ? [ ^ @ ] + / ) ! [ 0 ]
156+ if ( ! node ) {
157+ node = {
158+ name,
159+ identName : name ,
160+ reference : key . match ( / @ ? [ ^ @ ] + @ ? ( .+ ) ? / ) ! [ 1 ] || `` ,
161+ dependencies : new Set < HoisterTree > ( ) ,
162+ peerNames : new Set < string > ( [ ] ) ,
163+ }
164+ nodes . set ( key , node )
103165
104- // pnpm returns an array of dependency trees
105- if ( Array . isArray ( dependencyTree ) ) {
106- const tree = dependencyTree [ 0 ]
107- if ( tree . optionalDependencies ) {
108- tree . dependencies = { ...tree . dependencies , ...tree . optionalDependencies }
166+ for ( const dep of ( obj [ key ] || { } ) . dependencies || [ ] ) {
167+ node . dependencies . add ( this . transToHoisterTree ( obj , dep , nodes ) )
109168 }
110- return tree
111169 }
112-
113- // yarn and npm return a single dependency tree
114- return dependencyTree
170+ return node
115171 }
116172
117173 private _getNodeModules ( dependencies : Set < HoisterResult > , result : NodeModuleInfo [ ] ) {
@@ -133,32 +189,10 @@ export abstract class NodeModulesCollector {
133189 }
134190 result . push ( node )
135191 if ( d . dependencies . size > 0 ) {
136- node [ " dependencies" ] = [ ]
137- this . _getNodeModules ( d . dependencies , node [ " dependencies" ] )
192+ node . dependencies = [ ]
193+ this . _getNodeModules ( d . dependencies , node . dependencies )
138194 }
139195 }
140196 result . sort ( ( a , b ) => a . name . localeCompare ( b . name ) )
141197 }
142-
143- private getTreeFromWorkspaces ( tree : DependencyTree ) : DependencyTree {
144- if ( tree . workspaces && tree . dependencies ) {
145- for ( const [ key , value ] of Object . entries ( tree . dependencies ) ) {
146- if ( this . rootDir . endsWith ( path . normalize ( key ) ) ) {
147- return value
148- }
149- }
150- }
151- return tree
152- }
153-
154- public async getNodeModules ( ) : Promise < NodeModuleInfo [ ] > {
155- const tree = await this . getDependenciesTree ( )
156- const realTree = this . getTreeFromWorkspaces ( tree )
157- this . getAllDependencies ( realTree )
158- this . removeNonProductionDependencie ( realTree )
159- const dependencyGraph = this . convertToDependencyGraph ( realTree )
160- const hoisterResult = hoist ( this . transToHoisterTree ( dependencyGraph ) , { check : true } )
161- this . _getNodeModules ( hoisterResult . dependencies , this . nodeModules )
162- return this . nodeModules
163- }
164198}
0 commit comments