1
- import fs from 'node:fs'
1
+ import fs from 'node:fs/promises '
2
2
import path from 'node:path'
3
3
4
- let jsExtensions = [ '.js' , '.cjs' , '.mjs' ]
4
+ // Patterns we use to match dependencies in a file whether in CJS, ESM, or TypeScript
5
+ const DEPENDENCY_PATTERNS = [
6
+ / i m p o r t [ \s \S ] * ?[ ' " ] ( .{ 3 , } ?) [ ' " ] / gi,
7
+ / i m p o r t [ \s \S ] * f r o m [ \s \S ] * ?[ ' " ] ( .{ 3 , } ?) [ ' " ] / gi,
8
+ / e x p o r t [ \s \S ] * f r o m [ \s \S ] * ?[ ' " ] ( .{ 3 , } ?) [ ' " ] / gi,
9
+ / r e q u i r e \( [ ' " ` ] ( .+ ) [ ' " ` ] \) / gi,
10
+ ]
5
11
6
12
// Given the current file `a.ts`, we want to make sure that when importing `b` that we resolve
7
13
// `b.ts` before `b.js`
@@ -13,73 +19,88 @@ let jsExtensions = ['.js', '.cjs', '.mjs']
13
19
// c // .ts
14
20
// a.js
15
21
// b // .js or .ts
16
-
22
+ let jsExtensions = [ '.js' , '.cjs' , '.mjs' ]
17
23
let jsResolutionOrder = [ '' , '.js' , '.cjs' , '.mjs' , '.ts' , '.cts' , '.mts' , '.jsx' , '.tsx' ]
18
24
let tsResolutionOrder = [ '' , '.ts' , '.cts' , '.mts' , '.tsx' , '.js' , '.cjs' , '.mjs' , '.jsx' ]
19
25
20
- function resolveWithExtension ( file : string , extensions : string [ ] ) {
26
+ async function resolveWithExtension ( file : string , extensions : string [ ] ) {
21
27
// Try to find `./a.ts`, `./a.cts`, ... from `./a`
22
28
for ( let ext of extensions ) {
23
29
let full = `${ file } ${ ext } `
24
- if ( fs . existsSync ( full ) && fs . statSync ( full ) . isFile ( ) ) {
25
- return full
26
- }
30
+
31
+ let stats = await fs . stat ( full ) . catch ( ( ) => null )
32
+ if ( stats ?. isFile ( ) ) return full
27
33
}
28
34
29
35
// Try to find `./a/index.js` from `./a`
30
36
for ( let ext of extensions ) {
31
37
let full = `${ file } /index${ ext } `
32
- if ( fs . existsSync ( full ) ) {
38
+
39
+ let exists = await fs . access ( full ) . then (
40
+ ( ) => true ,
41
+ ( ) => false ,
42
+ )
43
+ if ( exists ) {
33
44
return full
34
45
}
35
46
}
36
47
37
48
return null
38
49
}
39
50
40
- function * _getModuleDependencies (
51
+ async function traceDependencies (
52
+ seen : Set < string > ,
41
53
filename : string ,
42
54
base : string ,
43
- seen : Set < string > ,
44
- ext = path . extname ( filename ) ,
45
- ) : Iterable < string > {
55
+ ext : string ,
56
+ ) : Promise < void > {
46
57
// Try to find the file
47
- let absoluteFile = resolveWithExtension (
48
- path . resolve ( base , filename ) ,
49
- jsExtensions . includes ( ext ) ? jsResolutionOrder : tsResolutionOrder ,
50
- )
58
+ let extensions = jsExtensions . includes ( ext ) ? jsResolutionOrder : tsResolutionOrder
59
+ let absoluteFile = await resolveWithExtension ( path . resolve ( base , filename ) , extensions )
51
60
if ( absoluteFile === null ) return // File doesn't exist
52
61
53
62
// Prevent infinite loops when there are circular dependencies
54
63
if ( seen . has ( absoluteFile ) ) return // Already seen
55
- seen . add ( absoluteFile )
56
64
57
65
// Mark the file as a dependency
58
- yield absoluteFile
66
+ seen . add ( absoluteFile )
59
67
60
68
// Resolve new base for new imports/requires
61
69
base = path . dirname ( absoluteFile )
62
70
ext = path . extname ( absoluteFile )
63
71
64
- let contents = fs . readFileSync ( absoluteFile , 'utf-8' )
72
+ let contents = await fs . readFile ( absoluteFile , 'utf-8' )
73
+
74
+ // Recursively trace dependencies in parallel
75
+ let promises = [ ]
65
76
66
- // Find imports/requires
67
- for ( let match of [
68
- ...contents . matchAll ( / i m p o r t [ \s \S ] * ?[ ' " ] ( .{ 3 , } ?) [ ' " ] / gi) ,
69
- ...contents . matchAll ( / i m p o r t [ \s \S ] * f r o m [ \s \S ] * ?[ ' " ] ( .{ 3 , } ?) [ ' " ] / gi) ,
70
- ...contents . matchAll ( / e x p o r t [ \s \S ] * f r o m [ \s \S ] * ?[ ' " ] ( .{ 3 , } ?) [ ' " ] / gi) ,
71
- ...contents . matchAll ( / r e q u i r e \( [ ' " ` ] ( .+ ) [ ' " ` ] \) / gi) ,
72
- ] ) {
73
- // Bail out if it's not a relative file
74
- if ( ! match [ 1 ] . startsWith ( '.' ) ) continue
77
+ for ( let pattern of DEPENDENCY_PATTERNS ) {
78
+ for ( let match of contents . matchAll ( pattern ) ) {
79
+ // Bail out if it's not a relative file
80
+ if ( ! match [ 1 ] . startsWith ( '.' ) ) continue
75
81
76
- yield * _getModuleDependencies ( match [ 1 ] , base , seen , ext )
82
+ promises . push ( traceDependencies ( seen , match [ 1 ] , base , ext ) )
83
+ }
77
84
}
85
+
86
+ await Promise . all ( promises )
78
87
}
79
88
80
- export function getModuleDependencies ( absoluteFilePath : string ) {
81
- if ( absoluteFilePath === null ) return new Set < string > ( )
82
- return new Set (
83
- _getModuleDependencies ( absoluteFilePath , path . dirname ( absoluteFilePath ) , new Set ( ) ) ,
89
+ /**
90
+ * Trace all dependencies of a module recursively
91
+ *
92
+ * The result in an unordered set of absolute file paths. Meaning that the order
93
+ * is not guaranteed to be equal to source order or across runs.
94
+ **/
95
+ export async function getModuleDependencies ( absoluteFilePath : string ) {
96
+ let seen = new Set < string > ( )
97
+
98
+ await traceDependencies (
99
+ seen ,
100
+ absoluteFilePath ,
101
+ path . dirname ( absoluteFilePath ) ,
102
+ path . extname ( absoluteFilePath ) ,
84
103
)
104
+
105
+ return seen
85
106
}
0 commit comments