@@ -15,6 +15,7 @@ import {
1515import { extname } from 'path' ;
1616import * as assert from 'assert' ;
1717import { normalizeSlashes } from './util' ;
18+ import { createRequire } from 'module' ;
1819const {
1920 createResolve,
2021} = require ( '../dist-raw/node-esm-resolve-implementation' ) ;
@@ -68,7 +69,7 @@ export namespace NodeLoaderHooksAPI2 {
6869 parentURL : string ;
6970 } ,
7071 defaultResolve : ResolveHook
71- ) => Promise < { url : string } > ;
72+ ) => Promise < { url : string ; format ?: NodeLoaderHooksFormat } > ;
7273 export type LoadHook = (
7374 url : string ,
7475 context : {
@@ -123,47 +124,93 @@ export function createEsmHooks(tsNodeService: Service) {
123124 const hooksAPI : NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 = newHooksAPI
124125 ? { resolve, load, getFormat : undefined , transformSource : undefined }
125126 : { resolve, getFormat, transformSource, load : undefined } ;
126- return hooksAPI ;
127127
128128 function isFileUrlOrNodeStyleSpecifier ( parsed : UrlWithStringQuery ) {
129129 // We only understand file:// URLs, but in node, the specifier can be a node-style `./foo` or `foo`
130130 const { protocol } = parsed ;
131131 return protocol === null || protocol === 'file:' ;
132132 }
133133
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+
134145 async function resolve (
135146 specifier : string ,
136147 context : { parentURL : string } ,
137148 defaultResolve : typeof resolve
138- ) : Promise < { url : string } > {
149+ ) : Promise < { url : string ; format ?: NodeLoaderHooksFormat } > {
139150 const defer = async ( ) => {
140151 const r = await defaultResolve ( specifier , context , defaultResolve ) ;
141152 return r ;
142153 } ;
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+ }
143188
144189 const parsed = parseUrl ( specifier ) ;
145190 const { pathname, protocol, hostname } = parsed ;
146191
147192 if ( ! isFileUrlOrNodeStyleSpecifier ( parsed ) ) {
148- return defer ( ) ;
193+ return entrypointFallback ( defer ) ;
149194 }
150195
151196 if ( protocol !== null && protocol !== 'file:' ) {
152- return defer ( ) ;
197+ return entrypointFallback ( defer ) ;
153198 }
154199
155200 // Malformed file:// URL? We should always see `null` or `''`
156201 if ( hostname ) {
157202 // TODO file://./foo sets `hostname` to `'.'`. Perhaps we should special-case this.
158- return defer ( ) ;
203+ return entrypointFallback ( defer ) ;
159204 }
160205
161206 // pathname is the path to be resolved
162207
163- return nodeResolveImplementation . defaultResolve (
164- specifier ,
165- context ,
166- defaultResolve
208+ return entrypointFallback ( ( ) =>
209+ nodeResolveImplementation . defaultResolve (
210+ specifier ,
211+ context ,
212+ defaultResolve
213+ )
167214 ) ;
168215 }
169216
@@ -230,10 +277,23 @@ export function createEsmHooks(tsNodeService: Service) {
230277 const defer = ( overrideUrl : string = url ) =>
231278 defaultGetFormat ( overrideUrl , context , defaultGetFormat ) ;
232279
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+
233293 const parsed = parseUrl ( url ) ;
234294
235295 if ( ! isFileUrlOrNodeStyleSpecifier ( parsed ) ) {
236- return defer ( ) ;
296+ return entrypointFallback ( defer ) ;
237297 }
238298
239299 const { pathname } = parsed ;
@@ -248,9 +308,11 @@ export function createEsmHooks(tsNodeService: Service) {
248308 const ext = extname ( nativePath ) ;
249309 let nodeSays : { format : NodeLoaderHooksFormat } ;
250310 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+ ) ;
252314 } else {
253- nodeSays = await defer ( ) ;
315+ nodeSays = await entrypointFallback ( defer ) ;
254316 }
255317 // For files compiled by ts-node that node believes are either CJS or ESM, check if we should override that classification
256318 if (
@@ -300,4 +362,6 @@ export function createEsmHooks(tsNodeService: Service) {
300362
301363 return { source : emittedJs } ;
302364 }
365+
366+ return hooksAPI ;
303367}
0 commit comments