Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(runtime): handle async transforms of same module #11220

Merged
merged 2 commits into from Mar 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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, {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignore whitespace

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