UNPKG

5.16 kBJavaScriptView Raw
1 function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
3import {TokenType as tt} from "../parser/tokenizer/types";
4
5
6import Transformer from "./Transformer";
7
8const JEST_GLOBAL_NAME = "jest";
9const HOISTED_METHODS = ["mock", "unmock", "enableAutomock", "disableAutomock"];
10
11/**
12 * Implementation of babel-plugin-jest-hoist, which hoists up some jest method
13 * calls above the imports to allow them to override other imports.
14 *
15 * To preserve line numbers, rather than directly moving the jest.mock code, we
16 * wrap each invocation in a function statement and then call the function from
17 * the top of the file.
18 */
19export default class JestHoistTransformer extends Transformer {
20 __init() {this.hoistedFunctionNames = []}
21
22 constructor(
23 rootTransformer,
24 tokens,
25 nameManager,
26 importProcessor,
27 ) {
28 super();this.rootTransformer = rootTransformer;this.tokens = tokens;this.nameManager = nameManager;this.importProcessor = importProcessor;JestHoistTransformer.prototype.__init.call(this);;
29 }
30
31 process() {
32 if (
33 this.tokens.currentToken().scopeDepth === 0 &&
34 this.tokens.matches4(tt.name, tt.dot, tt.name, tt.parenL) &&
35 this.tokens.identifierName() === JEST_GLOBAL_NAME
36 ) {
37 // TODO: This only works if imports transform is active, which it will be for jest.
38 // But if jest adds module support and we no longer need the import transform, this needs fixing.
39 if (_optionalChain([this, 'access', _ => _.importProcessor, 'optionalAccess', _2 => _2.getGlobalNames, 'call', _3 => _3(), 'optionalAccess', _4 => _4.has, 'call', _5 => _5(JEST_GLOBAL_NAME)])) {
40 return false;
41 }
42 return this.extractHoistedCalls();
43 }
44
45 return false;
46 }
47
48 getHoistedCode() {
49 if (this.hoistedFunctionNames.length > 0) {
50 // This will be placed before module interop code, but that's fine since
51 // imports aren't allowed in module mock factories.
52 return this.hoistedFunctionNames.map((name) => `${name}();`).join("");
53 }
54 return "";
55 }
56
57 /**
58 * Extracts any methods calls on the jest-object that should be hoisted.
59 *
60 * According to the jest docs, https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options,
61 * mock, unmock, enableAutomock, disableAutomock, are the methods that should be hoisted.
62 *
63 * We do not apply the same checks of the arguments as babel-plugin-jest-hoist does.
64 */
65 extractHoistedCalls() {
66 // We're handling a chain of calls where `jest` may or may not need to be inserted for each call
67 // in the chain, so remove the initial `jest` to make the loop implementation cleaner.
68 this.tokens.removeToken();
69 // Track some state so that multiple non-hoisted chained calls in a row keep their chaining
70 // syntax.
71 let followsNonHoistedJestCall = false;
72
73 // Iterate through all chained calls on the jest object.
74 while (this.tokens.matches3(tt.dot, tt.name, tt.parenL)) {
75 const methodName = this.tokens.identifierNameAtIndex(this.tokens.currentIndex() + 1);
76 const shouldHoist = HOISTED_METHODS.includes(methodName);
77 if (shouldHoist) {
78 // We've matched e.g. `.mock(...)` or similar call.
79 // Replace the initial `.` with `function __jestHoist(){jest.`
80 const hoistedFunctionName = this.nameManager.claimFreeName("__jestHoist");
81 this.hoistedFunctionNames.push(hoistedFunctionName);
82 this.tokens.replaceToken(`function ${hoistedFunctionName}(){${JEST_GLOBAL_NAME}.`);
83 this.tokens.copyToken();
84 this.tokens.copyToken();
85 this.rootTransformer.processBalancedCode();
86 this.tokens.copyExpectedToken(tt.parenR);
87 this.tokens.appendCode(";}");
88 followsNonHoistedJestCall = false;
89 } else {
90 // This is a non-hoisted method, so just transform the code as usual.
91 if (followsNonHoistedJestCall) {
92 // If we didn't hoist the previous call, we can leave the code as-is to chain off of the
93 // previous method call. It's important to preserve the code here because we don't know
94 // for sure that the method actually returned the jest object for chaining.
95 this.tokens.copyToken();
96 } else {
97 // If we hoisted the previous call, we know it returns the jest object back, so we insert
98 // the identifier `jest` to continue the chain.
99 this.tokens.replaceToken(`${JEST_GLOBAL_NAME}.`);
100 }
101 this.tokens.copyToken();
102 this.tokens.copyToken();
103 this.rootTransformer.processBalancedCode();
104 this.tokens.copyExpectedToken(tt.parenR);
105 followsNonHoistedJestCall = true;
106 }
107 }
108
109 return true;
110 }
111}