Skip to content

Commit 3f42ae3

Browse files
authoredMay 23, 2024
feat(esm api): configurable tsconfig
1 parent 52d696c commit 3f42ae3

File tree

7 files changed

+227
-20
lines changed

7 files changed

+227
-20
lines changed
 

‎docs/node/ts-import.md

+18
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ const { tsImport } = require('tsx/esm/api')
3434
const loaded = await tsImport('./file.ts', __filename)
3535
```
3636
37+
## `tsconfig.json`
38+
39+
### Custom `tsconfig.json` path
40+
```ts
41+
tsImport('./file.ts', {
42+
parentURL: import.meta.url,
43+
tsconfig: './custom-tsconfig.json'
44+
})
45+
```
46+
47+
### Disable `tsconfig.json` lookup
48+
```ts
49+
tsImport('./file.ts', {
50+
parentURL: import.meta.url,
51+
tsconfig: false
52+
})
53+
```
54+
3755
## Tracking loaded files
3856
3957
Detect files that get loaded with the `onImport` hook:

‎src/cjs/api/global-require-patch.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import Module from 'node:module';
2+
import { loadTsconfig } from '../../utils/tsconfig.js';
23
import { extensions } from './module-extensions.js';
34
import { resolveFilename } from './module-resolve-filename.js';
45

56
export const register = () => {
67
const { sourceMapsEnabled } = process;
78
const { _extensions, _resolveFilename } = Module;
89

10+
loadTsconfig(process.env.TSX_TSCONFIG_PATH);
11+
912
// register
1013
process.setSourceMapsEnabled(true);
1114
// @ts-expect-error overwriting read-only property

‎src/esm/api/register.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ import { MessageChannel, type MessagePort } from 'node:worker_threads';
33
import type { Message } from '../types.js';
44
import { createScopedImport, type ScopedImport } from './scoped-import.js';
55

6+
export type TsconfigOptions = false | string;
7+
68
export type InitializationOptions = {
79
namespace?: string;
810
port?: MessagePort;
11+
tsconfig?: TsconfigOptions;
912
};
1013

1114
export type RegisterOptions = {
1215
namespace?: string;
1316
onImport?: (url: string) => void;
17+
tsconfig?: TsconfigOptions;
1418
};
1519

1620
export type Unregister = () => Promise<void>;
@@ -44,8 +48,9 @@ export const register: Register = (
4448
{
4549
parentURL: import.meta.url,
4650
data: {
47-
namespace: options?.namespace,
4851
port: port2,
52+
namespace: options?.namespace,
53+
tsconfig: options?.tsconfig,
4954
} satisfies InitializationOptions,
5055
transferList: [port2],
5156
},

‎src/esm/api/ts-import.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { register } from './register.js';
1+
import { register, type TsconfigOptions } from './register.js';
22

33
type Options = {
44
parentURL: string;
55
onImport?: (url: string) => void;
6+
tsconfig?: TsconfigOptions;
67
};
78
const tsImport = (
89
specifier: string,
@@ -27,10 +28,10 @@ const tsImport = (
2728
*/
2829
const api = register({
2930
namespace,
30-
onImport: (
31+
...(
3132
isOptionsString
32-
? undefined
33-
: options.onImport
33+
? {}
34+
: options
3435
),
3536
});
3637

‎src/esm/hook/initialize.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { GlobalPreloadHook, InitializeHook } from 'node:module';
22
import type { InitializationOptions } from '../api/register.js';
33
import type { Message } from '../types.js';
4+
import { loadTsconfig } from '../../utils/tsconfig.js';
45

56
type Data = InitializationOptions & {
67
active: boolean;
@@ -19,6 +20,10 @@ export const initialize: InitializeHook = async (
1920

2021
data.namespace = options.namespace;
2122

23+
if (options.tsconfig !== false) {
24+
loadTsconfig(options.tsconfig ?? process.env.TSX_TSCONFIG_PATH);
25+
}
26+
2227
if (options.port) {
2328
data.port = options.port;
2429

@@ -32,4 +37,7 @@ export const initialize: InitializeHook = async (
3237
}
3338
};
3439

35-
export const globalPreload: GlobalPreloadHook = () => 'process.setSourceMapsEnabled(true);';
40+
export const globalPreload: GlobalPreloadHook = () => {
41+
loadTsconfig(process.env.TSX_TSCONFIG_PATH);
42+
return 'process.setSourceMapsEnabled(true);';
43+
};

‎src/utils/tsconfig.ts

+29-11
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,37 @@ import {
44
parseTsconfig,
55
createFilesMatcher,
66
createPathsMatcher,
7+
type TsConfigResult,
8+
type FileMatcher,
79
} from 'get-tsconfig';
810

9-
const tsconfig = (
10-
process.env.TSX_TSCONFIG_PATH
11-
? {
12-
path: path.resolve(process.env.TSX_TSCONFIG_PATH),
13-
config: parseTsconfig(process.env.TSX_TSCONFIG_PATH),
14-
}
15-
: getTsconfig()
16-
);
11+
// eslint-disable-next-line import-x/no-mutable-exports
12+
export let fileMatcher: undefined | FileMatcher;
13+
14+
// eslint-disable-next-line import-x/no-mutable-exports
15+
export let tsconfigPathsMatcher: undefined | ReturnType<typeof createPathsMatcher>;
1716

18-
export const fileMatcher = tsconfig && createFilesMatcher(tsconfig);
17+
// eslint-disable-next-line import-x/no-mutable-exports
18+
export let allowJs = false;
1919

20-
export const tsconfigPathsMatcher = tsconfig && createPathsMatcher(tsconfig);
20+
export const loadTsconfig = (
21+
configPath?: string,
22+
) => {
23+
let tsconfig: TsConfigResult | null = null;
24+
if (configPath) {
25+
const resolvedConfigPath = path.resolve(configPath);
26+
tsconfig = {
27+
path: resolvedConfigPath,
28+
config: parseTsconfig(resolvedConfigPath),
29+
};
30+
} else {
31+
tsconfig = getTsconfig();
32+
if (!tsconfig) {
33+
return;
34+
}
35+
}
2136

22-
export const allowJs = tsconfig?.config.compilerOptions?.allowJs ?? false;
37+
fileMatcher = createFilesMatcher(tsconfig);
38+
tsconfigPathsMatcher = createPathsMatcher(tsconfig);
39+
allowJs = tsconfig?.config.compilerOptions?.allowJs ?? false;
40+
};

‎tests/specs/api.ts

+157-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
tsxEsmApiCjsPath,
1010
type NodeApis,
1111
} from '../utils/tsx.js';
12-
import { createPackageJson } from '../fixtures.js';
12+
import { createPackageJson, createTsconfig } from '../fixtures.js';
1313

1414
const tsFiles = {
1515
'file.ts': `
@@ -188,7 +188,7 @@ export default testSuite(({ describe }, node: NodeApis) => {
188188
expect(stdout).toBe('Fails as expected\nfoo bar');
189189
});
190190

