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(@jest/transform)!: require process() and processAsync() methods to always return structured data #12638

Merged
merged 12 commits into from Apr 6, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -43,6 +43,7 @@
- `[jest-runtime]` [**BREAKING**] `Runtime.createHasteMap` now returns a promise ([#12008](https://github.com/facebook/jest/pull/12008))
- `[jest-runtime]` Calling `jest.resetModules` function will clear FS and transform cache ([#12531](https://github.com/facebook/jest/pull/12531))
- `[@jest/schemas]` New module for JSON schemas for Jest's config ([#12384](https://github.com/facebook/jest/pull/12384))
- `[jest-transform]` [**BREAKING**] Make it required for `process()` and `processAsync()` methods to always return structured data ([#12638](https://github.com/facebook/jest/pull/12638))
- `[jest-test-result]` Add duration property to JSON test output ([#12518](https://github.com/facebook/jest/pull/12518))
- `[jest-watcher]` [**BREAKING**] Make `PatternPrompt` class to take `entityName` as third constructor parameter instead of `this._entityName` ([#12591](https://github.com/facebook/jest/pull/12591))
- `[jest-worker]` [**BREAKING**] Allow only absolute `workerPath` ([#12343](https://github.com/facebook/jest/pull/12343))
Expand Down
23 changes: 16 additions & 7 deletions docs/CodeTransformation.md
Expand Up @@ -22,10 +22,6 @@ If you override the `transform` configuration option `babel-jest` will no longer
You can write your own transformer. The API of a transformer is as follows:

```ts
// This version of the interface you are seeing on the website has been trimmed down for brevity
// For the full definition, see `packages/jest-transform/src/types.ts` in https://github.com/facebook/jest
// (taking care in choosing the right tag/commit for your version of Jest)

interface TransformOptions<OptionType = unknown> {
supportsDynamicImport: boolean;
supportsExportNamespaceFrom: boolean;
Expand All @@ -41,6 +37,11 @@ interface TransformOptions<OptionType = unknown> {
transformerConfig: OptionType;
}

type TransformedSource = {
code: string;
map?: RawSourceMap | string | null;
};

interface SyncTransformer<OptionType = unknown> {
canInstrument?: boolean;

Expand Down Expand Up @@ -111,6 +112,12 @@ type TransformerFactory<X extends Transformer> = {
};
```

:::note

The definitions above were trimmed down for brevity. Full code can be found in [Jest repo on GitHub](https://github.com/facebook/jest/blob/main/packages/jest-transform/src/types.ts) (remember to choose the right tag/commit for your version of Jest).
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved

:::

There are a couple of ways you can import code into Jest - using Common JS (`require`) or ECMAScript Modules (`import` - which exists in static and dynamic versions). Jest passes files through code transformation on demand (for instance when a `require` or `import` is evaluated). This process, also known as "transpilation", might happen _synchronously_ (in the case of `require`), or _asynchronously_ (in the case of `import` or `import()`, the latter of which also works from Common JS modules). For this reason, the interface exposes both pairs of methods for asynchronous and synchronous processes: `process{Async}` and `getCacheKey{Async}`. The latter is called to figure out if we need to call `process{Async}` at all. Since async transformation can happen synchronously without issue, it's possible for the async case to "fall back" to the sync variant, but not vice versa.

So if your code base is ESM only implementing the async variants is sufficient. Otherwise, if any code is loaded through `require` (including `createRequire` from within ESM), then you need to implement the synchronous variant. Be aware that `node_modules` is not transpiled with default config.
Expand All @@ -125,7 +132,7 @@ Note that [ECMAScript module](ECMAScriptModules.md) support is indicated by the

:::tip

Make sure `TransformedSource` contains a source map, so it is possible to report line information accurately in code coverage and test errors. Inline source maps also work but are slower.
Make sure `process{Async}` method returns source map alongside with transformed code, so it is possible to report line information accurately in code coverage and test errors. Inline source maps also work but are slower.

:::

Expand All @@ -143,8 +150,10 @@ Importing images is a way to include them in your browser bundle, but they are n
const path = require('path');

module.exports = {
process(src, filename, config, options) {
return `module.exports = ${JSON.stringify(path.basename(filename))};`;
process(sourceText, sourcePath, options) {
return {
code: `module.exports = ${JSON.stringify(path.basename(sourcePath))};`,
};
},
};
```
Expand Down
13 changes: 6 additions & 7 deletions docs/Webpack.md
Expand Up @@ -91,8 +91,10 @@ If `moduleNameMapper` cannot fulfill your requirements, you can use Jest's [`tra
const path = require('path');

module.exports = {
process(src, filename, config, options) {
return `module.exports = ${JSON.stringify(path.basename(filename))};`;
process(sourceText, sourcePath, options) {
return {
code: `module.exports = ${JSON.stringify(path.basename(sourcePath))};`,
};
},
};
```
Expand All @@ -118,7 +120,6 @@ _Note: if you are using babel-jest with additional code preprocessors, you have
"transform": {
"\\.js$": "babel-jest",
"\\.css$": "custom-transformer",
...
}
```

Expand Down Expand Up @@ -186,8 +187,7 @@ That's it! webpack is a complex and flexible tool, so you may have to make some

webpack 2 offers native support for ES modules. However, Jest runs in Node, and thus requires ES modules to be transpiled to CommonJS modules. As such, if you are using webpack 2, you most likely will want to configure Babel to transpile ES modules to CommonJS modules only in the `test` environment.

```json
// .babelrc
```json title=".babelrc"
{
"presets": [["env", {"modules": false}]],

Expand All @@ -203,8 +203,7 @@ webpack 2 offers native support for ES modules. However, Jest runs in Node, and

If you use dynamic imports (`import('some-file.js').then(module => ...)`), you need to enable the `dynamic-import-node` plugin.

```json
// .babelrc
```json title=".babelrc"
{
"presets": [["env", {"modules": false}]],

Expand Down
10 changes: 6 additions & 4 deletions e2e/__tests__/dependencyClash.test.ts
Expand Up @@ -64,10 +64,12 @@ test('does not require project modules from inside node_modules', () => {
if (!threw) {
throw new Error('It used the wrong invariant module!');
}
return script.replace(
'INVALID CODE FRAGMENT THAT WILL BE REMOVED BY THE TRANSFORMER',
''
);
return {
code: script.replace(
'INVALID CODE FRAGMENT THAT WILL BE REMOVED BY THE TRANSFORMER',
'',
),
};
},
};
`,
Expand Down
4 changes: 2 additions & 2 deletions e2e/__tests__/multiProjectRunner.test.ts
Expand Up @@ -450,7 +450,7 @@ test('Does transform files with the corresponding project transformer', () => {
};`,
'project1/transformer.js': `
module.exports = {
process: () => 'module.exports = "PROJECT1";',
process: () => ({code: 'module.exports = "PROJECT1";'}),
getCacheKey: () => 'PROJECT1_CACHE_KEY',
}
`,
Expand All @@ -465,7 +465,7 @@ test('Does transform files with the corresponding project transformer', () => {
};`,
'project2/transformer.js': `
module.exports = {
process: () => 'module.exports = "PROJECT2";',
process: () => ({code: 'module.exports = "PROJECT2";'}),
getCacheKey: () => 'PROJECT2_CACHE_KEY',
}
`,
Expand Down
Expand Up @@ -21,6 +21,6 @@ export default {

return {code: outputText, map: sourceMapText};
}
return sourceText;
return {code: sourceText};
},
};
2 changes: 1 addition & 1 deletion e2e/coverage-provider-v8/no-sourcemap/cssTransform.js
Expand Up @@ -7,5 +7,5 @@

module.exports = {
getCacheKey: () => 'cssTransform',
process: () => 'module.exports = {};',
process: () => ({code: 'module.exports = {};'}),
};
2 changes: 1 addition & 1 deletion e2e/coverage-remapping/typescriptPreprocessor.js
Expand Up @@ -22,6 +22,6 @@ module.exports = {
map: JSON.parse(result.sourceMapText),
};
}
return src;
return {code: src};
},
};
4 changes: 2 additions & 2 deletions e2e/global-setup-custom-transform/transformer.js
Expand Up @@ -12,9 +12,9 @@ const fileToTransform = require.resolve('./index.js');
module.exports = {
process(src, filename) {
if (filename === fileToTransform) {
return src.replace('hello', 'hello, world');
return {code: src.replace('hello', 'hello, world')};
}

return src;
return {code: src};
},
};
4 changes: 2 additions & 2 deletions e2e/snapshot-serializers/transformer.js
Expand Up @@ -10,8 +10,8 @@
module.exports = {
process(src, filename) {
if (/bar.js$/.test(filename)) {
return `${src};\nmodule.exports = createPlugin('bar');`;
return {code: `${src};\nmodule.exports = createPlugin('bar');`};
}
return src;
return {code: src};
},
};
23 changes: 14 additions & 9 deletions e2e/stack-trace-source-maps-with-coverage/preprocessor.js
Expand Up @@ -8,14 +8,19 @@
const tsc = require('typescript');

module.exports = {
process(src, path) {
return tsc.transpileModule(src, {
compilerOptions: {
inlineSourceMap: true,
module: tsc.ModuleKind.CommonJS,
target: 'es5',
},
fileName: path,
}).outputText;
process(sourceText, fileName) {
if (fileName.endsWith('.ts') || fileName.endsWith('.tsx')) {
const {outputText, sourceMapText} = tsc.transpileModule(sourceText, {
compilerOptions: {
inlineSourceMap: true,
module: tsc.ModuleKind.CommonJS,
target: 'es5',
},
fileName,
});

return {code: outputText, map: sourceMapText};
}
return {code: sourceText};
},
};
23 changes: 14 additions & 9 deletions e2e/stack-trace-source-maps/preprocessor.js
Expand Up @@ -8,14 +8,19 @@
const tsc = require('typescript');

module.exports = {
process(src, path) {
return tsc.transpileModule(src, {
compilerOptions: {
inlineSourceMap: true,
module: tsc.ModuleKind.CommonJS,
target: 'es5',
},
fileName: path,
}).outputText;
process(sourceText, fileName) {
if (fileName.endsWith('.ts') || fileName.endsWith('.tsx')) {
const {outputText, sourceMapText} = tsc.transpileModule(sourceText, {
compilerOptions: {
inlineSourceMap: true,
module: tsc.ModuleKind.CommonJS,
target: 'es5',
},
fileName,
});

return {code: outputText, map: sourceMapText};
}
return {code: sourceText};
},
};
2 changes: 1 addition & 1 deletion e2e/transform-linked-modules/preprocessor.js
Expand Up @@ -7,6 +7,6 @@

module.exports = {
process() {
return 'module.exports = "transformed"';
return {code: 'module.exports = "transformed"'};
},
};
12 changes: 7 additions & 5 deletions e2e/transform/async-transformer/my-transform.cjs
Expand Up @@ -24,12 +24,14 @@ module.exports = {
// we want to wait to ensure the module cache is populated with the correct module
await wait(100);

return src;
return {code: src};
}

return src.replace(
"export default 'It was not transformed!!'",
'export default 42',
);
return {
code: src.replace(
"export default 'It was not transformed!!'",
'export default 42',
),
};
},
};
4 changes: 2 additions & 2 deletions e2e/transform/cache/transformer.js
Expand Up @@ -6,11 +6,11 @@
*/

module.exports = {
process(src, path) {
process(code, path) {
if (path.includes('common')) {
console.log(path);
}

return src;
return {code};
},
};
Expand Up @@ -8,12 +8,12 @@
module.exports = {
canInstrument: true,
process(src, filename, options) {
src = `${src};\nglobalThis.__PREPROCESSED__ = true;`;
let code = `${src};\nglobalThis.__PREPROCESSED__ = true;`;

if (options.instrument) {
src = `${src};\nglobalThis.__INSTRUMENTED__ = true;`;
code = `${src};\nglobalThis.__INSTRUMENTED__ = true;`;
}

return src;
return {code};
},
};
4 changes: 2 additions & 2 deletions e2e/transform/esm-transformer/my-transform.mjs
Expand Up @@ -14,9 +14,9 @@ const fileToTransform = require.resolve('./module');
export default {
process(src, filepath) {
if (filepath === fileToTransform) {
return 'module.exports = 42;';
return {code: 'module.exports = 42;'};
}

return src;
return {code: src};
},
};
18 changes: 10 additions & 8 deletions e2e/transform/multiple-transformers/cssPreprocessor.js
Expand Up @@ -7,13 +7,15 @@

module.exports = {
process() {
return `
module.exports = {
root: 'App-root',
header: 'App-header',
logo: 'App-logo',
intro: 'App-intro',
};
`;
const code = `
module.exports = {
root: 'App-root',
header: 'App-header',
logo: 'App-logo',
intro: 'App-intro',
};
`;

return {code};
},
};
8 changes: 5 additions & 3 deletions e2e/transform/multiple-transformers/filePreprocessor.js
Expand Up @@ -9,8 +9,10 @@ const path = require('path');

module.exports = {
process(src, filename) {
return `
module.exports = '${path.basename(filename)}';
`;
const code = `
module.exports = '${path.basename(filename)}';
`;

return {code};
},
};
19 changes: 10 additions & 9 deletions e2e/typescript-coverage/typescriptPreprocessor.js
Expand Up @@ -8,18 +8,19 @@
const tsc = require('typescript');

module.exports = {
process(src, path) {
if (path.endsWith('.ts') || path.endsWith('.tsx')) {
return tsc.transpile(
src,
{
process(sourceText, fileName) {
if (fileName.endsWith('.ts') || fileName.endsWith('.tsx')) {
const {outputText, sourceMapText} = tsc.transpileModule(sourceText, {
SimenB marked this conversation as resolved.
Show resolved Hide resolved
compilerOptions: {
jsx: tsc.JsxEmit.React,
module: tsc.ModuleKind.CommonJS,
sourceMap: true, // if code is transformed, source map is necessary for coverage
},
path,
[],
);
fileName,
});

return {code: outputText, map: sourceMapText};
}
return src;
return {code: sourceText};
},
};