Skip to content

Commit a66eec7

Browse files
authored
chore(jest-transform): refactor transformer API to reduce number of arguments (#10834)
1 parent 29156d1 commit a66eec7

File tree

27 files changed

+377
-191
lines changed

27 files changed

+377
-191
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- `[jest-runtime]` [**BREAKING**] remove long-deprecated `jest.addMatchers`, `jest.resetModuleRegistry`, and `jest.runTimersToTime` ([#9853](https://github.com/facebook/jest/pull/9853))
2020
- `[jest-transform]` Show enhanced `SyntaxError` message for all `SyntaxError`s ([#10749](https://github.com/facebook/jest/pull/10749))
2121
- `[jest-transform]` [**BREAKING**] Refactor API to pass an options bag around rather than multiple boolean options ([#10753](https://github.com/facebook/jest/pull/10753))
22+
- `[jest-transform]` [**BREAKING**] Refactor API of transformers to pass an options bag rather than separate `config` and other options
2223

2324
### Chore & Maintenance
2425

docs/CodeTransformation.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
id: code-transformation
3+
title: Code Transformation
4+
---
5+
6+
Jest runs the code in your project as JavaScript, but if you use some syntax not supported by Node.js out of the box (such as JSX, types from TypeScript, Vue templates etc.) then you'll need to transform that code into plain JavaScript, similar to what you would do when building for browsers.
7+
8+
Jest supports this via the [`transform` configuration option](Configuration.md#transform-objectstring-pathtotransformer--pathtotransformer-object).
9+
10+
A transformer is a module that provides a synchronous function for transforming source files. For example, if you wanted to be able to use a new language feature in your modules or tests that aren't yet supported by Node, you might plug in one of many compilers that compile a future version of JavaScript to a current one.
11+
12+
Jest will cache the result of a transformation and attempt to invalidate that result based on a number of factors, such as the source of the file being transformed and changing configuration.
13+
14+
## Defaults
15+
16+
Jest ships with one transformer out of the box - `babel-jest`. It will automatically load your project's Babel configuration and transform any file matching the following RegEx: `/\.[jt]sx?$/` meaning any `.js`, `.jsx`, `.ts` and `.tsx` file. In addition, `babel-jest` will inject the Babel plugin necessary for mock hoisting talked about in [ES Module mocking](ManualMocks.md#using-with-es-module-imports).
17+
18+
If you override the `transform` configuration option `babel-jest` will no longer be active, and you'll need to add it manually if you wish to use Babel.
19+
20+
## Writing custom transformers
21+
22+
You can write you own transformer. The API of a transformer is as follows:
23+
24+
```ts
25+
interface Transformer<OptionType = unknown> {
26+
/**
27+
* Indicates if the transformer is capabale of instrumenting the code for code coverage.
28+
*
29+
* If V8 coverage is _not_ active, and this is `true`, Jest will assume the code is instrumented.
30+
* If V8 coverage is _not_ active, and this is `false`. Jest will instrument the code returned by this transformer using Babel.
31+
*/
32+
canInstrument?: boolean;
33+
createTransformer?: (options?: OptionType) => Transformer;
34+
35+
getCacheKey?: (
36+
sourceText: string,
37+
sourcePath: string,
38+
options: TransformOptions,
39+
) => string;
40+
41+
process: (
42+
sourceText: string,
43+
sourcePath: string,
44+
options: TransformOptions,
45+
) => TransformedSource;
46+
}
47+
48+
interface TransformOptions {
49+
config: Config.ProjectConfig;
50+
/** A stringified version of the configuration - useful in cache busting */
51+
configString: string;
52+
instrument: boolean;
53+
// names are copied from babel: https://babeljs.io/docs/en/options#caller
54+
supportsDynamicImport: boolean;
55+
supportsExportNamespaceFrom: boolean;
56+
supportsStaticESM: boolean;
57+
supportsTopLevelAwait: boolean;
58+
}
59+
60+
type TransformedSource =
61+
| {code: string; map?: RawSourceMap | string | null}
62+
| string;
63+
64+
// Config.ProjectConfig can be seen in in code [here](https://github.com/facebook/jest/blob/v26.6.3/packages/jest-types/src/Config.ts#L323)
65+
// RawSourceMap comes from [`source-map`](https://github.com/mozilla/source-map/blob/0.6.1/source-map.d.ts#L6-L12)
66+
```
67+
68+
As can be seen, only `process` is mandatory to implement, although we highly recommend implementing `getCacheKey` as well, so we don't waste resources transpiling the same source file when we can read its previous result from disk. You can use [`@jest/create-cache-key-function`](https://www.npmjs.com/package/@jest/create-cache-key-function) to help implement it.
69+
70+
Note that [ECMAScript module](ECMAScriptModules.md) support is indicated by the passed in `supports*` options. Specifically `supportsDynamicImport: true` means the transformer can return `import()` expressions, which is supported by both ESM and CJS. If `supportsStaticESM: true` it means top level `import` statements are supported and the code will be interpreted as ESM and not CJS. See [Node's docs](https://nodejs.org/api/esm.html#esm_differences_between_es_modules_and_commonjs) for details on the differences.
71+
72+
### Examples
73+
74+
### TypeScript with type checking
75+
76+
While `babel-jest` by default will transpile TypeScript files, Babel will not verify the types. If you want that you can use [`ts-jest`](https://github.com/kulshekhar/ts-jest).
77+
78+
#### Transforming images to their path
79+
80+
Importing images is a way to include them in your browser bundle, but they are not valid JavaScript. One way of handling it in Jest is to replace the imported value with its filename.
81+
82+
```js
83+
// fileTransformer.js
84+
const path = require('path');
85+
86+
module.exports = {
87+
process(src, filename, config, options) {
88+
return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';';
89+
},
90+
};
91+
```
92+
93+
```js
94+
// jest.config.js
95+
96+
module.exports = {
97+
transform: {
98+
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
99+
'<rootDir>/fileTransformer.js',
100+
},
101+
};
102+
```

docs/Configuration.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,8 +1263,7 @@ Examples of such compilers include:
12631263

12641264
- [Babel](https://babeljs.io/)
12651265
- [TypeScript](http://www.typescriptlang.org/)
1266-
- [async-to-gen](http://github.com/leebyron/async-to-gen#jest)
1267-
- To build your own please visit the [Custom Transformer](TutorialReact.md#custom-transformers) section
1266+
- To build your own please visit the [Custom Transformer](CodeTransformation.md#writing-custom-transformers) section
12681267

12691268
You can pass configuration to a transformer like `{filePattern: ['path-to-transformer', {options}]}` For example, to configure babel-jest for non-default behavior, `{"\\.js$": ['babel-jest', {rootMode: "upward"}]}`
12701269

docs/TutorialReact.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ The code for this example is available at [examples/enzyme](https://github.com/f
304304
305305
### Custom transformers
306306
307-
If you need more advanced functionality, you can also build your own transformer. Instead of using babel-jest, here is an example of using babel:
307+
If you need more advanced functionality, you can also build your own transformer. Instead of using `babel-jest`, here is an example of using `@babel/core`:
308308
309309
```javascript
310310
// custom-transformer.js
@@ -320,7 +320,7 @@ module.exports = {
320320
presets: [jestPreset],
321321
});
322322

323-
return result ? result.code : src;
323+
return result || src;
324324
},
325325
};
326326
```
@@ -329,7 +329,7 @@ Don't forget to install the `@babel/core` and `babel-preset-jest` packages for t
329329
330330
To make this work with Jest you need to update your Jest configuration with this: `"transform": {"\\.js$": "path/to/custom-transformer.js"}`.
331331
332-
If you'd like to build a transformer with babel support, you can also use babel-jest to compose one and pass in your custom configuration options:
332+
If you'd like to build a transformer with babel support, you can also use `babel-jest` to compose one and pass in your custom configuration options:
333333
334334
```javascript
335335
const babelJest = require('babel-jest');
@@ -338,3 +338,5 @@ module.exports = babelJest.createTransformer({
338338
presets: ['my-custom-preset'],
339339
});
340340
```
341+
342+
See [dedicated docs](CodeTransformation.md#writing-custom-transformers) for more details.

e2e/coverage-transform-instrumented/preprocessor.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@ const options = {
1818

1919
module.exports = {
2020
canInstrument: true,
21-
process(src, filename, config, transformOptions) {
21+
process(src, filename, transformOptions) {
2222
options.filename = filename;
2323

24-
if (transformOptions && transformOptions.instrument) {
24+
if (transformOptions.instrument) {
2525
options.auxiliaryCommentBefore = ' istanbul ignore next ';
2626
options.plugins = [
2727
[
2828
babelIstanbulPlugin,
2929
{
30-
cwd: config.rootDir,
30+
cwd: transformOptions.config.rootDir,
3131
exclude: [],
3232
},
3333
],

e2e/snapshot-serializers/transformer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
'use strict';
99

1010
module.exports = {
11-
process(src, filename, config, options) {
11+
process(src, filename) {
1212
if (/bar.js$/.test(filename)) {
1313
return `${src};\nmodule.exports = createPlugin('bar');`;
1414
}

e2e/transform/custom-instrumenting-preprocessor/preprocessor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
module.exports = {
99
canInstrument: true,
10-
process(src, filename, config, options) {
10+
process(src, filename, options) {
1111
src = `${src};\nglobal.__PREPROCESSED__ = true;`;
1212

1313
if (options.instrument) {

e2e/transform/multiple-transformers/cssPreprocessor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
module.exports = {
9-
process(src, filename, config, options) {
9+
process() {
1010
return `
1111
module.exports = {
1212
root: 'App-root',

e2e/transform/multiple-transformers/filePreprocessor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
const path = require('path');
99

1010
module.exports = {
11-
process(src, filename, config, options) {
11+
process(src, filename) {
1212
return `
1313
module.exports = '${path.basename(filename)}';
1414
`;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@
144144
},
145145
"resolutions": {
146146
"@types/jest/jest-diff": "^25.1.0",
147-
"@types/jest/pretty-format": "^25.1.0"
147+
"@types/jest/pretty-format": "^25.1.0",
148+
"fbjs-scripts": "patch:fbjs-scripts@^1.1.0#./patches/fbjs-scripts.patch"
148149
}
149150
}

packages/babel-jest/src/__tests__/index.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ beforeEach(() => {
3535
});
3636

3737
test('Returns source string with inline maps when no transformOptions is passed', () => {
38-
const result = babelJest.process(
39-
sourceString,
40-
'dummy_path.js',
41-
makeProjectConfig(),
42-
) as any;
38+
const result = babelJest.process(sourceString, 'dummy_path.js', {
39+
config: makeProjectConfig(),
40+
configString: JSON.stringify(makeProjectConfig()),
41+
instrument: false,
42+
}) as any;
4343
expect(typeof result).toBe('object');
4444
expect(result.code).toBeDefined();
4545
expect(result.map).toBeDefined();
@@ -86,7 +86,9 @@ describe('caller option correctly merges from defaults and options', () => {
8686
},
8787
],
8888
])('%j -> %j', (input, output) => {
89-
babelJest.process(sourceString, 'dummy_path.js', makeProjectConfig(), {
89+
babelJest.process(sourceString, 'dummy_path.js', {
90+
config: makeProjectConfig(),
91+
configString: JSON.stringify(makeProjectConfig()),
9092
instrument: false,
9193
...input,
9294
});
@@ -107,7 +109,9 @@ describe('caller option correctly merges from defaults and options', () => {
107109

108110
test('can pass null to createTransformer', () => {
109111
const transformer = babelJest.createTransformer(null);
110-
transformer.process(sourceString, 'dummy_path.js', makeProjectConfig(), {
112+
transformer.process(sourceString, 'dummy_path.js', {
113+
config: makeProjectConfig(),
114+
configString: JSON.stringify(makeProjectConfig()),
111115
instrument: false,
112116
});
113117

packages/babel-jest/src/index.ts

Lines changed: 23 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import {createHash} from 'crypto';
99
import * as path from 'path';
1010
import {
1111
PartialConfig,
12-
PluginItem,
13-
TransformCaller,
1412
TransformOptions,
1513
transformSync as babelTransform,
1614
} from '@babel/core';
@@ -28,23 +26,12 @@ const THIS_FILE = fs.readFileSync(__filename);
2826
const jestPresetPath = require.resolve('babel-preset-jest');
2927
const babelIstanbulPlugin = require.resolve('babel-plugin-istanbul');
3028

31-
// Narrow down the types
32-
interface BabelJestTransformer extends Transformer {
33-
canInstrument: true;
34-
}
35-
interface BabelJestTransformOptions extends TransformOptions {
36-
caller: TransformCaller;
37-
compact: false;
38-
plugins: Array<PluginItem>;
39-
presets: Array<PluginItem>;
40-
sourceMaps: 'both';
41-
}
42-
43-
const createTransformer = (
44-
userOptions?: TransformOptions | null,
45-
): BabelJestTransformer => {
46-
const inputOptions: TransformOptions = userOptions ?? {};
47-
const options: BabelJestTransformOptions = {
29+
type CreateTransformer = Transformer<TransformOptions>['createTransformer'];
30+
31+
const createTransformer: CreateTransformer = userOptions => {
32+
const inputOptions = userOptions ?? {};
33+
34+
const options = {
4835
...inputOptions,
4936
caller: {
5037
name: 'babel-jest',
@@ -58,7 +45,7 @@ const createTransformer = (
5845
plugins: inputOptions.plugins ?? [],
5946
presets: (inputOptions.presets ?? []).concat(jestPresetPath),
6047
sourceMaps: 'both',
61-
};
48+
} as const;
6249

6350
function loadBabelConfig(
6451
cwd: Config.Path,
@@ -102,13 +89,13 @@ const createTransformer = (
10289

10390
return {
10491
canInstrument: true,
105-
getCacheKey(fileData, filename, configString, cacheKeyOptions) {
106-
const {config, instrument, rootDir} = cacheKeyOptions;
92+
getCacheKey(sourceText, sourcePath, transformOptions) {
93+
const {config, configString, instrument} = transformOptions;
10794

10895
const babelOptions = loadBabelConfig(
10996
config.cwd,
110-
filename,
111-
cacheKeyOptions,
97+
sourcePath,
98+
transformOptions,
11299
);
113100
const configPath = [
114101
babelOptions.config || '',
@@ -120,9 +107,9 @@ const createTransformer = (
120107
.update('\0', 'utf8')
121108
.update(JSON.stringify(babelOptions.options))
122109
.update('\0', 'utf8')
123-
.update(fileData)
110+
.update(sourceText)
124111
.update('\0', 'utf8')
125-
.update(path.relative(rootDir, filename))
112+
.update(path.relative(config.rootDir, sourcePath))
126113
.update('\0', 'utf8')
127114
.update(configString)
128115
.update('\0', 'utf8')
@@ -135,9 +122,13 @@ const createTransformer = (
135122
.update(process.env.BABEL_ENV || '')
136123
.digest('hex');
137124
},
138-
process(src, filename, config, transformOptions) {
125+
process(sourceText, sourcePath, transformOptions) {
139126
const babelOptions = {
140-
...loadBabelConfig(config.cwd, filename, transformOptions).options,
127+
...loadBabelConfig(
128+
transformOptions.config.cwd,
129+
sourcePath,
130+
transformOptions,
131+
).options,
141132
};
142133

143134
if (transformOptions?.instrument) {
@@ -148,14 +139,14 @@ const createTransformer = (
148139
babelIstanbulPlugin,
149140
{
150141
// files outside `cwd` will not be instrumented
151-
cwd: config.rootDir,
142+
cwd: transformOptions.config.rootDir,
152143
exclude: [],
153144
},
154145
],
155146
]);
156147
}
157148

158-
const transformResult = babelTransform(src, babelOptions);
149+
const transformResult = babelTransform(sourceText, babelOptions);
159150

160151
if (transformResult) {
161152
const {code, map} = transformResult;
@@ -164,14 +155,12 @@ const createTransformer = (
164155
}
165156
}
166157

167-
return src;
158+
return sourceText;
168159
},
169160
};
170161
};
171162

172-
const transformer: BabelJestTransformer & {
173-
createTransformer: (options?: TransformOptions) => BabelJestTransformer;
174-
} = {
163+
const transformer: Transformer<TransformOptions> = {
175164
...createTransformer(),
176165
// Assigned here so only the exported transformer has `createTransformer`,
177166
// instead of all created transformers by the function

0 commit comments

Comments
 (0)