Skip to content

Commit 129c08d

Browse files
authored
fix(runtime): handle async transforms of same module (#11220)
1 parent 420bcb7 commit 129c08d

8 files changed

Lines changed: 100 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
- `[jest-runner]` [**BREAKING**] set exit code to 1 if test logs after teardown ([#10728](https://github.com/facebook/jest/pull/10728))
2222
- `[jest-runner]` [**BREAKING**] Run transforms over `runnner` ([#8823](https://github.com/facebook/jest/pull/8823))
2323
- `[jest-runner]` [**BREAKING**] Run transforms over `testRunnner` ([#8823](https://github.com/facebook/jest/pull/8823))
24-
- `[jest-runtime]` Support for async code transformations ([#11191](https://github.com/facebook/jest/pull/11191))
24+
- `[jest-runtime]` Support for async code transformations ([#11191](https://github.com/facebook/jest/pull/11191) & [#11220](https://github.com/facebook/jest/pull/11220))
2525
- `[jest-reporters]` Add static filepath property to all reporters ([#11015](https://github.com/facebook/jest/pull/11015))
2626
- `[jest-snapshot]` [**BREAKING**] Make prettier optional for inline snapshots - fall back to string replacement ([#7792](https://github.com/facebook/jest/pull/7792))
2727
- `[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))

e2e/__tests__/transform.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ onNodeVersions('^12.17.0 || >=13.2.0', () => {
261261
});
262262
expect(stderr).toMatch(/PASS/);
263263
expect(json.success).toBe(true);
264-
expect(json.numPassedTests).toBe(1);
264+
expect(json.numPassedTests).toBe(2);
265265
});
266266
});
267267

e2e/transform/async-transformer/__tests__/test.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import m from '../module-under-test';
8+
import m, {exportedSymbol} from '../module-under-test';
9+
import symbol from '../some-symbol';
910

1011
test('ESM transformer intercepts', () => {
1112
expect(m).toEqual(42);
1213
});
14+
15+
test('reexported symbol is same instance', () => {
16+
expect(exportedSymbol).toBe(symbol);
17+
});

e2e/transform/async-transformer/module-under-test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
import symbol from './some-symbol';
9+
10+
export const exportedSymbol = symbol;
11+
812
export default 'It was not transformed!!';

e2e/transform/async-transformer/my-transform.cjs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,29 @@
77

88
'use strict';
99

10+
const {promisify} = require('util');
11+
12+
const wait = promisify(setTimeout);
13+
1014
const fileToTransform = require.resolve('./module-under-test');
15+
const fileToTransform2 = require.resolve('./some-symbol');
1116

1217
module.exports = {
1318
async processAsync(src, filepath) {
14-
if (filepath !== fileToTransform) {
19+
if (filepath !== fileToTransform && filepath !== fileToTransform2) {
1520
throw new Error(`Unsupported filepath ${filepath}`);
1621
}
1722

18-
return 'export default 42;';
23+
if (filepath === fileToTransform2) {
24+
// we want to wait to ensure the module cache is populated with the correct module
25+
await wait(100);
26+
27+
return src;
28+
}
29+
30+
return src.replace(
31+
"export default 'It was not transformed!!'",
32+
'export default 42',
33+
);
1934
},
2035
};

e2e/transform/async-transformer/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"jest": {
44
"testEnvironment": "node",
55
"transform": {
6-
"module-under-test\\.js$": "<rootDir>/my-transform.cjs"
6+
"module-under-test\\.js$": "<rootDir>/my-transform.cjs",
7+
"some-symbol\\.js$": "<rootDir>/my-transform.cjs"
78
}
89
}
910
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
export default Symbol('hello!');

packages/jest-runtime/src/index.ts

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ export default class Runtime {
186186
private readonly _sourceMapRegistry: Map<string, string>;
187187
private readonly _scriptTransformer: ScriptTransformer;
188188
private readonly _fileTransforms: Map<string, RuntimeTransformResult>;
189+
private readonly _fileTransformsMutex: Map<string, Promise<void>>;
189190
private _v8CoverageInstrumenter: CoverageInstrumenter | undefined;
190191
private _v8CoverageResult: V8Coverage | undefined;
191192
private readonly _transitiveShouldMock: Map<string, boolean>;
@@ -232,6 +233,7 @@ export default class Runtime {
232233
this._shouldAutoMock = config.automock;
233234
this._sourceMapRegistry = new Map();
234235
this._fileTransforms = new Map();
236+
this._fileTransformsMutex = new Map();
235237
this._virtualMocks = new Map();
236238
this.jestObjectCaches = new Map();
237239

@@ -374,6 +376,10 @@ export default class Runtime {
374376
): Promise<VMModule> {
375377
const cacheKey = modulePath + query;
376378

379+
if (this._fileTransformsMutex.has(cacheKey)) {
380+
await this._fileTransformsMutex.get(cacheKey);
381+
}
382+
377383
if (!this._esmoduleRegistry.has(cacheKey)) {
378384
invariant(
379385
typeof this._environment.getVmContext === 'function',
@@ -384,9 +390,28 @@ export default class Runtime {
384390

385391
invariant(context, 'Test environment has been torn down');
386392

393+
let transformResolve: () => void;
394+
let transformReject: (error?: unknown) => void;
395+
396+
this._fileTransformsMutex.set(
397+
cacheKey,
398+
new Promise((resolve, reject) => {
399+
transformResolve = resolve;
400+
transformReject = reject;
401+
}),
402+
);
403+
404+
invariant(
405+
transformResolve! && transformReject!,
406+
'Promise initialization should be sync - please report this bug to Jest!',
407+
);
408+
387409
if (this._resolver.isCoreModule(modulePath)) {
388410
const core = this._importCoreModule(modulePath, context);
389411
this._esmoduleRegistry.set(cacheKey, core);
412+
413+
transformResolve();
414+
390415
return core;
391416
}
392417

@@ -398,31 +423,43 @@ export default class Runtime {
398423
supportsTopLevelAwait,
399424
});
400425

401-
const module = new SourceTextModule(transformedCode, {
402-
context,
403-
identifier: modulePath,
404-
importModuleDynamically: async (
405-
specifier: string,
406-
referencingModule: VMModule,
407-
) => {
408-
invariant(
409-
runtimeSupportsVmModules,
410-
'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',
411-
);
412-
const module = await this.resolveModule(
413-
specifier,
414-
referencingModule.identifier,
415-
referencingModule.context,
416-
);
426+
try {
427+
const module = new SourceTextModule(transformedCode, {
428+
context,
429+
identifier: modulePath,
430+
importModuleDynamically: async (
431+
specifier: string,
432+
referencingModule: VMModule,
433+
) => {
434+
invariant(
435+
runtimeSupportsVmModules,
436+
'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',
437+
);
438+
const module = await this.resolveModule(
439+
specifier,
440+
referencingModule.identifier,
441+
referencingModule.context,
442+
);
443+
444+
return this.linkAndEvaluateModule(module);
445+
},
446+
initializeImportMeta(meta: ImportMeta) {
447+
meta.url = pathToFileURL(modulePath).href;
448+
},
449+
});
417450

418-
return this.linkAndEvaluateModule(module);
419-
},
420-
initializeImportMeta(meta: ImportMeta) {
421-
meta.url = pathToFileURL(modulePath).href;
422-
},
423-
});
451+
invariant(
452+
!this._esmoduleRegistry.has(cacheKey),
453+
`Module cache already has entry ${cacheKey}. This is a bug in Jest, please report it!`,
454+
);
424455

425-
this._esmoduleRegistry.set(cacheKey, module);
456+
this._esmoduleRegistry.set(cacheKey, module);
457+
458+
transformResolve();
459+
} catch (error: unknown) {
460+
transformReject(error);
461+
throw error;
462+
}
426463
}
427464

428465
const module = this._esmoduleRegistry.get(cacheKey);
@@ -990,6 +1027,7 @@ export default class Runtime {
9901027
this._sourceMapRegistry.clear();
9911028

9921029
this._fileTransforms.clear();
1030+
this._fileTransformsMutex.clear();
9931031
this.jestObjectCaches.clear();
9941032

9951033
this._v8CoverageResult = [];

0 commit comments

Comments
 (0)