Skip to content

Commit 75d1789

Browse files
committed
refactor(app-tools): unify esm ts path resolution
1 parent 5d58ef1 commit 75d1789

File tree

9 files changed

+186
-279
lines changed

9 files changed

+186
-279
lines changed

packages/server/utils/src/compilers/typescript/tsconfigPathsPlugin.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
import * as os from 'os';
22
import path, { dirname, posix } from 'path';
3-
import { findMatchedSourcePath, toEsmOutputPath } from '@modern-js/utils';
3+
import { findMatchedSourcePath, findSourceEntry } from '@modern-js/utils';
44
import type { MatchPath } from '@modern-js/utils/tsconfig-paths';
55
import { createMatchPath } from '@modern-js/utils/tsconfig-paths';
66
import * as ts from 'typescript';
77

8+
// Convert a resolved source path into the specifier that native ESM output
9+
// should reference at runtime, which is always the emitted `.js` file.
10+
const toEsmOutputPath = (resolvedPath: string) => {
11+
const sourcePath = findSourceEntry(resolvedPath) || resolvedPath;
12+
const ext = path.extname(sourcePath);
13+
14+
return ext ? `${sourcePath.slice(0, -ext.length)}.js` : `${sourcePath}.js`;
15+
};
16+
817
const resolveRelativeEsmSpecifier = (sf: ts.SourceFile, text: string) => {
918
if (!text.startsWith('./') && !text.startsWith('../')) {
1019
return;
1120
}
1221

1322
const importerDir = dirname(sf.fileName);
14-
const resolvedPath = path.resolve(importerDir, text);
15-
16-
return toEsmOutputPath(resolvedPath);
23+
return path.resolve(importerDir, text);
1724
};
1825

1926
const isRegExpKey = (str: string) => {
@@ -196,8 +203,6 @@ export function tsconfigPathsBeforeHookFactory(
196203
};
197204
}
198205

