Skip to content

Commit

Permalink
feat(runtime): add support for async code transformation (#11191)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Mar 13, 2021
1 parent 858c50b commit dd13096
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 11 deletions.
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>;

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>;

0 comments on commit dd13096

Please sign in to comment.