Skip to content

Commit fdecf04

Browse files
committed
WIP
1 parent 9b91b62 commit fdecf04

File tree

5 files changed

+119
-63
lines changed

5 files changed

+119
-63
lines changed

e2e/babel-plugin-jest-hoist/__tests__/importJest.test.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,19 @@
77
*/
88

99
import {jest} from '@jest/globals';
10+
import * as JestGlobals from '@jest/globals';
1011

1112
// The virtual mock call below will be hoisted above this `require` call.
1213
const virtualModule = require('virtual-module');
14+
const virtualModule2 = require('virtual-module-2');
1315

1416
jest.mock('virtual-module', () => 'kiwi', {virtual: true});
17+
JestGlobals.jest.mock('virtual-module', () => 'banana', {virtual: true});
1518

16-
test('works with virtual modules', () => {
19+
test('named import', () => {
1720
expect(virtualModule).toBe('kiwi');
1821
});
22+
23+
test('namespace import', () => {
24+
expect(virtualModule2).toBe('banana');
25+
});

e2e/babel-plugin-jest-hoist/__tests__/integration.test.js

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

9+
// TODO deep in blocks not hoisted
10+
// TODO shadowed not hoisted
11+
912
'use strict';
1013

1114
import React from 'react';

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
}
2121
},
2222
"dependencies": {
23-
"@types/babel__traverse": "^7.0.6"
23+
"@types/babel__traverse": "^7.0.6",
24+
"lodash.sortby": "^4.7.0"
2425
},
2526
"devDependencies": {
2627
"@babel/types": "^7.3.3",
28+
"@types/lodash.sortby": "^4.7.6",
2729
"@types/node": "*"
2830
},
2931
"publishConfig": {

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

Lines changed: 93 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
*/
88

99
import type {NodePath, Visitor} from '@babel/traverse';
10-
import type {Identifier} from '@babel/types';
10+
import {
11+
Identifier,
12+
Node,
13+
Expression,
14+
isImportDeclaration,
15+
} from '@babel/types';
16+
import sortBy = require('lodash.sortby');
1117

1218
// We allow `jest`, `expect`, `require`, all default Node.js globals and all
1319
// ES2015 built-ins to be used inside of a `jest.mock` factory.
@@ -70,7 +76,10 @@ const WHITELISTED_IDENTIFIERS = new Set<string>(
7076
].sort(),
7177
);
7278

