Skip to content

Commit cfc90d2

Browse files
fix(hoist-plugin): hoist pure constants to support experimental JSX transform in mocks (#10723)
Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
1 parent cf6dffa commit cfc90d2

6 files changed

Lines changed: 123 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
### Fixes
88

99
- `[babel-plugin-jest-hoist]` Preserve order of hoisted mock nodes within containing block ([#10536](https://github.com/facebook/jest/pull/10536))
10+
- `[babel-plugin-jest-hoist]` Hoist pure constants to support experimental JSX transform in hoisted mocks ([#10723](https://github.com/facebook/jest/pull/10723))
1011
- `[babel-preset-jest]` Update `babel-preset-current-node-syntax` to support top level await ([#10747](https://github.com/facebook/jest/pull/10747))
1112
- `[expect]` Stop modifying the sample in `expect.objectContaining()` ([#10711](https://github.com/facebook/jest/pull/10711))
1213
- `[jest-circus, jest-jasmine2]` fix: don't assume `stack` is always a string ([#10697](https://github.com/facebook/jest/pull/10697))

packages/babel-plugin-jest-hoist/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
},
2121
"devDependencies": {
2222
"@babel/core": "^7.11.6",
23+
"@babel/preset-react": "^7.12.1",
2324
"@types/babel__template": "^7.0.2",
2425
"@types/node": "*",
25-
"babel-plugin-tester": "^10.0.0"
26+
"@types/prettier": "^2.0.0",
27+
"babel-plugin-tester": "^10.0.0",
28+
"prettier": "^2.1.1"
2629
},
2730
"publishConfig": {
2831
"access": "public"

packages/babel-plugin-jest-hoist/src/__tests__/__snapshots__/hoistPlugin.test.ts.snap

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,43 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`babel-plugin-jest-hoist automatic react runtime: automatic react runtime 1`] = `
4+
5+
jest.mock('./App', () => () => <div>Hello world</div>);
6+
7+
↓ ↓ ↓ ↓ ↓ ↓
8+
9+
var _jsxFileName = "/root/project/src/file.js";
10+
11+
_getJestObj().mock("./App", () => () =>
12+
/*#__PURE__*/ _jsxDEV(
13+
"div",
14+
{
15+
children: "Hello world"
16+
},
17+
void 0,
18+
false,
19+
{
20+
fileName: _jsxFileName,
21+
lineNumber: 1,
22+
columnNumber: 32
23+
},
24+
this
25+
)
26+
);
27+
28+
import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
29+
30+
function _getJestObj() {
31+
const { jest } = require("@jest/globals");
32+
33+
_getJestObj = () => jest;
34+
35+
return jest;
36+
}
37+
38+
39+
`;
40+
341
exports[`babel-plugin-jest-hoist top level mocking: top level mocking 1`] = `
442
543
require('x');

packages/babel-plugin-jest-hoist/src/__tests__/hoistPlugin.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,41 @@
66
*
77
*/
88

9+
import * as path from 'path';
910
import pluginTester from 'babel-plugin-tester';
11+
import {format as formatCode} from 'prettier';
1012
import babelPluginJestHoist from '..';
1113

1214
pluginTester({
1315
plugin: babelPluginJestHoist,
1416
pluginName: 'babel-plugin-jest-hoist',
1517
tests: {
18+
'automatic react runtime': {
19+
babelOptions: {
20+
babelrc: false,
21+
configFile: false,
22+
filename: path.resolve(__dirname, '../file.js'),
23+
presets: [
24+
[
25+
require.resolve('@babel/preset-react'),
26+
{development: true, runtime: 'automatic'},
27+
],
28+
],
29+
},
30+
code: `
31+
jest.mock('./App', () => () => <div>Hello world</div>);
32+
`,
33+
formatResult(code) {
34+
// replace the filename with something that will be the same across OSes and machine
35+
const codeWithoutSystemPath = code.replace(
36+
/var _jsxFileName = ".*";/,
37+
'var _jsxFileName = "/root/project/src/file.js";',
38+
);
39+
40+
return formatCode(codeWithoutSystemPath, {parser: 'babel'});
41+
},
42+
snapshot: true,
43+
},
1644
'top level mocking': {
1745
code: `
1846
require('x');

packages/babel-plugin-jest-hoist/src/index.ts

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@ import {
1616
Identifier,
1717
Node,
1818
Program,
19+
VariableDeclaration,
20+
VariableDeclarator,
1921
callExpression,
2022
emptyStatement,
2123
isIdentifier,
24+
variableDeclaration,
2225
} from '@babel/types';
2326

2427
const JEST_GLOBAL_NAME = 'jest';
2528
const JEST_GLOBALS_MODULE_NAME = '@jest/globals';
2629
const JEST_GLOBALS_MODULE_JEST_EXPORT_NAME = 'jest';
2730

31+
const hoistedVariables = new WeakSet<VariableDeclarator>();
32+
2833
// We allow `jest`, `expect`, `require`, all default Node.js globals and all
2934
// ES2015 built-ins to be used inside of a `jest.mock` factory.
3035
// We also allow variables prefixed with `mock` as an escape-hatch.
@@ -133,12 +138,26 @@ FUNCTIONS.mock = args => {
133138
}
134139

135140
if (!found) {
136-
const isAllowedIdentifier =
141+
let isAllowedIdentifier =
137142
(scope.hasGlobal(name) && ALLOWED_IDENTIFIERS.has(name)) ||
138143
/^mock/i.test(name) ||
139144
// Allow istanbul's coverage variable to pass.
140145
/^(?:__)?cov/.test(name);
141146

147+
if (!isAllowedIdentifier) {
148+
const binding = scope.bindings[name];
149+
150+
if (binding?.path.isVariableDeclarator()) {
151+
const {node} = binding.path;
152+
const initNode = node.init;
153+
154+
if (initNode && binding.constant && scope.isPure(initNode, true)) {
155+
hoistedVariables.add(node);
156+
isAllowedIdentifier = true;
157+
}
158+
}
159+
}
160+
142161
if (!isAllowedIdentifier) {
143162
throw id.buildCodeFrameError(
144163
'The module factory of `jest.mock()` is not allowed to ' +
@@ -273,7 +292,7 @@ export default (): PluginObj<{
273292
visitor: {
274293
ExpressionStatement(exprStmt) {
275294
const jestObjExpr = extractJestObjExprIfHoistable(
276-
exprStmt.get<'expression'>('expression'),
295+
exprStmt.get('expression'),
277296
);
278297
if (jestObjExpr) {
279298
jestObjExpr.replaceWith(
@@ -285,24 +304,25 @@ export default (): PluginObj<{
285304
// in `post` to make sure we come after an import transform and can unshift above the `require`s
286305
post({path: program}) {
287306
const self = this;
307+
288308
visitBlock(program);
289-
program.traverse({
290-
BlockStatement: visitBlock,
291-
});
309+
program.traverse({BlockStatement: visitBlock});
292310

293311
function visitBlock(block: NodePath<BlockStatement> | NodePath<Program>) {
294312
// use a temporary empty statement instead of the real first statement, which may itself be hoisted
295-
const [firstNonHoistedStatementOfBlock] = block.unshiftContainer(
296-
'body',
313+
const [varsHoistPoint, callsHoistPoint] = block.unshiftContainer('body', [
297314
emptyStatement(),
298-
);
315+
emptyStatement(),
316+
]);
299317
block.traverse({
300318
CallExpression: visitCallExpr,
319+
VariableDeclarator: visitVariableDeclarator,
301320
// do not traverse into nested blocks, or we'll hoist calls in there out to this block
302321
// @ts-expect-error blacklist is not known
303322
blacklist: ['BlockStatement'],
304323
});
305-
firstNonHoistedStatementOfBlock.remove();
324+
callsHoistPoint.remove();
325+
varsHoistPoint.remove();
306326

307327
function visitCallExpr(callExpr: NodePath<CallExpression>) {
308328
const {
@@ -315,15 +335,32 @@ export default (): PluginObj<{
315335
const mockStmt = callExpr.getStatementParent();
316336

317337
if (mockStmt) {
318-
const mockStmtNode = mockStmt.node;
319338
const mockStmtParent = mockStmt.parentPath;
320339
if (mockStmtParent.isBlock()) {
340+
const mockStmtNode = mockStmt.node;
321341
mockStmt.remove();
322-
firstNonHoistedStatementOfBlock.insertBefore(mockStmtNode);
342+
callsHoistPoint.insertBefore(mockStmtNode);
323343
}
324344
}
325345
}
326346
}
347+
348+
function visitVariableDeclarator(varDecl: NodePath<VariableDeclarator>) {
349+
if (hoistedVariables.has(varDecl.node)) {
350+
// should be assert function, but it's not. So let's cast below
351+
varDecl.parentPath.assertVariableDeclaration();
352+
353+
const {kind, declarations} = varDecl.parent as VariableDeclaration;
354+
if (declarations.length === 1) {
355+
varDecl.parentPath.remove();
356+
} else {
357+
varDecl.remove();
358+
}
359+
varsHoistPoint.insertBefore(
360+
variableDeclaration(kind, [varDecl.node]),
361+
);
362+
}
363+
}
327364
}
328365
},
329366
});

yarn.lock

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1456,7 +1456,7 @@ __metadata:
14561456
languageName: node
14571457
linkType: hard
14581458

1459-
"@babel/preset-react@npm:*, @babel/preset-react@npm:^7.0.0, @babel/preset-react@npm:^7.9.4":
1459+
"@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":
14601460
version: 7.12.1
14611461
resolution: "@babel/preset-react@npm:7.12.1"
14621462
dependencies:
@@ -4830,13 +4830,16 @@ __metadata:
48304830
resolution: "babel-plugin-jest-hoist@workspace:packages/babel-plugin-jest-hoist"
48314831
dependencies:
48324832
"@babel/core": ^7.11.6
4833+
"@babel/preset-react": ^7.12.1
48334834
"@babel/template": ^7.3.3
48344835
"@babel/types": ^7.3.3
48354836
"@types/babel__core": ^7.0.0
48364837
"@types/babel__template": ^7.0.2
48374838
"@types/babel__traverse": ^7.0.6
48384839
"@types/node": "*"
4840+
"@types/prettier": ^2.0.0
48394841
babel-plugin-tester: ^10.0.0
4842+
prettier: ^2.1.1
48404843
languageName: unknown
48414844
linkType: soft
48424845

0 commit comments

Comments
 (0)