diff --git a/CHANGELOG.md b/CHANGELOG.md index 60842ef395ff..8bddfa129c71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - `[jest-runner]` [**BREAKING**] Run transforms over `runnner` ([#8823](https://github.com/facebook/jest/pull/8823)) - `[jest-runner]` [**BREAKING**] Run transforms over `testRunnner` ([#8823](https://github.com/facebook/jest/pull/8823)) - `[jest-runtime, jest-transform]` share `cacheFS` between runtime and transformer ([#10901](https://github.com/facebook/jest/pull/10901)) +- `[jest-runtime]` Support for async code transformations ([#11191](https://github.com/facebook/jest/pull/11191)) - `[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)) diff --git a/docs/Configuration.md b/docs/Configuration.md index 9e4b813ce44d..d1eaa4c69fe9 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1299,6 +1299,8 @@ _Note: when adding additional code transformers, this will overwrite the default 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. +If the tests are written using [native ESM](ECMAScriptModules.md) the transformer can export `processAsync` and `getCacheKeyAsync` instead or in addition to the synchronous variants. + ### `transformIgnorePatterns` \[array<string>] Default: `["/node_modules/", "\\.pnp\\.[^\\\/]+$"]` diff --git a/e2e/__tests__/transform.test.ts b/e2e/__tests__/transform.test.ts index 8fc3cef8612b..a325a7a4e7f9 100644 --- a/e2e/__tests__/transform.test.ts +++ b/e2e/__tests__/transform.test.ts @@ -251,4 +251,17 @@ onNodeVersions('^12.17.0 || >=13.2.0', () => { expect(json.numPassedTests).toBe(1); }); }); + + describe('async-transformer', () => { + const dir = path.resolve(__dirname, '../transform/async-transformer'); + + it('should transform with transformer with only async transforms', () => { + const {json, stderr} = runWithJson(dir, ['--no-cache'], { + nodeOptions: '--experimental-vm-modules', + }); + expect(stderr).toMatch(/PASS/); + expect(json.success).toBe(true); + expect(json.numPassedTests).toBe(1); + }); + }); }); diff --git a/e2e/transform/async-transformer/__tests__/test.js b/e2e/transform/async-transformer/__tests__/test.js new file mode 100644 index 000000000000..1acbcde96c4e --- /dev/null +++ b/e2e/transform/async-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. + */ + +import m from '../module-under-test'; + +test('ESM transformer intercepts', () => { + expect(m).toEqual(42); +}); diff --git a/e2e/transform/async-transformer/module-under-test.js b/e2e/transform/async-transformer/module-under-test.js new file mode 100644 index 000000000000..2d892012f8fe --- /dev/null +++ b/e2e/transform/async-transformer/module-under-test.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. + */ + +export default 'It was not transformed!!'; diff --git a/e2e/transform/async-transformer/my-transform.cjs b/e2e/transform/async-transformer/my-transform.cjs new file mode 100644 index 000000000000..2c226c9e565c --- /dev/null +++ b/e2e/transform/async-transformer/my-transform.cjs @@ -0,0 +1,20 @@ +/** + * 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. + */ + +'use strict'; + +const fileToTransform = require.resolve('./module-under-test'); + +module.exports = { + async processAsync(src, filepath) { + if (filepath !== fileToTransform) { + throw new Error(`Unsupported filepath ${filepath}`); + } + + return 'export default 42;'; + }, +}; diff --git a/e2e/transform/async-transformer/package.json b/e2e/transform/async-transformer/package.json new file mode 100644 index 000000000000..b4477b1a9603 --- /dev/null +++ b/e2e/transform/async-transformer/package.json @@ -0,0 +1,9 @@ +{ + "type": "module", + "jest": { + "testEnvironment": "node", + "transform": { + "module-under-test\\.js$": "/my-transform.cjs" + } + } +} diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 1c592bb5d713..2494d17976a0 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -36,6 +36,7 @@ import { CallerTransformOptions, ScriptTransformer, ShouldInstrumentOptions, + TransformResult, TransformationOptions, handlePotentialSyntaxError, shouldInstrument, @@ -389,7 +390,7 @@ export default class Runtime { return core; } - const transformedCode = this.transformFile(modulePath, { + const transformedCode = await this.transformFileAsync(modulePath, { isInternalModule: false, supportsDynamicImport: true, supportsExportNamespaceFrom: true, @@ -1182,7 +1183,50 @@ export default class Runtime { return source; } - const transformedFile = this._scriptTransformer.transform( + let transformedFile: TransformResult | undefined = this._fileTransforms.get( + filename, + ); + + if (transformedFile) { + return transformedFile.code; + } + + transformedFile = this._scriptTransformer.transform( + filename, + this._getFullTransformationOptions(options), + source, + ); + + this._fileTransforms.set(filename, { + ...transformedFile, + wrapperLength: this.constructModuleWrapperStart().length, + }); + + if (transformedFile.sourceMapPath) { + this._sourceMapRegistry.set(filename, transformedFile.sourceMapPath); + } + return transformedFile.code; + } + + private async transformFileAsync( + filename: string, + options?: InternalModuleOptions, + ): Promise { + const source = this.readFile(filename); + + if (options?.isInternalModule) { + return source; + } + + let transformedFile: TransformResult | undefined = this._fileTransforms.get( + filename, + ); + + if (transformedFile) { + return transformedFile.code; + } + + transformedFile = await this._scriptTransformer.transformAsync( filename, this._getFullTransformationOptions(options), source, diff --git a/packages/jest-transform/src/types.ts b/packages/jest-transform/src/types.ts index 972dfda3b440..f2e5d44b27cf 100644 --- a/packages/jest-transform/src/types.ts +++ b/packages/jest-transform/src/types.ts @@ -65,7 +65,7 @@ export interface TransformOptions export interface SyncTransformer { canInstrument?: boolean; - createTransformer?: (options?: OptionType) => SyncTransformer; + createTransformer?: (options?: OptionType) => SyncTransformer; getCacheKey?: ( sourceText: string, @@ -76,7 +76,7 @@ export interface SyncTransformer { getCacheKeyAsync?: ( sourceText: string, sourcePath: Config.Path, - options: TransformOptions, + options: TransformOptions, ) => Promise; process: ( @@ -88,37 +88,39 @@ export interface SyncTransformer { processAsync?: ( sourceText: string, sourcePath: Config.Path, - options?: TransformOptions, + options: TransformOptions, ) => Promise; } export interface AsyncTransformer { canInstrument?: boolean; - createTransformer?: (options?: OptionType) => AsyncTransformer; + createTransformer?: (options?: OptionType) => AsyncTransformer; getCacheKey?: ( sourceText: string, sourcePath: Config.Path, - options: TransformOptions, + options: TransformOptions, ) => string; getCacheKeyAsync?: ( sourceText: string, sourcePath: Config.Path, - options: TransformOptions, + options: TransformOptions, ) => Promise; process?: ( sourceText: string, sourcePath: Config.Path, - options?: TransformOptions, + options: TransformOptions, ) => TransformedSource; processAsync: ( sourceText: string, sourcePath: Config.Path, - options?: TransformOptions, + options: TransformOptions, ) => Promise; } -export type Transformer = SyncTransformer | AsyncTransformer; +export type Transformer = + | SyncTransformer + | AsyncTransformer;