191-
describe('register / unregister', ({ test }) => {
191+
describe('register / unregister', ({ test, describe }) => {
192192
test('register / unregister', async () => {
193193
await using fixture = await createFixture({
194194
'package.json': createPackageJson({ type: 'module' }),
@@ -285,9 +285,141 @@ export default testSuite(({ describe }, node: NodeApis) => {
285285
});
286286
expect(stdout).toBe('file.ts\nfoo.ts\nbar.ts\nindex.js');
287287
});
288+
289+
describe('tsconfig', ({ test }) => {
290+
test('should error on unresolvable tsconfig', async () => {
291+
await using fixture = await createFixture({
292+
'tsconfig.json': createTsconfig({
293+
extends: 'doesnt-exist',
294+
}),
295+
'register.mjs': `
296+
import { register } from ${JSON.stringify(tsxEsmApiPath)};
297+
register();
298+
`,
299+
});
300+
301+
const { exitCode, stderr } = await execaNode('register.mjs', [], {
302+
reject: false,
303+
cwd: fixture.path,
304+
nodePath: node.path,
305+
nodeOptions: [],
306+
});
307+
expect(exitCode).toBe(1);
308+
expect(stderr).toMatch('File \'doesnt-exist\' not found.');
309+
});
310+
311+
test('disable lookup', async () => {
312+
await using fixture = await createFixture({
313+
'tsconfig.json': createTsconfig({
314+
extends: 'doesnt-exist',
315+
}),
316+
'register.mjs': `
317+
import { register } from ${JSON.stringify(tsxEsmApiPath)};
318+
register({
319+
tsconfig: false,
320+
});
321+
`,
322+
});
323+
324+
await execaNode('register.mjs', [], {
325+
cwd: fixture.path,
326+
nodePath: node.path,
327+
nodeOptions: [],
328+
});
329+
});
330+
331+
test('custom path', async () => {
332+
await using fixture = await createFixture({
333+
'package.json': createPackageJson({ type: 'module' }),
334+
'tsconfig.json': createTsconfig({
335+
extends: 'doesnt-exist',
336+
}),
337+
'tsconfig-custom.json': createTsconfig({
338+
compilerOptions: {
339+
jsxFactory: 'Array',
340+
jsxFragmentFactory: 'null',
341+
},
342+
}),
343+
'register.mjs': `
344+
import { register } from ${JSON.stringify(tsxEsmApiPath)};
345+
register({
346+
tsconfig: './tsconfig-custom.json',
347+
});
348+
await import('./tsx.tsx');
349+
`,
350+
'tsx.tsx': `
351+
console.log(<>hi</>);
352+
`,
353+
});
354+
355+
const { stdout } = await execaNode('register.mjs', [], {
356+
cwd: fixture.path,
357+
nodePath: node.path,
358+
nodeOptions: [],
359+
});
360+
expect(stdout).toBe('[ null, null, \'hi\' ]');
361+
});
362+
363+
test('custom path - invalid', async () => {
364+
await using fixture = await createFixture({
365+
'package.json': createPackageJson({ type: 'module' }),
366+
'register.mjs': `
367+
import { register } from ${JSON.stringify(tsxEsmApiPath)};
368+
register({
369+
tsconfig: './doesnt-exist',
370+
});
371+
await import('./tsx.tsx');
372+
`,
373+
'tsx.tsx': `
374+
console.log(<>hi</>);
375+
`,
376+
});
377+
378+
const { exitCode, stderr } = await execaNode('register.mjs', [], {
379+
reject: false,
380+
cwd: fixture.path,
381+
nodePath: node.path,
382+
nodeOptions: [],
383+
});
384+
expect(exitCode).toBe(1);
385+
expect(stderr).toMatch('Cannot resolve tsconfig at path');
386+
});
387+
388+
test('fallsback to env var', async () => {
389+
await using fixture = await createFixture({
390+
'package.json': createPackageJson({ type: 'module' }),
391+
'tsconfig.json': createTsconfig({
392+
extends: 'doesnt-exist',
393+
}),
394+
'tsconfig-custom.json': createTsconfig({
395+
compilerOptions: {
396+
jsxFactory: 'Array',
397+
jsxFragmentFactory: 'null',
398+
},
399+
}),
400+
'register.mjs': `
401+
import { register } from ${JSON.stringify(tsxEsmApiPath)};
402+
register();
403+
await import('./tsx.tsx');
404+
`,
405+
'tsx.tsx': `
406+
console.log(<>hi</>);
407+
`,
408+
});
409+
410+
const { stdout } = await execaNode('register.mjs', [], {
411+
cwd: fixture.path,
412+
nodePath: node.path,
413+
nodeOptions: [],
414+
env: {
415+
TSX_TSCONFIG_PATH: 'tsconfig-custom.json',
416+
},
417+
});
418+
expect(stdout).toBe('[ null, null, \'hi\' ]');
419+
});
420+
});
288421
});
289422

290-
// add CJS test
291423
describe('tsImport()', ({ test }) => {
292424
test('module', async () => {
293425
await using fixture = await createFixture({
@@ -436,6 +568,28 @@ export default testSuite(({ describe }, node: NodeApis) => {
436568
});
437569
expect(stdout).toBe('foo\nfoo');
438570
});
571+
572+
test('tsconfig disable', async () => {
573+
await using fixture = await createFixture({
574+
'package.json': createPackageJson({ type: 'module' }),
575+
'tsconfig.json': createTsconfig({ extends: 'doesnt-exist' }),
576+
'import.mjs': `
577+
import { tsImport } from ${JSON.stringify(tsxEsmApiPath)};
578+
579+
await tsImport('./file.ts', {
580+
parentURL: import.meta.url,
581+
tsconfig: false,
582+
});
583+
`,
584+
...tsFiles,
585+
});
586+
587+
await execaNode('import.mjs', [], {
588+
cwd: fixture.path,
589+
nodePath: node.path,
590+
nodeOptions: [],
591+
});
592+
});
439593
});
440594
} else {
441595
test('no module.register error', async () => {

0 commit comments

Comments
 (0)
Please sign in to comment.