Skip to content

Commit 5d58ef1

Browse files
committed
chore: improve code
1 parent 9013b02 commit 5d58ef1

File tree

5 files changed

+118
-144
lines changed

5 files changed

+118
-144
lines changed

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

Lines changed: 24 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,10 @@
1-
import fs from 'node:fs';
21
import * as os from 'os';
32
import path, { dirname, posix } from 'path';
3+
import { findMatchedSourcePath, toEsmOutputPath } 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-
const JS_LIKE_EXTENSION_RE = /\.(?:c|m)?js$/;
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-
578
const 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

6819
const 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
}

packages/server/utils/tests/ts.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ describe('typescript', () => {
3636

3737
const api = require(distApiDir).default;
3838
expect(api()).toEqual('runtime-shared-api');
39-
const jsAliasApi = require(path.join(distApiDir, './js-alias.js')).default;
40-
expect(jsAliasApi()).toEqual('shared-js-alias');
4139

4240
const distServerDir = path.join(distDir, './server');
4341
const server = require(distServerDir).default;

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

Lines changed: 8 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { createRequire } from 'module';
2-
import fs from 'node:fs';
32
import path from 'path';
4-
import { getAliasConfig, mergeAlias } from '@modern-js/utils';
3+
import {
4+
findMatchedSourcePath,
5+
getAliasConfig,
6+
mergeAlias,
7+
resolveSourcePath,
8+
} from '@modern-js/utils';
59
import { createMatchPath as oCreateMatchPath } from '@modern-js/utils/tsconfig-paths';
610

711
const require = createRequire(import.meta.url);
8-
const EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs'];
9-
const JS_LIKE_EXTENSION_RE = /\.(?:c|m)?js$/;
1012

1113
const normalizePathValue = ({ key, value, absoluteBaseUrl, appDir }) => {
1214
let normalizedValue = value;
@@ -102,71 +104,5 @@ export function createMatchPath({ alias, appDir, tsconfigPath }) {
102104
return oCreateMatchPath(absoluteBaseUrl, tsPaths);
103105
}
104106

105-
export const findMatchedPath = (matchPath, specifier) => {
106-
let matchedPath = matchPath(specifier, undefined, undefined, EXTENSIONS);
107-
108-
if (!matchedPath && JS_LIKE_EXTENSION_RE.test(specifier)) {
109-
matchedPath = matchPath(
110-
specifier.replace(JS_LIKE_EXTENSION_RE, ''),
111-
undefined,
112-
undefined,
113-
EXTENSIONS,
114-
);
115-
}
116-
117-
return matchedPath;
118-
};
119-
120-
export const resolvePathWithExtensions = matchedPath => {
121-
const ext = path.extname(matchedPath);
122-
123-
if (ext && fs.existsSync(matchedPath) && fs.statSync(matchedPath).isFile()) {
124-
return matchedPath;
125-
}
126-
127-
if (ext && JS_LIKE_EXTENSION_RE.test(matchedPath)) {
128-
const stem = matchedPath.slice(0, -ext.length);
129-
const jsLikeCandidates = [
130-
matchedPath,
131-
`${stem}.ts`,
132-
`${stem}.tsx`,
133-
`${stem}.mts`,
134-
`${stem}.cts`,
135-
];
136-
137-
for (const candidate of jsLikeCandidates) {
138-
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
139-
return candidate;
140-
}
141-
}
142-
143-
return matchedPath;
144-
}
145-
146-
if (ext) {
147-
return matchedPath;
148-
}
149-
150-
const fileCandidates = [
151-
matchedPath,
152-
...EXTENSIONS.map(ext => `${matchedPath}${ext}`),
153-
];
154-
155-
for (const candidate of fileCandidates) {
156-
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
157-
return candidate;
158-
}
159-
}
160-
161-
const indexCandidates = EXTENSIONS.map(ext =>
162-
path.join(matchedPath, `index${ext}`),
163-
);
164-
165-
for (const candidate of indexCandidates) {
166-
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
167-
return candidate;
168-
}
169-
}
170-
171-
return matchedPath;
172-
};
107+
export const findMatchedPath = findMatchedSourcePath;
108+
export const resolvePathWithExtensions = resolveSourcePath;

packages/toolkit/utils/src/cli/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export * from './watch';
1919
export * from './config';
2020
export * from './version';
2121
export * from './route';
22+
export * from './modulePath';
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import type { MatchPath } from '../../compiled/tsconfig-paths';
4+
5+
export const SOURCE_EXTENSIONS = ['.ts', '.js'];
6+
export const JS_LIKE_EXTENSION_RE = /\.(?:c|m)?js$/;
7+
8+
const isFile = (filepath: string) =>
9+
fs.existsSync(filepath) && fs.statSync(filepath).isFile();
10+
11+
const findSourceEntry = (resolvedPath: string) => {
12+
const ext = path.extname(resolvedPath);
13+
14+
if (ext) {
15+
if (isFile(resolvedPath)) {
16+
return resolvedPath;
17+
}
18+
19+
if (JS_LIKE_EXTENSION_RE.test(resolvedPath)) {
20+
const tsPath = `${resolvedPath.slice(0, -ext.length)}.ts`;
21+
if (isFile(tsPath)) {
22+
return tsPath;
23+
}
24+
}
25+
26+
return;
27+
}
28+
29+
if (isFile(`${resolvedPath}.ts`)) {
30+
return `${resolvedPath}.ts`;
31+
}
32+
33+
if (isFile(`${resolvedPath}.js`)) {
34+
return `${resolvedPath}.js`;
35+
}
36+
37+
if (isFile(path.join(resolvedPath, 'index.ts'))) {
38+
return path.join(resolvedPath, 'index.ts');
39+
}
40+
41+
if (isFile(path.join(resolvedPath, 'index.js'))) {
42+
return path.join(resolvedPath, 'index.js');
43+
}
44+
};
45+
46+
// Allow `.js` specifiers to point at `.ts` source files so runtime loaders and
47+
// compile-time transforms follow the same alias matching rules.
48+
export const findMatchedSourcePath = (
49+
matchPath: MatchPath,
50+
specifier: string,
51+
) => {
52+
let matchedPath = matchPath(
53+
specifier,
54+
undefined,
55+
undefined,
56+
SOURCE_EXTENSIONS,
57+
);
58+
59+
if (!matchedPath && JS_LIKE_EXTENSION_RE.test(specifier)) {
60+
matchedPath = matchPath(
61+
specifier.replace(JS_LIKE_EXTENSION_RE, ''),
62+
undefined,
63+
undefined,
64+
SOURCE_EXTENSIONS,
65+
);
66+
}
67+
68+
return matchedPath;
69+
};
70+
71+
// Resolve a matched source entry to the real file on disk. This is used by the
72+
// runtime loaders before handing the file back to Node / ts-node.
73+
export const resolveSourcePath = (resolvedPath: string) => {
74+
return findSourceEntry(resolvedPath) || resolvedPath;
75+
};
76+
77+
// Convert a resolved source path into the specifier that native ESM output
78+
// should reference at runtime, which is always the emitted `.js` file.
79+
export const toEsmOutputPath = (resolvedPath: string) => {
80+
const sourceEntry = findSourceEntry(resolvedPath);
81+
const finalPath = sourceEntry || resolvedPath;
82+
const ext = path.extname(finalPath);
83+
84+
return ext ? `${finalPath.slice(0, -ext.length)}.js` : `${finalPath}.js`;
85+
};

0 commit comments

Comments
 (0)