@@ -295,16 +295,31 @@ function folderIsModule(folder: string): boolean {
295
295
return require ( packageJsonPath ) . type === 'module' ;
296
296
}
297
297
298
- // This follows the --moduleResolution=bundler strategy from tsc.
299
- // https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta/#moduleresolution-bundler
298
+ const packageJsonMainFieldCache = new Map < string , string | undefined > ( ) ;
299
+
300
+ function getMainFieldFromPackageJson ( packageJsonPath : string ) {
301
+ if ( ! packageJsonMainFieldCache . has ( packageJsonPath ) ) {
302
+ let mainField : string | undefined ;
303
+ try {
304
+ mainField = JSON . parse ( fs . readFileSync ( packageJsonPath , 'utf8' ) ) . main ;
305
+ } catch {
306
+ }
307
+ packageJsonMainFieldCache . set ( packageJsonPath , mainField ) ;
308
+ }
309
+ return packageJsonMainFieldCache . get ( packageJsonPath ) ;
310
+ }
311
+
312
+ // This method performs "file extension subsitution" to find the ts, js or similar source file
313
+ // based on the import specifier, which might or might not have an extension. See TypeScript docs:
314
+ // https://www.typescriptlang.org/docs/handbook/modules/reference.html#file-extension-substitution.
300
315
const kExtLookups = new Map ( [
301
316
[ '.js' , [ '.jsx' , '.ts' , '.tsx' ] ] ,
302
317
[ '.jsx' , [ '.tsx' ] ] ,
303
318
[ '.cjs' , [ '.cts' ] ] ,
304
319
[ '.mjs' , [ '.mts' ] ] ,
305
320
[ '' , [ '.js' , '.ts' , '.jsx' , '.tsx' , '.cjs' , '.mjs' , '.cts' , '.mts' ] ] ,
306
321
] ) ;
307
- export function resolveImportSpecifierExtension ( resolved : string ) : string | undefined {
322
+ function resolveImportSpecifierExtension ( resolved : string ) : string | undefined {
308
323
if ( fileExists ( resolved ) )
309
324
return resolved ;
310
325
@@ -318,13 +333,45 @@ export function resolveImportSpecifierExtension(resolved: string): string | unde
318
333
}
319
334
break ; // Do not try '' when a more specific extension like '.jsx' matched.
320
335
}
336
+ }
337
+
338
+ // This method resolves directory imports and performs "file extension subsitution".
339
+ // It is intended to be called after the path mapping resolution.
340
+ //
341
+ // Directory imports follow the --moduleResolution=bundler strategy from tsc.
342
+ // https://www.typescriptlang.org/docs/handbook/modules/reference.html#directory-modules-index-file-resolution
343
+ // https://www.typescriptlang.org/docs/handbook/modules/reference.html#bundler
344
+ //
345
+ // See also Node.js "folder as module" behavior:
346
+ // https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#folders-as-modules.
347
+ export function resolveImportSpecifierAfterMapping ( resolved : string , afterPathMapping : boolean ) : string | undefined {
348
+ const resolvedFile = resolveImportSpecifierExtension ( resolved ) ;
349
+ if ( resolvedFile )
350
+ return resolvedFile ;
321
351
322
352
if ( dirExists ( resolved ) ) {
353
+ const packageJsonPath = path . join ( resolved , 'package.json' ) ;
354
+
355
+ if ( afterPathMapping ) {
356
+ // Most notably, the module resolution algorithm is not performed after the path mapping.
357
+ // This means no node_modules lookup or package.json#exports.
358
+ //
359
+ // Only the "folder as module" Node.js behavior is respected:
360
+ // - consult `package.json#main`;
361
+ // - look for `index.js` or similar.
362
+ const mainField = getMainFieldFromPackageJson ( packageJsonPath ) ;
363
+ const mainFieldResolved = mainField ? resolveImportSpecifierExtension ( path . resolve ( resolved , mainField ) ) : undefined ;
364
+ return mainFieldResolved || resolveImportSpecifierExtension ( path . join ( resolved , 'index' ) ) ;
365
+ }
366
+
323
367
// If we import a package, let Node.js figure out the correct import based on package.json.
324
- if ( fileExists ( path . join ( resolved , 'package.json' ) ) )
368
+ // This also covers the "main" field for "folder as module".
369
+ if ( fileExists ( packageJsonPath ) )
325
370
return resolved ;
326
371
327
- // Otherwise, try to find a corresponding index file.
372
+ // Implement the "folder as module" Node.js behavior.
373
+ // Note that we do not delegate to Node.js, because we support this for ESM as well,
374
+ // following the TypeScript "bundler" mode.
328
375
const dirImport = path . join ( resolved , 'index' ) ;
329
376
return resolveImportSpecifierExtension ( dirImport ) ;
330
377
}
0 commit comments