Skip to content

Commit

Permalink
fix(runtime): handle async transforms of same module (#11220)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Mar 20, 2021
1 parent 420bcb7 commit 129c08d
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 29 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion e2e/__tests__/transform.test.ts
Expand Up @@ -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);
});
});

Expand Down
7 changes: 6 additions & 1 deletion e2e/transform/async-transformer/__tests__/test.js
Expand Up @@ -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);
});
4 changes: 4 additions & 0 deletions e2e/transform/async-transformer/module-under-test.js
Expand Up @@ -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!!';
19 changes: 17 additions & 2 deletions e2e/transform/async-transformer/my-transform.cjs
Expand Up @@ -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',
);
},
};
3 changes: 2 additions & 1 deletion e2e/transform/async-transformer/package.json
Expand Up @@ -3,7 +3,8 @@
"jest": {
"testEnvironment": "node",
"transform": {
"module-under-test\\.js$": "<rootDir>/my-transform.cjs"
"module-under-test\\.js$": "<rootDir>/my-transform.cjs",
"some-symbol\\.js$": "<rootDir>/my-transform.cjs"
}
}
}
8 changes: 8 additions & 0 deletions 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!');
84 changes: 61 additions & 23 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -186,6 +186,7 @@ export default class Runtime {
private readonly _sourceMapRegistry: Map<string, string>;
private readonly _scriptTransformer: ScriptTransformer;
private readonly _fileTransforms: Map<string, RuntimeTransformResult>;
private readonly _fileTransformsMutex: Map<string, Promise<void>>;
private _v8CoverageInstrumenter: CoverageInstrumenter | undefined;
private _v8CoverageResult: V8Coverage | undefined;
private readonly _transitiveShouldMock: Map<string, boolean>;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -374,6 +376,10 @@ export default class Runtime {
): Promise<VMModule> {
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',
Expand All @@ -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;
}

Expand All @@ -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);
Expand Down Expand Up @@ -990,6 +1027,7 @@ export default class Runtime {
this._sourceMapRegistry.clear();

this._fileTransforms.clear();
this._fileTransformsMutex.clear();
this.jestObjectCaches.clear();

this._v8CoverageResult = [];
Expand Down

0 comments on commit 129c08d

Please sign in to comment.