From 50cc73a829229dc2899813484512d73ec088001b Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 6 Mar 2021 16:09:57 +0100 Subject: [PATCH 1/9] feat(jest-transform): support transformers written in ESM --- CHANGELOG.md | 2 + babel.config.js | 18 +- e2e/__tests__/transform.test.ts | 11 + .../esm-transformer/__tests__/test.js | 12 ++ e2e/transform/esm-transformer/module.js | 8 + .../esm-transformer/my-transform.mjs | 22 ++ e2e/transform/esm-transformer/package.json | 8 + package.json | 2 - packages/jest-core/src/TestScheduler.ts | 36 ++-- .../src/__tests__/watchFileChanges.test.ts | 5 +- packages/jest-core/src/runGlobalHook.ts | 4 +- packages/jest-repl/src/cli/runtime-cli.ts | 5 +- packages/jest-reporters/src/CoverageWorker.ts | 2 +- .../__tests__/generateEmptyCoverage.test.js | 12 +- .../src/generateEmptyCoverage.ts | 26 ++- packages/jest-runner/src/runTest.ts | 8 +- .../src/__mocks__/createRuntime.js | 12 +- .../src/__tests__/instrumentation.test.ts | 17 +- packages/jest-runtime/src/index.ts | 3 +- .../jest-transform/src/ScriptTransformer.ts | 126 ++++++++---- .../src/__tests__/ScriptTransformer.test.ts | 189 ++++++++---------- packages/jest-transform/src/index.ts | 3 +- yarn.lock | 4 +- 23 files changed, 307 insertions(+), 228 deletions(-) create mode 100644 e2e/transform/esm-transformer/__tests__/test.js create mode 100644 e2e/transform/esm-transformer/module.js create mode 100644 e2e/transform/esm-transformer/my-transform.mjs create mode 100644 e2e/transform/esm-transformer/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index b3d034ce2a98..1605cba43699 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ - `[jest-reporters]` Add static filepath property to all reporters ([#11015](https://github.com/facebook/jest/pull/11015)) - `[jest-snapshot]` [**BREAKING**] Make prettier optional for inline snapshots - fall back to string replacement ([#7792](https://github.com/facebook/jest/pull/7792)) - `[jest-transform]` Pass config options defined in Jest's config to transformer's `process` and `getCacheKey` functions ([#10926](https://github.com/facebook/jest/pull/10926)) +- `[jest-transform]` Add support for transformers written in ESM +- `[jest-transform]` [**BREAKING**] Do not export `ScriptTransformer` class, instead export the async function `createScriptTransformer` - `[jest-worker]` Add support for custom task queues and adds a `PriorityQueue` implementation. ([#10921](https://github.com/facebook/jest/pull/10921)) - `[jest-worker]` Add in-order scheduling policy to jest worker ([10902](https://github.com/facebook/jest/pull/10902)) diff --git a/babel.config.js b/babel.config.js index d0ead878ed16..710bcf0556a5 100644 --- a/babel.config.js +++ b/babel.config.js @@ -35,22 +35,6 @@ module.exports = { ], test: /\.tsx?$/, }, - // we want this file to keep `import()`, so exclude the transform for it - { - plugins: ['@babel/plugin-syntax-dynamic-import'], - presets: [ - '@babel/preset-typescript', - [ - '@babel/preset-env', - { - exclude: ['@babel/plugin-proposal-dynamic-import'], - shippedProposals: true, - targets: {node: supportedNodeVersion}, - }, - ], - ], - test: 'packages/jest-config/src/readConfigFileAndSetRootDir.ts', - }, ], plugins: [ ['@babel/plugin-transform-modules-commonjs', {allowTopLevelThis: true}], @@ -63,6 +47,8 @@ module.exports = { '@babel/preset-env', { bugfixes: true, + // a runtime error is preferable, and we need a real `import` + exclude: ['@babel/plugin-proposal-dynamic-import'], shippedProposals: true, targets: {node: supportedNodeVersion}, }, diff --git a/e2e/__tests__/transform.test.ts b/e2e/__tests__/transform.test.ts index 6f98abdbab28..3ec924150158 100644 --- a/e2e/__tests__/transform.test.ts +++ b/e2e/__tests__/transform.test.ts @@ -238,3 +238,14 @@ describe('transform-testrunner', () => { expect(json.numPassedTests).toBe(1); }); }); + +describe('esm-transformer', () => { + const dir = path.resolve(__dirname, '../transform/esm-transformer'); + + it('should transform with transformer written in ESM', () => { + const {json, stderr} = runWithJson(dir, ['--no-cache']); + expect(stderr).toMatch(/PASS/); + expect(json.success).toBe(true); + expect(json.numPassedTests).toBe(1); + }); +}); diff --git a/e2e/transform/esm-transformer/__tests__/test.js b/e2e/transform/esm-transformer/__tests__/test.js new file mode 100644 index 000000000000..69f0feb07740 --- /dev/null +++ b/e2e/transform/esm-transformer/__tests__/test.js @@ -0,0 +1,12 @@ +/** + * 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. + */ + +const m = require('../module'); + +test('ESM transformer intercepts', () => { + expect(m).toEqual(42); +}); diff --git a/e2e/transform/esm-transformer/module.js b/e2e/transform/esm-transformer/module.js new file mode 100644 index 000000000000..1894c6f8eab1 --- /dev/null +++ b/e2e/transform/esm-transformer/module.js @@ -0,0 +1,8 @@ +/** + * 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. + */ + +module.exports = 'It was not transformed!!'; diff --git a/e2e/transform/esm-transformer/my-transform.mjs b/e2e/transform/esm-transformer/my-transform.mjs new file mode 100644 index 000000000000..0010375177f0 --- /dev/null +++ b/e2e/transform/esm-transformer/my-transform.mjs @@ -0,0 +1,22 @@ +/** + * 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. + */ + +import {createRequire} from 'module'; +import {fileURLToPath} from 'url'; + +const thisFile = fileURLToPath(import.meta.url); +const fileToTransform = createRequire(thisFile).resolve('./module') + +export default { + process(src, filepath) { + if (filepath === fileToTransform) { + return 'module.exports = 42;'; + } + + return src; + }, +}; diff --git a/e2e/transform/esm-transformer/package.json b/e2e/transform/esm-transformer/package.json new file mode 100644 index 000000000000..67dbfe24910b --- /dev/null +++ b/e2e/transform/esm-transformer/package.json @@ -0,0 +1,8 @@ +{ + "jest": { + "testEnvironment": "node", + "transform": { + "\\.js$": "/my-transform.mjs" + } + } +} diff --git a/package.json b/package.json index f9078e8f0170..d49e4818f684 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,6 @@ "devDependencies": { "@babel/core": "^7.3.4", "@babel/plugin-proposal-class-properties": "^7.3.4", - "@babel/plugin-proposal-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-transform-modules-commonjs": "^7.1.0", "@babel/plugin-transform-strict-mode": "^7.0.0", "@babel/preset-env": "^7.1.0", diff --git a/packages/jest-core/src/TestScheduler.ts b/packages/jest-core/src/TestScheduler.ts index a6e06a3b2883..75d2b433b371 100644 --- a/packages/jest-core/src/TestScheduler.ts +++ b/packages/jest-core/src/TestScheduler.ts @@ -25,7 +25,7 @@ import { buildFailureTestResult, makeEmptyAggregatedTestResult, } from '@jest/test-result'; -import {ScriptTransformer} from '@jest/transform'; +import {createScriptTransformer} from '@jest/transform'; import type {Config} from '@jest/types'; import {formatExecError} from 'jest-message-util'; import TestRunner, {Test} from 'jest-runner'; @@ -188,22 +188,24 @@ export default class TestScheduler { const testRunners: {[key: string]: TestRunner} = Object.create(null); const contextsByTestRunner = new WeakMap(); - contexts.forEach(context => { - const {config} = context; - if (!testRunners[config.runner]) { - const transformer = new ScriptTransformer(config); - const Runner: typeof TestRunner = interopRequireDefault( - transformer.requireAndTranspileModule(config.runner), - ).default; - const runner = new Runner(this._globalConfig, { - changedFiles: this._context?.changedFiles, - sourcesRelatedToTestsInChangedFiles: this._context - ?.sourcesRelatedToTestsInChangedFiles, - }); - testRunners[config.runner] = runner; - contextsByTestRunner.set(runner, context); - } - }); + await Promise.all( + Array.from(contexts).map(async context => { + const {config} = context; + if (!testRunners[config.runner]) { + const transformer = await createScriptTransformer(config); + const Runner: typeof TestRunner = interopRequireDefault( + transformer.requireAndTranspileModule(config.runner), + ).default; + const runner = new Runner(this._globalConfig, { + changedFiles: this._context?.changedFiles, + sourcesRelatedToTestsInChangedFiles: this._context + ?.sourcesRelatedToTestsInChangedFiles, + }); + testRunners[config.runner] = runner; + contextsByTestRunner.set(runner, context); + } + }), + ); const testsByRunner = this._partitionTests(testRunners, tests); diff --git a/packages/jest-core/src/__tests__/watchFileChanges.test.ts b/packages/jest-core/src/__tests__/watchFileChanges.test.ts index 50acc79be2cb..6da1802ce517 100644 --- a/packages/jest-core/src/__tests__/watchFileChanges.test.ts +++ b/packages/jest-core/src/__tests__/watchFileChanges.test.ts @@ -14,6 +14,7 @@ import type {AggregatedResult} from '@jest/test-result'; import {normalize} from 'jest-config'; import type HasteMap from 'jest-haste-map'; import Runtime from 'jest-runtime'; +import {interopRequireDefault} from 'jest-util'; import {JestHook} from 'jest-watcher'; describe('Watch mode flows with changed files', () => { @@ -31,8 +32,8 @@ describe('Watch mode flows with changed files', () => { const cacheDirectory = path.resolve(tmpdir(), `tmp${Math.random()}`); let hasteMapInstance: HasteMap; - beforeEach(async () => { - watch = (await import('../watch')).default; + beforeEach(() => { + watch = interopRequireDefault(require('../watch')).default; pipe = {write: jest.fn()} as unknown; stdin = new MockStdin(); rimraf.sync(cacheDirectory); diff --git a/packages/jest-core/src/runGlobalHook.ts b/packages/jest-core/src/runGlobalHook.ts index 82dc3a5c1071..900749b68b9d 100644 --- a/packages/jest-core/src/runGlobalHook.ts +++ b/packages/jest-core/src/runGlobalHook.ts @@ -7,7 +7,7 @@ import * as util from 'util'; import pEachSeries = require('p-each-series'); -import {ScriptTransformer} from '@jest/transform'; +import {createScriptTransformer} from '@jest/transform'; import type {Config} from '@jest/types'; import type {Test} from 'jest-runner'; import {interopRequireDefault} from 'jest-util'; @@ -45,7 +45,7 @@ export default async ({ : // Fallback to first config allTests[0].context.config; - const transformer = new ScriptTransformer(projectConfig); + const transformer = await createScriptTransformer(projectConfig); try { await transformer.requireAndTranspileModule(modulePath, async m => { diff --git a/packages/jest-repl/src/cli/runtime-cli.ts b/packages/jest-repl/src/cli/runtime-cli.ts index 3615ceca806b..3d8a8c871676 100644 --- a/packages/jest-repl/src/cli/runtime-cli.ts +++ b/packages/jest-repl/src/cli/runtime-cli.ts @@ -11,7 +11,7 @@ import chalk = require('chalk'); import yargs = require('yargs'); import {CustomConsole} from '@jest/console'; import type {JestEnvironment} from '@jest/environment'; -import {ScriptTransformer} from '@jest/transform'; +import {createScriptTransformer} from '@jest/transform'; import type {Config} from '@jest/types'; import {deprecationEntries, readConfig} from 'jest-config'; import Runtime from 'jest-runtime'; @@ -74,7 +74,7 @@ export async function run( watchman: globalConfig.watchman, }); - const transformer = new ScriptTransformer(config); + const transformer = await createScriptTransformer(config); const Environment: typeof JestEnvironment = interopRequireDefault( transformer.requireAndTranspileModule(config.testEnvironment), ).default; @@ -91,6 +91,7 @@ export async function run( config, environment, hasteMap.resolver, + transformer, new Map(), { changedFiles: undefined, diff --git a/packages/jest-reporters/src/CoverageWorker.ts b/packages/jest-reporters/src/CoverageWorker.ts index 0b887919e1a3..11396ac9cbe2 100644 --- a/packages/jest-reporters/src/CoverageWorker.ts +++ b/packages/jest-reporters/src/CoverageWorker.ts @@ -33,7 +33,7 @@ export function worker({ globalConfig, path, options, -}: CoverageWorkerData): CoverageWorkerResult | null { +}: CoverageWorkerData): Promise { return generateEmptyCoverage( fs.readFileSync(path, 'utf8'), path, diff --git a/packages/jest-reporters/src/__tests__/generateEmptyCoverage.test.js b/packages/jest-reporters/src/__tests__/generateEmptyCoverage.test.js index 0fcf687e1951..942152dbfab2 100644 --- a/packages/jest-reporters/src/__tests__/generateEmptyCoverage.test.js +++ b/packages/jest-reporters/src/__tests__/generateEmptyCoverage.test.js @@ -24,7 +24,7 @@ describe('generateEmptyCoverage', () => { const rootDir = __dirname; const filepath = path.join(rootDir, './sum.js'); - it('generates an empty coverage object for a file without running it', () => { + it('generates an empty coverage object for a file without running it', async () => { const src = ` throw new Error('this should not be thrown'); @@ -42,7 +42,7 @@ describe('generateEmptyCoverage', () => { shouldInstrument.mockReturnValueOnce(true); - const emptyCoverage = generateEmptyCoverage( + const emptyCoverage = await generateEmptyCoverage( src, filepath, makeGlobalConfig(), @@ -71,7 +71,7 @@ describe('generateEmptyCoverage', () => { }); }); - it('generates a null coverage result when using /* istanbul ignore file */', () => { + it('generates a null coverage result when using /* istanbul ignore file */', async () => { const src = ` /* istanbul ignore file */ const a = (b, c) => { @@ -86,7 +86,7 @@ describe('generateEmptyCoverage', () => { shouldInstrument.mockReturnValueOnce(true); - const nullCoverage = generateEmptyCoverage( + const nullCoverage = await generateEmptyCoverage( src, filepath, makeGlobalConfig(), @@ -101,7 +101,7 @@ describe('generateEmptyCoverage', () => { expect(nullCoverage).toBeNull(); }); - it('generates a null coverage result when collectCoverage global config is false', () => { + it('generates a null coverage result when collectCoverage global config is false', async () => { const src = ` const a = (b, c) => { if (b) { @@ -115,7 +115,7 @@ describe('generateEmptyCoverage', () => { shouldInstrument.mockReturnValueOnce(false); - const nullCoverage = generateEmptyCoverage( + const nullCoverage = await generateEmptyCoverage( src, filepath, makeGlobalConfig(), diff --git a/packages/jest-reporters/src/generateEmptyCoverage.ts b/packages/jest-reporters/src/generateEmptyCoverage.ts index 443293c9f782..f9e43c8099c2 100644 --- a/packages/jest-reporters/src/generateEmptyCoverage.ts +++ b/packages/jest-reporters/src/generateEmptyCoverage.ts @@ -9,7 +9,7 @@ import type {V8Coverage} from 'collect-v8-coverage'; import * as fs from 'graceful-fs'; import {FileCoverage, createFileCoverage} from 'istanbul-lib-coverage'; import {readInitialCoverage} from 'istanbul-lib-instrument'; -import {ScriptTransformer, shouldInstrument} from '@jest/transform'; +import {createScriptTransformer, shouldInstrument} from '@jest/transform'; import type {Config} from '@jest/types'; type SingleV8Coverage = V8Coverage[number]; @@ -24,14 +24,14 @@ export type CoverageWorkerResult = result: SingleV8Coverage; }; -export default function ( +export default async function ( source: string, filename: Config.Path, globalConfig: Config.GlobalConfig, config: Config.ProjectConfig, changedFiles?: Set, sourcesRelatedToTestsInChangedFiles?: Set, -): CoverageWorkerResult | null { +): Promise { const coverageOptions = { changedFiles, collectCoverage: globalConfig.collectCoverage, @@ -66,18 +66,16 @@ export default function ( }; } + const scriptTransformer = await createScriptTransformer(config); + // Transform file with instrumentation to make sure initial coverage data is well mapped to original code. - const {code} = new ScriptTransformer(config).transformSource( - filename, - source, - { - instrument: true, - supportsDynamicImport: true, - supportsExportNamespaceFrom: true, - supportsStaticESM: true, - supportsTopLevelAwait: true, - }, - ); + const {code} = scriptTransformer.transformSource(filename, source, { + instrument: true, + supportsDynamicImport: true, + supportsExportNamespaceFrom: true, + supportsStaticESM: true, + supportsTopLevelAwait: true, + }); // TODO: consider passing AST const extracted = readInitialCoverage(code); // Check extracted initial coverage is not null, this can happen when using /* istanbul ignore file */ diff --git a/packages/jest-runner/src/runTest.ts b/packages/jest-runner/src/runTest.ts index d68c7cfb7677..4ec5d9d4ca4c 100644 --- a/packages/jest-runner/src/runTest.ts +++ b/packages/jest-runner/src/runTest.ts @@ -19,7 +19,7 @@ import { } from '@jest/console'; import type {JestEnvironment} from '@jest/environment'; import type {TestResult} from '@jest/test-result'; -import {ScriptTransformer} from '@jest/transform'; +import {createScriptTransformer} from '@jest/transform'; import type {Config} from '@jest/types'; import {getTestEnvironment} from 'jest-config'; import * as docblock from 'jest-docblock'; @@ -103,7 +103,9 @@ async function runTestInternal( }); } - const transformer = new ScriptTransformer(config); + const cacheFS = new Map([[path, testSource]]); + const transformer = await createScriptTransformer(config, cacheFS); + const TestEnvironment: typeof JestEnvironment = interopRequireDefault( transformer.requireAndTranspileModule(testEnvironment), ).default; @@ -146,13 +148,13 @@ async function runTestInternal( ? new LeakDetector(environment) : null; - const cacheFS = new Map([[path, testSource]]); setGlobal(environment.global, 'console', testConsole); const runtime = new Runtime( config, environment, resolver, + transformer, cacheFS, { changedFiles: context?.changedFiles, diff --git a/packages/jest-runtime/src/__mocks__/createRuntime.js b/packages/jest-runtime/src/__mocks__/createRuntime.js index 3d8b016a607e..771204ea03da 100644 --- a/packages/jest-runtime/src/__mocks__/createRuntime.js +++ b/packages/jest-runtime/src/__mocks__/createRuntime.js @@ -8,7 +8,10 @@ import {tmpdir} from 'os'; import path from 'path'; import {makeProjectConfig} from '@jest/test-utils'; +import {createScriptTransformer} from '@jest/transform'; +import NodeEnvironment from 'jest-environment-node'; import {tryRealpath} from 'jest-util'; +import Runtime from '../'; // Copy from jest-config (since we don't want to depend on this package) const getCacheDirectory = () => { @@ -46,9 +49,6 @@ const setupTransform = (config, rootDir) => { }; module.exports = async function createRuntime(filename, config) { - const {default: NodeEnvironment} = await import('jest-environment-node'); - const {default: Runtime} = await import('../'); - const rootDir = path.resolve(path.dirname(filename), 'test_root'); const moduleNameMapper = setupModuleNameMapper(config, rootDir); @@ -90,11 +90,15 @@ module.exports = async function createRuntime(filename, config) { resetCache: false, }).build(); + const cacheFS = new Map(); + const scriptTransformer = await createScriptTransformer(config, cacheFS); + const runtime = new Runtime( config, environment, Runtime.createResolver(config, hasteMap.moduleMap), - new Map(), + scriptTransformer, + cacheFS, { changedFiles: undefined, collectCoverage: false, diff --git a/packages/jest-runtime/src/__tests__/instrumentation.test.ts b/packages/jest-runtime/src/__tests__/instrumentation.test.ts index a9de9c5ca7e3..46cb778200bf 100644 --- a/packages/jest-runtime/src/__tests__/instrumentation.test.ts +++ b/packages/jest-runtime/src/__tests__/instrumentation.test.ts @@ -9,7 +9,7 @@ import * as os from 'os'; import * as path from 'path'; import {makeGlobalConfig, makeProjectConfig} from '@jest/test-utils'; -import {ScriptTransformer} from '@jest/transform'; +import {createScriptTransformer} from '@jest/transform'; jest.mock('vm'); @@ -18,20 +18,19 @@ const FILE_PATH_TO_INSTRUMENT = path.resolve( './module_dir/to_be_instrumented.js', ); -it('instruments files', () => { +it('instruments files', async () => { const config = makeProjectConfig({ cache: false, cacheDirectory: os.tmpdir(), cwd: __dirname, rootDir: __dirname, }); - const instrumented = new ScriptTransformer(config).transform( - FILE_PATH_TO_INSTRUMENT, - { - ...makeGlobalConfig({collectCoverage: true}), - changedFiles: undefined, - }, - ); + const scriptTransformer = await createScriptTransformer(config); + + const instrumented = scriptTransformer.transform(FILE_PATH_TO_INSTRUMENT, { + ...makeGlobalConfig({collectCoverage: true}), + changedFiles: undefined, + }); // We can't really snapshot the resulting coverage, because it depends on // absolute path of the file, which will be different on different // machines diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 0183d54905b0..99e233d074d8 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -198,6 +198,7 @@ export default class Runtime { config: Config.ProjectConfig, environment: JestEnvironment, resolver: Resolver, + transformer: ScriptTransformer, cacheFS: Map, coverageOptions: ShouldInstrumentOptions, testPath: Config.Path, @@ -226,7 +227,7 @@ export default class Runtime { this._esmModuleLinkingMap = new WeakMap(); this._testPath = testPath; this._resolver = resolver; - this._scriptTransformer = new ScriptTransformer(config, this._cacheFS); + this._scriptTransformer = transformer; this._shouldAutoMock = config.automock; this._sourceMapRegistry = new Map(); this._fileTransforms = new Map(); diff --git a/packages/jest-transform/src/ScriptTransformer.ts b/packages/jest-transform/src/ScriptTransformer.ts index 9e5704517000..ae4f6fc9ddf4 100644 --- a/packages/jest-transform/src/ScriptTransformer.ts +++ b/packages/jest-transform/src/ScriptTransformer.ts @@ -7,6 +7,7 @@ import {createHash} from 'crypto'; import * as path from 'path'; +import {pathToFileURL} from 'url'; 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'; @@ -63,7 +64,7 @@ async function waitForPromiseWithCleanup( } } -export default class ScriptTransformer { +class ScriptTransformer { private readonly _cache: ProjectCache; private readonly _cacheFS: StringMap; private readonly _config: Config.ProjectConfig; @@ -71,7 +72,7 @@ export default class ScriptTransformer { Config.Path, {transformer: Transformer; transformerConfig: unknown} >; - private readonly _transformConfigCache: Map; + private _transformsAreLoaded = false; constructor( config: Config.ProjectConfig, @@ -80,7 +81,6 @@ export default class ScriptTransformer { this._config = config; this._cacheFS = cacheFS; this._transformCache = new Map(); - this._transformConfigCache = new Map(); const configString = stableStringify(this._config); let projectCache = projectCaches.get(configString); @@ -165,18 +165,69 @@ export default class ScriptTransformer { for (let i = 0; i < transformRegExp.length; i++) { if (transformRegExp[i][0].test(filename)) { - const transformPath = transformRegExp[i][1]; - this._transformConfigCache.set(transformPath, transformRegExp[i][2]); - - return transformPath; + return transformRegExp[i][1]; } } return undefined; } + async loadTransformers(): Promise { + await Promise.all( + this._config.transform.map( + async ([, transformPath, transformerConfig]) => { + let transformer: Transformer; + + try { + transformer = require(transformPath); + } catch (error) { + if (error.code === 'ERR_REQUIRE_ESM') { + const configUrl = pathToFileURL(transformPath); + + // node `import()` supports URL, but TypeScript doesn't know that + const importedConfig = await import(configUrl.href); + + if (!importedConfig.default) { + throw new Error( + `Jest: Failed to load ESM transformer at ${transformPath} - did you use a default export?`, + ); + } + + transformer = importedConfig.default; + } else { + throw error; + } + } + + if (!transformer) { + throw new TypeError('Jest: a transform must export something.'); + } + if (typeof transformer.createTransformer === 'function') { + transformer = transformer.createTransformer(transformerConfig); + } + if (typeof transformer.process !== 'function') { + throw new TypeError( + 'Jest: a transform must export a `process` function.', + ); + } + + const res = {transformer, transformerConfig}; + this._transformCache.set(transformPath, res); + }, + ), + ); + + this._transformsAreLoaded = true; + } + private _getTransformer(filename: Config.Path) { - if (!this._config.transform || !this._config.transform.length) { + if (!this._transformsAreLoaded) { + throw new Error( + 'Jest: Transformers have not been loaded yet - make sure to run `loadTransformers` and wait for it to complete before starting to transform files', + ); + } + + if (this._config.transform.length === 0) { return null; } @@ -191,25 +242,9 @@ export default class ScriptTransformer { return cached; } - let transformer: Transformer = require(transformPath); - - if (!transformer) { - throw new TypeError('Jest: a transform must export something.'); - } - const transformerConfig = - this._transformConfigCache.get(transformPath) || {}; - if (typeof transformer.createTransformer === 'function') { - transformer = transformer.createTransformer(transformerConfig); - } - if (typeof transformer.process !== 'function') { - throw new TypeError( - 'Jest: a transform must export a `process` function.', - ); - } - const res = {transformer, transformerConfig}; - this._transformCache.set(transformPath, res); - - return res; + throw new Error( + `Jest was unable to load the transformer defined for ${filename}. This is a bug in Jest, please open up an issue`, + ); } private _instrumentFile( @@ -257,12 +292,6 @@ export default class ScriptTransformer { return input; } - // We don't want to expose transformers to the outside - this function is just - // to warm up `this._transformCache` - preloadTransformer(filepath: Config.Path): void { - this._getTransformer(filepath); - } - transformSource( filepath: Config.Path, content: string, @@ -504,10 +533,6 @@ export default class ScriptTransformer { supportsTopLevelAwait: false, }, ): ModuleType | Promise { - // Load the transformer to avoid a cycle where we need to load a - // transformer in order to transform it in the require hooks - this.preloadTransformer(moduleName); - let transforming = false; const revertHook = addHook( (code, filename) => { @@ -566,13 +591,15 @@ export default class ScriptTransformer { } // TODO: do we need to define the generics twice? -export function createTranspilingRequire( +export async function createTranspilingRequire( config: Config.ProjectConfig, -): ( - resolverPath: string, - applyInteropRequireDefault?: boolean, -) => TModuleType { - const transformer = new ScriptTransformer(config); +): Promise< + ( + resolverPath: string, + applyInteropRequireDefault?: boolean, + ) => TModuleType +> { + const transformer = await createScriptTransformer(config); return function requireAndTranspileModule( resolverPath: string, @@ -731,3 +758,16 @@ const calcTransformRegExp = (config: Config.ProjectConfig) => { return transformRegexp; }; + +export type TransformerType = ScriptTransformer; + +export async function createScriptTransformer( + config: Config.ProjectConfig, + cacheFS = new Map(), +): Promise { + const transformer = new ScriptTransformer(config, cacheFS); + + await transformer.loadTransformers(); + + return transformer; +} diff --git a/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts b/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts index 88f4faddc9d7..6c6de830749d 100644 --- a/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts +++ b/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts @@ -153,7 +153,7 @@ const getCachePath = ( return null; }; -let ScriptTransformer: typeof import('../ScriptTransformer').default; +let createScriptTransformer: typeof import('../ScriptTransformer').createScriptTransformer; let config: Config.ProjectConfig; let fs: typeof import('fs'); let mockFs: Record; @@ -228,14 +228,15 @@ describe('ScriptTransformer', () => { transformIgnorePatterns: ['/node_modules/'], }); - ScriptTransformer = require('../ScriptTransformer').default; + createScriptTransformer = require('../ScriptTransformer') + .createScriptTransformer; }; beforeEach(reset); afterEach(() => jest.unmock('../shouldInstrument')); - it('transforms a file properly', () => { - const scriptTransformer = new ScriptTransformer(config); + it('transforms a file properly', async () => { + const scriptTransformer = await createScriptTransformer(config); const transformedBananaWithCoverage = scriptTransformer.transform( '/fruits/banana.js', getCoverageOptions({collectCoverage: true}), @@ -278,11 +279,11 @@ describe('ScriptTransformer', () => { ); }); - it('does not transform Node core modules', () => { + it('does not transform Node core modules', async () => { jest.mock('../shouldInstrument'); const shouldInstrument = require('../shouldInstrument').default; - const scriptTransformer = new ScriptTransformer(config); + const scriptTransformer = await createScriptTransformer(config); const fsSourceCode = 'muaha, fake source!'; const response = scriptTransformer.transform( @@ -297,61 +298,52 @@ describe('ScriptTransformer', () => { expect(shouldInstrument).toHaveBeenCalledTimes(0); }); - it( - "throws an error if `process` doesn't return a string or an object" + - 'containing `code` key with processed string', - () => { - config = { - ...config, - transform: [['\\.js$', 'passthrough-preprocessor', {}]], - }; - const scriptTransformer = new ScriptTransformer(config); - - const incorrectReturnValues = [ - [undefined, '/fruits/banana.js'], - [{a: 'a'}, '/fruits/kiwi.js'], - [[], '/fruits/grapefruit.js'], - ]; - - incorrectReturnValues.forEach(([returnValue, filePath]) => { - invariant(typeof filePath === 'string'); - require('passthrough-preprocessor').process.mockReturnValue( - returnValue, - ); - expect(() => - scriptTransformer.transform(filePath, getCoverageOptions()), - ).toThrow('must return a string'); - }); - - const correctReturnValues = [ - ['code', '/fruits/banana.js'], - [{code: 'code'}, '/fruits/kiwi.js'], - ]; - - correctReturnValues.forEach(([returnValue, filePath]) => { - invariant(typeof filePath === 'string'); - require('passthrough-preprocessor').process.mockReturnValue( - returnValue, - ); - expect(() => - scriptTransformer.transform(filePath, getCoverageOptions()), - ).not.toThrow(); - }); - }, - ); + it("throws an error if `process` doesn't return a string or an object containing `code` key with processed string", async () => { + config = { + ...config, + transform: [['\\.js$', 'passthrough-preprocessor', {}]], + }; + const scriptTransformer = await createScriptTransformer(config); + + const incorrectReturnValues = [ + [undefined, '/fruits/banana.js'], + [{a: 'a'}, '/fruits/kiwi.js'], + [[], '/fruits/grapefruit.js'], + ]; + + incorrectReturnValues.forEach(([returnValue, filePath]) => { + invariant(typeof filePath === 'string'); + require('passthrough-preprocessor').process.mockReturnValue(returnValue); + expect(() => + scriptTransformer.transform(filePath, getCoverageOptions()), + ).toThrow('must return a string'); + }); + + const correctReturnValues = [ + ['code', '/fruits/banana.js'], + [{code: 'code'}, '/fruits/kiwi.js'], + ]; + + correctReturnValues.forEach(([returnValue, filePath]) => { + invariant(typeof filePath === 'string'); + require('passthrough-preprocessor').process.mockReturnValue(returnValue); + expect(() => + scriptTransformer.transform(filePath, getCoverageOptions()), + ).not.toThrow(); + }); + }); - it("throws an error if `process` doesn't defined", () => { + it("throws an error if `process` isn't defined", async () => { config = { ...config, transform: [['\\.js$', 'skipped-required-props-preprocessor', {}]], }; - const scriptTransformer = new ScriptTransformer(config); - expect(() => - scriptTransformer.transformSource('sample.js', '', {instrument: false}), - ).toThrow('Jest: a transform must export a `process` function.'); + await expect(() => createScriptTransformer(config)).rejects.toThrow( + 'Jest: a transform must export a `process` function.', + ); }); - it('throws an error if createTransformer returns object without `process` method', () => { + it('throws an error if createTransformer returns object without `process` method', async () => { config = { ...config, transform: [ @@ -362,26 +354,25 @@ describe('ScriptTransformer', () => { ], ], }; - const scriptTransformer = new ScriptTransformer(config); - expect(() => - scriptTransformer.transformSource('sample.js', '', {instrument: false}), - ).toThrow('Jest: a transform must export a `process` function.'); + await expect(() => createScriptTransformer(config)).rejects.toThrow( + 'Jest: a transform must export a `process` function.', + ); }); - it("shouldn't throw error without process method. But with corrent createTransformer method", () => { + it("shouldn't throw error without process method. But with corrent createTransformer method", async () => { config = { ...config, transform: [['\\.js$', 'skipped-process-method-preprocessor', {}]], }; - const scriptTransformer = new ScriptTransformer(config); + const scriptTransformer = await createScriptTransformer(config); expect(() => scriptTransformer.transformSource('sample.js', '', {instrument: false}), ).not.toThrow(); }); - it('uses the supplied preprocessor', () => { + it('uses the supplied preprocessor', async () => { config = {...config, transform: [['\\.js$', 'test_preprocessor', {}]]}; - const scriptTransformer = new ScriptTransformer(config); + const scriptTransformer = await createScriptTransformer(config); const res1 = scriptTransformer.transform( '/fruits/banana.js', getCoverageOptions(), @@ -399,7 +390,7 @@ describe('ScriptTransformer', () => { expect(wrap(res2.code)).toMatchSnapshot(); }); - it('uses multiple preprocessors', () => { + it('uses multiple preprocessors', async () => { config = { ...config, transform: [ @@ -407,7 +398,7 @@ describe('ScriptTransformer', () => { ['\\.css$', 'css-preprocessor', {}], ], }; - const scriptTransformer = new ScriptTransformer(config); + const scriptTransformer = await createScriptTransformer(config); const res1 = scriptTransformer.transform( '/fruits/banana.js', @@ -431,12 +422,12 @@ describe('ScriptTransformer', () => { expect(wrap(res3.code)).toMatchSnapshot(); }); - it('writes source map if preprocessor supplies it', () => { + it('writes source map if preprocessor supplies it', async () => { config = { ...config, transform: [['\\.js$', 'preprocessor-with-sourcemaps', {}]], }; - const scriptTransformer = new ScriptTransformer(config); + const scriptTransformer = await createScriptTransformer(config); const map = { mappings: ';AAAA', @@ -461,12 +452,12 @@ describe('ScriptTransformer', () => { }); }); - it('writes source map if preprocessor inlines it', () => { + it('writes source map if preprocessor inlines it', async () => { config = { ...config, transform: [['\\.js$', 'preprocessor-with-sourcemaps', {}]], }; - const scriptTransformer = new ScriptTransformer(config); + const scriptTransformer = await createScriptTransformer(config); const sourceMap = JSON.stringify({ mappings: 'AAAA,IAAM,CAAC,GAAW,CAAC,CAAC', @@ -493,7 +484,7 @@ describe('ScriptTransformer', () => { ); }); - it('warns of unparseable inlined source maps from the preprocessor', () => { + it('warns of unparseable inlined source maps from the preprocessor', async () => { const warn = console.warn; console.warn = jest.fn(); @@ -501,7 +492,7 @@ describe('ScriptTransformer', () => { ...config, transform: [['\\.js$', 'preprocessor-with-sourcemaps', {}]], }; - const scriptTransformer = new ScriptTransformer(config); + const scriptTransformer = await createScriptTransformer(config); const sourceMap = JSON.stringify({ mappings: 'AAAA,IAAM,CAAC,GAAW,CAAC,CAAC', @@ -528,12 +519,12 @@ describe('ScriptTransformer', () => { console.warn = warn; }); - it('writes source maps if given by the transformer', () => { + it('writes source maps if given by the transformer', async () => { config = { ...config, transform: [['\\.js$', 'preprocessor-with-sourcemaps', {}]], }; - const scriptTransformer = new ScriptTransformer(config); + const scriptTransformer = await createScriptTransformer(config); const map = { mappings: ';AAAA', @@ -561,12 +552,12 @@ describe('ScriptTransformer', () => { ); }); - it('does not write source map if not given by the transformer', () => { + it('does not write source map if not given by the transformer', async () => { config = { ...config, transform: [['\\.js$', 'preprocessor-with-sourcemaps', {}]], }; - const scriptTransformer = new ScriptTransformer(config); + const scriptTransformer = await createScriptTransformer(config); require('preprocessor-with-sourcemaps').process.mockReturnValue({ code: 'content', @@ -581,12 +572,12 @@ describe('ScriptTransformer', () => { expect(writeFileAtomic.sync).toHaveBeenCalledTimes(1); }); - it('should write a source map for the instrumented file when transformed', () => { + it('should write a source map for the instrumented file when transformed', async () => { const transformerConfig: Config.ProjectConfig = { ...config, transform: [['\\.js$', 'preprocessor-with-sourcemaps', {}]], }; - const scriptTransformer = new ScriptTransformer(transformerConfig); + const scriptTransformer = await createScriptTransformer(transformerConfig); const map = { mappings: ';AAAA', @@ -626,8 +617,8 @@ describe('ScriptTransformer', () => { expect(result.code).toContain('//# sourceMappingURL'); }); - it('should write a source map for the instrumented file when not transformed', () => { - const scriptTransformer = new ScriptTransformer(config); + it('should write a source map for the instrumented file when not transformed', async () => { + const scriptTransformer = await createScriptTransformer(config); // A map from the original source to the instrumented output /* eslint-disable sort-keys */ @@ -662,12 +653,12 @@ describe('ScriptTransformer', () => { expect(result.code).toContain('//# sourceMappingURL'); }); - it('passes expected transform options to getCacheKey', () => { + it('passes expected transform options to getCacheKey', async () => { config = { ...config, transform: [['\\.js$', 'test_preprocessor', {configKey: 'configValue'}]], }; - const scriptTransformer = new ScriptTransformer(config); + const scriptTransformer = await createScriptTransformer(config); scriptTransformer.transform( '/fruits/banana.js', @@ -678,12 +669,12 @@ describe('ScriptTransformer', () => { expect(getCacheKey).toMatchSnapshot(); }); - it('creates transformer with config', () => { + it('creates transformer with config', async () => { const transformerConfig = {}; config = Object.assign(config, { transform: [['\\.js$', 'configureable-preprocessor', transformerConfig]], }); - const scriptTransformer = new ScriptTransformer(config); + const scriptTransformer = await createScriptTransformer(config); scriptTransformer.transform('/fruits/banana.js', {}); expect( @@ -691,12 +682,12 @@ describe('ScriptTransformer', () => { ).toHaveBeenCalledWith(transformerConfig); }); - it('reads values from the cache', () => { + it('reads values from the cache', async () => { const transformConfig: Config.ProjectConfig = { ...config, transform: [['\\.js$', 'test_preprocessor', {}]], }; - let scriptTransformer = new ScriptTransformer(transformConfig); + let scriptTransformer = await createScriptTransformer(transformConfig); scriptTransformer.transform('/fruits/banana.js', getCoverageOptions()); const cachePath = getCachePath(mockFs, config); @@ -710,7 +701,7 @@ describe('ScriptTransformer', () => { // Restore the cached fs mockFs = mockFsCopy; - scriptTransformer = new ScriptTransformer(transformConfig); + scriptTransformer = await createScriptTransformer(transformConfig); scriptTransformer.transform('/fruits/banana.js', getCoverageOptions()); expect(fs.readFileSync).toHaveBeenCalledTimes(2); @@ -723,7 +714,7 @@ describe('ScriptTransformer', () => { reset(); mockFs = mockFsCopy; transformConfig.cache = false; - scriptTransformer = new ScriptTransformer(transformConfig); + scriptTransformer = await createScriptTransformer(transformConfig); scriptTransformer.transform('/fruits/banana.js', getCoverageOptions()); expect(fs.readFileSync).toHaveBeenCalledTimes(1); @@ -732,12 +723,12 @@ describe('ScriptTransformer', () => { expect(writeFileAtomic.sync).toBeCalled(); }); - it('reads values from the cache when the file contains colons', () => { + it('reads values from the cache when the file contains colons', async () => { const transformConfig: Config.ProjectConfig = { ...config, transform: [['\\.js$', 'test_preprocessor', {}]], }; - let scriptTransformer = new ScriptTransformer(transformConfig); + let scriptTransformer = await createScriptTransformer(transformConfig); scriptTransformer.transform( '/fruits/banana:colon.js', getCoverageOptions(), @@ -754,7 +745,7 @@ describe('ScriptTransformer', () => { // Restore the cached fs mockFs = mockFsCopy; - scriptTransformer = new ScriptTransformer(transformConfig); + scriptTransformer = await createScriptTransformer(transformConfig); scriptTransformer.transform('/fruits/banana:colon.js', {}); expect(fs.readFileSync).toHaveBeenCalledTimes(2); @@ -763,10 +754,10 @@ describe('ScriptTransformer', () => { expect(writeFileAtomic.sync).not.toBeCalled(); }); - it('should reuse the value from in-memory cache which is set by custom transformer', () => { + it('should reuse the value from in-memory cache which is set by custom transformer', async () => { const cacheFS = new Map(); const testPreprocessor = require('cache_fs_preprocessor'); - const scriptTransformer = new ScriptTransformer( + const scriptTransformer = await createScriptTransformer( { ...config, transform: [['\\.js$', 'cache_fs_preprocessor', {}]], @@ -788,15 +779,15 @@ describe('ScriptTransformer', () => { expect(fs.readFileSync).toBeCalledWith(fileName1, 'utf8'); }); - it('does not reuse the in-memory cache between different projects', () => { - const scriptTransformer = new ScriptTransformer({ + it('does not reuse the in-memory cache between different projects', async () => { + const scriptTransformer = await createScriptTransformer({ ...config, transform: [['\\.js$', 'test_preprocessor', {}]], }); scriptTransformer.transform('/fruits/banana.js', getCoverageOptions()); - const anotherScriptTransformer = new ScriptTransformer({ + const anotherScriptTransformer = await createScriptTransformer({ ...config, transform: [['\\.js$', 'css-preprocessor', {}]], }); @@ -810,18 +801,12 @@ describe('ScriptTransformer', () => { expect(fs.readFileSync).toBeCalledWith('/fruits/banana.js', 'utf8'); }); - it('preload transformer when using `preloadTransformer`', () => { - const scriptTransformer = new ScriptTransformer({ + it('preload transformer when using `createScriptTransformer`', async () => { + const scriptTransformer = await createScriptTransformer({ ...config, transform: [['\\.js$', 'test_preprocessor', {}]], }); - expect(Array.from(scriptTransformer._transformCache.entries())).toEqual([]); - - expect( - scriptTransformer.preloadTransformer('/fruits/banana.js'), - ).toBeUndefined(); - expect(Array.from(scriptTransformer._transformCache.entries())).toEqual([ ['test_preprocessor', expect.any(Object)], ]); diff --git a/packages/jest-transform/src/index.ts b/packages/jest-transform/src/index.ts index 3695baded523..471612ffb6c4 100644 --- a/packages/jest-transform/src/index.ts +++ b/packages/jest-transform/src/index.ts @@ -6,9 +6,10 @@ */ export { - default as ScriptTransformer, + createScriptTransformer, createTranspilingRequire, } from './ScriptTransformer'; +export type {TransformerType as ScriptTransformer} from './ScriptTransformer'; export {default as shouldInstrument} from './shouldInstrument'; export type { CallerTransformOptions, diff --git a/yarn.lock b/yarn.lock index c44e1b9c4655..c8c48b1c3ad1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -475,7 +475,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-proposal-dynamic-import@npm:^7.13.8, @babel/plugin-proposal-dynamic-import@npm:^7.8.3": +"@babel/plugin-proposal-dynamic-import@npm:^7.13.8": version: 7.13.8 resolution: "@babel/plugin-proposal-dynamic-import@npm:7.13.8" dependencies: @@ -1805,8 +1805,6 @@ __metadata: dependencies: "@babel/core": ^7.3.4 "@babel/plugin-proposal-class-properties": ^7.3.4 - "@babel/plugin-proposal-dynamic-import": ^7.8.3 - "@babel/plugin-syntax-dynamic-import": ^7.8.3 "@babel/plugin-transform-modules-commonjs": ^7.1.0 "@babel/plugin-transform-strict-mode": ^7.0.0 "@babel/preset-env": ^7.1.0 From 59e3820942ed9917b65babe997637f7aff884afb Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 6 Mar 2021 16:32:53 +0100 Subject: [PATCH 2/9] changelog link --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1605cba43699..7053c1b579d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,8 @@ - `[jest-reporters]` Add static filepath property to all reporters ([#11015](https://github.com/facebook/jest/pull/11015)) - `[jest-snapshot]` [**BREAKING**] Make prettier optional for inline snapshots - fall back to string replacement ([#7792](https://github.com/facebook/jest/pull/7792)) - `[jest-transform]` Pass config options defined in Jest's config to transformer's `process` and `getCacheKey` functions ([#10926](https://github.com/facebook/jest/pull/10926)) -- `[jest-transform]` Add support for transformers written in ESM -- `[jest-transform]` [**BREAKING**] Do not export `ScriptTransformer` class, instead export the async function `createScriptTransformer` +- `[jest-transform]` Add support for transformers written in ESM ([#11163](https://github.com/facebook/jest/pull/7792)) +- `[jest-transform]` [**BREAKING**] Do not export `ScriptTransformer` class, instead export the async function `createScriptTransformer` ([#11163](https://github.com/facebook/jest/pull/11163)) - `[jest-worker]` Add support for custom task queues and adds a `PriorityQueue` implementation. ([#10921](https://github.com/facebook/jest/pull/10921)) - `[jest-worker]` Add in-order scheduling policy to jest worker ([10902](https://github.com/facebook/jest/pull/10902)) From 00617937ceec6ada6b814e48856eec891913b14e Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 6 Mar 2021 16:34:14 +0100 Subject: [PATCH 3/9] pass url to createRequire --- e2e/transform/esm-transformer/my-transform.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/transform/esm-transformer/my-transform.mjs b/e2e/transform/esm-transformer/my-transform.mjs index 0010375177f0..241e27ee29db 100644 --- a/e2e/transform/esm-transformer/my-transform.mjs +++ b/e2e/transform/esm-transformer/my-transform.mjs @@ -6,10 +6,10 @@ */ import {createRequire} from 'module'; -import {fileURLToPath} from 'url'; -const thisFile = fileURLToPath(import.meta.url); -const fileToTransform = createRequire(thisFile).resolve('./module') +const require = createRequire(import.meta.url); + +const fileToTransform = require.resolve('./module'); export default { process(src, filepath) { From 476cb116ac0c822f10dfbcd32e3e3b7248105895 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 6 Mar 2021 16:47:04 +0100 Subject: [PATCH 4/9] fix test using import --- packages/jest-create-cache-key-function/package.json | 3 ++- .../src/__tests__/index.test.ts | 7 +++++-- packages/jest-create-cache-key-function/tsconfig.json | 4 +--- yarn.lock | 1 + 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/jest-create-cache-key-function/package.json b/packages/jest-create-cache-key-function/package.json index 2f40f1f63955..54927a1da477 100644 --- a/packages/jest-create-cache-key-function/package.json +++ b/packages/jest-create-cache-key-function/package.json @@ -10,7 +10,8 @@ "@jest/types": "^27.0.0-next.3" }, "devDependencies": { - "@types/node": "*" + "@types/node": "*", + "jest-util": "^27.0.0-next.3" }, "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" diff --git a/packages/jest-create-cache-key-function/src/__tests__/index.test.ts b/packages/jest-create-cache-key-function/src/__tests__/index.test.ts index dfe9cea4b54e..09e434634ff6 100644 --- a/packages/jest-create-cache-key-function/src/__tests__/index.test.ts +++ b/packages/jest-create-cache-key-function/src/__tests__/index.test.ts @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import {interopRequireDefault} from 'jest-util'; + let NODE_ENV: string; let BABEL_ENV: string; @@ -20,8 +22,9 @@ afterEach(() => { process.env.BABEL_ENV = BABEL_ENV; }); -test('creation of a cache key', async () => { - const createCacheKeyFunction = (await import('../index')).default; +test('creation of a cache key', () => { + const createCacheKeyFunction = interopRequireDefault(require('../index')) + .default; const createCacheKey = createCacheKeyFunction([], ['value']); const hashA = createCacheKey('test', 'test.js', null, { config: {}, diff --git a/packages/jest-create-cache-key-function/tsconfig.json b/packages/jest-create-cache-key-function/tsconfig.json index 25e34a0f4511..01b609f62df5 100644 --- a/packages/jest-create-cache-key-function/tsconfig.json +++ b/packages/jest-create-cache-key-function/tsconfig.json @@ -4,7 +4,5 @@ "rootDir": "src", "outDir": "build" }, - "references": [ - {"path": "../jest-types"} - ] + "references": [{"path": "../jest-types"}, {"path": "../jest-utils"}] } diff --git a/yarn.lock b/yarn.lock index c8c48b1c3ad1..151436ff10a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1749,6 +1749,7 @@ __metadata: dependencies: "@jest/types": ^27.0.0-next.3 "@types/node": "*" + jest-util: ^27.0.0-next.3 languageName: unknown linkType: soft From e4a277ab3c38565fa3f71607d553597adae1ecd2 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 6 Mar 2021 16:55:27 +0100 Subject: [PATCH 5/9] skip new test on older nodes --- e2e/__tests__/transform.test.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/e2e/__tests__/transform.test.ts b/e2e/__tests__/transform.test.ts index 3ec924150158..8fc3cef8612b 100644 --- a/e2e/__tests__/transform.test.ts +++ b/e2e/__tests__/transform.test.ts @@ -8,6 +8,7 @@ import {tmpdir} from 'os'; import * as path from 'path'; import {wrap} from 'jest-snapshot-serializer-raw'; +import {onNodeVersions} from '@jest/test-utils'; import { cleanup, copyDir, @@ -239,13 +240,15 @@ describe('transform-testrunner', () => { }); }); -describe('esm-transformer', () => { - const dir = path.resolve(__dirname, '../transform/esm-transformer'); +onNodeVersions('^12.17.0 || >=13.2.0', () => { + describe('esm-transformer', () => { + const dir = path.resolve(__dirname, '../transform/esm-transformer'); - it('should transform with transformer written in ESM', () => { - const {json, stderr} = runWithJson(dir, ['--no-cache']); - expect(stderr).toMatch(/PASS/); - expect(json.success).toBe(true); - expect(json.numPassedTests).toBe(1); + it('should transform with transformer written in ESM', () => { + const {json, stderr} = runWithJson(dir, ['--no-cache']); + expect(stderr).toMatch(/PASS/); + expect(json.success).toBe(true); + expect(json.numPassedTests).toBe(1); + }); }); }); From dc64c56fb002f8dc7162c47d02c68439865aa14b Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 6 Mar 2021 16:58:26 +0100 Subject: [PATCH 6/9] typ0 --- packages/jest-create-cache-key-function/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-create-cache-key-function/tsconfig.json b/packages/jest-create-cache-key-function/tsconfig.json index 01b609f62df5..23c08b0da24e 100644 --- a/packages/jest-create-cache-key-function/tsconfig.json +++ b/packages/jest-create-cache-key-function/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "src", "outDir": "build" }, - "references": [{"path": "../jest-types"}, {"path": "../jest-utils"}] + "references": [{"path": "../jest-types"}, {"path": "../jest-util"}] } From fd1a2dbbdbaab45953238a95d302aaa02da8707e Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 7 Mar 2021 09:39:35 +0100 Subject: [PATCH 7/9] correct link in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7053c1b579d2..9f8f7905a388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ - `[jest-reporters]` Add static filepath property to all reporters ([#11015](https://github.com/facebook/jest/pull/11015)) - `[jest-snapshot]` [**BREAKING**] Make prettier optional for inline snapshots - fall back to string replacement ([#7792](https://github.com/facebook/jest/pull/7792)) - `[jest-transform]` Pass config options defined in Jest's config to transformer's `process` and `getCacheKey` functions ([#10926](https://github.com/facebook/jest/pull/10926)) -- `[jest-transform]` Add support for transformers written in ESM ([#11163](https://github.com/facebook/jest/pull/7792)) +- `[jest-transform]` Add support for transformers written in ESM ([#11163](https://github.com/facebook/jest/pull/11163)) - `[jest-transform]` [**BREAKING**] Do not export `ScriptTransformer` class, instead export the async function `createScriptTransformer` ([#11163](https://github.com/facebook/jest/pull/11163)) - `[jest-worker]` Add support for custom task queues and adds a `PriorityQueue` implementation. ([#10921](https://github.com/facebook/jest/pull/10921)) - `[jest-worker]` Add in-order scheduling policy to jest worker ([10902](https://github.com/facebook/jest/pull/10902)) From 480f2730b50e1ac5a4b498ed3f9d9317fd17d343 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 7 Mar 2021 09:44:42 +0100 Subject: [PATCH 8/9] remove some lines of code --- packages/jest-transform/src/ScriptTransformer.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/jest-transform/src/ScriptTransformer.ts b/packages/jest-transform/src/ScriptTransformer.ts index ae4f6fc9ddf4..2e65aa152214 100644 --- a/packages/jest-transform/src/ScriptTransformer.ts +++ b/packages/jest-transform/src/ScriptTransformer.ts @@ -66,8 +66,6 @@ async function waitForPromiseWithCleanup( class ScriptTransformer { private readonly _cache: ProjectCache; - private readonly _cacheFS: StringMap; - private readonly _config: Config.ProjectConfig; private readonly _transformCache: Map< Config.Path, {transformer: Transformer; transformerConfig: unknown} @@ -75,11 +73,9 @@ class ScriptTransformer { private _transformsAreLoaded = false; constructor( - config: Config.ProjectConfig, - cacheFS: StringMap = new Map(), + private readonly _config: Config.ProjectConfig, + private readonly _cacheFS: StringMap, ) { - this._config = config; - this._cacheFS = cacheFS; this._transformCache = new Map(); const configString = stableStringify(this._config); @@ -763,8 +759,8 @@ export type TransformerType = ScriptTransformer; export async function createScriptTransformer( config: Config.ProjectConfig, - cacheFS = new Map(), -): Promise { + cacheFS: StringMap = new Map(), +): Promise { const transformer = new ScriptTransformer(config, cacheFS); await transformer.loadTransformers(); From f1844a0fa5988f1898006df38fffd28366e78a2f Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 7 Mar 2021 14:49:50 +0100 Subject: [PATCH 9/9] docs --- docs/Configuration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/Configuration.md b/docs/Configuration.md index a419b32977aa..2144e81b512d 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1295,6 +1295,8 @@ _Note: a transformer is only run once per file unless the file has changed. Duri _Note: when adding additional code transformers, this will overwrite the default config and `babel-jest` is no longer automatically loaded. If you want to use it to compile JavaScript or Typescript, it has to be explicitly defined by adding `{"\\.[jt]sx?$": "babel-jest"}` to the transform property. See [babel-jest plugin](https://github.com/facebook/jest/tree/master/packages/babel-jest#setup)_ +A transformer must be an object with at least a `process` function, and it's also recommended to include a `getCacheKey` function. If your transformer is written in ESM you should have a default export with that object. + ### `transformIgnorePatterns` [array\] Default: `["/node_modules/", "\\.pnp\\.[^\\\/]+$"]`