Skip to content

Commit 6979f28

Browse files
fix: resolve tsconfig path aliases containing a colon (#780)
Co-authored-by: Hiroki Osame <hiroki.osame@gmail.com>
1 parent b29f6ee commit 6979f28

3 files changed

Lines changed: 151 additions & 23 deletions

File tree

src/esm/hook/resolve.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import { readJsonFile } from '../../utils/read-json-file.js';
1111
import { mapTsExtensions } from '../../utils/map-ts-extensions.js';
1212
import type { NodeError } from '../../types.js';
1313
import {
14-
requestAcceptsQuery,
1514
fileUrlPrefix,
1615
tsExtensionsPattern,
1716
isDirectoryPattern,
1817
isRelativePath,
18+
isFilePath,
1919
} from '../../utils/path-utils.js';
2020
import type { TsxRequest } from '../types.js';
2121
import { isGlobalCjsLoaderActive } from '../../utils/cjs-loader-state.js';
@@ -31,6 +31,15 @@ import { data as defaultData, type Data } from './initialize.js';
3131
type NextResolve = Parameters<ResolveHook>[2];
3232
type NextResolveSync = Parameters<ResolveHookSync>[2];
3333

34+
const urlLikeSpecifierPattern = /^(?:[a-z][\d+.a-z-]*:\/\/|data:|file:|node:)/i;
35+
36+
const isTsconfigPathAliasSpecifier = (
37+
specifier: string,
38+
) => (
39+
!isFilePath(specifier)
40+
&& !urlLikeSpecifierPattern.test(specifier)
41+
);
42+
3443
const getMissingPathFromNotFound = (
3544
nodeError: NodeError,
3645
) => {
@@ -425,17 +434,18 @@ const resolveTsPaths = async (
425434
nextResolve: NextResolve,
426435
hookData: Data,
427436
) => {
437+
const tsconfigPathAliasSpecifier = isTsconfigPathAliasSpecifier(specifier);
428438
log(3, 'resolveTsPaths', {
429439
specifier,
430440
context,
431441

432-
requestAcceptsQuery: requestAcceptsQuery(specifier),
442+
tsconfigPathAliasSpecifier,
433443
tsconfig: hookData.parsedTsconfig,
434444
fromNodeModules: context.parentURL?.includes('/node_modules/'),
435445
});
436446
if (
437-
// Bare specifier
438-
!requestAcceptsQuery(specifier)
447+
// Bare specifier or TS path alias (e.g. `ns:foo`)
448+
tsconfigPathAliasSpecifier
439449
// TS path alias
440450
&& hookData.parsedTsconfig
441451
&& !context.parentURL?.includes('/node_modules/')
@@ -465,17 +475,18 @@ const resolveTsPathsSync = (
465475
nextResolve: NextResolveSync,
466476
hookData: Data,
467477
) => {
478+
const tsconfigPathAliasSpecifier = isTsconfigPathAliasSpecifier(specifier);
468479
log(3, 'resolveTsPathsSync', {
469480
specifier,
470481
context,
471482

472-
requestAcceptsQuery: requestAcceptsQuery(specifier),
483+
tsconfigPathAliasSpecifier,
473484
tsconfig: hookData.parsedTsconfig,
474485
fromNodeModules: context.parentURL?.includes('/node_modules/'),
475486
});
476487
if (
477-
// Bare specifier
478-
!requestAcceptsQuery(specifier)
488+
// Bare specifier or TS path alias (e.g. `ns:foo`)
489+
tsconfigPathAliasSpecifier
479490
// TS path alias
480491
&& hookData.parsedTsconfig
481492
&& !context.parentURL?.includes('/node_modules/')

tests/specs/cli.ts

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { setTimeout } from 'node:timers/promises';
2+
import { on } from 'node:events';
23
import {
34
describe, test, onFinish, onTestFail, expect,
45
} from 'manten';
@@ -42,6 +43,58 @@ const isProcessAlive = (pid: number) => {
4243
return false;
4344
};
4445

46+
const isAbortError = (
47+
error: unknown,
48+
) => error instanceof Error && error.name === 'AbortError';
49+
50+
const waitForSignalRelay = async (
51+
stdoutStream: NodeJS.ReadableStream,
52+
signalName: string,
53+
sendSignal: () => void,
54+
) => {
55+
let stdout = '';
56+
let sentFirstSignal = false;
57+
let sentSecondSignal = isWindows;
58+
59+
try {
60+
for await (const [data] of on(stdoutStream, 'data', {
61+
close: ['end', 'close'],
62+
signal: AbortSignal.timeout(10_000),
63+
})) {
64+
stdout += data.toString();
65+
66+
if (!sentFirstSignal && stdout.includes('READY')) {
67+
sentFirstSignal = true;
68+
sendSignal();
69+
70+
if (sentSecondSignal) {
71+
return stdout;
72+
}
73+
}
74+
75+
if (!sentSecondSignal && stdout.includes(`${signalName} PRESS AGAIN`)) {
76+
sentSecondSignal = true;
77+
sendSignal();
78+
return stdout;
79+
}
80+
}
81+
} catch (error) {
82+
if (isAbortError(error)) {
83+
throw Object.assign(
84+
new Error(`Timed out waiting for ${signalName} relay`),
85+
{ stdout },
86+
);
87+
}
88+
89+
throw error;
90+
}
91+
92+
throw Object.assign(
93+
new Error(`Process exited before ${signalName} relay completed`),
94+
{ stdout },
95+
);
96+
};
97+
4598
export const cli = (node: NodeApis) => describe('CLI', () => {
4699
const { tsx } = node;
47100
describe('argv', async () => {
@@ -183,10 +236,10 @@ export const cli = (node: NodeApis) => describe('CLI', () => {
183236
184237
for (const name of signals) {
185238
process.once(name, () => {
186-
console.log(name, 'PRESS AGAIN');
187239
process.once(name, () => {
188240
process.exit(200);
189241
});
242+
console.log(name, 'PRESS AGAIN');
190243
});
191244
}
192245
@@ -234,25 +287,27 @@ export const cli = (node: NodeApis) => describe('CLI', () => {
234287
fixture.getPath('catch-signals.js'),
235288
]);
236289

237-
tsxProcess.stdout!.once('data', () => {
238-
tsxProcess.kill(signal, {
239-
forceKillAfterTimeout: false,
240-
});
290+
let stdout = '';
291+
let tsxProcessResolved: Awaited<typeof tsxProcess> | undefined;
241292

242-
tsxProcess.stdout!.once('data', () => {
243-
tsxProcess.kill(signal, {
244-
forceKillAfterTimeout: false,
245-
});
293+
onTestFail(() => {
294+
console.log({
295+
tsxProcessResolved,
296+
stdout,
246297
});
247298
});
248299

249-
const tsxProcessResolved = await tsxProcess;
300+
stdout = await waitForSignalRelay(
301+
tsxProcess.stdout!,
302+
signal,
303+
() => tsxProcess.kill(signal, {
304+
forceKillAfterTimeout: false,
305+
}),
306+
);
250307

251-
onTestFail(() => {
252-
console.log(tsxProcessResolved);
253-
});
308+
tsxProcessResolved = await tsxProcess;
254309

255-
if (process.platform === 'win32') {
310+
if (isWindows) {
256311
/**
257312
* Windows doesn't support sending signals to processes.
258313
* https://nodejs.org/api/process.html#signal-events
@@ -261,10 +316,10 @@ export const cli = (node: NodeApis) => describe('CLI', () => {
261316
* of the target process, and afterwards, subprocess will report that the process
262317
* was terminated by signal.
263318
*/
264-
expect(tsxProcessResolved.stdout).toBe('READY');
319+
expect(stdout.trim()).toBe('READY');
265320
} else {
266321
expect(tsxProcessResolved.exitCode).toBe(200);
267-
expectMatchInOrder(tsxProcessResolved.stdout, [
322+
expectMatchInOrder(stdout, [
268323
'READY\n',
269324
`${signal} PRESS AGAIN`,
270325
]);

tests/specs/tsconfig.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,68 @@ export const tsconfig = ({ tsx }: NodeApis) => describe('tsconfig', () => {
176176
expect(pTsconfig.stderr).toBe('');
177177
expect(pTsconfig.stdout).toBe('resolved via configDir');
178178
});
179+
180+
test('tsconfig paths can match colon aliases', async () => {
181+
await using fixture = await createFixture({
182+
'package.json': createPackageJson(packageType ? { type: packageType } : {}),
183+
'tsconfig.json': createTsconfig({
184+
compilerOptions: {
185+
baseUrl: '.',
186+
paths: {
187+
'ns:*': ['./src/*'],
188+
},
189+
},
190+
}),
191+
'index.mts': `
192+
import { value } from 'ns:utils.mjs';
193+
194+
console.log(value);
195+
`,
196+
src: {
197+
'utils.mts': "export const value = 'success';",
198+
},
199+
});
200+
201+
const pTsconfig = await tsx(['index.mts'], fixture.path);
202+
onTestFail((error) => {
203+
console.error(error);
204+
console.log(pTsconfig);
205+
});
206+
expect(pTsconfig.failed).toBe(false);
207+
expect(pTsconfig.stderr).toBe('');
208+
expect(pTsconfig.stdout).toBe('success');
209+
});
210+
211+
test('tsconfig paths do not resolve data URLs', async () => {
212+
await using fixture = await createFixture({
213+
'package.json': createPackageJson(packageType ? { type: packageType } : {}),
214+
'tsconfig.json': createTsconfig({
215+
compilerOptions: {
216+
baseUrl: '.',
217+
paths: {
218+
'data:*': ['./src/wrong.mjs'],
219+
},
220+
},
221+
}),
222+
'index.mts': `
223+
import { value } from 'data:text/javascript,export%20const%20value%20%3D%20%22data-url%22%3B';
224+
225+
console.log(value);
226+
`,
227+
src: {
228+
'wrong.mts': "export const value = 'wrong';",
229+
},
230+
});
231+
232+
const pTsconfig = await tsx(['index.mts'], fixture.path);
233+
onTestFail((error) => {
234+
console.error(error);
235+
console.log(pTsconfig);
236+
});
237+
expect(pTsconfig.failed).toBe(false);
238+
expect(pTsconfig.stderr).toBe('');
239+
expect(pTsconfig.stdout).toBe('data-url');
240+
});
179241
});
180242

181243
describe('custom tsconfig', () => {

0 commit comments

Comments
 (0)