1
1
import { hoist , type HoisterTree , type HoisterResult } from "./hoist"
2
2
import * as path from "path"
3
3
import * as fs from "fs"
4
- import { NodeModuleInfo , DependencyTree , DependencyGraph } from "./types"
4
+ import type { NodeModuleInfo , DependencyTree , DependencyGraph , Dependency } from "./types"
5
5
import { exec , log } from "builder-util"
6
6
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 ( )
11
11
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
15
28
}
16
29
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
29
34
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 ) ,
33
55
}
34
- return node
56
+
57
+ return tree
35
58
}
36
59
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 {
38
75
try {
39
76
const stats = fs . lstatSync ( filePath )
40
77
if ( stats . isSymbolicLink ( ) ) {
@@ -48,70 +85,89 @@ export abstract class NodeModulesCollector {
48
85
}
49
86
}
50
87
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
74
127
}
75
- }
76
128
77
- flatten ( tree )
78
- return result
129
+ return { ... acc , ... this . convertToDependencyGraph ( dependencies , newKey ) }
130
+ } , { } )
79
131
}
80
132
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 ) {
85
136
this . allDependencies . set ( `${ key } @${ value . version } ` , value )
86
- this . getAllDependencies ( value )
137
+ this . collectAllDependencies ( value )
87
138
}
88
139
}
89
140
}
90
141
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
+ }
94
152
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 )
103
165
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 ) )
109
168
}
110
- return tree
111
169
}
112
-
113
- // yarn and npm return a single dependency tree
114
- return dependencyTree
170
+ return node
115
171
}
116
172
117
173
private _getNodeModules ( dependencies : Set < HoisterResult > , result : NodeModuleInfo [ ] ) {
@@ -133,32 +189,10 @@ export abstract class NodeModulesCollector {
133
189
}
134
190
result . push ( node )
135
191
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 )
138
194
}
139
195
}
140
196
result . sort ( ( a , b ) => a . name . localeCompare ( b . name ) )
141
197
}
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
- }
164
198
}
0 commit comments