1- import fs from 'node:fs' ;
21import * as os from 'os' ;
32import path , { dirname , posix } from 'path' ;
3+ import { findMatchedSourcePath , toEsmOutputPath } from '@modern-js/utils' ;
44import type { MatchPath } from '@modern-js/utils/tsconfig-paths' ;
55import { createMatchPath } from '@modern-js/utils/tsconfig-paths' ;
66import * as ts from 'typescript' ;
77
8- const JS_LIKE_EXTENSION_RE = / \. (?: c | m ) ? j s $ / ;
9- const RESOLVE_EXTENSIONS = [
10- '.ts' ,
11- '.tsx' ,
12- '.js' ,
13- '.jsx' ,
14- '.mts' ,
15- '.cts' ,
16- '.mjs' ,
17- '.cjs' ,
18- ] ;
19-
20- const getEsmOutputExtension = ( ext : string ) => {
21- if ( ext === '.mts' || ext === '.mjs' ) {
22- return '.mjs' ;
23- }
24- if ( ext === '.cts' || ext === '.cjs' ) {
25- return '.cjs' ;
26- }
27- return '.js' ;
28- } ;
29-
30- const appendEsmExtension = ( resolvedPath : string ) => {
31- const ext = path . extname ( resolvedPath ) ;
32-
33- if ( ext ) {
34- return resolvedPath . slice ( 0 , - ext . length ) + getEsmOutputExtension ( ext ) ;
35- }
36-
37- for ( const candidateExt of RESOLVE_EXTENSIONS ) {
38- const candidate = `${ resolvedPath } ${ candidateExt } ` ;
39- if ( fs . existsSync ( candidate ) && fs . statSync ( candidate ) . isFile ( ) ) {
40- return `${ resolvedPath } ${ getEsmOutputExtension ( candidateExt ) } ` ;
41- }
42- }
43-
44- for ( const candidateExt of RESOLVE_EXTENSIONS ) {
45- const candidate = path . join ( resolvedPath , `index${ candidateExt } ` ) ;
46- if ( fs . existsSync ( candidate ) && fs . statSync ( candidate ) . isFile ( ) ) {
47- return path . join (
48- resolvedPath ,
49- `index${ getEsmOutputExtension ( candidateExt ) } ` ,
50- ) ;
51- }
52- }
53-
54- return `${ resolvedPath } .js` ;
55- } ;
56-
578const resolveRelativeEsmSpecifier = ( sf : ts . SourceFile , text : string ) => {
589 if ( ! text . startsWith ( './' ) && ! text . startsWith ( '../' ) ) {
5910 return ;
@@ -62,7 +13,7 @@ const resolveRelativeEsmSpecifier = (sf: ts.SourceFile, text: string) => {
6213 const importerDir = dirname ( sf . fileName ) ;
6314 const resolvedPath = path . resolve ( importerDir , text ) ;
6415
65- return appendEsmExtension ( resolvedPath ) ;
16+ return toEsmOutputPath ( resolvedPath ) ;
6617} ;
6718
6819const isRegExpKey = ( str : string ) => {
@@ -253,34 +204,32 @@ function getNotAliasedPath(
253204 text : string ,
254205 moduleType ?: 'module' | 'commonjs' ,
255206) {
256- let result = matcher ( text , undefined , undefined , RESOLVE_EXTENSIONS ) ;
257-
258- if ( ! result && JS_LIKE_EXTENSION_RE . test ( text ) ) {
259- result = matcher (
260- text . replace ( JS_LIKE_EXTENSION_RE , '' ) ,
261- undefined ,
262- undefined ,
263- RESOLVE_EXTENSIONS ,
264- ) ;
207+ // Resolve aliases and tsconfig paths using the same `.js` -> `.ts` fallback
208+ // rules as the runtime loaders.
209+ let result = findMatchedSourcePath ( matcher , text ) ;
210+
211+ // For CommonJS we only rewrite known alias matches. For native ESM we also
212+ // need to normalize relative imports like `../service/user` into a path that
213+ // can be emitted as `../service/user.js`.
214+ if ( ! result && moduleType === 'module' ) {
215+ // This branch is only for relative specifiers. Bare package imports should
216+ // stay untouched when they are not matched by alias rules.
217+ if ( ! result ) {
218+ result = resolveRelativeEsmSpecifier ( sf , text ) ;
219+ }
265220 }
266221
267222 if ( ! result ) {
268- if ( moduleType !== 'module' ) {
269- return ;
270- }
271-
272- result = resolveRelativeEsmSpecifier ( sf , text ) ;
273- if ( ! result ) {
274- return ;
275- }
223+ return ;
276224 }
277225
278226 if ( os . platform ( ) === 'win32' ) {
279227 result = result . replace ( / \\ / g, '/' ) ;
280228 }
281229
282230 if ( ! path . isAbsolute ( result ) ) {
283- // handle alias to alias
231+ // If an alias resolves to another bare specifier, prefer leaving it as a
232+ // package import when Node can resolve that package.
284233 if ( ! result . startsWith ( '.' ) && ! result . startsWith ( '..' ) ) {
285234 try {
286235 // Installed packages (node modules) should take precedence over root files with the same name.
@@ -294,6 +243,8 @@ function getNotAliasedPath(
294243 } catch { }
295244 }
296245 try {
246+ // Likewise, if the original specifier already resolves as a package,
247+ // keep the original text instead of forcing a relative filesystem path.
297248 // Installed packages (node modules) should take precedence over root files with the same name.
298249 // Ref: https://github.com/nestjs/nest-cli/issues/838
299250 const packagePath = require . resolve ( text , {
@@ -306,9 +257,12 @@ function getNotAliasedPath(
306257 }
307258
308259 if ( moduleType === 'module' ) {
309- result = appendEsmExtension ( result ) ;
260+ // Native ESM output must reference the emitted file extension that Node
261+ // will load at runtime, typically `.js`.
262+ result = toEsmOutputPath ( result ) ;
310263 }
311264
265+ // Emit a relative specifier from the current source file to the resolved target.
312266 const resolvedPath = posix . relative ( dirname ( sf . fileName ) , result ) || './' ;
313267 return resolvedPath [ 0 ] === '.' ? resolvedPath : `./${ resolvedPath } ` ;
314268}
0 commit comments