@@ -5,51 +5,10 @@ import { logger } from "./logger";
5
5
import { PackageManager , getUserPackageManager } from "./getUserPackageManager" ;
6
6
import { PackageJson } from "type-fest" ;
7
7
import { assertExhaustive } from "./assertExhaustive" ;
8
+ import { builtinModules } from "node:module" ;
8
9
9
10
export type ResolveOptions = { allowDev : boolean } ;
10
11
11
- const BuiltInModules = new Set ( [
12
- "assert" ,
13
- "async_hooks" ,
14
- "buffer" ,
15
- "child_process" ,
16
- "cluster" ,
17
- "console" ,
18
- "constants" ,
19
- "crypto" ,
20
- "dgram" ,
21
- "dns" ,
22
- "domain" ,
23
- "events" ,
24
- "fs" ,
25
- "http" ,
26
- "http2" ,
27
- "https" ,
28
- "inspector" ,
29
- "module" ,
30
- "net" ,
31
- "os" ,
32
- "path" ,
33
- "perf_hooks" ,
34
- "process" ,
35
- "punycode" ,
36
- "querystring" ,
37
- "readline" ,
38
- "repl" ,
39
- "stream" ,
40
- "string_decoder" ,
41
- "timers" ,
42
- "tls" ,
43
- "trace_events" ,
44
- "tty" ,
45
- "url" ,
46
- "util" ,
47
- "v8" ,
48
- "vm" ,
49
- "worker_threads" ,
50
- "zlib" ,
51
- ] ) ;
52
-
53
12
export class JavascriptProject {
54
13
private _packageJson ?: PackageJson ;
55
14
private _packageManager ?: PackageManager ;
@@ -84,26 +43,72 @@ export class JavascriptProject {
84
43
}
85
44
}
86
45
87
- async resolve ( packageName : string , options ?: ResolveOptions ) : Promise < string | undefined > {
88
- if ( BuiltInModules . has ( packageName ) ) {
89
- return undefined ;
90
- }
46
+ async resolveAll (
47
+ packageNames : string [ ] ,
48
+ options ?: ResolveOptions
49
+ ) : Promise < Record < string , string > > {
50
+ const externalPackages = packageNames . filter ( ( packageName ) => ! isBuiltInModule ( packageName ) ) ;
91
51
92
52
const opts = { allowDev : false , ...options } ;
93
53
94
- const packageJsonVersion = this . packageJson . dependencies ?. [ packageName ] ;
54
+ const command = await this . #getCommand ( ) ;
95
55
96
- if ( typeof packageJsonVersion === "string" ) {
97
- return packageJsonVersion ;
98
- }
56
+ try {
57
+ const versions = await command . resolveDependencyVersions ( externalPackages , {
58
+ cwd : this . projectPath ,
59
+ } ) ;
60
+
61
+ if ( versions ) {
62
+ logger . debug ( `Resolved [${ externalPackages . join ( ", " ) } ] version using ${ command . name } ` , {
63
+ versions,
64
+ } ) ;
65
+ }
66
+
67
+ // Merge the resolved versions with the package.json dependencies
68
+ const missingPackages = externalPackages . filter ( ( packageName ) => ! versions [ packageName ] ) ;
69
+ const missingPackageVersions : Record < string , string > = { } ;
99
70
100
- if ( opts . allowDev ) {
101
- const devPackageJsonVersion = this . packageJson . devDependencies ?. [ packageName ] ;
71
+ for ( const packageName of missingPackages ) {
72
+ const packageJsonVersion = this . packageJson . dependencies ?. [ packageName ] ;
102
73
103
- if ( typeof devPackageJsonVersion === "string" ) {
104
- return devPackageJsonVersion ;
74
+ if ( typeof packageJsonVersion === "string" ) {
75
+ logger . debug ( `Resolved ${ packageName } version using package.json` , {
76
+ packageJsonVersion,
77
+ } ) ;
78
+
79
+ missingPackageVersions [ packageName ] = packageJsonVersion ;
80
+ }
81
+
82
+ if ( opts . allowDev ) {
83
+ const devPackageJsonVersion = this . packageJson . devDependencies ?. [ packageName ] ;
84
+
85
+ if ( typeof devPackageJsonVersion === "string" ) {
86
+ logger . debug ( `Resolved ${ packageName } version using devDependencies` , {
87
+ devPackageJsonVersion,
88
+ } ) ;
89
+
90
+ missingPackageVersions [ packageName ] = devPackageJsonVersion ;
91
+ }
92
+ }
105
93
}
94
+
95
+ return { ...versions , ...missingPackageVersions } ;
96
+ } catch ( error ) {
97
+ logger . debug ( `Failed to resolve dependency versions using ${ command . name } ` , {
98
+ packageNames,
99
+ error,
100
+ } ) ;
101
+
102
+ return { } ;
106
103
}
104
+ }
105
+
106
+ async resolve ( packageName : string , options ?: ResolveOptions ) : Promise < string | undefined > {
107
+ if ( isBuiltInModule ( packageName ) ) {
108
+ return undefined ;
109
+ }
110
+
111
+ const opts = { allowDev : false , ...options } ;
107
112
108
113
const command = await this . #getCommand( ) ;
109
114
@@ -113,8 +118,30 @@ export class JavascriptProject {
113
118
} ) ;
114
119
115
120
if ( version ) {
121
+ logger . debug ( `Resolved ${ packageName } version using ${ command . name } ` , { version } ) ;
122
+
116
123
return version ;
117
124
}
125
+
126
+ const packageJsonVersion = this . packageJson . dependencies ?. [ packageName ] ;
127
+
128
+ if ( typeof packageJsonVersion === "string" ) {
129
+ logger . debug ( `Resolved ${ packageName } version using package.json` , { packageJsonVersion } ) ;
130
+
131
+ return packageJsonVersion ;
132
+ }
133
+
134
+ if ( opts . allowDev ) {
135
+ const devPackageJsonVersion = this . packageJson . devDependencies ?. [ packageName ] ;
136
+
137
+ if ( typeof devPackageJsonVersion === "string" ) {
138
+ logger . debug ( `Resolved ${ packageName } version using devDependencies` , {
139
+ devPackageJsonVersion,
140
+ } ) ;
141
+
142
+ return devPackageJsonVersion ;
143
+ }
144
+ }
118
145
} catch ( error ) {
119
146
logger . debug ( `Failed to resolve dependency version using ${ command . name } ` , {
120
147
packageName,
@@ -176,6 +203,11 @@ interface PackageManagerCommands {
176
203
packageName : string ,
177
204
options : PackageManagerOptions
178
205
) : Promise < string | undefined > ;
206
+
207
+ resolveDependencyVersions (
208
+ packageNames : string [ ] ,
209
+ options : PackageManagerOptions
210
+ ) : Promise < Record < string , string > > ;
179
211
}
180
212
181
213
class PNPMCommands implements PackageManagerCommands {
@@ -197,7 +229,7 @@ class PNPMCommands implements PackageManagerCommands {
197
229
const { stdout } = await $ ( { cwd : options . cwd } ) `${ this . cmd } list ${ packageName } -r --json` ;
198
230
const result = JSON . parse ( stdout ) as PnpmList ;
199
231
200
- logger . debug ( `Resolving ${ packageName } version using ${ this . name } ` , { result } ) ;
232
+ logger . debug ( `Resolving ${ packageName } version using ${ this . name } ` ) ;
201
233
202
234
// Return the first dependency version that matches the package name
203
235
for ( const dep of result ) {
@@ -208,6 +240,31 @@ class PNPMCommands implements PackageManagerCommands {
208
240
}
209
241
}
210
242
}
243
+
244
+ async resolveDependencyVersions (
245
+ packageNames : string [ ] ,
246
+ options : PackageManagerOptions
247
+ ) : Promise < Record < string , string > > {
248
+ const { stdout } = await $ ( { cwd : options . cwd } ) `${ this . cmd } list ${ packageNames } -r --json` ;
249
+ const result = JSON . parse ( stdout ) as PnpmList ;
250
+
251
+ logger . debug ( `Resolving ${ packageNames . join ( " " ) } version using ${ this . name } ` ) ;
252
+
253
+ const results : Record < string , string > = { } ;
254
+
255
+ // Return the first dependency version that matches the package name
256
+ for ( const dep of result ) {
257
+ for ( const packageName of packageNames ) {
258
+ const dependency = dep . dependencies ?. [ packageName ] ;
259
+
260
+ if ( dependency ) {
261
+ results [ packageName ] = dependency . version ;
262
+ }
263
+ }
264
+ }
265
+
266
+ return results ;
267
+ }
211
268
}
212
269
213
270
type NpmDependency = {
@@ -246,6 +303,28 @@ class NPMCommands implements PackageManagerCommands {
246
303
return this . #recursivelySearchDependencies( output . dependencies , packageName ) ;
247
304
}
248
305
306
+ async resolveDependencyVersions (
307
+ packageNames : string [ ] ,
308
+ options : PackageManagerOptions
309
+ ) : Promise < Record < string , string > > {
310
+ const { stdout } = await $ ( { cwd : options . cwd } ) `${ this . cmd } list ${ packageNames } --json` ;
311
+ const output = JSON . parse ( stdout ) as NpmListOutput ;
312
+
313
+ logger . debug ( `Resolving ${ packageNames . join ( " " ) } version using ${ this . name } ` , { output } ) ;
314
+
315
+ const results : Record < string , string > = { } ;
316
+
317
+ for ( const packageName of packageNames ) {
318
+ const version = this . #recursivelySearchDependencies( output . dependencies , packageName ) ;
319
+
320
+ if ( version ) {
321
+ results [ packageName ] = version ;
322
+ }
323
+ }
324
+
325
+ return results ;
326
+ }
327
+
249
328
#recursivelySearchDependencies(
250
329
dependencies : Record < string , NpmDependency > ,
251
330
packageName : string
@@ -286,7 +365,7 @@ class YarnCommands implements PackageManagerCommands {
286
365
287
366
const lines = stdout . split ( "\n" ) ;
288
367
289
- logger . debug ( `Resolving ${ packageName } version using ${ this . name } ` , { lines } ) ;
368
+ logger . debug ( `Resolving ${ packageName } version using ${ this . name } ` ) ;
290
369
291
370
for ( const line of lines ) {
292
371
const json = JSON . parse ( line ) ;
@@ -296,4 +375,54 @@ class YarnCommands implements PackageManagerCommands {
296
375
}
297
376
}
298
377
}
378
+
379
+ async resolveDependencyVersions (
380
+ packageNames : string [ ] ,
381
+ options : PackageManagerOptions
382
+ ) : Promise < Record < string , string > > {
383
+ const { stdout } = await $ ( { cwd : options . cwd } ) `${ this . cmd } info ${ packageNames } --json` ;
384
+
385
+ const lines = stdout . split ( "\n" ) ;
386
+
387
+ logger . debug ( `Resolving ${ packageNames . join ( " " ) } version using ${ this . name } ` ) ;
388
+
389
+ const results : Record < string , string > = { } ;
390
+
391
+ for ( const line of lines ) {
392
+ const json = JSON . parse ( line ) ;
393
+
394
+ const packageName = this . #parseYarnValueIntoPackageName( json . value ) ;
395
+
396
+ if ( packageNames . includes ( packageName ) ) {
397
+ results [ packageName ] = json . children . Version ;
398
+ }
399
+ }
400
+
401
+ return results ;
402
+ }
403
+
404
+ // The "value" when doing yarn info is formatted like this:
405
+ // "package-name@npm:version" or "package-name@workspace:version"
406
+ // This function will parse the value into just the package name.
407
+ // This correctly handles scoped packages as well e.g. @scope /package-name@npm:version
408
+ #parseYarnValueIntoPackageName( value : string ) : string {
409
+ const parts = value . split ( "@" ) ;
410
+
411
+ // If the value does not contain an "@" symbol, then it's just the package name
412
+ if ( parts . length === 3 ) {
413
+ return parts [ 1 ] as string ;
414
+ }
415
+
416
+ // If the value contains an "@" symbol, then the package name is the first part
417
+ return parts [ 0 ] as string ;
418
+ }
419
+ }
420
+
421
+ function isBuiltInModule ( module : string ) : boolean {
422
+ // if the module has node: prefix, it's a built-in module
423
+ if ( module . startsWith ( "node:" ) ) {
424
+ return true ;
425
+ }
426
+
427
+ return builtinModules . includes ( module ) ;
299
428
}
0 commit comments