Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
# Per-shard key: each shard only instruments the ~1/8 of suites it runs, so a single
# shared key freezes one shard's partial cache for all shards. The hash refreshes the
# cache when dependencies or Babel config change; restore-keys warms it in between.
key: ${{ runner.os }}-jest-${{ matrix.chunk }}-${{ hashFiles('package-lock.json', 'patches/**', 'babel.config.js') }}
key: ${{ runner.os }}-jest-${{ matrix.chunk }}-${{ hashFiles('package-lock.json', 'patches/**', 'babel.config.js', 'jest.config.js', 'config/babel/oxcJestTransformer.js', 'config/babel/reactCompilerConfig.js') }}
restore-keys: ${{ runner.os }}-jest-${{ matrix.chunk }}-

- name: Jest tests
Expand Down
73 changes: 57 additions & 16 deletions config/babel/oxcJestTransformer.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const esbuild = require('esbuild');
const babel = require('@babel/core');
const {transformSync} = require('oxc-transform-react');

const babelJest = require('babel-jest');
const BABEL_CORE_VERSION = require('@babel/core/package.json').version;
const BLOCK_SCOPING_PLUGIN_VERSION = require('@babel/plugin-transform-block-scoping/package.json').version;
const DYNAMIC_IMPORT_PLUGIN_VERSION = require('@babel/plugin-transform-dynamic-import/package.json').version;
const CJS_PLUGIN_VERSION = require('@babel/plugin-transform-modules-commonjs/package.json').version;
const JEST_HOIST_VERSION = require('babel-plugin-jest-hoist/package.json').version;
const OXC_TRANSFORM_REACT_VERSION = require('oxc-transform-react/package.json').version;
const BaseReactCompilerConfig = require('./reactCompilerConfig');

Expand All @@ -14,6 +19,7 @@
const TESTS_RE = /[/\\]tests[/\\]/;
const JEST_SETUP_RE = /[/\\]jest[/\\]/;
const MOCKS_RE = /[/\\]__mocks__[/\\]/;
const JEST_HOIST_RE = /\bjest\s*\.\s*(mock|unmock|deepUnmock|disableAutomock|enableAutomock)\b/;

const TRANSFORMER_SOURCE = fs.readFileSync(__filename);
const REACT_COMPILER_CONFIG_KEY = JSON.stringify(BaseReactCompilerConfig);
Expand All @@ -24,6 +30,14 @@
eslintSuppressionRules: [],
};

const CJS_PLUGIN_OPTIONS = {loose: true, strictMode: false};
const CJS_PLUGINS = [
'@babel/plugin-transform-block-scoping',
'@babel/plugin-transform-dynamic-import',
['@babel/plugin-transform-modules-commonjs', CJS_PLUGIN_OPTIONS],
];
const CJS_AND_HOIST_PLUGINS = [...CJS_PLUGINS, 'babel-plugin-jest-hoist'];

