Skip to content

Commit

Permalink
feat(jest-config): Allow using esbuild-register as TS loader
Browse files Browse the repository at this point in the history
Signed-off-by: Matthew Peveler <[email protected]>
  • Loading branch information
MasterOdin committed Jan 7, 2023
1 parent d2420aa commit 402b669
Show file tree
Hide file tree
Showing 8 changed files with 337 additions and 20 deletions.
2 changes: 1 addition & 1 deletion docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default async (): Promise<Config> => {

:::tip

To read TypeScript configuration files Jest requires [`ts-node`](https://npmjs.com/package/ts-node). Make sure it is installed in your project.
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.

:::

Expand Down
18 changes: 15 additions & 3 deletions e2e/__tests__/readInitialOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,21 @@ describe('readInitialOptions', () => {
expect(config).toEqual({jestConfig: 'package.json', rootDir});
expect(configPath).toEqual(configFile);
});
test('should read a jest.config.ts file', async () => {
const configFile = resolveFixture('ts-config', 'jest.config.ts');
const rootDir = resolveFixture('ts-config');
test('should read a jest.config.ts file with ts-node', async () => {
const configFile = resolveFixture('ts-node-config', 'jest.config.ts');
const rootDir = resolveFixture('ts-node-config');
const {config, configPath} = await proxyReadInitialOptions(undefined, {
cwd: rootDir,
});
expect(config).toEqual({jestConfig: 'jest.config.ts', rootDir});
expect(configPath).toEqual(configFile);
});
test('should read a jest.config.ts file with esbuild-register', async () => {
const configFile = resolveFixture(
'ts-esbuild-register-config',
'jest.config.ts',
);
const rootDir = resolveFixture('ts-esbuild-register-config');
const {config, configPath} = await proxyReadInitialOptions(undefined, {
cwd: rootDir,
});
Expand Down
11 changes: 11 additions & 0 deletions e2e/read-initial-options/ts-esbuild-register-config/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @jest-config-loader esbuild-register
*/
export default {
jestConfig: 'jest.config.ts',
};
7 changes: 7 additions & 0 deletions packages/jest-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@
},
"peerDependencies": {
"@types/node": "*",
"esbuild-register": ">=3.1.0",
"ts-node": ">=9.0.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"esbuild-register": {
"optional": true
},
"ts-node": {
"optional": true
}
Expand All @@ -39,6 +43,7 @@
"glob": "^7.1.3",
"graceful-fs": "^4.2.9",
"jest-circus": "workspace:^",
"jest-docblock": "workspace:^",
"jest-environment-node": "workspace:^",
"jest-get-type": "workspace:^",
"jest-regex-util": "workspace:^",
Expand All @@ -57,6 +62,8 @@
"@types/graceful-fs": "^4.1.3",
"@types/micromatch": "^4.0.1",
"@types/parse-json": "^4.0.0",
"esbuild": "^0.15.0",
"esbuild-register": "^3.1.0",
"semver": "^7.3.5",
"ts-node": "^10.5.0",
"typescript": "^4.8.2"
Expand Down
70 changes: 54 additions & 16 deletions packages/jest-config/src/readConfigFileAndSetRootDir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,21 @@ import * as path from 'path';
import * as fs from 'graceful-fs';
import parseJson = require('parse-json');
import stripJsonComments = require('strip-json-comments');
import type {Service} from 'ts-node';
import type {Config} from '@jest/types';
import {extract, parse} from 'jest-docblock';
import {interopRequireDefault, requireOrImportModule} from 'jest-util';
import {
JEST_CONFIG_EXT_JSON,
JEST_CONFIG_EXT_TS,
PACKAGE_JSON,
} from './constants';

interface TsLoader {
enabled: (bool: boolean) => void;
}

type TsLoaderModule = 'ts-node' | 'esbuild-register';

// Read the configuration and set its `rootDir`
// 1. If it's a `package.json` file, we look into its "jest" property
// 2. If it's a `jest.config.ts` file, we use `ts-node` to transpile & require it
Expand Down Expand Up @@ -82,7 +88,19 @@ const loadTSConfigFile = async (
configPath: string,
): Promise<Config.InitialOptions> => {
// Get registered TypeScript compiler instance
const registeredCompiler = await getRegisteredCompiler();
const docblockPragmas = parse(extract(fs.readFileSync(configPath, 'utf8')));
const tsLoader = docblockPragmas['jest-config-loader'] || 'ts-node';
if (Array.isArray(tsLoader)) {
throw new Error(
`You can only define a single test environment through docblocks, got "${tsLoader.join(
', ',
)}"`,
);
}

const registeredCompiler = await getRegisteredCompiler(
tsLoader as TsLoaderModule,
);

registeredCompiler.enabled(true);

Expand All @@ -98,30 +116,50 @@ const loadTSConfigFile = async (
return configObject;
};

let registeredCompilerPromise: Promise<Service>;
let registeredCompilerPromise: Promise<TsLoader>;

function getRegisteredCompiler() {
function getRegisteredCompiler(loader: TsLoaderModule) {
// Cache the promise to avoid multiple registrations
registeredCompilerPromise = registeredCompilerPromise ?? registerTsNode();
registeredCompilerPromise =
registeredCompilerPromise ?? registerTsLoader(loader);
return registeredCompilerPromise;
}

async function registerTsNode(): Promise<Service> {
async function registerTsLoader(loader: TsLoaderModule): Promise<TsLoader> {
try {
// Register TypeScript compiler instance
const tsNode = await import('ts-node');
return tsNode.register({
compilerOptions: {
module: 'CommonJS',
},
moduleTypes: {
'**': 'cjs',
},
});
if (loader === 'ts-node') {
const tsLoader = await import('ts-node');
return tsLoader.register({
compilerOptions: {
module: 'CommonJS',
},
moduleTypes: {
'**': 'cjs',
},
});
} else if (loader === 'esbuild-register') {
const tsLoader = await import('esbuild-register/dist/node');
let instance: {unregister: () => void} | undefined;
return {
enabled: (bool: boolean) => {
if (bool) {
instance = tsLoader.register({
target: `node${process.version.slice(1)}`,
});
} else {
instance?.unregister();
}
},
};
}
throw new Error(
`Jest: '${loader}' is not a valid TypeScript configuration loader.`,
);
} catch (e: any) {
if (e.code === 'ERR_MODULE_NOT_FOUND') {
throw new Error(
`Jest: 'ts-node' is required for the TypeScript configuration files. Make sure it is installed\nError: ${e.message}`,
`Jest: '${loader}' is required for the TypeScript configuration files. Make sure it is installed\nError: ${e.message}`,
);
}

Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// jest-test-sequencer, but that is just `require.resolve`d, so no real use
// for their types
"references": [
{"path": "../jest-docblock"},
{"path": "../jest-environment-node"},
{"path": "../jest-get-type"},
{"path": "../jest-regex-util"},
Expand Down
Loading

0 comments on commit 402b669

Please sign in to comment.