From 2e092b01235625c39a57c9b1e20f87eec97bf305 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Wed, 6 Apr 2022 13:59:43 +0300 Subject: [PATCH] feat(@jest/transform)!: require `process()` and `processAsync()` methods to always return structured data (#12638) --- .../react-native-npm-0.68.0-9eb3ecb60a.patch | 22 ++++- CHANGELOG.md | 1 + docs/CodeTransformation.md | 46 ++++++++--- docs/Configuration.md | 25 +++--- docs/Webpack.md | 25 +++--- e2e/__tests__/dependencyClash.test.ts | 10 ++- e2e/__tests__/multiProjectRunner.test.ts | 4 +- .../typescriptPreprocessor.js | 2 +- .../no-sourcemap/cssTransform.js | 2 +- .../typescriptPreprocessor.js | 2 +- .../transformer.js | 4 +- e2e/snapshot-serializers/transformer.js | 4 +- .../preprocessor.js | 23 +++--- e2e/stack-trace-source-maps/preprocessor.js | 23 +++--- e2e/transform-linked-modules/preprocessor.js | 2 +- .../async-transformer/my-transform.cjs | 12 +-- e2e/transform/cache/transformer.js | 4 +- .../preprocessor.js | 6 +- .../esm-transformer/my-transform.mjs | 4 +- .../multiple-transformers/cssPreprocessor.js | 18 +++-- .../multiple-transformers/filePreprocessor.js | 8 +- .../typescriptPreprocessor.js | 19 ++--- package.json | 2 +- packages/babel-jest/src/index.ts | 4 +- .../test_root/test_json_preprocessor.js | 2 +- .../__tests__/test_root/test_preprocessor.js | 4 +- .../jest-transform/src/ScriptTransformer.ts | 6 +- .../src/__tests__/ScriptTransformer.test.ts | 70 +++++++++------- .../ScriptTransformer.test.ts.snap | 80 +++++++++++++++++++ .../src/runtimeErrorsAndWarnings.ts | 6 +- packages/jest-transform/src/types.ts | 8 +- yarn.lock | 6 +- 32 files changed, 301 insertions(+), 153 deletions(-) rename patches/react-native.patch => .yarn/patches/react-native-npm-0.68.0-9eb3ecb60a.patch (50%) diff --git a/patches/react-native.patch b/.yarn/patches/react-native-npm-0.68.0-9eb3ecb60a.patch similarity index 50% rename from patches/react-native.patch rename to .yarn/patches/react-native-npm-0.68.0-9eb3ecb60a.patch index 0720ad2a58f4..bb6b711f1e52 100644 --- a/patches/react-native.patch +++ b/.yarn/patches/react-native-npm-0.68.0-9eb3ecb60a.patch @@ -1,8 +1,17 @@ diff --git a/jest/preprocessor.js b/jest/preprocessor.js -index 5920c0a6f23c056f27366fabf32dd13c6f86465b..2658e52f9127ac58849e7f830f6342d8b683672c 100644 +index f5e34763d840193e243a974e448b619f8f635095..cc6e05ab4c9c25a3d257379a1bf19c21fb9ef5ca 100644 --- a/jest/preprocessor.js +++ b/jest/preprocessor.js -@@ -64,8 +64,6 @@ module.exports = { +@@ -38,7 +38,7 @@ module.exports = { + sourceType: 'script', + ...nodeOptions, + ast: false, +- }).code; ++ }); + } + + const {ast} = transformer.transform({ +@@ -66,8 +66,6 @@ module.exports = { [require('@babel/plugin-transform-flow-strip-types')], [ require('@babel/plugin-proposal-class-properties'), @@ -11,3 +20,12 @@ index 5920c0a6f23c056f27366fabf32dd13c6f86465b..2658e52f9127ac58849e7f830f6342d8 ], [require('@babel/plugin-transform-computed-properties')], [require('@babel/plugin-transform-destructuring')], +@@ -112,7 +110,7 @@ module.exports = { + sourceMaps: true, + }, + src, +- ).code; ++ ); + }, + + getCacheKey: (createCacheKeyFunction([ diff --git a/CHANGELOG.md b/CHANGELOG.md index ce5503c5fcf3..96b32db89ccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/docs/CodeTransformation.md b/docs/CodeTransformation.md index 2393fac9928e..b94873c7eb0f 100644 --- a/docs/CodeTransformation.md +++ b/docs/CodeTransformation.md @@ -3,29 +3,36 @@ id: code-transformation title: Code Transformation --- -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. +Jest runs the code in your project as JavaScript, but if you use some syntax not supported by Node out of the box (such as JSX, TypeScript, Vue templates) then you'll need to transform that code into plain JavaScript, similar to what you would do when building for browsers. -Jest supports this via the [`transform` configuration option](Configuration.md#transform-objectstring-pathtotransformer--pathtotransformer-object). +Jest supports this via the [`transform`](Configuration.md#transform-objectstring-pathtotransformer--pathtotransformer-object) configuration option. -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. +A transformer is a module that provides a method 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 a code preprocessor that would transpile a future version of JavaScript to a current one. 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. ## Defaults -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). +Jest ships with one transformer out of the box – [`babel-jest`](https://github.com/facebook/jest/tree/main/packages/babel-jest#setup). It will load your project's Babel configuration and transform any file matching the `/\.[jt]sx?$/` RegExp (in other words, any `.js`, `.jsx`, `.ts` or `.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). -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. +:::tip + +Remember to include the default `babel-jest` transformer explicitly, if you wish to use it alongside with additional code preprocessors: + +```json +"transform": { + "\\.[jt]sx?$": "babel-jest", + "\\.css$": "some-css-transformer", +} +``` + +::: ## Writing custom transformers 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 { supportsDynamicImport: boolean; supportsExportNamespaceFrom: boolean; @@ -41,6 +48,11 @@ interface TransformOptions { transformerConfig: OptionType; } +type TransformedSource = { + code: string; + map?: RawSourceMap | string | null; +}; + interface SyncTransformer { canInstrument?: boolean; @@ -111,6 +123,12 @@ type TransformerFactory = { }; ``` +:::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). + +::: + 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. @@ -125,7 +143,9 @@ 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. + +During the development of a transformer it can be useful to run Jest with `--no-cache` to frequently [delete cache](Troubleshooting.md#caching-issues). ::: @@ -143,8 +163,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))};`, + }; }, }; ``` diff --git a/docs/Configuration.md b/docs/Configuration.md index 277cf0b8f990..ab5dc416f632 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1564,27 +1564,26 @@ Default timeout of a test in milliseconds. Default: `{"\\.[jt]sx?$": "babel-jest"}` -A map from regular expressions to paths to transformers. 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. Example: see the [examples/typescript](https://github.com/facebook/jest/blob/main/examples/typescript/package.json#L16) example or the [webpack tutorial](Webpack.md). +A map from regular expressions to paths to transformers. Optionally, a tuple with configuration options can be passed as second argument: `{filePattern: ['path-to-transformer', {options}]}`. For example, here is how you can configure `babel-jest` for non-default behavior: `{'\\.js$': ['babel-jest', {rootMode: 'upward'}]}`. -Examples of such compilers include: +Jest runs the code of your project as JavaScript, hence a transformer is needed if you use some syntax not supported by Node out of the box (such as JSX, TypeScript, Vue templates). By default, Jest will use [`babel-jest`](https://github.com/facebook/jest/tree/main/packages/babel-jest#setup) transformer, which will load your project's Babel configuration and transform any file matching the `/\.[jt]sx?$/` RegExp (in other words, any `.js`, `.jsx`, `.ts` or `.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). -- [Babel](https://babeljs.io/) -- [TypeScript](http://www.typescriptlang.org/) -- To build your own please visit the [Custom Transformer](CodeTransformation.md#writing-custom-transformers) section - -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"}]}` +See the [Code Transformation](CodeTransformation.md) section for more details and instructions on building your own transformer. :::tip -A transformer is only run once per file unless the file has changed. During the development of a transformer it can be useful to run Jest with `--no-cache` to frequently [delete Jest's cache](Troubleshooting.md#caching-issues). - -When adding additional code transformers, this will overwrite the default config and `babel-jest` is no longer automatically loaded. If you want to use it to compile JavaScript or TypeScript, it has to be explicitly defined by adding `{"\\.[jt]sx?$": "babel-jest"}` to the transform property. See [babel-jest plugin](https://github.com/facebook/jest/tree/main/packages/babel-jest#setup). +Keep in mind that a transformer only runs once per file unless the file has changed. -::: +Remember to include the default `babel-jest` transformer explicitly, if you wish to use it alongside with additional code preprocessors: -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. +```json +"transform": { + "\\.[jt]sx?$": "babel-jest", + "\\.css$": "some-css-transformer", +} +``` -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>] diff --git a/docs/Webpack.md b/docs/Webpack.md index e02c1d323db5..533e5b653ac1 100644 --- a/docs/Webpack.md +++ b/docs/Webpack.md @@ -83,16 +83,16 @@ Then all your className lookups on the styles object will be returned as-is (e.g } ``` -> Notice that Proxy is enabled in Node 6 by default. If you are not on Node 6 yet, make sure you invoke Jest using `node --harmony_proxies node_modules/.bin/jest`. - If `moduleNameMapper` cannot fulfill your requirements, you can use Jest's [`transform`](Configuration.md#transform-objectstring-pathtotransformer--pathtotransformer-object) config option to specify how assets are transformed. For example, a transformer that returns the basename of a file (such that `require('logo.jpg');` returns `'logo'`) can be written as: ```js title="fileTransformer.js" 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))};`, + }; }, }; ``` @@ -112,16 +112,19 @@ module.exports = { We've told Jest to ignore files matching a stylesheet or image extension, and instead, require our mock files. You can adjust the regular expression to match the file types your webpack config handles. -_Note: if you are using babel-jest with additional code preprocessors, you have to explicitly define babel-jest as a transformer for your JavaScript code to map `.js` files to the babel-jest module._ +:::tip + +Remember to include the default `babel-jest` transformer explicitly, if you wish to use it alongside with additional code preprocessors: ```json "transform": { - "\\.js$": "babel-jest", - "\\.css$": "custom-transformer", - ... + "\\.[jt]sx?$": "babel-jest", + "\\.css$": "some-css-transformer", } ``` +::: + ### Configuring Jest to find our files Now that Jest knows how to process our files, we need to tell it how to _find_ them. For webpack's `modulesDirectories`, and `extensions` options there are direct analogs in Jest's `moduleDirectories` and `moduleFileExtensions` options. @@ -186,8 +189,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}]], @@ -203,8 +205,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}]], diff --git a/e2e/__tests__/dependencyClash.test.ts b/e2e/__tests__/dependencyClash.test.ts index d6ea667631a1..98fc1267e453 100644 --- a/e2e/__tests__/dependencyClash.test.ts +++ b/e2e/__tests__/dependencyClash.test.ts @@ -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', + '', + ), + }; }, }; `, diff --git a/e2e/__tests__/multiProjectRunner.test.ts b/e2e/__tests__/multiProjectRunner.test.ts index b4b42588ccf8..fc17f55e931c 100644 --- a/e2e/__tests__/multiProjectRunner.test.ts +++ b/e2e/__tests__/multiProjectRunner.test.ts @@ -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', } `, @@ -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', } `, diff --git a/e2e/coverage-provider-v8/esm-with-custom-transformer/typescriptPreprocessor.js b/e2e/coverage-provider-v8/esm-with-custom-transformer/typescriptPreprocessor.js index 3fec16a6891b..1fdcb803c166 100644 --- a/e2e/coverage-provider-v8/esm-with-custom-transformer/typescriptPreprocessor.js +++ b/e2e/coverage-provider-v8/esm-with-custom-transformer/typescriptPreprocessor.js @@ -21,6 +21,6 @@ export default { return {code: outputText, map: sourceMapText}; } - return sourceText; + return {code: sourceText}; }, }; diff --git a/e2e/coverage-provider-v8/no-sourcemap/cssTransform.js b/e2e/coverage-provider-v8/no-sourcemap/cssTransform.js index c973ad34fe44..4772932d79dd 100644 --- a/e2e/coverage-provider-v8/no-sourcemap/cssTransform.js +++ b/e2e/coverage-provider-v8/no-sourcemap/cssTransform.js @@ -7,5 +7,5 @@ module.exports = { getCacheKey: () => 'cssTransform', - process: () => 'module.exports = {};', + process: () => ({code: 'module.exports = {};'}), }; diff --git a/e2e/coverage-remapping/typescriptPreprocessor.js b/e2e/coverage-remapping/typescriptPreprocessor.js index dd8575ce8cba..25b9a60dab28 100644 --- a/e2e/coverage-remapping/typescriptPreprocessor.js +++ b/e2e/coverage-remapping/typescriptPreprocessor.js @@ -22,6 +22,6 @@ module.exports = { map: JSON.parse(result.sourceMapText), }; } - return src; + return {code: src}; }, }; diff --git a/e2e/global-setup-custom-transform/transformer.js b/e2e/global-setup-custom-transform/transformer.js index 178565a491e2..77db60a2742d 100644 --- a/e2e/global-setup-custom-transform/transformer.js +++ b/e2e/global-setup-custom-transform/transformer.js @@ -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}; }, }; diff --git a/e2e/snapshot-serializers/transformer.js b/e2e/snapshot-serializers/transformer.js index 5606ac36381f..44d68384a734 100644 --- a/e2e/snapshot-serializers/transformer.js +++ b/e2e/snapshot-serializers/transformer.js @@ -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}; }, }; diff --git a/e2e/stack-trace-source-maps-with-coverage/preprocessor.js b/e2e/stack-trace-source-maps-with-coverage/preprocessor.js index 133d42ec44a2..ded4db6d8929 100644 --- a/e2e/stack-trace-source-maps-with-coverage/preprocessor.js +++ b/e2e/stack-trace-source-maps-with-coverage/preprocessor.js @@ -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}; }, }; diff --git a/e2e/stack-trace-source-maps/preprocessor.js b/e2e/stack-trace-source-maps/preprocessor.js index 133d42ec44a2..ded4db6d8929 100644 --- a/e2e/stack-trace-source-maps/preprocessor.js +++ b/e2e/stack-trace-source-maps/preprocessor.js @@ -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}; }, }; diff --git a/e2e/transform-linked-modules/preprocessor.js b/e2e/transform-linked-modules/preprocessor.js index acf4723313d1..67c48b478bc3 100644 --- a/e2e/transform-linked-modules/preprocessor.js +++ b/e2e/transform-linked-modules/preprocessor.js @@ -7,6 +7,6 @@ module.exports = { process() { - return 'module.exports = "transformed"'; + return {code: 'module.exports = "transformed"'}; }, }; diff --git a/e2e/transform/async-transformer/my-transform.cjs b/e2e/transform/async-transformer/my-transform.cjs index 9f9b4a91398f..e1c13f1171a3 100644 --- a/e2e/transform/async-transformer/my-transform.cjs +++ b/e2e/transform/async-transformer/my-transform.cjs @@ -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', + ), + }; }, }; diff --git a/e2e/transform/cache/transformer.js b/e2e/transform/cache/transformer.js index 10d02e637f80..84b368ab9b57 100644 --- a/e2e/transform/cache/transformer.js +++ b/e2e/transform/cache/transformer.js @@ -6,11 +6,11 @@ */ module.exports = { - process(src, path) { + process(code, path) { if (path.includes('common')) { console.log(path); } - return src; + return {code}; }, }; diff --git a/e2e/transform/custom-instrumenting-preprocessor/preprocessor.js b/e2e/transform/custom-instrumenting-preprocessor/preprocessor.js index 99e12ff008e4..bb1dbf38c648 100644 --- a/e2e/transform/custom-instrumenting-preprocessor/preprocessor.js +++ b/e2e/transform/custom-instrumenting-preprocessor/preprocessor.js @@ -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}; }, }; diff --git a/e2e/transform/esm-transformer/my-transform.mjs b/e2e/transform/esm-transformer/my-transform.mjs index 241e27ee29db..96d8c23058f3 100644 --- a/e2e/transform/esm-transformer/my-transform.mjs +++ b/e2e/transform/esm-transformer/my-transform.mjs @@ -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}; }, }; diff --git a/e2e/transform/multiple-transformers/cssPreprocessor.js b/e2e/transform/multiple-transformers/cssPreprocessor.js index 5ca52dd30e54..6e5da5fce882 100644 --- a/e2e/transform/multiple-transformers/cssPreprocessor.js +++ b/e2e/transform/multiple-transformers/cssPreprocessor.js @@ -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}; }, }; diff --git a/e2e/transform/multiple-transformers/filePreprocessor.js b/e2e/transform/multiple-transformers/filePreprocessor.js index c49b641e62d2..b419a52779c8 100644 --- a/e2e/transform/multiple-transformers/filePreprocessor.js +++ b/e2e/transform/multiple-transformers/filePreprocessor.js @@ -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}; }, }; diff --git a/e2e/typescript-coverage/typescriptPreprocessor.js b/e2e/typescript-coverage/typescriptPreprocessor.js index 8633bdab5a95..251c68f5e38f 100644 --- a/e2e/typescript-coverage/typescriptPreprocessor.js +++ b/e2e/typescript-coverage/typescriptPreprocessor.js @@ -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, { + 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}; }, }; diff --git a/package.json b/package.json index 3dd98afc3ab7..64974a50de2b 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ "babel-jest": "workspace:*", "jest": "workspace:*", "jest-environment-node": "workspace:*", - "react-native": "patch:react-native@0.68.0#./patches/react-native.patch" + "react-native": "patch:react-native@npm:0.68.0#.yarn/patches/react-native-npm-0.68.0-9eb3ecb60a.patch" }, "packageManager": "yarn@3.2.0" } diff --git a/packages/babel-jest/src/index.ts b/packages/babel-jest/src/index.ts index ff5fc8b85ce6..b48ebe71a40c 100644 --- a/packages/babel-jest/src/index.ts +++ b/packages/babel-jest/src/index.ts @@ -244,7 +244,7 @@ export const createTransformer: TransformerCreator< } } - return sourceText; + return {code: sourceText}; }, async processAsync(sourceText, sourcePath, transformOptions) { const babelOptions = await loadBabelOptionsAsync( @@ -266,7 +266,7 @@ export const createTransformer: TransformerCreator< } } - return sourceText; + return {code: sourceText}; }, }; }; diff --git a/packages/jest-runtime/src/__tests__/test_root/test_json_preprocessor.js b/packages/jest-runtime/src/__tests__/test_root/test_json_preprocessor.js index ec9b48f69cc2..7597efa50e3f 100644 --- a/packages/jest-runtime/src/__tests__/test_root/test_json_preprocessor.js +++ b/packages/jest-runtime/src/__tests__/test_root/test_json_preprocessor.js @@ -10,5 +10,5 @@ module.exports.process = source => { const json = JSON.parse(source); Object.keys(json).forEach(k => (json[k] = k)); - return JSON.stringify(json); + return {code: JSON.stringify(json)}; }; diff --git a/packages/jest-runtime/src/__tests__/test_root/test_preprocessor.js b/packages/jest-runtime/src/__tests__/test_root/test_preprocessor.js index cf5e591fe05b..f5ce630fc144 100644 --- a/packages/jest-runtime/src/__tests__/test_root/test_preprocessor.js +++ b/packages/jest-runtime/src/__tests__/test_root/test_preprocessor.js @@ -7,4 +7,6 @@ 'use strict'; -module.exports.process = () => "throw new Error('preprocessor must not run.');"; +module.exports.process = () => ({ + code: "throw new Error('preprocessor must not run.');", +}); diff --git a/packages/jest-transform/src/ScriptTransformer.ts b/packages/jest-transform/src/ScriptTransformer.ts index 4959c200d5cc..3472042669b4 100644 --- a/packages/jest-transform/src/ScriptTransformer.ts +++ b/packages/jest-transform/src/ScriptTransformer.ts @@ -379,9 +379,7 @@ class ScriptTransformer { }; if (transformer && shouldCallTransform) { - if (typeof processed === 'string') { - transformed.code = processed; - } else if (processed != null && typeof processed.code === 'string') { + if (processed != null && typeof processed.code === 'string') { transformed = processed; } else { throw new Error(makeInvalidReturnValueError()); @@ -483,7 +481,7 @@ class ScriptTransformer { }; } - let processed = null; + let processed: TransformedSource | null = null; let shouldCallTransform = false; diff --git a/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts b/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts index 3e3533f781ce..7ca030ca726b 100644 --- a/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts +++ b/packages/jest-transform/src/__tests__/ScriptTransformer.test.ts @@ -12,7 +12,7 @@ import type {Options, ShouldInstrumentOptions, Transformer} from '../types'; jest .mock('graceful-fs', () => ({ - ...jest.requireActual('graceful-fs'), + ...jest.requireActual('graceful-fs'), /* eslint-disable sort-keys */ readFileSync: jest.fn((path, options) => { mockInvariant(typeof path === 'string'); @@ -55,7 +55,7 @@ jest }, })) .mock('jest-util', () => ({ - ...jest.requireActual('jest-util'), + ...jest.requireActual('jest-util'), createDirectory: jest.fn(), })) .mock('path', () => jest.requireActual('path').posix); @@ -67,13 +67,15 @@ jest.mock( const transformer: Transformer = { getCacheKey: jest.fn(() => 'ab'), - process: (content, filename, config) => require('dedent')` + process: (content, filename, config) => ({ + code: require('dedent')` const TRANSFORMED = { filename: '${escapeStrings(filename)}', script: '${escapeStrings(content)}', config: '${escapeStrings(JSON.stringify(config))}', }; `, + }), }; return transformer; @@ -88,14 +90,15 @@ jest.mock( const transformer: Transformer = { getCacheKeyAsync: jest.fn().mockResolvedValue('ab'), - processAsync: async (content, filename, config) => - require('dedent')` + processAsync: async (content, filename, config) => ({ + code: require('dedent')` const TRANSFORMED = { filename: '${escapeStrings(filename)}', script: '${escapeStrings(content)}', config: '${escapeStrings(JSON.stringify(config))}', }; `, + }), }; return transformer; @@ -107,7 +110,7 @@ jest.mock( 'configureable-preprocessor', () => ({ createTransformer: jest.fn(() => ({ - process: jest.fn(() => 'processedCode'), + process: jest.fn().mockReturnValue({code: 'processedCode'}), })), }), {virtual: true}, @@ -117,7 +120,7 @@ jest.mock( 'cache_fs_preprocessor', () => ({ getCacheKey: jest.fn(() => 'ab'), - process: jest.fn(() => 'processedCode'), + process: jest.fn().mockReturnValue({code: 'processedCode'}), }), {virtual: true}, ); @@ -126,7 +129,7 @@ jest.mock( 'cache_fs_async_preprocessor', () => ({ getCacheKeyAsync: jest.fn().mockResolvedValue('ab'), - processAsync: jest.fn().mockResolvedValue('processedCode'), + processAsync: jest.fn().mockResolvedValue({code: 'processedCode'}), }), {virtual: true}, ); @@ -154,12 +157,14 @@ jest.mock( () => { const transformer: Transformer = { getCacheKey: jest.fn(() => 'cd'), - process: (content, filename) => jest.requireActual('dedent')` + process: (content, filename) => ({ + code: require('dedent')` module.exports = { filename: ${filename}, rawFirstLine: ${content.split('\n')[0]}, }; `, + }), }; return transformer; @@ -177,14 +182,14 @@ jest.mock('skipped-required-props-preprocessor', () => ({}), {virtual: true}); // Bad preprocessor jest.mock( 'skipped-required-props-preprocessor-only-sync', - () => ({process: () => ''}), + () => ({process: () => ({code: ''})}), {virtual: true}, ); // Bad preprocessor jest.mock( 'skipped-required-props-preprocessor-only-async', - () => ({processAsync: async () => ''}), + () => ({processAsync: async () => ({code: ''})}), {virtual: true}, ); @@ -203,7 +208,7 @@ jest.mock( 'skipped-process-method-preprocessor', () => ({ createTransformer() { - return {process: jest.fn(() => 'code')}; + return {process: jest.fn().mockReturnValue({code: 'code'})}; }, }), {virtual: true}, @@ -213,7 +218,7 @@ jest.mock( 'factory-for-async-preprocessor', () => ({ createTransformer() { - return {processAsync: jest.fn().mockResolvedValue('code')}; + return {processAsync: jest.fn().mockResolvedValue({code: 'code'})}; }, }), {virtual: true}, @@ -376,7 +381,7 @@ describe('ScriptTransformer', () => { ); }); - it("throws an error if `process` doesn't return a string or an object containing `code` key with processed string", async () => { + it("throws an error if `process` doesn't return an object containing `code` key with processed string", async () => { config = { ...config, transform: [['\\.js$', 'passthrough-preprocessor', {}]], @@ -385,6 +390,7 @@ describe('ScriptTransformer', () => { const incorrectReturnValues = [ [undefined, '/fruits/banana.js'], + ['code', '/fruits/banana.js'], [{a: 'a'}, '/fruits/kiwi.js'], [[], '/fruits/grapefruit.js'], ]; @@ -394,13 +400,10 @@ describe('ScriptTransformer', () => { require('passthrough-preprocessor').process.mockReturnValue(returnValue); expect(() => scriptTransformer.transform(filePath, getCoverageOptions()), - ).toThrow('must return a string'); + ).toThrowErrorMatchingSnapshot(); }); - const correctReturnValues = [ - ['code', '/fruits/banana.js'], - [{code: 'code'}, '/fruits/kiwi.js'], - ]; + const correctReturnValues = [[{code: 'code'}, '/fruits/kiwi.js']]; correctReturnValues.forEach(([returnValue, filePath]) => { mockInvariant(typeof filePath === 'string'); @@ -411,15 +414,15 @@ describe('ScriptTransformer', () => { }); }); - it("throws an error if `processAsync` doesn't return a promise of string or object containing `code` key with processed string", async () => { + it("throws an error if `processAsync` doesn't return a promise of object containing `code` key with processed string", async () => { const incorrectReturnValues: Array<[any, string]> = [ [undefined, '/fruits/banana.js'], + ['code', '/fruits/avocado.js'], [{a: 'a'}, '/fruits/kiwi.js'], [[], '/fruits/grapefruit.js'], ]; const correctReturnValues: Array<[any, string]> = [ - ['code', '/fruits/avocado.js'], [{code: 'code'}, '/fruits/mango.js'], ]; @@ -453,10 +456,7 @@ describe('ScriptTransformer', () => { const promisesToReject = incorrectReturnValues .map(buildPromise) - .map(promise => - // Jest must throw error - expect(promise).rejects.toThrow(), - ); + .map(promise => expect(promise).rejects.toThrowErrorMatchingSnapshot()); const promisesToResolve = correctReturnValues .map(buildPromise) @@ -790,7 +790,9 @@ describe('ScriptTransformer', () => { sourceMap, ).toString('base64')}`; - require('preprocessor-with-sourcemaps').process.mockReturnValue(content); + require('preprocessor-with-sourcemaps').process.mockReturnValue({ + code: content, + }); const result = scriptTransformer.transform( '/fruits/banana.js', @@ -823,7 +825,9 @@ describe('ScriptTransformer', () => { sourceMap, ).toString('base64')}`; - require('preprocessor-with-sourcemaps').process.mockReturnValue(content); + require('preprocessor-with-sourcemaps').process.mockReturnValue({ + code: content, + }); const result = await scriptTransformer.transformAsync( '/fruits/banana.js', @@ -857,7 +861,7 @@ describe('ScriptTransformer', () => { ).toString('base64')}`; require('async-preprocessor-with-sourcemaps').processAsync.mockResolvedValue( - content, + {code: content}, ); const result = await scriptTransformer.transformAsync( @@ -897,7 +901,9 @@ describe('ScriptTransformer', () => { .toString('base64') .slice(0, 16)}`; - require('preprocessor-with-sourcemaps').process.mockReturnValue(content); + require('preprocessor-with-sourcemaps').process.mockReturnValue({ + code: content, + }); const result = scriptTransformer.transform( '/fruits/banana.js', @@ -935,7 +941,9 @@ describe('ScriptTransformer', () => { .toString('base64') .slice(0, 16)}`; - require('preprocessor-with-sourcemaps').process.mockReturnValue(content); + require('preprocessor-with-sourcemaps').process.mockReturnValue({ + code: content, + }); const result = await scriptTransformer.transformAsync( '/fruits/banana.js', @@ -974,7 +982,7 @@ describe('ScriptTransformer', () => { .slice(0, 16)}`; require('async-preprocessor-with-sourcemaps').processAsync.mockResolvedValue( - content, + {code: content}, ); const result = await scriptTransformer.transformAsync( diff --git a/packages/jest-transform/src/__tests__/__snapshots__/ScriptTransformer.test.ts.snap b/packages/jest-transform/src/__tests__/__snapshots__/ScriptTransformer.test.ts.snap index 582da17801e8..1749735a183f 100644 --- a/packages/jest-transform/src/__tests__/__snapshots__/ScriptTransformer.test.ts.snap +++ b/packages/jest-transform/src/__tests__/__snapshots__/ScriptTransformer.test.ts.snap @@ -356,6 +356,86 @@ exports[`ScriptTransformer passes expected transform options to getCacheKeyAsync } `; +exports[`ScriptTransformer throws an error if \`process\` doesn't return an object containing \`code\` key with processed string 1`] = ` +"● Invalid return value: + Code transformer's \`process\` method must return an object containing \`code\` key + with processed string. If \`processAsync\` method is implemented it must return + a Promise resolving to an object containing \`code\` key with processed string. + Code Transformation Documentation: + https://jestjs.io/docs/code-transformation +" +`; + +exports[`ScriptTransformer throws an error if \`process\` doesn't return an object containing \`code\` key with processed string 2`] = ` +"● Invalid return value: + Code transformer's \`process\` method must return an object containing \`code\` key + with processed string. If \`processAsync\` method is implemented it must return + a Promise resolving to an object containing \`code\` key with processed string. + Code Transformation Documentation: + https://jestjs.io/docs/code-transformation +" +`; + +exports[`ScriptTransformer throws an error if \`process\` doesn't return an object containing \`code\` key with processed string 3`] = ` +"● Invalid return value: + Code transformer's \`process\` method must return an object containing \`code\` key + with processed string. If \`processAsync\` method is implemented it must return + a Promise resolving to an object containing \`code\` key with processed string. + Code Transformation Documentation: + https://jestjs.io/docs/code-transformation +" +`; + +exports[`ScriptTransformer throws an error if \`process\` doesn't return an object containing \`code\` key with processed string 4`] = ` +"● Invalid return value: + Code transformer's \`process\` method must return an object containing \`code\` key + with processed string. If \`processAsync\` method is implemented it must return + a Promise resolving to an object containing \`code\` key with processed string. + Code Transformation Documentation: + https://jestjs.io/docs/code-transformation +" +`; + +exports[`ScriptTransformer throws an error if \`processAsync\` doesn't return a promise of object containing \`code\` key with processed string 1`] = ` +"● Invalid return value: + Code transformer's \`process\` method must return an object containing \`code\` key + with processed string. If \`processAsync\` method is implemented it must return + a Promise resolving to an object containing \`code\` key with processed string. + Code Transformation Documentation: + https://jestjs.io/docs/code-transformation +" +`; + +exports[`ScriptTransformer throws an error if \`processAsync\` doesn't return a promise of object containing \`code\` key with processed string 2`] = ` +"● Invalid return value: + Code transformer's \`process\` method must return an object containing \`code\` key + with processed string. If \`processAsync\` method is implemented it must return + a Promise resolving to an object containing \`code\` key with processed string. + Code Transformation Documentation: + https://jestjs.io/docs/code-transformation +" +`; + +exports[`ScriptTransformer throws an error if \`processAsync\` doesn't return a promise of object containing \`code\` key with processed string 3`] = ` +"● Invalid return value: + Code transformer's \`process\` method must return an object containing \`code\` key + with processed string. If \`processAsync\` method is implemented it must return + a Promise resolving to an object containing \`code\` key with processed string. + Code Transformation Documentation: + https://jestjs.io/docs/code-transformation +" +`; + +exports[`ScriptTransformer throws an error if \`processAsync\` doesn't return a promise of object containing \`code\` key with processed string 4`] = ` +"● Invalid return value: + Code transformer's \`process\` method must return an object containing \`code\` key + with processed string. If \`processAsync\` method is implemented it must return + a Promise resolving to an object containing \`code\` key with processed string. + Code Transformation Documentation: + https://jestjs.io/docs/code-transformation +" +`; + exports[`ScriptTransformer throws an error if createTransformer returns object without \`process\` method 1`] = ` "● Invalid transformer module: "skipped-required-create-transformer-props-preprocessor" specified in the "transform" object of Jest configuration diff --git a/packages/jest-transform/src/runtimeErrorsAndWarnings.ts b/packages/jest-transform/src/runtimeErrorsAndWarnings.ts index 1b31299130ef..c57292ac5b14 100644 --- a/packages/jest-transform/src/runtimeErrorsAndWarnings.ts +++ b/packages/jest-transform/src/runtimeErrorsAndWarnings.ts @@ -19,9 +19,9 @@ export const makeInvalidReturnValueError = (): string => chalk.red( [ chalk.bold(`${BULLET}Invalid return value:`), - " Code transformer's `process` function must return a string or an object", - ' with `code` key containing a string. If `processAsync` function is implemented,', - ' it must return a Promise resolving to one of these values.', + " Code transformer's `process` method must return an object containing `code` key ", + ' with processed string. If `processAsync` method is implemented it must return ', + ' a Promise resolving to an object containing `code` key with processed string.', '', ].join('\n') + DOCUMENTATION_NOTE, ); diff --git a/packages/jest-transform/src/types.ts b/packages/jest-transform/src/types.ts index 85bb4a31ba54..543583477a6a 100644 --- a/packages/jest-transform/src/types.ts +++ b/packages/jest-transform/src/types.ts @@ -31,10 +31,10 @@ interface FixedRawSourceMap extends Omit { version: number; } -// TODO: For Jest 26 normalize this (always structured data, never a string) -export type TransformedSource = - | {code: string; map?: FixedRawSourceMap | string | null} - | string; +export type TransformedSource = { + code: string; + map?: FixedRawSourceMap | string | null; +}; export type TransformResult = TransformTypes.TransformResult; diff --git a/yarn.lock b/yarn.lock index 9cdc0bab46f4..a26e933d6633 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18672,9 +18672,9 @@ __metadata: languageName: node linkType: hard -"react-native@patch:react-native@0.68.0#./patches/react-native.patch::locator=%40jest%2Fmonorepo%40workspace%3A.": +"react-native@patch:react-native@npm:0.68.0#.yarn/patches/react-native-npm-0.68.0-9eb3ecb60a.patch::locator=%40jest%2Fmonorepo%40workspace%3A.": version: 0.68.0 - resolution: "react-native@patch:react-native@npm%3A0.68.0#./patches/react-native.patch::version=0.68.0&hash=5ca1dc&locator=%40jest%2Fmonorepo%40workspace%3A." + resolution: "react-native@patch:react-native@npm%3A0.68.0#.yarn/patches/react-native-npm-0.68.0-9eb3ecb60a.patch::version=0.68.0&hash=c5a757&locator=%40jest%2Fmonorepo%40workspace%3A." dependencies: "@jest/create-cache-key-function": ^27.0.1 "@react-native-community/cli": ^7.0.3 @@ -18712,7 +18712,7 @@ __metadata: react: 17.0.2 bin: react-native: cli.js - checksum: f953ba6590354181e1331466c54b3d90d2d0a4b60c077808cd23b9c88b6008f332078e5541ad69a7ca5c66123cdda4ec8ea085bcfa9d61e28535d2233059cf96 + checksum: 14f503f007e6372c9c31c397df1cc6ecc581a597a8752009721402c132905aa9381d1bb5459da8862dde498348eb0c18a9e93ce1ee911a4947ff01cc5df9928d languageName: node linkType: hard