babel-plugin-jest-hoist is a Babel plugin that transforms Jest test files by automatically hoisting specific Jest API calls (jest.mock, jest.unmock, jest.disableAutomock, jest.enableAutomock) above import statements. This ensures proper module loading order during test execution and prevents timing issues with Jest's module mocking system.
npm install babel-plugin-jest-hoist// Via babel.config.js (Recommended)
module.exports = {
plugins: ['jest-hoist'],
};// Via Node API
const babel = require('@babel/core');
const jestHoistPlugin = require('babel-plugin-jest-hoist');
babel.transform(code, {
plugins: [jestHoistPlugin],
});ESM Import:
import jestHoistPlugin from 'babel-plugin-jest-hoist';The plugin is typically used automatically via babel-jest when running Jest tests, but can be configured manually:
// babel.config.js
module.exports = {
plugins: ['jest-hoist'],
};Integration with Jest:
When using Jest, this plugin is automatically applied by babel-jest to ensure mock calls are properly hoisted. No manual configuration is typically needed.
// jest.config.js - Plugin is applied automatically
module.exports = {
transform: {
'^.+\\.jsx?$': 'babel-jest', // Plugin applied here automatically
},
};Before transformation:
import { someFunction } from './module';
jest.mock('./api', () => ({
getData: jest.fn(),
}));
jest.enableAutomock();After transformation:
_getJestObj().mock('./api', () => ({
getData: _getJestObj().fn(),
}));
_getJestObj().enableAutomock();
function _getJestObj() {
const { jest } = require('@jest/globals');
_getJestObj = () => jest;
return jest;
}
import { someFunction } from './module';The plugin operates in three phases:
Key components:
jest, imported jest from @jest/globals, and namespace importsThe plugin uses Babel's visitor pattern with these hooks:
interface PluginVisitors {
// Pre-hook: Sets up Jest object getter identifier
pre(file: { path: NodePath<Program> }): void;
// Main visitor: Processes expression statements
visitor: {
ExpressionStatement(exprStmt: NodePath<ExpressionStatement>): void;
};
// Post-hook: Performs actual hoisting of calls and variables
post(file: { path: NodePath<Program> }): void;
}Processing Flow:
_getJestObj getter function when first neededCreates a Babel plugin object that transforms Jest method calls.
/**
* Default export function that returns a Babel plugin object
* @returns PluginObj with pre, visitor, and post hooks
*/
export default function jestHoist(): PluginObj<{
declareJestObjGetterIdentifier: () => Identifier;
jestObjGetterIdentifier?: Identifier;
}>;
interface PluginObj<State = {}> {
pre?(this: State, file: { path: NodePath<Program> }): void;
visitor: Visitor<State>;
post?(this: State, file: { path: NodePath<Program> }): void;
}The plugin hoists these Jest methods when called on the Jest object:
// Mock module with optional factory and options
jest.mock(moduleName: string, factory?: () => any, options?: any): void;
// Unmock a previously mocked module
jest.unmock(moduleName: string): void;
// Deep unmock a module and its dependencies
jest.deepUnmock(moduleName: string): void;
// Disable automatic mocking
jest.disableAutomock(): void;
// Enable automatic mocking
jest.enableAutomock(): void;Note: Methods like jest.requireActual, jest.spyOn, and jest.fn are NOT hoisted by this plugin. They remain in their original positions and work normally within mock factories.
The plugin recognizes Jest objects in multiple forms:
// Global jest object (when not shadowed by local binding)
jest.mock('./module');
// Named import from @jest/globals
import { jest } from '@jest/globals';
jest.mock('./module');
// Namespace import from @jest/globals
import * as JestGlobals from '@jest/globals';
JestGlobals.jest.mock('./module');
// CommonJS require
const { jest } = require('@jest/globals');
jest.mock('./module');Advanced Recognition Examples:
// ✅ Recognized and hoisted - chained Jest methods
jest.mock('./api').disableAutomock();
// ✅ Recognized and hoisted - within blocks
describe('test suite', () => {
beforeEach(() => {
jest.mock('./dependency');
});
});
// ❌ Not recognized - shadowed by local binding
function test() {
const jest = { mock: () => {} };
jest.mock('./module'); // Local variable, not global Jest
}Mock factory functions are validated to only reference allowed identifiers:
// Allowed in mock factories:
// - ES2015 built-ins: Array, Object, Promise, etc.
// - Node.js globals: require, module, __dirname, etc.
// - Jest globals: jest, expect
// - Variables prefixed with 'mock' (case insensitive)
// - Pure constants that can be safely hoisted
// - Coverage instrumentation variables
jest.mock('./api', () => {
// ✅ Allowed - ES2015 built-in
const data = Object.keys(mockData);
// ✅ Allowed - mock prefixed variable
const mockValue = 42;
// ✅ Allowed - jest global
return jest.fn();
});
// ❌ Not allowed - references out-of-scope variable
const externalVar = 'test';
jest.mock('./api', () => {
return externalVar; // Error: Invalid variable access
});The plugin provides detailed error messages for invalid usage:
// TypeError: The second argument of `jest.mock` must be an inline function
const mockFactory = () => ({ foo: 'bar' });
jest.mock('./module', mockFactory); // ❌ Error - factory must be inline
// ReferenceError: Invalid variable access in mock factory
const external = 'value';
jest.mock('./module', () => external); // ❌ Error - references out-of-scope variable
// ✅ Valid - references allowed identifier
jest.mock('./module', () => ({
method: jest.fn(),
constant: mockValue, // ✅ OK - prefixed with 'mock'
}));
// ✅ Valid - references global built-ins
jest.mock('./module', () => ({
data: Object.keys({}),
promise: Promise.resolve(),
}));Specific Error Messages:
// "The second argument of `jest.mock` must be an inline function."
jest.mock('./module', variableFunction);
// "The module factory of `jest.mock()` is not allowed to reference any out-of-scope variables."
// "Invalid variable access: variableName"
// "Allowed objects: Array, Boolean, Date, Error, ..."
const someVar = 'test';
jest.mock('./module', () => someVar);// Babel core types
import type { PluginObj } from '@babel/core';
import type { NodePath } from '@babel/traverse';
import type {
Identifier,
Expression,
CallExpression,
Program,
ImportDeclaration,
MemberExpression,
Node,
Statement,
Super,
VariableDeclaration,
VariableDeclarator
} from '@babel/types';
// Plugin state interface
interface PluginState {
declareJestObjGetterIdentifier: () => Identifier;
jestObjGetterIdentifier?: Identifier;
}
// Jest object information for hoisting decisions
interface JestObjInfo {
hoist: boolean;
path: NodePath<Expression>;
}
// Visitor interface for AST traversal
interface Visitor<State = {}> {
[key: string]: any;
}// Jest object identifiers
const JEST_GLOBAL_NAME = 'jest';
const JEST_GLOBALS_MODULE_NAME = '@jest/globals';
const JEST_GLOBALS_MODULE_JEST_EXPORT_NAME = 'jest';
// Allowed identifiers in mock factories (includes ES2015 built-ins, Node.js globals, and Jest globals)
const ALLOWED_IDENTIFIERS: Set<string>;
// Internal tracking WeakSets for hoisting state
const hoistedVariables: WeakSet<VariableDeclarator>;
const hoistedJestGetters: WeakSet<CallExpression>;
const hoistedJestExpressions: WeakSet<Expression>;
// Function validation mapping
const FUNCTIONS: Record<string, <T extends Node>(args: Array<NodePath<T>>) => boolean>;/**
* Checks if an expression is a Jest object reference
* @param expression - The expression to check
* @returns True if the expression references a Jest object
*/
function isJestObject(
expression: NodePath<Expression | Super>
): expression is NodePath<Identifier | MemberExpression>;
/**
* Extracts Jest object expression if it's hoistable
* @param expr - The expression to analyze
* @returns Jest object info or null if not hoistable
*/
function extractJestObjExprIfHoistable(expr: NodePath): JestObjInfo | null;
/**
* Babel template for creating the Jest object getter function
* Generates: function _getJestObj() { const { jest } = require('@jest/globals'); ... }
*/
const createJestObjectGetter: ReturnType<typeof statement>;