Skip to content

Commit 688005c

Browse files
committed
feat(jest-config): Support using esbuild-register for loading TS configs
Signed-off-by: Matthew Peveler <[email protected]>
1 parent d2420aa commit 688005c

File tree

9 files changed

+338
-20
lines changed

9 files changed

+338
-20
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### Features
44

55
- `[expect, @jest/expect-utils]` Support custom equality testers ([#13654](https://github.com/facebook/jest/pull/13654))
6+
- `[jest-config]` Support using esbuild-register for loading TS configs ([#13742](https://github.com/facebook/jest/pull/13742))
67
- `[jest-config, jest-worker]` Use `os.availableParallelism` if available to calculate number of workers to spawn ([#13738](https://github.com/facebook/jest/pull/13738))
78
- `[@jest/globals, jest-mock]` Add `jest.replaceProperty()` that replaces property value ([#13496](https://github.com/facebook/jest/pull/13496))
89
- `[jest-haste-map]` ignore Sapling vcs directories (`.sl/`) ([#13674](https://github.com/facebook/jest/pull/13674))

docs/Configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export default async (): Promise<Config> => {
5757

5858
:::tip
5959

60-
To read TypeScript configuration files Jest requires [`ts-node`](https://npmjs.com/package/ts-node). Make sure it is installed in your project.
60+
To read TypeScript configuration files Jest by default requires [`ts-node`](https://npmjs.com/package/ts-node). You can override this behavior by adding a `@jest-config-loader` docblock at the top of the file. Currently, [`ts-node`](https://npmjs.com/package/ts-node) and [`esbuild-register`](https://npmjs.com/package/esbuild-register) is supported. Make sure `ts-node` or the loader you specify is installed.
6161

6262
:::
6363

e2e/__tests__/readInitialOptions.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,21 @@ describe('readInitialOptions', () => {
6464
expect(config).toEqual({jestConfig: 'package.json', rootDir});
6565
expect(configPath).toEqual(configFile);
6666
});
67-
test('should read a jest.config.ts file', async () => {
68-
const configFile = resolveFixture('ts-config', 'jest.config.ts');
69-
const rootDir = resolveFixture('ts-config');
67+
test('should read a jest.config.ts file with ts-node', async () => {
68+
const configFile = resolveFixture('ts-node-config', 'jest.config.ts');
69+
const rootDir = resolveFixture('ts-node-config');
70+
const {config, configPath} = await proxyReadInitialOptions(undefined, {
71+
cwd: rootDir,
72+
});
73+
expect(config).toEqual({jestConfig: 'jest.config.ts', rootDir});
74+
expect(configPath).toEqual(configFile);
75+
});
76+
test('should read a jest.config.ts file with esbuild-register', async () => {
77+
const configFile = resolveFixture(
78+
'ts-esbuild-register-config',
79+
'jest.config.ts',
80+
);
81+
const rootDir = resolveFixture('ts-esbuild-register-config');
7082
const {config, configPath} = await proxyReadInitialOptions(undefined, {
7183
cwd: rootDir,
7284
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @jest-config-loader esbuild-register
8+
*/
9+
export default {
10+
jestConfig: 'jest.config.ts',
11+
};

packages/jest-config/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@
1818
},
1919
"peerDependencies": {
2020
"@types/node": "*",
21+
"esbuild-register": ">=3.1.0",
2122
"ts-node": ">=9.0.0"
2223
},
2324
"peerDependenciesMeta": {
2425
"@types/node": {
2526
"optional": true
2627
},
28+
"esbuild-register": {
29+
"optional": true
30+
},
2731
"ts-node": {
2832
"optional": true
2933
}
@@ -39,6 +43,7 @@
3943
"glob": "^7.1.3",
4044
"graceful-fs": "^4.2.9",
4145
"jest-circus": "workspace:^",
46+
"jest-docblock": "workspace:^",
4247
"jest-environment-node": "workspace:^",
4348
"jest-get-type": "workspace:^",
4449
"jest-regex-util": "workspace:^",
@@ -57,6 +62,8 @@
5762
"@types/graceful-fs": "^4.1.3",
5863
"@types/micromatch": "^4.0.1",
5964
"@types/parse-json": "^4.0.0",
65+
"esbuild": "^0.15.0",
66+
"esbuild-register": "^3.1.0",
6067
"semver": "^7.3.5",
6168
"ts-node": "^10.5.0",
6269
"typescript": "^4.8.2"

packages/jest-config/src/readConfigFileAndSetRootDir.ts

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,21 @@ import * as path from 'path';
99
import * as fs from 'graceful-fs';
1010
import parseJson = require('parse-json');
1111
import stripJsonComments = require('strip-json-comments');
12-
import type {Service} from 'ts-node';
1312
import type {Config} from '@jest/types';
13+
import {extract, parse} from 'jest-docblock';
1414
import {interopRequireDefault, requireOrImportModule} from 'jest-util';
1515
import {
1616
JEST_CONFIG_EXT_JSON,
1717
JEST_CONFIG_EXT_TS,
1818
PACKAGE_JSON,
1919
} from './constants';
2020

21+
interface TsLoader {
22+
enabled: (bool: boolean) => void;
23+
}
24+
25+
type TsLoaderModule = 'ts-node' | 'esbuild-register';
26+
2127
// Read the configuration and set its `rootDir`
2228
// 1. If it's a `package.json` file, we look into its "jest" property
2329
// 2. If it's a `jest.config.ts` file, we use `ts-node` to transpile & require it
@@ -82,7 +88,19 @@ const loadTSConfigFile = async (
8288
configPath: string,
8389
): Promise<Config.InitialOptions> => {
8490
// Get registered TypeScript compiler instance
85-
const registeredCompiler = await getRegisteredCompiler();
91+
const docblockPragmas = parse(extract(fs.readFileSync(configPath, 'utf8')));
92+
const tsLoader = docblockPragmas['jest-config-loader'] || 'ts-node';
93+
if (Array.isArray(tsLoader)) {
94+
throw new Error(
95+
`You can only define a single test environment through docblocks, got "${tsLoader.join(
96+
', ',
97+
)}"`,
98+
);
99+
}
100+
101+
const registeredCompiler = await getRegisteredCompiler(
102+
tsLoader as TsLoaderModule,
103+
);
86104

87105
registeredCompiler.enabled(true);
88106

@@ -98,30 +116,50 @@ const loadTSConfigFile = async (
98116
return configObject;
99117
};
100118

101-
let registeredCompilerPromise: Promise<Service>;
119+
let registeredCompilerPromise: Promise<TsLoader>;
102120

103-
function getRegisteredCompiler() {
121+
function getRegisteredCompiler(loader: TsLoaderModule) {
104122
// Cache the promise to avoid multiple registrations
105-
registeredCompilerPromise = registeredCompilerPromise ?? registerTsNode();
123+
registeredCompilerPromise =
124+
registeredCompilerPromise ?? registerTsLoader(loader);
106125
return registeredCompilerPromise;
107126
}
108127

109-
async function registerTsNode(): Promise<Service> {
128+
async function registerTsLoader(loader: TsLoaderModule): Promise<TsLoader> {
110129
try {
111130
// Register TypeScript compiler instance
112-
const tsNode = await import('ts-node');
113-
return tsNode.register({
114-
compilerOptions: {
115-
module: 'CommonJS',
116-
},
117-
moduleTypes: {
118-
'**': 'cjs',
119-
},
120-
});
131+
if (loader === 'ts-node') {
132+
const tsLoader = await import('ts-node');
133+
return tsLoader.register({
134+
compilerOptions: {
135+
module: 'CommonJS',
136+
},
137+
moduleTypes: {
138+
'**': 'cjs',
139+
},
140+
});
141+
} else if (loader === 'esbuild-register') {
142+
const tsLoader = await import('esbuild-register/dist/node');
143+
let instance: {unregister: () => void} | undefined;
144+
return {
145+
enabled: (bool: boolean) => {
146+
if (bool) {
147+
instance = tsLoader.register({
148+
target: `node${process.version.slice(1)}`,
149+
});
150+
} else {
151+
instance?.unregister();
152+
}
153+
},
154+
};
155+
}
156+
throw new Error(
157+
`Jest: '${loader}' is not a valid TypeScript configuration loader.`,
158+
);
121159
} catch (e: any) {
122160
if (e.code === 'ERR_MODULE_NOT_FOUND') {
123161
throw new Error(
124-
`Jest: 'ts-node' is required for the TypeScript configuration files. Make sure it is installed\nError: ${e.message}`,
162+
`Jest: '${loader}' is required for the TypeScript configuration files. Make sure it is installed\nError: ${e.message}`,
125163
);
126164
}
127165

packages/jest-config/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
// jest-test-sequencer, but that is just `require.resolve`d, so no real use
1111
// for their types
1212
"references": [
13+
{"path": "../jest-docblock"},
1314
{"path": "../jest-environment-node"},
1415
{"path": "../jest-get-type"},
1516
{"path": "../jest-regex-util"},

0 commit comments

Comments
 (0)