73-
const JEST_GLOBAL = {name: 'jest'};
79+
const JEST_GLOBAL_NAME = 'jest';
80+
const JEST_GLOBALS_MODULE_NAME = '@jest/globals';
81+
const JEST_GLOBALS_MODULE_JEST_EXPORT_NAME = 'jest';
82+
7483
// TODO: Should be Visitor<{ids: Set<NodePath<Identifier>>}>, but `ReferencedIdentifier` doesn't exist
7584
const IDVisitor = {
7685
ReferencedIdentifier(path: NodePath<Identifier>) {
@@ -82,7 +91,7 @@ const IDVisitor = {
8291

8392
const FUNCTIONS: Record<
8493
string,
85-
(args: Array<NodePath>) => boolean
94+
<T extends Node>(args: Array<NodePath<T>>) => boolean
8695
> = Object.create(null);
8796

8897
FUNCTIONS.mock = args => {
@@ -152,72 +161,95 @@ FUNCTIONS.deepUnmock = args => args.length === 1 && args[0].isStringLiteral();
152161
FUNCTIONS.disableAutomock = FUNCTIONS.enableAutomock = args =>
153162
args.length === 0;
154163

155-
export default (): {visitor: Visitor} => {
156-
const shouldHoistExpression = (expr: NodePath): boolean => {
157-
if (!expr.isCallExpression()) {
158-
return false;
159-
}
164+
const isIdentifierJestObject = (identifier: NodePath<Identifier>): boolean => {
165+
// global
166+
if (
167+
identifier.node.name === JEST_GLOBAL_NAME &&
168+
!identifier.scope.hasBinding(JEST_GLOBAL_NAME)
169+
) {
170+
return true;
171+
}
172+
// import from '@jest/globals'
173+
if (
174+
identifier.referencesImport(
175+
JEST_GLOBALS_MODULE_NAME,
176+
JEST_GLOBALS_MODULE_JEST_EXPORT_NAME,
177+
)
178+
) {
179+
return true;
180+
}
160181

161-
// TODO: avoid type casts - the types can be arrays (is it possible to ignore that without casting?)
162-
const callee = expr.get('callee') as NodePath;
163-
const expressionArguments = expr.get('arguments');
164-
const object = callee.get('object') as NodePath;
165-
const property = callee.get('property') as NodePath;
166-
return (
167-
property.isIdentifier() &&
168-
FUNCTIONS[property.node.name] &&
169-
(object.isIdentifier(JEST_GLOBAL) ||
170-
(callee.isMemberExpression() && shouldHoistExpression(object))) &&
171-
FUNCTIONS[property.node.name](
172-
Array.isArray(expressionArguments)
173-
? expressionArguments
174-
: [expressionArguments],
175-
)
176-
);
177-
};
182+
return false;
183+
};
178184

179-
const visitor: Visitor = {
180-
ExpressionStatement(path) {
181-
if (shouldHoistExpression(path.get('expression') as NodePath)) {
182-
// @ts-ignore: private, magical property
183-
path.node._blockHoist = Infinity;
184-
}
185-
},
186-
ImportDeclaration(path) {
187-
if (path.node.source.value === '@jest/globals') {
188-
// @ts-ignore: private, magical property
189-
path.node._blockHoist = Infinity;
190-
}
191-
},
192-
VariableDeclaration(path) {
193-
const declarations = path.get('declarations');
185+
const shouldHoistExpression = <T extends Node>(expr: NodePath<T>): boolean => {
186+
if (!expr.isCallExpression()) {
187+
return false;
188+
}
189+
190+
const callee = expr.get<'callee'>('callee');
191+
const args = expr.get<'arguments'>('arguments');
194192

195-
if (declarations.length === 1) {
196-
const declarationInit = declarations[0].get('init');
193+
if (!callee.isMemberExpression()) {
194+
return false;
195+
}
196+
197+
const object = callee.get<'object'>('object');
198+
const property = callee.get<'property'>('property') as
199+
| NodePath<Expression>
200+
| NodePath<Identifier>;
197201

198-
if (declarationInit.isCallExpression()) {
199-
const callee = declarationInit.get('callee') as NodePath;
200-
const callArguments = declarationInit.get('arguments') as Array<
201-
NodePath
202-
>;
202+
if (!property.isIdentifier()) {
203+
return false;
204+
}
205+
const propertyName = property.node.name;
206+
207+
const objectIsJest =
208+
(object.isIdentifier() && isIdentifierJestObject(object)) ||
209+
// The Jest object could be returned from another call since the functions are all chainable.
210+
shouldHoistExpression(object);
211+
if (!objectIsJest) {
212+
return false;
213+
}
214+
215+
// Important: Call the function check last
216+
// It might throw an error to display to the user,
217+
// which should only happen if we're already sure it's a call on the Jest object.
218+
const functionLooksHoistable =
219+
FUNCTIONS[propertyName] && FUNCTIONS[propertyName](args);
220+
221+
return functionLooksHoistable;
222+
};
223+
224+
// TODO `require`s
225+
export default (): {visitor: Visitor} => {
226+
const visitor: Visitor = {
227+
Program: {
228+
enter(path) {
229+
path.node.body = sortBy(path.node.body, node => {
230+
console.log(require('@babel/generator').default(node));
203231

204232
if (
205-
callee.isIdentifier() &&
206-
callee.node.name === 'require' &&
207-
callArguments.length === 1
233+
isImportDeclaration(node) &&
234+
node.source.value === JEST_GLOBALS_MODULE_NAME
208235
) {
209-
const [argument] = callArguments;
210-
211-
if (
212-
argument.isStringLiteral() &&
213-
argument.node.value === '@jest/globals'
214-
) {
215-
// @ts-ignore: private, magical property
216-
path.node._blockHoist = Infinity;
217-
}
236+
console.log('found import');
237+
return 0;
218238
}
219-
}
220-
}
239+
const nodePath = path.get('body').find(p => p.node === node);
240+
if (
241+
nodePath &&
242+
nodePath.isExpressionStatement() &&
243+
shouldHoistExpression(nodePath.get<'expression'>('expression'))
244+
) {
245+
console.log('found stmt');
246+
return 1;
247+
}
248+
249+
console.log('nope');
250+
return 2;
251+
});
252+
},
221253
},
222254
};
223255

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2382,6 +2382,18 @@
23822382
resolved "https://registry.yarnpkg.com/@types/lockfile/-/lockfile-1.0.1.tgz#434a3455e89843312f01976e010c60f1bcbd56f7"
23832383
integrity sha512-65WZedEm4AnOsBDdsapJJG42MhROu3n4aSSiu87JXF/pSdlubxZxp3S1yz3kTfkJ2KBPud4CpjoHVAptOm9Zmw==
23842384

2385+
"@types/lodash.sortby@^4.7.6":
2386+
version "4.7.6"
2387+
resolved "https://registry.yarnpkg.com/@types/lodash.sortby/-/lodash.sortby-4.7.6.tgz#eed689835f274b553db4ae16a4a23f58b79618a1"
2388+
integrity sha512-EnvAOmKvEg7gdYpYrS6+fVFPw5dL9rBnJi3vcKI7wqWQcLJVF/KRXK9dH29HjGNVvFUj0s9prRP3J8jEGnGKDw==
2389+
dependencies:
2390+
"@types/lodash" "*"
2391+
2392+
"@types/lodash@*":
2393+
version "4.14.149"
2394+
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440"
2395+
integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==
2396+
23852397
"@types/lolex@^5.1.0":
23862398
version "5.1.0"
23872399
resolved "https://registry.yarnpkg.com/@types/lolex/-/lolex-5.1.0.tgz#11b4c4756c007306d0feeaf2f08f88350c635d2b"

0 commit comments

Comments
 (0)