1
- import type { LoadHook , ResolveHook } from 'node:module'
2
- import { fileURLToPath , pathToFileURL } from 'url'
1
+ import { readFile } from 'fs/promises'
2
+ import { createRequire , type LoadFnOutput , type LoadHook , type ResolveFnOutput , type ResolveHook } from 'node:module'
3
+ import { extname } from 'path'
4
+ import { fileURLToPath , parse as parseUrl , pathToFileURL } from 'url'
3
5
6
+ import debugFactory from 'debug'
4
7
import ts from 'typescript'
5
8
6
9
// @ts -expect-error
7
10
import { readDefaultTsConfig } from '../lib/read-default-tsconfig.js'
8
11
// @ts -expect-error
9
- import { AVAILABLE_EXTENSION_PATTERN , AVAILABLE_TS_EXTENSION_PATTERN , compile } from '../lib/register.js'
12
+ import { AVAILABLE_TS_EXTENSION_PATTERN , compile } from '../lib/register.js'
13
+
14
+ const debug = debugFactory ( '@swc-node' )
10
15
11
16
const tsconfig : ts . CompilerOptions = readDefaultTsConfig ( )
12
17
tsconfig . module = ts . ModuleKind . ESNext
@@ -17,21 +22,151 @@ const host: ts.ModuleResolutionHost = {
17
22
readFile : ts . sys . readFile ,
18
23
}
19
24
25
+ const addShortCircuitSignal = < T extends ResolveFnOutput | LoadFnOutput > ( input : T ) : T => {
26
+ return {
27
+ ...input ,
28
+ shortCircuit : true ,
29
+ }
30
+ }
31
+
32
+ interface PackageJson {
33
+ name : string
34
+ version : string
35
+ type ?: 'module' | 'commonjs'
36
+ main ?: string
37
+ }
38
+
39
+ const packageJSONCache = new Map < string , undefined | PackageJson > ( )
40
+
41
+ const readFileIfExists = async ( path : string ) => {
42
+ try {
43
+ const content = await readFile ( path , 'utf-8' )
44
+
45
+ return JSON . parse ( content )
46
+ } catch ( e ) {
47
+ // eslint-disable-next-line no-undef
48
+ if ( ( e as NodeJS . ErrnoException ) . code === 'ENOENT' ) {
49
+ return undefined
50
+ }
51
+
52
+ throw e
53
+ }
54
+ }
55
+
56
+ const readPackageJSON = async ( path : string ) => {
57
+ if ( packageJSONCache . has ( path ) ) {
58
+ return packageJSONCache . get ( path )
59
+ }
60
+
61
+ const res = ( await readFileIfExists ( path ) ) as PackageJson
62
+ packageJSONCache . set ( path , res )
63
+ return res
64
+ }
65
+
66
+ const getPackageForFile = async ( url : string ) => {
67
+ // use URL instead path.resolve to handle relative path
68
+ let packageJsonURL = new URL ( './package.json' , url )
69
+
70
+ // eslint-disable-next-line no-constant-condition
71
+ while ( true ) {
72
+ const path = fileURLToPath ( packageJsonURL )
73
+
74
+ // for special case by some package manager
75
+ if ( path . endsWith ( 'node_modules/package.json' ) ) {
76
+ break
77
+ }
78
+
79
+ const packageJson = await readPackageJSON ( path )
80
+
81
+ if ( ! packageJson ) {
82
+ const lastPath = packageJsonURL . pathname
83
+ packageJsonURL = new URL ( '../package.json' , packageJsonURL )
84
+
85
+ // root level /package.json
86
+ if ( packageJsonURL . pathname === lastPath ) {
87
+ break
88
+ }
89
+
90
+ continue
91
+ }
92
+
93
+ if ( packageJson . type && packageJson . type !== 'module' && packageJson . type !== 'commonjs' ) {
94
+ packageJson . type = undefined
95
+ }
96
+
97
+ return packageJson
98
+ }
99
+
100
+ return undefined
101
+ }
102
+
103
+ export const getPackageType = async ( url : string ) => {
104
+ const packageJson = await getPackageForFile ( url )
105
+
106
+ return packageJson ?. type ?? undefined
107
+ }
108
+
109
+ const INTERNAL_MODULE_PATTERN = / ^ ( n o d e | n o d e j s ) : /
110
+
111
+ const EXTENSION_MODULE_MAP = {
112
+ '.mjs' : 'module' ,
113
+ '.cjs' : 'commonjs' ,
114
+ '.ts' : 'module' ,
115
+ '.mts' : 'module' ,
116
+ '.cts' : 'commonjs' ,
117
+ '.json' : 'json' ,
118
+ '.wasm' : 'wasm' ,
119
+ '.node' : 'commonjs' ,
120
+ } as const
121
+
20
122
export const resolve : ResolveHook = async ( specifier , context , nextResolve ) => {
21
- if ( ! AVAILABLE_EXTENSION_PATTERN . test ( specifier ) ) {
22
- return nextResolve ( specifier )
123
+ debug ( 'resolve' , specifier , JSON . stringify ( context ) )
124
+
125
+ if ( INTERNAL_MODULE_PATTERN . test ( specifier ) ) {
126
+ debug ( 'skip resolve: internal format' , specifier )
127
+
128
+ return addShortCircuitSignal ( {
129
+ url : specifier ,
130
+ format : 'builtin' ,
131
+ } )
23
132
}
24
133
25
- // entrypoint
26
- if ( ! context . parentURL ) {
27
- return {
28
- importAttributes : {
29
- ...context . importAttributes ,
30
- swc : 'entrypoint' ,
31
- } ,
134
+ if ( specifier . startsWith ( 'data:' ) ) {
135
+ debug ( 'skip resolve: data url' , specifier )
136
+
137
+ return addShortCircuitSignal ( {
32
138
url : specifier ,
33
- shortCircuit : true ,
139
+ } )
140
+ }
141
+
142
+ const parsedUrl = parseUrl ( specifier )
143
+
144
+ // as entrypoint, just return specifier
145
+ if ( ! context . parentURL || parsedUrl . protocol === 'file:' ) {
146
+ debug ( 'skip resolve: absolute path or entrypoint' , specifier )
147
+
148
+ let format : ResolveFnOutput [ 'format' ] = null
149
+
150
+ const specifierPath = fileURLToPath ( specifier )
151
+ const ext = extname ( specifierPath )
152
+
153
+ if ( ext === '.js' ) {
154
+ format = ( await getPackageType ( specifier ) ) === 'module' ? 'module' : 'commonjs'
155
+ } else {
156
+ format = EXTENSION_MODULE_MAP [ ext as keyof typeof EXTENSION_MODULE_MAP ]
34
157
}
158
+
159
+ return addShortCircuitSignal ( {
160
+ url : specifier ,
161
+ format,
162
+ } )
163
+ }
164
+
165
+ // import attributes, support json currently
166
+ if ( context . importAttributes ?. type ) {
167
+ debug ( 'skip resolve: import attributes' , specifier )
168
+
169
+ return addShortCircuitSignal ( await nextResolve ( specifier ) )
35
170
}
36
171
37
172
const { resolvedModule } = ts . resolveModuleName (
@@ -45,21 +180,39 @@ export const resolve: ResolveHook = async (specifier, context, nextResolve) => {
45
180
// local project file
46
181
if (
47
182
resolvedModule &&
48
- ( ! resolvedModule . resolvedFileName . includes ( '/node_modules/' ) ||
49
- AVAILABLE_TS_EXTENSION_PATTERN . test ( resolvedModule . resolvedFileName ) )
183
+ ! resolvedModule . resolvedFileName . includes ( '/node_modules/' ) &&
184
+ AVAILABLE_TS_EXTENSION_PATTERN . test ( resolvedModule . resolvedFileName )
50
185
) {
51
- return {
186
+ debug ( 'resolved: typescript' , specifier , resolvedModule . resolvedFileName )
187
+
188
+ return addShortCircuitSignal ( {
189
+ ...context ,
52
190
url : pathToFileURL ( resolvedModule . resolvedFileName ) . href ,
53
- shortCircuit : true ,
54
- importAttributes : {
55
- ...context . importAttributes ,
56
- swc : resolvedModule . resolvedFileName ,
57
- } ,
58
- }
191
+ format : 'module' ,
192
+ } )
59
193
}
60
194
61
- // files could not resolved by typescript
62
- return nextResolve ( specifier )
195
+ try {
196
+ // files could not resolved by typescript or resolved as dts, fallback to use node resolver
197
+ const res = await nextResolve ( specifier )
198
+ debug ( 'resolved: fallback node' , specifier , res . url , res . format )
199
+ return addShortCircuitSignal ( res )
200
+ } catch ( resolveError ) {
201
+ // fallback to cjs resolve as may import non-esm files
202
+ try {
203
+ const resolution = pathToFileURL ( createRequire ( process . cwd ( ) ) . resolve ( specifier ) ) . toString ( )
204
+
205
+ debug ( 'resolved: fallback commonjs' , specifier , resolution )
206
+
207
+ return addShortCircuitSignal ( {
208
+ format : 'commonjs' ,
209
+ url : resolution ,
210
+ } )
211
+ } catch ( error ) {
212
+ debug ( 'resolved by cjs error' , specifier , error )
213
+ throw resolveError
214
+ }
215
+ }
63
216
}
64
217
65
218
const tsconfigForSWCNode = {
@@ -69,24 +222,31 @@ const tsconfigForSWCNode = {
69
222
}
70
223
71
224
export const load : LoadHook = async ( url , context , nextLoad ) => {
72
- const swcAttribute = context . importAttributes . swc
225
+ debug ( 'load' , url , JSON . stringify ( context ) )
73
226
74
- if ( swcAttribute ) {
75
- delete context . importAttributes . swc
227
+ if ( url . startsWith ( 'data:' ) ) {
228
+ debug ( 'skip load: data url' , url )
76
229
77
- const { source } = await nextLoad ( url , {
78
- ...context ,
79
- format : 'ts' as any ,
80
- } )
230
+ return nextLoad ( url , context )
231
+ }
81
232
82
- const code = ! source || typeof source === 'string' ? source : Buffer . from ( source ) . toString ( )
83
- const compiled = await compile ( code , fileURLToPath ( url ) , tsconfigForSWCNode , true )
84
- return {
85
- format : 'module' ,
86
- source : compiled ,
87
- shortCircuit : true ,
88
- }
89
- } else {
233
+ if ( [ 'builtin' , 'json' , 'wasm' ] . includes ( context . format ) ) {
234
+ debug ( 'loaded: internal format' , url )
90
235
return nextLoad ( url , context )
91
236
}
237
+
238
+ const { source, format : resolvedFormat } = await nextLoad ( url , context )
239
+
240
+ debug ( 'loaded' , url , resolvedFormat )
241
+
242
+ const code = ! source || typeof source === 'string' ? source : Buffer . from ( source ) . toString ( )
243
+ const compiled = await compile ( code , url , tsconfigForSWCNode , true )
244
+
245
+ debug ( 'compiled' , url , resolvedFormat )
246
+
247
+ return addShortCircuitSignal ( {
248
+ // for lazy: ts-node think format would undefined, actually it should not, keep it as original temporarily
249
+ format : resolvedFormat ,
250
+ source : compiled ,
251
+ } )
92
252
}
0 commit comments