From 51824dabe89b08903f6f4ef0bae9a4c5c22f2754 Mon Sep 17 00:00:00 2001 From: ahnpnl Date: Tue, 4 Nov 2025 10:48:04 +0700 Subject: [PATCH] fix: add support for ESM test environments with TypeScript (.ts, .mts) --- CHANGELOG.md | 1 + e2e/__tests__/testEnvironmentEsm.ts | 43 ++++- ...TestEnv.test.js => testUsingJsEnv.test.js} | 4 +- .../__tests__/testUsingMjsEnv.test.js | 13 ++ .../__tests__/testUsingMtsEnv.test.js | 13 ++ .../__tests__/testUsingTsEnv.test.js | 13 ++ e2e/test-environment-esm/babel.config.cjs | 13 ++ e2e/test-environment-esm/env-1.mts | 26 +++ e2e/test-environment-esm/env-2.ts | 26 +++ .../{EnvESM.js => env-3.js} | 7 +- e2e/test-environment-esm/env-4.mjs | 16 ++ e2e/test-environment-esm/package.json | 14 +- packages/jest-runner/src/runTest.ts | 14 +- .../jest-transform/src/ScriptTransformer.ts | 83 +++++++- .../src/__tests__/ScriptTransformer.test.ts | 177 ++++++++++++++++++ packages/jest-transform/src/types.ts | 5 + 16 files changed, 453 insertions(+), 15 deletions(-) rename e2e/test-environment-esm/__tests__/{testUsingESMTestEnv.test.js => testUsingJsEnv.test.js} (77%) create mode 100644 e2e/test-environment-esm/__tests__/testUsingMjsEnv.test.js create mode 100644 e2e/test-environment-esm/__tests__/testUsingMtsEnv.test.js create mode 100644 e2e/test-environment-esm/__tests__/testUsingTsEnv.test.js create mode 100644 e2e/test-environment-esm/babel.config.cjs create mode 100644 e2e/test-environment-esm/env-1.mts create mode 100644 e2e/test-environment-esm/env-2.ts rename e2e/test-environment-esm/{EnvESM.js => env-3.js} (68%) create mode 100644 e2e/test-environment-esm/env-4.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index c12bd0ed0e2c..4e622b2040fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - `[jest-reporters]` Fix issue where console output not displayed for GHA reporter even with `silent: false` option ([#15864](https://github.com/jestjs/jest/pull/15864)) - `[jest-runtime]` Fix issue where user cannot utilize dynamic import despite specifying `--experimental-vm-modules` Node option ([#15842](https://github.com/jestjs/jest/pull/15842)) - `[jest-test-sequencer]` Fix issue where failed tests due to compilation errors not getting re-executed even with `--onlyFailures` CLI option ([#15851](https://github.com/jestjs/jest/pull/15851)) +- `[jest-runner, jest-transformer]` support custom test environment written in ESM with `TypeScript` extensions ([#15885](https://github.com/jestjs/jest/pull/15885)), fixes downstream [issue](https://github.com/kulshekhar/ts-jest/issues/4195) ### Chore & Maintenance diff --git a/e2e/__tests__/testEnvironmentEsm.ts b/e2e/__tests__/testEnvironmentEsm.ts index 6e9b534655c5..da7592e225a9 100644 --- a/e2e/__tests__/testEnvironmentEsm.ts +++ b/e2e/__tests__/testEnvironmentEsm.ts @@ -6,11 +6,46 @@ */ import {resolve} from 'path'; -import runJest from '../runJest'; +import {json as runJestJson} from '../runJest'; -it('support test environment written in ESM', () => { - const DIR = resolve(__dirname, '../test-environment-esm'); - const {exitCode} = runJest(DIR); +const DIR = resolve(__dirname, '../test-environment-esm'); + +it('support test environment written in ESM with `.ts` extension', () => { + const {exitCode, json} = runJestJson(DIR, ['testUsingTsEnv.test.js'], { + nodeOptions: '--experimental-vm-modules --no-warnings', + }); + + expect(exitCode).toBe(0); + expect(json.numTotalTests).toBe(1); + expect(json.numPassedTests).toBe(1); +}); + +it('support test environment written in ESM with `.mts` extension', () => { + const {exitCode, json} = runJestJson(DIR, ['testUsingMtsEnv.test.js'], { + nodeOptions: '--experimental-vm-modules --no-warnings', + }); + + expect(exitCode).toBe(0); + expect(json.numTotalTests).toBe(1); + expect(json.numPassedTests).toBe(1); +}); + +it('support test environment written in ESM with `.js` extension', () => { + const {exitCode, json} = runJestJson(DIR, ['testUsingJsEnv.test.js'], { + nodeOptions: '--experimental-vm-modules --no-warnings', + }); + + expect(exitCode).toBe(0); + expect(json.numTotalTests).toBe(1); + expect(json.numPassedTests).toBe(1); +}); + +it('support test environment written in ESM with `.mjs` extension', () => { + const {exitCode, json} = runJestJson(DIR, ['testUsingMjsEnv.test.js'], { + nodeOptions: '--experimental-vm-modules --no-warnings', + }); expect(exitCode).toBe(0); + expect(json.numTotalTests).toBe(1); + expect(json.numPassedTests).toBe(1); }); diff --git a/e2e/test-environment-esm/__tests__/testUsingESMTestEnv.test.js b/e2e/test-environment-esm/__tests__/testUsingJsEnv.test.js similarity index 77% rename from e2e/test-environment-esm/__tests__/testUsingESMTestEnv.test.js rename to e2e/test-environment-esm/__tests__/testUsingJsEnv.test.js index 994098e5f37d..8df4049f4182 100644 --- a/e2e/test-environment-esm/__tests__/testUsingESMTestEnv.test.js +++ b/e2e/test-environment-esm/__tests__/testUsingJsEnv.test.js @@ -3,9 +3,11 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. + * + * @jest-environment /env-3.js */ 'use strict'; -test('dummy', () => { +test('should pass', () => { expect(globalThis.someVar).toBe(42); }); diff --git a/e2e/test-environment-esm/__tests__/testUsingMjsEnv.test.js b/e2e/test-environment-esm/__tests__/testUsingMjsEnv.test.js new file mode 100644 index 000000000000..bb79eae074fa --- /dev/null +++ b/e2e/test-environment-esm/__tests__/testUsingMjsEnv.test.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @jest-environment /env-4.mjs + */ +'use strict'; + +test('should pass', () => { + expect(globalThis.someVar).toBe(42); +}); diff --git a/e2e/test-environment-esm/__tests__/testUsingMtsEnv.test.js b/e2e/test-environment-esm/__tests__/testUsingMtsEnv.test.js new file mode 100644 index 000000000000..778912a3d708 --- /dev/null +++ b/e2e/test-environment-esm/__tests__/testUsingMtsEnv.test.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @jest-environment /env-1.mts + */ +'use strict'; + +test('should pass', () => { + expect(globalThis.someVar).toBe(42); +}); diff --git a/e2e/test-environment-esm/__tests__/testUsingTsEnv.test.js b/e2e/test-environment-esm/__tests__/testUsingTsEnv.test.js new file mode 100644 index 000000000000..12d192fec55a --- /dev/null +++ b/e2e/test-environment-esm/__tests__/testUsingTsEnv.test.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @jest-environment /env-2.ts + */ +'use strict'; + +test('should pass', () => { + expect(globalThis.someVar).toBe(42); +}); diff --git a/e2e/test-environment-esm/babel.config.cjs b/e2e/test-environment-esm/babel.config.cjs new file mode 100644 index 000000000000..ad3cbda540db --- /dev/null +++ b/e2e/test-environment-esm/babel.config.cjs @@ -0,0 +1,13 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +module.exports = { + presets: [ + ['@babel/preset-env', {targets: {node: 'current'}}], + '@babel/preset-typescript', + ], +}; diff --git a/e2e/test-environment-esm/env-1.mts b/e2e/test-environment-esm/env-1.mts new file mode 100644 index 000000000000..ac73a3b60dc6 --- /dev/null +++ b/e2e/test-environment-esm/env-1.mts @@ -0,0 +1,26 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type { + EnvironmentContext, + JestEnvironmentConfig, +} from '@jest/environment'; +import {TestEnvironment} from 'jest-environment-node'; + +declare global { + interface ImportMeta { + someVar: number; + } +} + +export default class Env extends TestEnvironment { + constructor(config: JestEnvironmentConfig, context: EnvironmentContext) { + super(config, context); + import.meta.someVar = 42; + this.global.someVar = import.meta.someVar; + } +} diff --git a/e2e/test-environment-esm/env-2.ts b/e2e/test-environment-esm/env-2.ts new file mode 100644 index 000000000000..ac73a3b60dc6 --- /dev/null +++ b/e2e/test-environment-esm/env-2.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type { + EnvironmentContext, + JestEnvironmentConfig, +} from '@jest/environment'; +import {TestEnvironment} from 'jest-environment-node'; + +declare global { + interface ImportMeta { + someVar: number; + } +} + +export default class Env extends TestEnvironment { + constructor(config: JestEnvironmentConfig, context: EnvironmentContext) { + super(config, context); + import.meta.someVar = 42; + this.global.someVar = import.meta.someVar; + } +} diff --git a/e2e/test-environment-esm/EnvESM.js b/e2e/test-environment-esm/env-3.js similarity index 68% rename from e2e/test-environment-esm/EnvESM.js rename to e2e/test-environment-esm/env-3.js index a38a8bdf8437..aee500705640 100644 --- a/e2e/test-environment-esm/EnvESM.js +++ b/e2e/test-environment-esm/env-3.js @@ -8,8 +8,9 @@ import {TestEnvironment} from 'jest-environment-node'; export default class Env extends TestEnvironment { - constructor(...args) { - super(...args); - this.global.someVar = 42; + constructor(config, context) { + super(config, context); + import.meta.someVar = 42; + this.global.someVar = import.meta.someVar; } } diff --git a/e2e/test-environment-esm/env-4.mjs b/e2e/test-environment-esm/env-4.mjs new file mode 100644 index 000000000000..aee500705640 --- /dev/null +++ b/e2e/test-environment-esm/env-4.mjs @@ -0,0 +1,16 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {TestEnvironment} from 'jest-environment-node'; + +export default class Env extends TestEnvironment { + constructor(config, context) { + super(config, context); + import.meta.someVar = 42; + this.global.someVar = import.meta.someVar; + } +} diff --git a/e2e/test-environment-esm/package.json b/e2e/test-environment-esm/package.json index 32b3c79ef350..a60143089dca 100644 --- a/e2e/test-environment-esm/package.json +++ b/e2e/test-environment-esm/package.json @@ -1,7 +1,17 @@ { "type": "module", "jest": { - "testEnvironment": "/EnvESM.js", - "transform": {} + "transform": { + "^.+\\.(ts|mts)$": "babel-jest" + }, + "extensionsToTreatAsEsm": [ + ".ts", + ".mts" + ] + }, + "dependencies": { + "@babel/preset-env": "^7.0.0", + "@babel/preset-typescript": "^7.0.0", + "jest-environment-node": "file:../../packages/jest-environment-node" } } diff --git a/packages/jest-runner/src/runTest.ts b/packages/jest-runner/src/runTest.ts index 05256db47aa7..875f691ca77d 100644 --- a/packages/jest-runner/src/runTest.ts +++ b/packages/jest-runner/src/runTest.ts @@ -25,7 +25,6 @@ import type {Config} from '@jest/types'; import * as docblock from 'jest-docblock'; import LeakDetector from 'jest-leak-detector'; import {formatExecError} from 'jest-message-util'; -// eslint-disable-next-line @typescript-eslint/consistent-type-imports import Resolver, {resolveTestEnvironment} from 'jest-resolve'; import type RuntimeClass from 'jest-runtime'; import {ErrorWithStack, interopRequireDefault, setGlobal} from 'jest-util'; @@ -109,9 +108,18 @@ async function runTestInternal( const cacheFS = new Map([[path, testSource]]); const transformer = await createScriptTransformer(projectConfig, cacheFS); + const shouldLoadTestEnvAsEsm = Resolver.unstable_shouldLoadAsEsm( + testEnvironment, + projectConfig.extensionsToTreatAsEsm, + ); - const TestEnvironment: typeof JestEnvironment = - await transformer.requireAndTranspileModule(testEnvironment); + const TestEnvironment: typeof JestEnvironment = shouldLoadTestEnvAsEsm + ? await transformer.importAndTranspileModule(testEnvironment, undefined, { + collectCoverage: globalConfig.collectCoverage, + collectCoverageFrom: globalConfig.collectCoverageFrom, + coverageProvider: globalConfig.coverageProvider, + }) + : await transformer.requireAndTranspileModule(testEnvironment); const testFramework: TestFramework = await transformer.requireAndTranspileModule( process.env.JEST_JASMINE === '1' diff --git a/packages/jest-transform/src/ScriptTransformer.ts b/packages/jest-transform/src/ScriptTransformer.ts index 49564b5d8777..67109297d2a2 100644 --- a/packages/jest-transform/src/ScriptTransformer.ts +++ b/packages/jest-transform/src/ScriptTransformer.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import {createHash} from 'crypto'; -import * as path from 'path'; +import {createHash} from 'node:crypto'; +import * as path from 'node:path'; import {transformSync as babelTransform} from '@babel/core'; // @ts-expect-error: should just be `require.resolve`, but the tests mess that up import babelPluginIstanbul from 'babel-plugin-istanbul'; @@ -35,6 +35,7 @@ import { import shouldInstrument from './shouldInstrument'; import type { FixedRawSourceMap, + ImportAndTranspileModuleOptions, Options, ReducedTransformOptions, RequireAndTranspileModuleOptions, @@ -82,6 +83,32 @@ function isTransformerFactory( return typeof (t as TransformerFactory).createTransformer === 'function'; } +async function loadEsmModule( + filePath: string, + callback?: (module: ModuleType) => void | Promise, +): Promise { + const moduleType: object = await requireOrImportModule(filePath); + const esmModule = ( + 'default' in moduleType ? moduleType.default : moduleType + ) as ModuleType; + + if (callback) { + const cbResult = callback(esmModule); + if (isPromise(cbResult)) { + await cbResult; + } + } + + return esmModule; +} + +function getEsmCacheExtension(ext: string): string { + if (ext === '.mts' || ext === '.mtsx') return '.mjs'; + if (ext === '.ts' || ext === '.tsx') return '.js'; + + return ext; +} + class ScriptTransformer { private readonly _cache: ProjectCache; private readonly _transformCache = new Map< @@ -843,6 +870,58 @@ class ScriptTransformer { return this._config.transform.length > 0 && !isIgnored; } + + /** + * Transforms and imports an ES module. + * Unlike {@link requireAndTranspileModule} this method first attempts to import + * the file via {@link requireOrImportModule} and fallback to transform files and + * then imports it again. + * + * The fallback case is necessary for TypeScript files in ESM format + */ + async importAndTranspileModule( + moduleName: string, + callback?: (module: ModuleType) => void | Promise, + options?: ImportAndTranspileModuleOptions, + ): Promise { + const transformOptions: Options = { + collectCoverage: options?.collectCoverage ?? false, + collectCoverageFrom: options?.collectCoverageFrom ?? [], + coverageProvider: options?.coverageProvider ?? 'v8', + isInternalModule: false, + supportsDynamicImport: true, + supportsExportNamespaceFrom: true, + supportsStaticESM: true, + supportsTopLevelAwait: true, + ...options, + }; + try { + return await loadEsmModule(moduleName, callback); + } catch { + const transformResult = await this.transformAsync( + moduleName, + transformOptions, + ); + const fileExt = path.extname(moduleName); + const targetExt = getEsmCacheExtension(fileExt); + const tempTransformedPath = moduleName.replace( + fileExt, + `.jest-esm-transform-${Date.now()}${targetExt}`, + ); + + fs.writeFileSync(tempTransformedPath, transformResult.code, 'utf8'); + + try { + return await loadEsmModule(tempTransformedPath, callback); + } finally { + try { + fs.unlinkSync(tempTransformedPath); + } catch { + // Ignore cleanup errors + } + } + } + } } // TODO: do we need to define the generics twice? diff --git a/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts b/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts index b1399872f51f..05c6720ed3ea 100644 --- a/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts +++ b/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts @@ -2072,6 +2072,183 @@ describe('ScriptTransformer', () => { ['\\.js$test_preprocessor', expect.any(Object)], ]); }); + + it('importAndTranspileModule should load ESM module directly if possible', async () => { + const scriptTransformer = await createScriptTransformer({ + ...config, + extensionsToTreatAsEsm: ['.mjs'], + transform: [['\\.mjs$', 'test_async_preprocessor', {}]], + }); + mockFs['/fruits/banana.mjs'] = 'export default class Banana {}'; + class MockBanana {} + const mockRequireOrImportModule = jest.fn((_fileName: string) => + Promise.resolve({ + default: MockBanana, + }), + ); + jest + .spyOn( + require('jest-util') as typeof import('jest-util'), + 'requireOrImportModule', + ) + .mockImplementation(mockRequireOrImportModule); + + const result = + await scriptTransformer.importAndTranspileModule('/fruits/banana.mjs'); + + expect(result).toStrictEqual(MockBanana); + expect(mockRequireOrImportModule).toHaveBeenCalledWith( + '/fruits/banana.mjs', + ); + }); + + it('should use module as-is when no default export', async () => { + const scriptTransformer = await createScriptTransformer({ + ...config, + extensionsToTreatAsEsm: ['.mts'], + transform: [['\\.mts$', 'test_async_preprocessor', {}]], + }); + mockFs['/fruits/banana.mts'] = 'export const banana = "yellow";'; + const mockModule = {banana: 'yellow'}; + jest + .spyOn( + require('jest-util') as typeof import('jest-util'), + 'requireOrImportModule', + ) + .mockImplementation(() => Promise.resolve(mockModule)); + + const result = + await scriptTransformer.importAndTranspileModule('/fruits/banana.mts'); + + expect(result).toStrictEqual(mockModule); + }); + + it('importAndTranspileModule should transform and create temp file when direct load fails', async () => { + const scriptTransformer = await createScriptTransformer({ + ...config, + extensionsToTreatAsEsm: ['.ts'], + transform: [['\\.ts$', 'test_async_preprocessor', {}]], + }); + mockFs['/fruits/banana.ts'] = 'export default class Banana {}'; + class MockBanana {} + let callCount = 0; + const mockRequireOrImportModule = jest.fn(() => { + callCount++; + if (callCount === 1) { + // First call (direct load) fails + throw new Error('Cannot load untransformed TypeScript'); + } + + // Second call (after transformation) succeeds + return Promise.resolve({default: MockBanana}); + }); + jest + .spyOn( + require('jest-util') as typeof import('jest-util'), + 'requireOrImportModule', + ) + .mockImplementation(mockRequireOrImportModule); + + const result = + await scriptTransformer.importAndTranspileModule('/fruits/banana.ts'); + + expect(result).toStrictEqual(MockBanana); + // Should be called twice: once for direct load, once for transformed file + expect(mockRequireOrImportModule).toHaveBeenCalledTimes(2); + + // Check that writeFileSync was called with a temp file path + const writeFileCalls = (fs.writeFileSync as jest.Mock).mock.calls; + const tempFileCalls = writeFileCalls.filter( + call => + typeof call[0] === 'string' && call[0].includes('.jest-esm-transform-'), + ); + + expect(tempFileCalls.length).toBeGreaterThan(0); + // Verify the temp file path doesn't overwrite the original + const tempFilePath = tempFileCalls[0][0]; + expect(tempFilePath).not.toBe('/fruits/banana.ts'); + expect(tempFilePath).toMatch(/\.jest-esm-transform-\d+\.js$/); + }); + + it.each(['/fruits/banana.mts', '/fruits/banana.ts'])( + 'importAndTranspileModule should create temp file with correct extension for TypeScript files', + async fileName => { + const scriptTransformer = await createScriptTransformer({ + ...config, + extensionsToTreatAsEsm: ['ts', '.mts'], + transform: [['\\.(mts|ts)$', 'test_async_preprocessor', {}]], + }); + mockFs[fileName] = 'export default class Banana {}'; + class MockBanana {} + let callCount = 0; + const mockRequireOrImportModule = jest.fn(() => { + callCount++; + if (callCount === 1) { + throw new Error('Cannot load untransformed TypeScript'); + } + return Promise.resolve({default: MockBanana}); + }); + + jest + .spyOn( + require('jest-util') as typeof import('jest-util'), + 'requireOrImportModule', + ) + .mockImplementation(mockRequireOrImportModule); + + await scriptTransformer.importAndTranspileModule(fileName); + + // Check that writeFileSync was called with a temp file path + const writeFileCalls = (fs.writeFileSync as jest.Mock).mock.calls; + const tempFileCalls = writeFileCalls.filter( + call => + typeof call[0] === 'string' && + call[0].includes('.jest-esm-transform-'), + ); + + expect(tempFileCalls.length).toBeGreaterThan(0); + // Verify the temp file path doesn't overwrite the original + const tempFilePath = tempFileCalls[0][0]; + expect(tempFilePath).not.toBe(fileName); + expect(tempFilePath).toMatch(/\.jest-esm-transform-\d+\.(mjs|js)$/); + }, + ); + + it('importAndTranspileModule should cleanup temp file after transformation', async () => { + const scriptTransformer = await createScriptTransformer({ + ...config, + extensionsToTreatAsEsm: ['.mts'], + transform: [['\\.mts$', 'test_async_preprocessor', {}]], + }); + mockFs['/fruits/banana.mts'] = 'export default class Banana {}'; + class MockBanana {} + let callCount = 0; + const mockRequireOrImportModule = jest.fn(() => { + callCount++; + if (callCount === 1) { + throw new Error('Cannot load untransformed TypeScript'); + } + + return Promise.resolve({default: MockBanana}); + }); + + jest + .spyOn( + require('jest-util') as typeof import('jest-util'), + 'requireOrImportModule', + ) + .mockImplementation(mockRequireOrImportModule); + + await scriptTransformer.importAndTranspileModule('/fruits/banana.mts'); + + expect(fs.unlinkSync).toHaveBeenCalled(); + const unlinkCalls = (fs.unlinkSync as jest.Mock).mock.calls; + const tempFileUnlinks = unlinkCalls.filter( + call => + typeof call[0] === 'string' && call[0].includes('.jest-esm-transform-'), + ); + expect(tempFileUnlinks.length).toBeGreaterThan(0); + }); }); function getTransformOptions(instrument: boolean): ReducedTransformOptions { diff --git a/packages/jest-transform/src/types.ts b/packages/jest-transform/src/types.ts index aa37fa5b450a..76155f4777eb 100644 --- a/packages/jest-transform/src/types.ts +++ b/packages/jest-transform/src/types.ts @@ -52,6 +52,11 @@ export interface RequireAndTranspileModuleOptions applyInteropRequireDefault: boolean; } +export type ImportAndTranspileModuleOptions = Pick< + Config.GlobalConfig, + 'collectCoverage' | 'collectCoverageFrom' | 'coverageProvider' +>; + export type StringMap = Map; export interface TransformOptions