or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.md
tile.json

index.mddocs/

babel-plugin-jest-hoist

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.

Package Information

  • Package Name: babel-plugin-jest-hoist
  • Package Type: npm
  • Language: TypeScript
  • Installation: npm install babel-plugin-jest-hoist

Core Imports

// 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';

Basic Usage

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';

Architecture

The plugin operates in three phases:

  1. Analysis Phase: Identifies Jest object method calls that need hoisting
  2. Transformation Phase: Replaces Jest object references with getter function calls
  3. Hoisting Phase: Moves hoistable calls to the top of their containing scope

Key components:

  • Jest Object Detection: Recognizes global jest, imported jest from @jest/globals, and namespace imports
  • Mock Factory Validation: Ensures mock factory functions only reference allowed identifiers
  • Variable Hoisting: Moves pure constant variables referenced in mock factories
  • Scope Management: Maintains proper scoping while hoisting across block boundaries

Plugin Visitor Pattern

The 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:

  1. Pre: Declares the _getJestObj getter function when first needed
  2. Visitor: Identifies and transforms Jest calls in expression statements
  3. Post: Hoists transformed calls and referenced variables to appropriate scopes

Capabilities

Plugin Function

Creates 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;
}

Supported Jest Methods

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.

Jest Object Recognition

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 Validation

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
});

Error Handling

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);

Types

// 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;
}

Constants

// 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>;

Internal Functions

/**
 * 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>;