diff --git a/CHANGELOG.md b/CHANGELOG.md index 067791482e22..c40e79183f3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features - `[expect, @jest/expect-utils]` Support custom equality testers ([#13654](https://github.com/facebook/jest/pull/13654)) +- `[jest-config]` Support using esbuild-register for loading TS configs ([#13742](https://github.com/facebook/jest/pull/13742)) - `[jest-config, jest-worker]` Use `os.availableParallelism` if available to calculate number of workers to spawn ([#13738](https://github.com/facebook/jest/pull/13738)) - `[@jest/globals, jest-mock]` Add `jest.replaceProperty()` that replaces property value ([#13496](https://github.com/facebook/jest/pull/13496)) - `[jest-haste-map]` ignore Sapling vcs directories (`.sl/`) ([#13674](https://github.com/facebook/jest/pull/13674)) diff --git a/docs/Configuration.md b/docs/Configuration.md index 0a071c26c713..11d77bed5de7 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -57,7 +57,7 @@ export default async (): Promise => { :::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. ::: diff --git a/e2e/__tests__/readInitialOptions.test.ts b/e2e/__tests__/readInitialOptions.test.ts index 9d2ba417eb4f..c8b798207261 100644 --- a/e2e/__tests__/readInitialOptions.test.ts +++ b/e2e/__tests__/readInitialOptions.test.ts @@ -46,58 +46,21 @@ describe('readInitialOptions', () => { expect(config).toEqual({jestConfig: 'jest.config.js', rootDir}); expect(configPath).toEqual(configFile); }); - test('should read a jest.config.js file', async () => { - const configFile = resolveFixture('js-config', 'jest.config.js'); - const rootDir = resolveFixture('js-config'); - const {config, configPath} = await proxyReadInitialOptions(undefined, { - cwd: rootDir, - }); - expect(config).toEqual({jestConfig: 'jest.config.js', rootDir}); - expect(configPath).toEqual(configFile); - }); - test('should read a package.json file', async () => { - const configFile = resolveFixture('pkg-config', 'package.json'); - const rootDir = resolveFixture('pkg-config'); - const {config, configPath} = await proxyReadInitialOptions(undefined, { - cwd: rootDir, - }); - 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'); - 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.mjs file', async () => { - const configFile = resolveFixture('mjs-config', 'jest.config.mjs'); - const rootDir = resolveFixture('mjs-config'); - const {config, configPath} = await proxyReadInitialOptions(undefined, { - cwd: rootDir, - }); - expect(config).toEqual({jestConfig: 'jest.config.mjs', rootDir}); - expect(configPath).toEqual(configFile); - }); - test('should read a jest.config.json file', async () => { - const configFile = resolveFixture('json-config', 'jest.config.json'); - const rootDir = resolveFixture('json-config'); - const {config, configPath} = await proxyReadInitialOptions(undefined, { - cwd: rootDir, - }); - expect(config).toEqual({jestConfig: 'jest.config.json', rootDir}); - expect(configPath).toEqual(configFile); - }); - test('should read a jest config exporting an async function', async () => { - const configFile = resolveFixture('async-config', 'jest.config.js'); - const rootDir = resolveFixture('async-config'); + test.each([ + ['js-config', 'jest.config.js'], + ['pkg-config', 'package.json'], + ['ts-node-config', 'jest.config.ts'], + ['ts-esbuild-register-config', 'jest.config.ts'], + ['mjs-config', 'jest.config.mjs'], + ['json-config', 'jest.config.json'], + ['async-config', 'jest.config.js'], + ])('should read %s/%s file', async (directory: string, filename: string) => { + const configFile = resolveFixture(directory, filename); + const rootDir = resolveFixture(directory); const {config, configPath} = await proxyReadInitialOptions(undefined, { cwd: rootDir, }); - expect(config).toEqual({jestConfig: 'async-config', rootDir}); + expect(config).toEqual({jestConfig: filename, rootDir}); expect(configPath).toEqual(configFile); }); diff --git a/e2e/read-initial-options/async-config/jest.config.js b/e2e/read-initial-options/async-config/jest.config.js index 00df2a4f3837..6747ae8b570a 100644 --- a/e2e/read-initial-options/async-config/jest.config.js +++ b/e2e/read-initial-options/async-config/jest.config.js @@ -6,6 +6,6 @@ */ module.exports = async function () { return { - jestConfig: 'async-config', + jestConfig: 'jest.config.js', }; }; diff --git a/e2e/read-initial-options/ts-esbuild-register-config/jest.config.ts b/e2e/read-initial-options/ts-esbuild-register-config/jest.config.ts new file mode 100644 index 000000000000..9b003d380264 --- /dev/null +++ b/e2e/read-initial-options/ts-esbuild-register-config/jest.config.ts @@ -0,0 +1,15 @@ +/** + * 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 + */ +interface Config { + jestConfig: string; +} + +export default { + jestConfig: 'jest.config.ts', +} as Config; diff --git a/e2e/read-initial-options/ts-config/jest.config.ts b/e2e/read-initial-options/ts-node-config/jest.config.ts similarity index 81% rename from e2e/read-initial-options/ts-config/jest.config.ts rename to e2e/read-initial-options/ts-node-config/jest.config.ts index bf05ee62de87..2575e19befb2 100644 --- a/e2e/read-initial-options/ts-config/jest.config.ts +++ b/e2e/read-initial-options/ts-node-config/jest.config.ts @@ -4,6 +4,10 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ +interface Config { + jestConfig: string; +} + export default { jestConfig: 'jest.config.ts', -}; +} as Config; diff --git a/packages/jest-config/package.json b/packages/jest-config/package.json index 0bdcc09d501e..292be0abdd6b 100644 --- a/packages/jest-config/package.json +++ b/packages/jest-config/package.json @@ -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 } @@ -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:^", @@ -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" diff --git a/packages/jest-config/src/readConfigFileAndSetRootDir.ts b/packages/jest-config/src/readConfigFileAndSetRootDir.ts index a4496333ae32..948756ba52ed 100644 --- a/packages/jest-config/src/readConfigFileAndSetRootDir.ts +++ b/packages/jest-config/src/readConfigFileAndSetRootDir.ts @@ -9,8 +9,8 @@ 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, @@ -18,6 +18,12 @@ import { 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 @@ -82,7 +88,19 @@ const loadTSConfigFile = async ( configPath: string, ): Promise => { // 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); @@ -98,30 +116,50 @@ const loadTSConfigFile = async ( return configObject; }; -let registeredCompilerPromise: Promise; +let registeredCompilerPromise: Promise; -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 { +async function registerTsLoader(loader: TsLoaderModule): Promise { 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}`, ); } diff --git a/packages/jest-config/tsconfig.json b/packages/jest-config/tsconfig.json index 6ed9554e6476..1f8bb9aed574 100644 --- a/packages/jest-config/tsconfig.json +++ b/packages/jest-config/tsconfig.json @@ -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"}, diff --git a/yarn.lock b/yarn.lock index 3b8fd2747236..3863ab7f0ee2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2496,6 +2496,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.15.18": + version: 0.15.18 + resolution: "@esbuild/android-arm@npm:0.15.18" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.15.18": + version: 0.15.18 + resolution: "@esbuild/linux-loong64@npm:0.15.18" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^1.4.1": version: 1.4.1 resolution: "@eslint/eslintrc@npm:1.4.1" @@ -9114,6 +9128,234 @@ __metadata: languageName: node linkType: hard +"esbuild-android-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-android-64@npm:0.15.18" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"esbuild-android-arm64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-android-arm64@npm:0.15.18" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"esbuild-darwin-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-darwin-64@npm:0.15.18" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"esbuild-darwin-arm64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-darwin-arm64@npm:0.15.18" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"esbuild-freebsd-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-freebsd-64@npm:0.15.18" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"esbuild-freebsd-arm64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-freebsd-arm64@npm:0.15.18" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"esbuild-linux-32@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-32@npm:0.15.18" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"esbuild-linux-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-64@npm:0.15.18" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"esbuild-linux-arm64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-arm64@npm:0.15.18" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"esbuild-linux-arm@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-arm@npm:0.15.18" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"esbuild-linux-mips64le@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-mips64le@npm:0.15.18" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"esbuild-linux-ppc64le@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-ppc64le@npm:0.15.18" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"esbuild-linux-riscv64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-riscv64@npm:0.15.18" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"esbuild-linux-s390x@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-s390x@npm:0.15.18" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"esbuild-netbsd-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-netbsd-64@npm:0.15.18" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"esbuild-openbsd-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-openbsd-64@npm:0.15.18" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"esbuild-register@npm:^3.1.0": + version: 3.4.2 + resolution: "esbuild-register@npm:3.4.2" + dependencies: + debug: ^4.3.4 + peerDependencies: + esbuild: ">=0.12 <1" + checksum: f65d1ccb58b1ccbba376efb1fc023abe22731d9b79eead1b0120e57d4413318f063696257a5af637b527fa1d3f009095aa6edb1bf6ff69d637a9ab281fb727b3 + languageName: node + linkType: hard + +"esbuild-sunos-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-sunos-64@npm:0.15.18" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"esbuild-windows-32@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-windows-32@npm:0.15.18" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"esbuild-windows-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-windows-64@npm:0.15.18" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"esbuild-windows-arm64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-windows-arm64@npm:0.15.18" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"esbuild@npm:^0.15.0": + version: 0.15.18 + resolution: "esbuild@npm:0.15.18" + dependencies: + "@esbuild/android-arm": 0.15.18 + "@esbuild/linux-loong64": 0.15.18 + esbuild-android-64: 0.15.18 + esbuild-android-arm64: 0.15.18 + esbuild-darwin-64: 0.15.18 + esbuild-darwin-arm64: 0.15.18 + esbuild-freebsd-64: 0.15.18 + esbuild-freebsd-arm64: 0.15.18 + esbuild-linux-32: 0.15.18 + esbuild-linux-64: 0.15.18 + esbuild-linux-arm: 0.15.18 + esbuild-linux-arm64: 0.15.18 + esbuild-linux-mips64le: 0.15.18 + esbuild-linux-ppc64le: 0.15.18 + esbuild-linux-riscv64: 0.15.18 + esbuild-linux-s390x: 0.15.18 + esbuild-netbsd-64: 0.15.18 + esbuild-openbsd-64: 0.15.18 + esbuild-sunos-64: 0.15.18 + esbuild-windows-32: 0.15.18 + esbuild-windows-64: 0.15.18 + esbuild-windows-arm64: 0.15.18 + dependenciesMeta: + "@esbuild/android-arm": + optional: true + "@esbuild/linux-loong64": + optional: true + esbuild-android-64: + optional: true + esbuild-android-arm64: + optional: true + esbuild-darwin-64: + optional: true + esbuild-darwin-arm64: + optional: true + esbuild-freebsd-64: + optional: true + esbuild-freebsd-arm64: + optional: true + esbuild-linux-32: + optional: true + esbuild-linux-64: + optional: true + esbuild-linux-arm: + optional: true + esbuild-linux-arm64: + optional: true + esbuild-linux-mips64le: + optional: true + esbuild-linux-ppc64le: + optional: true + esbuild-linux-riscv64: + optional: true + esbuild-linux-s390x: + optional: true + esbuild-netbsd-64: + optional: true + esbuild-openbsd-64: + optional: true + esbuild-sunos-64: + optional: true + esbuild-windows-32: + optional: true + esbuild-windows-64: + optional: true + esbuild-windows-arm64: + optional: true + bin: + esbuild: bin/esbuild + checksum: ec12682b2cb2d4f0669d0e555028b87a9284ca7f6a1b26e35e69a8697165b35cc682ad598abc70f0bbcfdc12ca84ef888caf5ceee389237862e8f8c17da85f89 + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -12580,9 +12822,12 @@ __metadata: chalk: ^4.0.0 ci-info: ^3.2.0 deepmerge: ^4.2.2 + esbuild: ^0.15.0 + esbuild-register: ^3.1.0 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:^" @@ -12600,10 +12845,13 @@ __metadata: typescript: ^4.8.2 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 languageName: unknown