From 402b6691c95da6b9e07b4c2ee5eaddc1c55a248d Mon Sep 17 00:00:00 2001 From: Matthew Peveler Date: Sat, 7 Jan 2023 13:29:26 -0500 Subject: [PATCH] feat(jest-config): Allow using esbuild-register as TS loader Signed-off-by: Matthew Peveler --- docs/Configuration.md | 2 +- e2e/__tests__/readInitialOptions.test.ts | 18 +- .../ts-esbuild-register-config/jest.config.ts | 11 + .../jest.config.ts | 0 packages/jest-config/package.json | 7 + .../src/readConfigFileAndSetRootDir.ts | 70 +++-- packages/jest-config/tsconfig.json | 1 + yarn.lock | 248 ++++++++++++++++++ 8 files changed, 337 insertions(+), 20 deletions(-) create mode 100644 e2e/read-initial-options/ts-esbuild-register-config/jest.config.ts rename e2e/read-initial-options/{ts-config => ts-node-config}/jest.config.ts (100%) 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..2a0e9db7f8a5 100644 --- a/e2e/__tests__/readInitialOptions.test.ts +++ b/e2e/__tests__/readInitialOptions.test.ts @@ -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, }); 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..93158262a214 --- /dev/null +++ b/e2e/read-initial-options/ts-esbuild-register-config/jest.config.ts @@ -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', +}; 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 100% rename from e2e/read-initial-options/ts-config/jest.config.ts rename to e2e/read-initial-options/ts-node-config/jest.config.ts 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