@@ -15,6 +15,7 @@ import {
15
15
import { extname } from 'path' ;
16
16
import * as assert from 'assert' ;
17
17
import { normalizeSlashes } from './util' ;
18
+ import { createRequire } from 'module' ;
18
19
const {
19
20
createResolve,
20
21
} = require ( '../dist-raw/node-esm-resolve-implementation' ) ;
@@ -68,7 +69,7 @@ export namespace NodeLoaderHooksAPI2 {
68
69
parentURL : string ;
69
70
} ,
70
71
defaultResolve : ResolveHook
71
- ) => Promise < { url : string } > ;
72
+ ) => Promise < { url : string ; format ?: NodeLoaderHooksFormat } > ;
72
73
export type LoadHook = (
73
74
url : string ,
74
75
context : {
@@ -123,47 +124,93 @@ export function createEsmHooks(tsNodeService: Service) {
123
124
const hooksAPI : NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 = newHooksAPI
124
125
? { resolve, load, getFormat : undefined , transformSource : undefined }
125
126
: { resolve, getFormat, transformSource, load : undefined } ;
126
- return hooksAPI ;
127
127
128
128
function isFileUrlOrNodeStyleSpecifier ( parsed : UrlWithStringQuery ) {
129
129
// We only understand file:// URLs, but in node, the specifier can be a node-style `./foo` or `foo`
130
130
const { protocol } = parsed ;
131
131
return protocol === null || protocol === 'file:' ;
132
132
}
133
133
134
+ /**
135
+ * Named "probably" as a reminder that this is a guess.
136
+ * node does not explicitly tell us if we're resolving the entrypoint or not.
137
+ */
138
+ function isProbablyEntrypoint ( specifier : string , parentURL : string ) {
139
+ return parentURL === undefined && specifier . startsWith ( 'file://' ) ;
140
+ }
141
+ // Side-channel between `resolve()` and `load()` hooks
142
+ const rememberIsProbablyEntrypoint = new Set ( ) ;
143
+ const rememberResolvedViaCommonjsFallback = new Set ( ) ;
144
+
134
145
async function resolve (
135
146
specifier : string ,
136
147
context : { parentURL : string } ,
137
148
defaultResolve : typeof resolve
138
- ) : Promise < { url : string } > {
149
+ ) : Promise < { url : string ; format ?: NodeLoaderHooksFormat } > {
139
150
const defer = async ( ) => {
140
151
const r = await defaultResolve ( specifier , context , defaultResolve ) ;
141
152
return r ;
142
153
} ;
154
+ // See: https://github.com/nodejs/node/discussions/41711
155
+ // nodejs will likely implement a similar fallback. Till then, we can do our users a favor and fallback today.
156
+ async function entrypointFallback (
157
+ cb : ( ) => ReturnType < typeof resolve >
158
+ ) : ReturnType < typeof resolve > {
159
+ try {
160
+ const resolution = await cb ( ) ;
161
+ if (
162
+ resolution ?. url &&
163
+ isProbablyEntrypoint ( specifier , context . parentURL )
164
+ )
165
+ rememberIsProbablyEntrypoint . add ( resolution . url ) ;
166
+ return resolution ;
167
+ } catch ( esmResolverError ) {
168
+ if ( ! isProbablyEntrypoint ( specifier , context . parentURL ) )
169
+ throw esmResolverError ;
170
+ try {
171
+ let cjsSpecifier = specifier ;
172
+ // Attempt to convert from ESM file:// to CommonJS path
173
+ try {
174
+ if ( specifier . startsWith ( 'file://' ) )
175
+ cjsSpecifier = fileURLToPath ( specifier ) ;
176
+ } catch { }
177
+ const resolution = pathToFileURL (
178
+ createRequire ( process . cwd ( ) ) . resolve ( cjsSpecifier )
179
+ ) . toString ( ) ;
180
+ rememberIsProbablyEntrypoint . add ( resolution ) ;
181
+ rememberResolvedViaCommonjsFallback . add ( resolution ) ;
182
+ return { url : resolution , format : 'commonjs' } ;
183
+ } catch ( commonjsResolverError ) {
184
+ throw esmResolverError ;
185
+ }
186
+ }
187
+ }
143
188
144
189
const parsed = parseUrl ( specifier ) ;
145
190
const { pathname, protocol, hostname } = parsed ;
146
191
147
192
if ( ! isFileUrlOrNodeStyleSpecifier ( parsed ) ) {
148
- return defer ( ) ;
193
+ return entrypointFallback ( defer ) ;
149
194
}
150
195
151
196
if ( protocol !== null && protocol !== 'file:' ) {
152
- return defer ( ) ;
197
+ return entrypointFallback ( defer ) ;
153
198
}
154
199
155
200
// Malformed file:// URL? We should always see `null` or `''`
156
201
if ( hostname ) {
157
202
// TODO file://./foo sets `hostname` to `'.'`. Perhaps we should special-case this.
158
- return defer ( ) ;
203
+ return entrypointFallback ( defer ) ;
159
204
}
160
205
161
206
// pathname is the path to be resolved
162
207
163
- return nodeResolveImplementation . defaultResolve (
164
- specifier ,
165
- context ,
166
- defaultResolve
208
+ return entrypointFallback ( ( ) =>
209
+ nodeResolveImplementation . defaultResolve (
210
+ specifier ,
211
+ context ,
212
+ defaultResolve
213
+ )
167
214
) ;
168
215
}
169
216
@@ -230,10 +277,23 @@ export function createEsmHooks(tsNodeService: Service) {
230
277
const defer = ( overrideUrl : string = url ) =>
231
278
defaultGetFormat ( overrideUrl , context , defaultGetFormat ) ;
232
279
280
+ // See: https://github.com/nodejs/node/discussions/41711
281
+ // nodejs will likely implement a similar fallback. Till then, we can do our users a favor and fallback today.
282
+ async function entrypointFallback (
283
+ cb : ( ) => ReturnType < typeof getFormat >
284
+ ) : ReturnType < typeof getFormat > {
285
+ try {
286
+ return await cb ( ) ;
287
+ } catch ( getFormatError ) {
288
+ if ( ! rememberIsProbablyEntrypoint . has ( url ) ) throw getFormatError ;
289
+ return { format : 'commonjs' } ;
290
+ }
291
+ }
292
+
233
293
const parsed = parseUrl ( url ) ;
234
294
235
295
if ( ! isFileUrlOrNodeStyleSpecifier ( parsed ) ) {
236
- return defer ( ) ;
296
+ return entrypointFallback ( defer ) ;
237
297
}
238
298
239
299
const { pathname } = parsed ;
@@ -248,9 +308,11 @@ export function createEsmHooks(tsNodeService: Service) {
248
308
const ext = extname ( nativePath ) ;
249
309
let nodeSays : { format : NodeLoaderHooksFormat } ;
250
310
if ( ext !== '.js' && ! tsNodeService . ignored ( nativePath ) ) {
251
- nodeSays = await defer ( formatUrl ( pathToFileURL ( nativePath + '.js' ) ) ) ;
311
+ nodeSays = await entrypointFallback ( ( ) =>
312
+ defer ( formatUrl ( pathToFileURL ( nativePath + '.js' ) ) )
313
+ ) ;
252
314
} else {
253
- nodeSays = await defer ( ) ;
315
+ nodeSays = await entrypointFallback ( defer ) ;
254
316
}
255
317
// For files compiled by ts-node that node believes are either CJS or ESM, check if we should override that classification
256
318
if (
@@ -300,4 +362,6 @@ export function createEsmHooks(tsNodeService: Service) {
300
362
301
363
return { source : emittedJs } ;
302
364
}
365
+
366
+ return hooksAPI ;
303
367
}
0 commit comments