diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cef7f40d44a..1eb04b1638e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ - `[jest-runner]` [**BREAKING**] set exit code to 1 if test logs after teardown ([#10728](https://github.com/facebook/jest/pull/10728)) - `[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]` Support for async code transformations ([#11191](https://github.com/facebook/jest/pull/11191)) +- `[jest-runtime]` Support for async code transformations ([#11191](https://github.com/facebook/jest/pull/11191) & [#11220](https://github.com/facebook/jest/pull/11220)) - `[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/e2e/__tests__/transform.test.ts b/e2e/__tests__/transform.test.ts index e58da7e802b2..d64ccbfc8ed2 100644 --- a/e2e/__tests__/transform.test.ts +++ b/e2e/__tests__/transform.test.ts @@ -261,7 +261,7 @@ onNodeVersions('^12.17.0 || >=13.2.0', () => { }); expect(stderr).toMatch(/PASS/); expect(json.success).toBe(true); - expect(json.numPassedTests).toBe(1); + expect(json.numPassedTests).toBe(2); }); }); diff --git a/e2e/transform/async-transformer/__tests__/test.js b/e2e/transform/async-transformer/__tests__/test.js index 1acbcde96c4e..b3ac5dab1b19 100644 --- a/e2e/transform/async-transformer/__tests__/test.js +++ b/e2e/transform/async-transformer/__tests__/test.js @@ -5,8 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -import m from '../module-under-test'; +import m, {exportedSymbol} from '../module-under-test'; +import symbol from '../some-symbol'; test('ESM transformer intercepts', () => { expect(m).toEqual(42); }); + +test('reexported symbol is same instance', () => { + expect(exportedSymbol).toBe(symbol); +}); diff --git a/e2e/transform/async-transformer/module-under-test.js b/e2e/transform/async-transformer/module-under-test.js index 2d892012f8fe..ee3052a99ccf 100644 --- a/e2e/transform/async-transformer/module-under-test.js +++ b/e2e/transform/async-transformer/module-under-test.js @@ -5,4 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import symbol from './some-symbol'; + +export const exportedSymbol = symbol; + export default 'It was not transformed!!'; diff --git a/e2e/transform/async-transformer/my-transform.cjs b/e2e/transform/async-transformer/my-transform.cjs index 2c226c9e565c..9f9b4a91398f 100644 --- a/e2e/transform/async-transformer/my-transform.cjs +++ b/e2e/transform/async-transformer/my-transform.cjs @@ -7,14 +7,29 @@ 'use strict'; +const {promisify} = require('util'); + +const wait = promisify(setTimeout); + const fileToTransform = require.resolve('./module-under-test'); +const fileToTransform2 = require.resolve('./some-symbol'); module.exports = { async processAsync(src, filepath) { - if (filepath !== fileToTransform) { + if (filepath !== fileToTransform && filepath !== fileToTransform2) { throw new Error(`Unsupported filepath ${filepath}`); } - return 'export default 42;'; + if (filepath === fileToTransform2) { + // we want to wait to ensure the module cache is populated with the correct module + await wait(100); + + return src; + } + + return src.replace( + "export default 'It was not transformed!!'", + 'export default 42', + ); }, }; diff --git a/e2e/transform/async-transformer/package.json b/e2e/transform/async-transformer/package.json index b4477b1a9603..9f0122ab1f16 100644 --- a/e2e/transform/async-transformer/package.json +++ b/e2e/transform/async-transformer/package.json @@ -3,7 +3,8 @@ "jest": { "testEnvironment": "node", "transform": { - "module-under-test\\.js$": "/my-transform.cjs" + "module-under-test\\.js$": "/my-transform.cjs", + "some-symbol\\.js$": "/my-transform.cjs" } } } diff --git a/e2e/transform/async-transformer/some-symbol.js b/e2e/transform/async-transformer/some-symbol.js new file mode 100644 index 000000000000..cf2680d0fa06 --- /dev/null +++ b/e2e/transform/async-transformer/some-symbol.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 Symbol('hello!'); diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 2494d17976a0..04521637efc0 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -186,6 +186,7 @@ export default class Runtime { private readonly _sourceMapRegistry: Map; private readonly _scriptTransformer: ScriptTransformer; private readonly _fileTransforms: Map; + private readonly _fileTransformsMutex: Map>; private _v8CoverageInstrumenter: CoverageInstrumenter | undefined; private _v8CoverageResult: V8Coverage | undefined; private readonly _transitiveShouldMock: Map; @@ -232,6 +233,7 @@ export default class Runtime { this._shouldAutoMock = config.automock; this._sourceMapRegistry = new Map(); this._fileTransforms = new Map(); + this._fileTransformsMutex = new Map(); this._virtualMocks = new Map(); this.jestObjectCaches = new Map(); @@ -374,6 +376,10 @@ export default class Runtime { ): Promise { const cacheKey = modulePath + query; + if (this._fileTransformsMutex.has(cacheKey)) { + await this._fileTransformsMutex.get(cacheKey); + } + if (!this._esmoduleRegistry.has(cacheKey)) { invariant( typeof this._environment.getVmContext === 'function', @@ -384,9 +390,28 @@ export default class Runtime { invariant(context, 'Test environment has been torn down'); + let transformResolve: () => void; + let transformReject: (error?: unknown) => void; + + this._fileTransformsMutex.set( + cacheKey, + new Promise((resolve, reject) => { + transformResolve = resolve; + transformReject = reject; + }), + ); + + invariant( + transformResolve! && transformReject!, + 'Promise initialization should be sync - please report this bug to Jest!', + ); + if (this._resolver.isCoreModule(modulePath)) { const core = this._importCoreModule(modulePath, context); this._esmoduleRegistry.set(cacheKey, core); + + transformResolve(); + return core; } @@ -398,31 +423,43 @@ export default class Runtime { supportsTopLevelAwait, }); - const module = new SourceTextModule(transformedCode, { - context, - identifier: modulePath, - importModuleDynamically: async ( - specifier: string, - referencingModule: VMModule, - ) => { - invariant( - runtimeSupportsVmModules, - 'You need to run with a version of node that supports ES Modules in the VM API. See https://jestjs.io/docs/en/ecmascript-modules', - ); - const module = await this.resolveModule( - specifier, - referencingModule.identifier, - referencingModule.context, - ); + try { + const module = new SourceTextModule(transformedCode, { + context, + identifier: modulePath, + importModuleDynamically: async ( + specifier: string, + referencingModule: VMModule, + ) => { + invariant( + runtimeSupportsVmModules, + 'You need to run with a version of node that supports ES Modules in the VM API. See https://jestjs.io/docs/en/ecmascript-modules', + ); + const module = await this.resolveModule( + specifier, + referencingModule.identifier, + referencingModule.context, + ); + + return this.linkAndEvaluateModule(module); + }, + initializeImportMeta(meta: ImportMeta) { + meta.url = pathToFileURL(modulePath).href; + }, + }); - return this.linkAndEvaluateModule(module); - }, - initializeImportMeta(meta: ImportMeta) { - meta.url = pathToFileURL(modulePath).href; - }, - }); + invariant( + !this._esmoduleRegistry.has(cacheKey), + `Module cache already has entry ${cacheKey}. This is a bug in Jest, please report it!`, + ); - this._esmoduleRegistry.set(cacheKey, module); + this._esmoduleRegistry.set(cacheKey, module); + + transformResolve(); + } catch (error: unknown) { + transformReject(error); + throw error; + } } const module = this._esmoduleRegistry.get(cacheKey); @@ -990,6 +1027,7 @@ export default class Runtime { this._sourceMapRegistry.clear(); this._fileTransforms.clear(); + this._fileTransformsMutex.clear(); this.jestObjectCaches.clear(); this._v8CoverageResult = [];