function getLang(filename) {
const ext = path.extname(filename).slice(1);
if (ext === 'tsx') {
Expand All @@ -36,30 +50,48 @@
}

function shouldUseOxc(filename) {
return !NODE_MODULES_RE.test(filename) && !TESTS_RE.test(filename) && !JEST_SETUP_RE.test(filename) && !MOCKS_RE.test(filename);
return !NODE_MODULES_RE.test(filename);
}

function shouldRunReactCompiler(filename) {
return !TESTS_RE.test(filename) && !JEST_SETUP_RE.test(filename) && !MOCKS_RE.test(filename);
}

function toCommonJS(code, sourcePath, inputSourceMap) {
const plugins = JEST_HOIST_RE.test(code) ? CJS_AND_HOIST_PLUGINS : CJS_PLUGINS;
const result = babel.transformSync(code, {
filename: sourcePath,
ast: false,
code: true,
babelrc: false,
configFile: false,
compact: false,
sourceType: 'module',
sourceMaps: true,
inputSourceMap: inputSourceMap ?? undefined,
plugins,
});

if (!result?.code) {
return {code, map: inputSourceMap};
}

return {code: result.code, map: result.map ?? inputSourceMap};
}

function processWithOxc(sourceText, sourcePath) {
const oxcResult = transformSync(sourcePath, sourceText, {
lang: getLang(sourcePath),
sourcemap: true,
jsx: {runtime: 'automatic', development: true},
reactCompiler: REACT_COMPILER_OPTIONS,
reactCompiler: shouldRunReactCompiler(sourcePath) ? REACT_COMPILER_OPTIONS : false,
});

if (oxcResult.fatal || !oxcResult.code) {
return null;
}

const cjs = esbuild.transformSync(oxcResult.code, {
loader: 'js',
format: 'cjs',
supported: {'dynamic-import': false},
sourcefile: sourcePath,
sourcemap: true,
});

return {code: cjs.code, map: cjs.map};
return toCommonJS(oxcResult.code, sourcePath, oxcResult.map);
}

module.exports = {
Expand All @@ -76,15 +108,24 @@
.update(sourcePath)
.update(TRANSFORMER_SOURCE)
.update(REACT_COMPILER_CONFIG_KEY)
.update(esbuild.version)
.update(JSON.stringify(CJS_PLUGIN_OPTIONS))
.update(BABEL_CORE_VERSION)
.update(BLOCK_SCOPING_PLUGIN_VERSION)
.update(DYNAMIC_IMPORT_PLUGIN_VERSION)
.update(CJS_PLUGIN_VERSION)
.update(JEST_HOIST_VERSION)
.update(OXC_TRANSFORM_REACT_VERSION)
.digest('hex');
},
process(sourceText, sourcePath, transformOptions) {
if (shouldUseOxc(sourcePath)) {
const result = processWithOxc(sourceText, sourcePath);
if (result) {
return result;
try {
const result = processWithOxc(sourceText, sourcePath);
if (result) {
return result;
}
} catch {
// Fall through to babel-jest for syntax OXC or the CJS pass cannot parse.
}
}

Expand Down
9 changes: 5 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ module.exports = {
`<rootDir>/?(*.)+(spec|test).${testFileExtension}`,
],
transform: {
// Reassure re-transforms ~7k files under `--max-opt=1` (V8 sparkplug only), which
// makes Babel ~half of each measure job. OXC + esbuild is native and stays fast
// without TurboFan. Test files stay on babel-jest so `jest.mock` is still hoisted.
'^.+\\.[jt]sx?$': isPerfTestRun ? '<rootDir>/config/babel/oxcJestTransformer.js' : 'babel-jest',
// OXC compiles TS/JSX (and React Compiler on app sources). A small Babel pass then
// emits loose CommonJS, lowers import(), and hoists jest.mock so test files no
// longer need a babel-jest split. Native OXC stays fast under Reassure's
// `--max-opt=1` (V8 sparkplug only).
'^.+\\.[jt]sx?$': '<rootDir>/config/babel/oxcJestTransformer.js',
'^.+\\.svg?$': 'jest-transformer-svg',
},
transformIgnorePatterns: [
Expand Down
38 changes: 32 additions & 6 deletions tests/tooling/oxcTransformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {describe, expect, it} from 'bun:test';
import {createRequire} from 'node:module';
import path from 'node:path';

type TransformResult = {code: string};
type TransformResult = {code: string; map?: {sources?: string[]}};

type OxcTransformer = {
process: (sourceText: string, sourcePath: string, transformOptions: unknown) => TransformResult;
Expand Down Expand Up @@ -31,10 +31,10 @@ describe('oxcTransformer', () => {
}
`;
const result = oxcTransformer.process(source, path.resolve('src/libs/math.ts'), transformOptions);
expect(result.code).toContain('module.exports');
expect(result.code).toContain('add: () => add');
expect(result.code).toContain('exports.add = add');
expect(result.code).not.toMatch(/^export /m);
expect(result.code).not.toContain(': number');
expect(result.map?.sources?.some((mapSource) => mapSource.endsWith('math.ts'))).toBe(true);
});

it('runs React Compiler on app components', () => {
Expand All @@ -48,15 +48,41 @@ describe('oxcTransformer', () => {
expect(result.code).toContain('jsxDEV');
});

it('leaves test files on babel-jest so jest.mock is hoisted', () => {
it.each(['tests/unit/Hello.test.tsx', 'jest/setup.tsx', '__mocks__/Hello.tsx'])('skips React Compiler on %s', (relativePath) => {
const source = `
export function Hello({name}: {name: string}) {
return <div>{name.toUpperCase()}</div>;
}
`;
const result = oxcTransformer.process(source, path.resolve(relativePath), transformOptions);
expect(result.code).not.toMatch(/compiler-runtime|_c\(/);
expect(result.code).toContain('jsxDEV');
});

it('lowers const in jest.mock factories so circular imports do not TDZ', () => {
const source = `
const mockedReportID = '1';
jest.mock('./foo', () => ({
parseReportRouteParams: () => ({reportID: mockedReportID}),
}));
export const x = mockedReportID;
`;
const result = oxcTransformer.process(source, path.resolve('tests/unit/Hello.test.ts'), transformOptions);
expect(result.code).toMatch(/var mockedReportID/);
expect(result.code).not.toMatch(/\bconst mockedReportID\b/);
});

it('hoists jest.mock above require() after CJS conversion', () => {
const source = `
import foo from './foo';
jest.mock('./foo');
export const x = 1;
export const x = foo;
`;
const result = oxcTransformer.process(source, path.resolve('tests/perf-test/Hello.perf-test.tsx'), transformOptions);
expect(result.code).toContain('_getJestObj().mock("./foo")');
expect(result.code.indexOf('_getJestObj().mock')).toBeLessThan(result.code.indexOf('exports.x'));
expect(result.code).toMatch(/require\(['"]\.\/foo['"]\)/);
expect(result.code.indexOf('_getJestObj().mock')).toBeLessThan(result.code.search(/require\(['"]\.\/foo['"]\)/));
expect(result.code).toContain('exports.x');
});

it('lowers dynamic import() so Jest still owns the module graph', () => {
Expand Down
Loading