diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0017592475c4..c652a32acd75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
### Fixes
- `[babel-plugin-jest-hoist]` Preserve order of hoisted mock nodes within containing block ([#10536](https://github.com/facebook/jest/pull/10536))
+- `[babel-plugin-jest-hoist]` Hoist pure constants to support experimental JSX transform in hoisted mocks ([#10723](https://github.com/facebook/jest/pull/10723))
- `[babel-preset-jest]` Update `babel-preset-current-node-syntax` to support top level await ([#10747](https://github.com/facebook/jest/pull/10747))
- `[expect]` Stop modifying the sample in `expect.objectContaining()` ([#10711](https://github.com/facebook/jest/pull/10711))
- `[jest-circus, jest-jasmine2]` fix: don't assume `stack` is always a string ([#10697](https://github.com/facebook/jest/pull/10697))
diff --git a/packages/babel-plugin-jest-hoist/package.json b/packages/babel-plugin-jest-hoist/package.json
index 5da83c3e267f..4b0496692f2d 100644
--- a/packages/babel-plugin-jest-hoist/package.json
+++ b/packages/babel-plugin-jest-hoist/package.json
@@ -20,9 +20,12 @@
},
"devDependencies": {
"@babel/core": "^7.11.6",
+ "@babel/preset-react": "^7.12.1",
"@types/babel__template": "^7.0.2",
"@types/node": "*",
- "babel-plugin-tester": "^10.0.0"
+ "@types/prettier": "^2.0.0",
+ "babel-plugin-tester": "^10.0.0",
+ "prettier": "^2.1.1"
},
"publishConfig": {
"access": "public"
diff --git a/packages/babel-plugin-jest-hoist/src/__tests__/__snapshots__/hoistPlugin.test.ts.snap b/packages/babel-plugin-jest-hoist/src/__tests__/__snapshots__/hoistPlugin.test.ts.snap
index 0c1a8424ff4f..09486e91ae92 100644
--- a/packages/babel-plugin-jest-hoist/src/__tests__/__snapshots__/hoistPlugin.test.ts.snap
+++ b/packages/babel-plugin-jest-hoist/src/__tests__/__snapshots__/hoistPlugin.test.ts.snap
@@ -1,5 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`babel-plugin-jest-hoist automatic react runtime: automatic react runtime 1`] = `
+
+jest.mock('./App', () => () =>
Hello world
);
+
+ ↓ ↓ ↓ ↓ ↓ ↓
+
+var _jsxFileName = "/root/project/src/file.js";
+
+_getJestObj().mock("./App", () => () =>
+ /*#__PURE__*/ _jsxDEV(
+ "div",
+ {
+ children: "Hello world"
+ },
+ void 0,
+ false,
+ {
+ fileName: _jsxFileName,
+ lineNumber: 1,
+ columnNumber: 32
+ },
+ this
+ )
+);
+
+import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
+
+function _getJestObj() {
+ const { jest } = require("@jest/globals");
+
+ _getJestObj = () => jest;
+
+ return jest;
+}
+
+
+`;
+
exports[`babel-plugin-jest-hoist top level mocking: top level mocking 1`] = `
require('x');
diff --git a/packages/babel-plugin-jest-hoist/src/__tests__/hoistPlugin.test.ts b/packages/babel-plugin-jest-hoist/src/__tests__/hoistPlugin.test.ts
index 1e0554a2958c..65761852705e 100644
--- a/packages/babel-plugin-jest-hoist/src/__tests__/hoistPlugin.test.ts
+++ b/packages/babel-plugin-jest-hoist/src/__tests__/hoistPlugin.test.ts
@@ -6,13 +6,41 @@
*
*/
+import * as path from 'path';
import pluginTester from 'babel-plugin-tester';
+import {format as formatCode} from 'prettier';
import babelPluginJestHoist from '..';
pluginTester({
plugin: babelPluginJestHoist,
pluginName: 'babel-plugin-jest-hoist',
tests: {
+ 'automatic react runtime': {
+ babelOptions: {
+ babelrc: false,
+ configFile: false,
+ filename: path.resolve(__dirname, '../file.js'),
+ presets: [
+ [
+ require.resolve('@babel/preset-react'),
+ {development: true, runtime: 'automatic'},
+ ],
+ ],
+ },
+ code: `
+ jest.mock('./App', () => () => Hello world
);
+ `,
+ formatResult(code) {
+ // replace the filename with something that will be the same across OSes and machine
+ const codeWithoutSystemPath = code.replace(
+ /var _jsxFileName = ".*";/,
+ 'var _jsxFileName = "/root/project/src/file.js";',
+ );
+
+ return formatCode(codeWithoutSystemPath, {parser: 'babel'});
+ },
+ snapshot: true,
+ },
'top level mocking': {
code: `
require('x');
diff --git a/packages/babel-plugin-jest-hoist/src/index.ts b/packages/babel-plugin-jest-hoist/src/index.ts
index 99e0d8b50749..f5c59c44de25 100644
--- a/packages/babel-plugin-jest-hoist/src/index.ts
+++ b/packages/babel-plugin-jest-hoist/src/index.ts
@@ -16,15 +16,20 @@ import {
Identifier,
Node,
Program,
+ VariableDeclaration,
+ VariableDeclarator,
callExpression,
emptyStatement,
isIdentifier,
+ variableDeclaration,
} from '@babel/types';
const JEST_GLOBAL_NAME = 'jest';
const JEST_GLOBALS_MODULE_NAME = '@jest/globals';
const JEST_GLOBALS_MODULE_JEST_EXPORT_NAME = 'jest';
+const hoistedVariables = new WeakSet();
+
// We allow `jest`, `expect`, `require`, all default Node.js globals and all
// ES2015 built-ins to be used inside of a `jest.mock` factory.
// We also allow variables prefixed with `mock` as an escape-hatch.
@@ -133,12 +138,26 @@ FUNCTIONS.mock = args => {
}
if (!found) {
- const isAllowedIdentifier =
+ let isAllowedIdentifier =
(scope.hasGlobal(name) && ALLOWED_IDENTIFIERS.has(name)) ||
/^mock/i.test(name) ||
// Allow istanbul's coverage variable to pass.
/^(?:__)?cov/.test(name);
+ if (!isAllowedIdentifier) {
+ const binding = scope.bindings[name];
+
+ if (binding?.path.isVariableDeclarator()) {
+ const {node} = binding.path;
+ const initNode = node.init;
+
+ if (initNode && binding.constant && scope.isPure(initNode, true)) {
+ hoistedVariables.add(node);
+ isAllowedIdentifier = true;
+ }
+ }
+ }
+
if (!isAllowedIdentifier) {
throw id.buildCodeFrameError(
'The module factory of `jest.mock()` is not allowed to ' +
@@ -273,7 +292,7 @@ export default (): PluginObj<{
visitor: {
ExpressionStatement(exprStmt) {
const jestObjExpr = extractJestObjExprIfHoistable(
- exprStmt.get<'expression'>('expression'),
+ exprStmt.get('expression'),
);
if (jestObjExpr) {
jestObjExpr.replaceWith(
@@ -285,24 +304,25 @@ export default (): PluginObj<{
// in `post` to make sure we come after an import transform and can unshift above the `require`s
post({path: program}) {
const self = this;
+
visitBlock(program);
- program.traverse({
- BlockStatement: visitBlock,
- });
+ program.traverse({BlockStatement: visitBlock});
function visitBlock(block: NodePath | NodePath) {
// use a temporary empty statement instead of the real first statement, which may itself be hoisted
- const [firstNonHoistedStatementOfBlock] = block.unshiftContainer(
- 'body',
+ const [varsHoistPoint, callsHoistPoint] = block.unshiftContainer('body', [
emptyStatement(),
- );
+ emptyStatement(),
+ ]);
block.traverse({
CallExpression: visitCallExpr,
+ VariableDeclarator: visitVariableDeclarator,
// do not traverse into nested blocks, or we'll hoist calls in there out to this block
// @ts-expect-error blacklist is not known
blacklist: ['BlockStatement'],
});
- firstNonHoistedStatementOfBlock.remove();
+ callsHoistPoint.remove();
+ varsHoistPoint.remove();
function visitCallExpr(callExpr: NodePath) {
const {
@@ -315,15 +335,32 @@ export default (): PluginObj<{
const mockStmt = callExpr.getStatementParent();
if (mockStmt) {
- const mockStmtNode = mockStmt.node;
const mockStmtParent = mockStmt.parentPath;
if (mockStmtParent.isBlock()) {
+ const mockStmtNode = mockStmt.node;
mockStmt.remove();
- firstNonHoistedStatementOfBlock.insertBefore(mockStmtNode);
+ callsHoistPoint.insertBefore(mockStmtNode);
}
}
}
}
+
+ function visitVariableDeclarator(varDecl: NodePath) {
+ if (hoistedVariables.has(varDecl.node)) {
+ // should be assert function, but it's not. So let's cast below
+ varDecl.parentPath.assertVariableDeclaration();
+
+ const {kind, declarations} = varDecl.parent as VariableDeclaration;
+ if (declarations.length === 1) {
+ varDecl.parentPath.remove();
+ } else {
+ varDecl.remove();
+ }
+ varsHoistPoint.insertBefore(
+ variableDeclaration(kind, [varDecl.node]),
+ );
+ }
+ }
}
},
});
diff --git a/yarn.lock b/yarn.lock
index 6f5f7eece467..bbd5d5e0e225 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1456,7 +1456,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/preset-react@npm:*, @babel/preset-react@npm:^7.0.0, @babel/preset-react@npm:^7.9.4":
+"@babel/preset-react@npm:*, @babel/preset-react@npm:^7.0.0, @babel/preset-react@npm:^7.12.1, @babel/preset-react@npm:^7.9.4":
version: 7.12.1
resolution: "@babel/preset-react@npm:7.12.1"
dependencies:
@@ -4830,13 +4830,16 @@ __metadata:
resolution: "babel-plugin-jest-hoist@workspace:packages/babel-plugin-jest-hoist"
dependencies:
"@babel/core": ^7.11.6
+ "@babel/preset-react": ^7.12.1
"@babel/template": ^7.3.3
"@babel/types": ^7.3.3
"@types/babel__core": ^7.0.0
"@types/babel__template": ^7.0.2
"@types/babel__traverse": ^7.0.6
"@types/node": "*"
+ "@types/prettier": ^2.0.0
babel-plugin-tester: ^10.0.0
+ prettier: ^2.1.1
languageName: unknown
linkType: soft