199-
// fork from https://github.com/nestjs/nest-cli/blob/HEAD/lib/compiler/hooks/tsconfig-paths.hook.ts
200-
// license at https://github.com/nestjs/nest/blob/master/LICENSE
201206
function getNotAliasedPath(
202207
sf: ts.SourceFile,
203208
matcher: MatchPath,
@@ -208,15 +213,12 @@ function getNotAliasedPath(
208213
// rules as the runtime loaders.
209214
let result = findMatchedSourcePath(matcher, text);
210215

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`.
216+
// For native ESM, unresolved relative imports like `../service/user` must be
217+
// resolved to a source path before we convert them to the emitted `.js` specifier.
214218
if (!result && moduleType === 'module') {
215219
// This branch is only for relative specifiers. Bare package imports should
216220
// stay untouched when they are not matched by alias rules.
217-
if (!result) {
218-
result = resolveRelativeEsmSpecifier(sf, text);
219-
}
221+
result = resolveRelativeEsmSpecifier(sf, text);
220222
}
221223

222224
if (!result) {

packages/solutions/app-tools/src/esm/register-esm.mjs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import path from 'node:path';
2-
import { fs } from '@modern-js/utils';
32

43
/**
54
* Register Node.js module hooks for TypeScript support.
65
* Uses node:module register API to enable ts-node loader.
76
*/
8-
export const registerModuleHooks = async ({ appDir, distDir, alias }) => {
7+
export const registerModuleHooks = async ({
8+
appDir,
9+
distDir,
10+
baseUrl,
11+
paths,
12+
}) => {
913
const TS_CONFIG_FILENAME = `tsconfig.json`;
1014
const tsconfigPath = path.resolve(appDir, TS_CONFIG_FILENAME);
1115

@@ -21,10 +25,8 @@ export const registerModuleHooks = async ({ appDir, distDir, alias }) => {
2125
)}/`;
2226
register('./ts-node-loader.mjs', import.meta.url, {
2327
data: {
24-
appDir,
25-
distDir,
26-
alias,
27-
tsconfigPath,
28+
baseUrl,
29+
paths,
2830
},
2931
});
3032
};
Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
11
import { pathToFileURL } from 'url';
2+
import { findMatchedSourcePath, findSourceEntry } from '@modern-js/utils';
3+
import { createMatchPath } from '@modern-js/utils/tsconfig-paths';
24
import { resolve as tsNodeResolve } from 'ts-node/esm';
35
import { load as tsNodeLoad } from 'ts-node/esm';
4-
import {
5-
createMatchPath,
6-
findMatchedPath,
7-
resolvePathWithExtensions,
8-
} from './utils.mjs';
96

107
let matchPath;
11-
export async function initialize({ appDir, alias, tsconfigPath }) {
12-
matchPath = createMatchPath({
13-
alias,
14-
appDir,
15-
tsconfigPath,
16-
});
8+
9+
export async function initialize({ baseUrl, paths }) {
10+
matchPath = createMatchPath(baseUrl || './', paths || {});
1711
}
1812

1913
export function resolve(specifier, context, defaultResolve) {
20-
const match = findMatchedPath(matchPath, specifier);
14+
// Without this rewrite, aliases such as `@service/user` and
15+
// `@service/user.js` would never reach ts-node as real source files.
16+
const match = findMatchedSourcePath(matchPath, specifier);
17+
2118
return match
2219
? tsNodeResolve(
23-
pathToFileURL(resolvePathWithExtensions(match)).href,
20+
pathToFileURL(findSourceEntry(match) || match).href,
2421
context,
2522
defaultResolve,
2623
)
@@ -38,5 +35,7 @@ export function load(url, context, defaultLoad) {
3835
return defaultLoad(url, context);
3936
}
4037

38+
// Without ts-node here, local `.ts` files would be handed to Node as-is and
39+
// fail to execute in environments that do not natively run TypeScript.
4140
return tsNodeLoad(url, context, defaultLoad);
4241
}

packages/solutions/app-tools/src/esm/ts-paths-loader.mjs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
33
import { fileURLToPath, pathToFileURL } from 'url';
4+
import { findMatchedSourcePath, findSourceEntry } from '@modern-js/utils';
45
import { createMatchPath as oCreateMatchPath } from '@modern-js/utils/tsconfig-paths';
5-
import { findMatchedPath, resolvePathWithExtensions } from './utils.mjs';
66

77
let matchPath;
88
let appDir;
@@ -13,6 +13,8 @@ export async function initialize({ appDir: currentAppDir, baseUrl, paths }) {
1313
}
1414

1515
export function resolve(specifier, context, defaultResolve) {
16+
// Without this branch, app-local imports like `../service/user` would fail
17+
// under native ESM because Node does not try `.ts` / `.js` extensions here.
1618
const parentPath = context.parentURL
1719
? path.dirname(fileURLToPath(context.parentURL))
1820
: process.cwd();
@@ -30,9 +32,8 @@ export function resolve(specifier, context, defaultResolve) {
3032
!path.extname(specifier) &&
3133
isAppFile
3234
) {
33-
const resolvedPath = resolvePathWithExtensions(
34-
path.resolve(parentPath, specifier),
35-
);
35+
const matchedPath = path.resolve(parentPath, specifier);
36+
const resolvedPath = findSourceEntry(matchedPath) || matchedPath;
3637

3738
if (resolvedPath && fs.existsSync(resolvedPath)) {
3839
return defaultResolve(
@@ -47,12 +48,15 @@ export function resolve(specifier, context, defaultResolve) {
4748
return defaultResolve(specifier, context, defaultResolve);
4849
}
4950

50-
const match = findMatchedPath(matchPath, specifier);
51+
// Without this rewrite, aliases such as `@service/user` and
52+
// `@service/user.js` would be left to Node's default resolver, which cannot
53+
// map tsconfig paths to the real source files.
54+
const match = findMatchedSourcePath(matchPath, specifier);
5155
if (!match) {
5256
return defaultResolve(specifier, context, defaultResolve);
5357
}
5458

55-
const resolvedPath = resolvePathWithExtensions(match);
59+
const resolvedPath = findSourceEntry(match) || match;
5660
return defaultResolve(
5761
pathToFileURL(resolvedPath).href,
5862
context,

packages/solutions/app-tools/src/esm/utils.mjs

Lines changed: 0 additions & 108 deletions
This file was deleted.

0 commit comments

Comments
 (0)