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

feat(runtime): add support for async code transformation #11191

Merged
merged 4 commits into from Mar 13, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions docs/Configuration.md
Expand Up @@ -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\\.[^\\\/]+$"]`
Expand Down
13 changes: 13 additions & 0 deletions e2e/__tests__/transform.test.ts
Expand Up @@ -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);
});
});
});
12 changes: 12 additions & 0 deletions 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);
});
8 changes: 8 additions & 0 deletions 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!!';
20 changes: 20 additions & 0 deletions 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;';
},
};
9 changes: 9 additions & 0 deletions e2e/transform/async-transformer/package.json
@@ -0,0 +1,9 @@
{
"type": "module",
"jest": {
"testEnvironment": "node",
"transform": {
"module-under-test\\.js$": "<rootDir>/my-transform.cjs"
}
}
}
48 changes: 46 additions & 2 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -36,6 +36,7 @@ import {
CallerTransformOptions,
ScriptTransformer,
ShouldInstrumentOptions,
TransformResult,
TransformationOptions,
handlePotentialSyntaxError,
shouldInstrument,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<string> {
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,
Expand Down
20 changes: 11 additions & 9 deletions packages/jest-transform/src/types.ts
Expand Up @@ -65,7 +65,7 @@ export interface TransformOptions<OptionType = unknown>

export interface SyncTransformer<OptionType = unknown> {
canInstrument?: boolean;
createTransformer?: (options?: OptionType) => SyncTransformer;
createTransformer?: (options?: OptionType) => SyncTransformer<OptionType>;
Copy link
Member Author

Choose a reason for hiding this comment

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

all of the type changes here should have been in #9889, but I didn't test writing a transformer before merging it, just calling it from runtime. So snuck it in here 😀


getCacheKey?: (
sourceText: string,
Expand All @@ -76,7 +76,7 @@ export interface SyncTransformer<OptionType = unknown> {
getCacheKeyAsync?: (
sourceText: string,
sourcePath: Config.Path,
options: TransformOptions,
options: TransformOptions<OptionType>,
) => Promise<string>;

process: (
Expand All @@ -88,37 +88,39 @@ export interface SyncTransformer<OptionType = unknown> {
processAsync?: (
sourceText: string,
sourcePath: Config.Path,
options?: TransformOptions,
options: TransformOptions<OptionType>,
) => Promise<TransformedSource>;
}

export interface AsyncTransformer<OptionType = unknown> {
canInstrument?: boolean;
createTransformer?: (options?: OptionType) => AsyncTransformer;
createTransformer?: (options?: OptionType) => AsyncTransformer<OptionType>;

getCacheKey?: (
sourceText: string,
sourcePath: Config.Path,
options: TransformOptions,
options: TransformOptions<OptionType>,
) => string;

getCacheKeyAsync?: (
sourceText: string,
sourcePath: Config.Path,
options: TransformOptions,
options: TransformOptions<OptionType>,
) => Promise<string>;

process?: (
sourceText: string,
sourcePath: Config.Path,
options?: TransformOptions,
options: TransformOptions<OptionType>,
) => TransformedSource;

processAsync: (
sourceText: string,
sourcePath: Config.Path,
options?: TransformOptions,
options: TransformOptions<OptionType>,
) => Promise<TransformedSource>;
}

export type Transformer = SyncTransformer | AsyncTransformer;
export type Transformer<OptionType = unknown> =
| SyncTransformer<OptionType>
| AsyncTransformer<OptionType>;