diff --git a/CHANGELOG.md b/CHANGELOG.md index 2305497c06f0..24c1e45f2734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[babel-jest]` Add async transformation ([#11192](https://github.com/facebook/jest/pull/11192)) - `[jest-changed-files]` Use '--' to separate paths from revisions ([#11160](https://github.com/facebook/jest/pull/11160)) - `[jest-circus]` [**BREAKING**] Fail tests when multiple `done()` calls are made ([#10624](https://github.com/facebook/jest/pull/10624)) - `[jest-circus, jest-jasmine2]` [**BREAKING**] Fail the test instead of just warning when describe returns a value ([#10947](https://github.com/facebook/jest/pull/10947)) diff --git a/e2e/__tests__/__snapshots__/transform.test.ts.snap b/e2e/__tests__/__snapshots__/transform.test.ts.snap index c7310e6bfd8a..c2c46c8f8105 100644 --- a/e2e/__tests__/__snapshots__/transform.test.ts.snap +++ b/e2e/__tests__/__snapshots__/transform.test.ts.snap @@ -6,7 +6,7 @@ FAIL __tests__/ignoredFile.test.js babel-jest: Babel ignores __tests__/ignoredFile.test.js - make sure to include the file in Jest's transformIgnorePatterns as well. - at loadBabelConfig (../../../packages/babel-jest/build/index.js:195:13) + at assertLoadedBabelConfig (../../../packages/babel-jest/build/index.js:130:11) `; exports[`babel-jest instruments only specific files and collects coverage 1`] = ` diff --git a/e2e/__tests__/transform.test.ts b/e2e/__tests__/transform.test.ts index a325a7a4e7f9..e58da7e802b2 100644 --- a/e2e/__tests__/transform.test.ts +++ b/e2e/__tests__/transform.test.ts @@ -264,4 +264,21 @@ onNodeVersions('^12.17.0 || >=13.2.0', () => { expect(json.numPassedTests).toBe(1); }); }); + + describe('babel-jest-async', () => { + const dir = path.resolve(__dirname, '../transform/babel-jest-async'); + + beforeAll(() => { + runYarnInstall(dir); + }); + + it("should use babel-jest's 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); + }); + }); }); diff --git a/e2e/transform/babel-jest-async/__tests__/babelJest.test.js b/e2e/transform/babel-jest-async/__tests__/babelJest.test.js new file mode 100644 index 000000000000..2a68c85ba88c --- /dev/null +++ b/e2e/transform/babel-jest-async/__tests__/babelJest.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 nullReturningFunc from '../only-file-to-transform.js'; + +it('strips flowtypes using babel-jest', () => { + expect(nullReturningFunc()).toBe(null); +}); diff --git a/e2e/transform/babel-jest-async/only-file-to-transform.js b/e2e/transform/babel-jest-async/only-file-to-transform.js new file mode 100644 index 000000000000..058eea6bdaae --- /dev/null +++ b/e2e/transform/babel-jest-async/only-file-to-transform.js @@ -0,0 +1,10 @@ +/** + * 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. + */ + +const someFunction = (): null => null; + +export default someFunction; diff --git a/e2e/transform/babel-jest-async/package.json b/e2e/transform/babel-jest-async/package.json new file mode 100644 index 000000000000..8a506ac99e15 --- /dev/null +++ b/e2e/transform/babel-jest-async/package.json @@ -0,0 +1,12 @@ +{ + "type": "module", + "dependencies": { + "@babel/preset-flow": "^7.0.0" + }, + "jest": { + "testEnvironment": "node", + "transform": { + "only-file-to-transform\\.js$": "/transformer.js" + } + } +} diff --git a/e2e/transform/babel-jest-async/transformer.js b/e2e/transform/babel-jest-async/transformer.js new file mode 100644 index 000000000000..9bc35a2a9ad1 --- /dev/null +++ b/e2e/transform/babel-jest-async/transformer.js @@ -0,0 +1,19 @@ +/** + * 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 {fileURLToPath} from 'url'; +import babelJest from 'babel-jest'; + +export default { + ...babelJest.default.createTransformer({ + presets: ['@babel/preset-flow'], + root: fileURLToPath(import.meta.url), + }), + // remove the synchronous functions + getCacheKey: undefined, + process: undefined, +}; diff --git a/e2e/transform/babel-jest-async/yarn.lock b/e2e/transform/babel-jest-async/yarn.lock new file mode 100644 index 000000000000..a54bf6305382 --- /dev/null +++ b/e2e/transform/babel-jest-async/yarn.lock @@ -0,0 +1,56 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 4 + cacheKey: 7 + +"@babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.13.0": + version: 7.13.0 + resolution: "@babel/helper-plugin-utils@npm:7.13.0" + checksum: 229ac1917b43ad38732d2d4a9a826f87d8945719249efe1d6191f3e25ba6027a289af70380d82d62a03fc9e82558a0ea6f12739cbb55b64bb280d6b511b4ca65 + languageName: node + linkType: hard + +"@babel/plugin-syntax-flow@npm:^7.12.13": + version: 7.12.13 + resolution: "@babel/plugin-syntax-flow@npm:7.12.13" + dependencies: + "@babel/helper-plugin-utils": ^7.12.13 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: de8845354dda62b7857a518a54f85bf30809ed1d7cc5ace93ced6da16d095cba78487d18651f1b2277db58d8e749cb910c703f96529af198369226e374df5f73 + languageName: node + linkType: hard + +"@babel/plugin-transform-flow-strip-types@npm:^7.12.13": + version: 7.13.0 + resolution: "@babel/plugin-transform-flow-strip-types@npm:7.13.0" + dependencies: + "@babel/helper-plugin-utils": ^7.13.0 + "@babel/plugin-syntax-flow": ^7.12.13 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 60903f5e3619b4f4a19d6d00a4d10c5b97566f5d4c56dd35ccdaa6e621fc955ec4003f12cd73ec99475894a7eca6a34aa4b38f87c7c81e93d5fe03d006aae77b + languageName: node + linkType: hard + +"@babel/preset-flow@npm:^7.0.0": + version: 7.12.13 + resolution: "@babel/preset-flow@npm:7.12.13" + dependencies: + "@babel/helper-plugin-utils": ^7.12.13 + "@babel/plugin-transform-flow-strip-types": ^7.12.13 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 47fe1001194a57fbdb33250adcb4c3aa9ff551cfb4eea1a16b123ff5fe78730a7ebfb839bacbe18390fc50fa4bf67fdd5293703b859876de45d52f50d4da0d44 + languageName: node + linkType: hard + +"root-workspace-0b6124@workspace:.": + version: 0.0.0-use.local + resolution: "root-workspace-0b6124@workspace:." + dependencies: + "@babel/preset-flow": ^7.0.0 + languageName: unknown + linkType: soft diff --git a/packages/babel-jest/package.json b/packages/babel-jest/package.json index 077b6c00048f..0955d7a3f419 100644 --- a/packages/babel-jest/package.json +++ b/packages/babel-jest/package.json @@ -30,7 +30,7 @@ "@types/graceful-fs": "^4.1.3" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.8.0" }, "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" diff --git a/packages/babel-jest/src/__tests__/index.ts b/packages/babel-jest/src/__tests__/index.ts index 9d79de986d36..7d3db19bdc7a 100644 --- a/packages/babel-jest/src/__tests__/index.ts +++ b/packages/babel-jest/src/__tests__/index.ts @@ -14,6 +14,9 @@ jest.mock('../loadBabelConfig', () => { return { loadPartialConfig: jest.fn((...args) => actual.loadPartialConfig(...args)), + loadPartialConfigAsync: jest.fn((...args) => + actual.loadPartialConfigAsync(...args), + ), }; }); @@ -49,6 +52,25 @@ test('Returns source string with inline maps when no transformOptions is passed' expect(JSON.stringify(result.map!.sourcesContent)).toMatch('customMultiply'); }); +test('Returns source string with inline maps when no transformOptions is passed async', async () => { + const result: any = await babelJest.processAsync!( + sourceString, + 'dummy_path.js', + { + config: makeProjectConfig(), + configString: JSON.stringify(makeProjectConfig()), + instrument: false, + }, + ); + expect(typeof result).toBe('object'); + expect(result.code).toBeDefined(); + expect(result.map).toBeDefined(); + expect(result.code).toMatch('//# sourceMappingURL'); + expect(result.code).toMatch('customMultiply'); + expect(result.map!.sources).toEqual(['dummy_path.js']); + expect(JSON.stringify(result.map!.sourcesContent)).toMatch('customMultiply'); +}); + describe('caller option correctly merges from defaults and options', () => { test.each([ [ diff --git a/packages/babel-jest/src/index.ts b/packages/babel-jest/src/index.ts index c7048c37a395..da3f2d17d72f 100644 --- a/packages/babel-jest/src/index.ts +++ b/packages/babel-jest/src/index.ts @@ -11,6 +11,7 @@ import { PartialConfig, TransformOptions, transformSync as babelTransform, + transformAsync as babelTransformAsync, } from '@babel/core'; import chalk = require('chalk'); import * as fs from 'graceful-fs'; @@ -20,7 +21,7 @@ import type { SyncTransformer, } from '@jest/transform'; import type {Config} from '@jest/types'; -import {loadPartialConfig} from './loadBabelConfig'; +import {loadPartialConfig, loadPartialConfigAsync} from './loadBabelConfig'; const THIS_FILE = fs.readFileSync(__filename); const jestPresetPath = require.resolve('babel-preset-jest'); @@ -28,6 +29,124 @@ const babelIstanbulPlugin = require.resolve('babel-plugin-istanbul'); type CreateTransformer = SyncTransformer['createTransformer']; +function assertLoadedBabelConfig( + babelConfig: Readonly | null, + cwd: Config.Path, + filename: Config.Path, +): asserts babelConfig { + if (!babelConfig) { + throw new Error( + `babel-jest: Babel ignores ${chalk.bold( + slash(path.relative(cwd, filename)), + )} - make sure to include the file in Jest's ${chalk.bold( + 'transformIgnorePatterns', + )} as well.`, + ); + } +} + +function addIstanbulInstrumentation( + babelOptions: TransformOptions, + transformOptions: JestTransformOptions, +): TransformOptions { + if (transformOptions.instrument) { + const copiedBabelOptions: TransformOptions = {...babelOptions}; + copiedBabelOptions.auxiliaryCommentBefore = ' istanbul ignore next '; + // Copied from jest-runtime transform.js + copiedBabelOptions.plugins = (copiedBabelOptions.plugins || []).concat([ + [ + babelIstanbulPlugin, + { + // files outside `cwd` will not be instrumented + cwd: transformOptions.config.cwd, + exclude: [], + }, + ], + ]); + + return copiedBabelOptions; + } + + return babelOptions; +} + +function getCacheKeyFromConfig( + sourceText: string, + sourcePath: Config.Path, + babelOptions: PartialConfig, + transformOptions: JestTransformOptions, +): string { + const {config, configString, instrument} = transformOptions; + + const configPath = [babelOptions.config || '', babelOptions.babelrc || '']; + + return createHash('md5') + .update(THIS_FILE) + .update('\0', 'utf8') + .update(JSON.stringify(babelOptions.options)) + .update('\0', 'utf8') + .update(sourceText) + .update('\0', 'utf8') + .update(path.relative(config.rootDir, sourcePath)) + .update('\0', 'utf8') + .update(configString) + .update('\0', 'utf8') + .update(configPath.join('')) + .update('\0', 'utf8') + .update(instrument ? 'instrument' : '') + .update('\0', 'utf8') + .update(process.env.NODE_ENV || '') + .update('\0', 'utf8') + .update(process.env.BABEL_ENV || '') + .digest('hex'); +} + +function loadBabelConfig( + cwd: Config.Path, + filename: Config.Path, + transformOptions: TransformOptions, +): PartialConfig { + const babelConfig = loadPartialConfig(transformOptions); + + assertLoadedBabelConfig(babelConfig, cwd, filename); + + return babelConfig; +} + +async function loadBabelConfigAsync( + cwd: Config.Path, + filename: Config.Path, + transformOptions: TransformOptions, +): Promise { + const babelConfig = await loadPartialConfigAsync(transformOptions); + + assertLoadedBabelConfig(babelConfig, cwd, filename); + + return babelConfig; +} + +function loadBabelOptions( + cwd: Config.Path, + filename: Config.Path, + transformOptions: TransformOptions, + jestTransformOptions: JestTransformOptions, +): TransformOptions { + const {options} = loadBabelConfig(cwd, filename, transformOptions); + + return addIstanbulInstrumentation(options, jestTransformOptions); +} + +async function loadBabelOptionsAsync( + cwd: Config.Path, + filename: Config.Path, + transformOptions: TransformOptions, + jestTransformOptions: JestTransformOptions, +): Promise { + const {options} = await loadBabelConfigAsync(cwd, filename, transformOptions); + + return addIstanbulInstrumentation(options, jestTransformOptions); +} + const createTransformer: CreateTransformer = userOptions => { const inputOptions = userOptions ?? {}; @@ -47,13 +166,13 @@ const createTransformer: CreateTransformer = userOptions => { sourceMaps: 'both', } as const; - function loadBabelConfig( - cwd: Config.Path, + function mergeBabelTransformOptions( filename: Config.Path, transformOptions: JestTransformOptions, - ): PartialConfig { + ): TransformOptions { + const {cwd} = transformOptions.config; // `cwd` first to allow incoming options to override it - const babelConfig = loadPartialConfig({ + return { cwd, ...options, caller: { @@ -72,79 +191,46 @@ const createTransformer: CreateTransformer = userOptions => { options.caller.supportsTopLevelAwait, }, filename, - }); - - if (!babelConfig) { - throw new Error( - `babel-jest: Babel ignores ${chalk.bold( - slash(path.relative(cwd, filename)), - )} - make sure to include the file in Jest's ${chalk.bold( - 'transformIgnorePatterns', - )} as well.`, - ); - } - - return babelConfig; + }; } return { canInstrument: true, getCacheKey(sourceText, sourcePath, transformOptions) { - const {config, configString, instrument} = transformOptions; - const babelOptions = loadBabelConfig( - config.cwd, + transformOptions.config.cwd, + sourcePath, + mergeBabelTransformOptions(sourcePath, transformOptions), + ); + + return getCacheKeyFromConfig( + sourceText, sourcePath, + babelOptions, + transformOptions, + ); + }, + async getCacheKeyAsync(sourceText, sourcePath, transformOptions) { + const babelOptions = await loadBabelConfigAsync( + transformOptions.config.cwd, + sourcePath, + mergeBabelTransformOptions(sourcePath, transformOptions), + ); + + return getCacheKeyFromConfig( + sourceText, + sourcePath, + babelOptions, transformOptions, ); - const configPath = [ - babelOptions.config || '', - babelOptions.babelrc || '', - ]; - - return createHash('md5') - .update(THIS_FILE) - .update('\0', 'utf8') - .update(JSON.stringify(babelOptions.options)) - .update('\0', 'utf8') - .update(sourceText) - .update('\0', 'utf8') - .update(path.relative(config.rootDir, sourcePath)) - .update('\0', 'utf8') - .update(configString) - .update('\0', 'utf8') - .update(configPath.join('')) - .update('\0', 'utf8') - .update(instrument ? 'instrument' : '') - .update('\0', 'utf8') - .update(process.env.NODE_ENV || '') - .update('\0', 'utf8') - .update(process.env.BABEL_ENV || '') - .digest('hex'); }, process(sourceText, sourcePath, transformOptions) { - const babelOptions = { - ...loadBabelConfig( - transformOptions.config.cwd, - sourcePath, - transformOptions, - ).options, - }; - - if (transformOptions?.instrument) { - babelOptions.auxiliaryCommentBefore = ' istanbul ignore next '; - // Copied from jest-runtime transform.js - babelOptions.plugins = (babelOptions.plugins || []).concat([ - [ - babelIstanbulPlugin, - { - // files outside `cwd` will not be instrumented - cwd: transformOptions.config.rootDir, - exclude: [], - }, - ], - ]); - } + const babelOptions = loadBabelOptions( + transformOptions.config.cwd, + sourcePath, + mergeBabelTransformOptions(sourcePath, transformOptions), + transformOptions, + ); const transformResult = babelTransform(sourceText, babelOptions); @@ -155,6 +241,28 @@ const createTransformer: CreateTransformer = userOptions => { } } + return sourceText; + }, + async processAsync(sourceText, sourcePath, transformOptions) { + const babelOptions = await loadBabelOptionsAsync( + transformOptions.config.cwd, + sourcePath, + mergeBabelTransformOptions(sourcePath, transformOptions), + transformOptions, + ); + + const transformResult = await babelTransformAsync( + sourceText, + babelOptions, + ); + + if (transformResult) { + const {code, map} = transformResult; + if (typeof code === 'string') { + return {code, map}; + } + } + return sourceText; }, }; diff --git a/packages/babel-jest/src/loadBabelConfig.ts b/packages/babel-jest/src/loadBabelConfig.ts index 91bbd1725980..715c2ec84405 100644 --- a/packages/babel-jest/src/loadBabelConfig.ts +++ b/packages/babel-jest/src/loadBabelConfig.ts @@ -7,3 +7,14 @@ // this is a separate file so it can be mocked in tests export {loadPartialConfig} from '@babel/core'; + +import { + PartialConfig, + TransformOptions, + // @ts-expect-error: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/51741 + loadPartialConfigAsync as asyncVersion, +} from '@babel/core'; + +export const loadPartialConfigAsync: ( + options?: TransformOptions, +) => Promise | null> = asyncVersion; diff --git a/yarn.lock b/yarn.lock index 99a1bd11e13d..dcdb81c36e34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6249,7 +6249,7 @@ __metadata: graceful-fs: ^4.2.4 slash: ^3.0.0 peerDependencies: - "@babel/core": ^7.0.0 + "@babel/core": ^7.8.0 languageName: unknown linkType: soft