Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f267e33
Run all Jest tests through the OXC transformer
roryabraham Sep 5, 2026
2c0cc38
Lower const/let in the OXC Jest CJS pass
roryabraham Sep 6, 2026
4c82bcb
Merge remote-tracking branch 'origin/main' into rory/oxc-jest-all-tests
roryabraham Sep 7, 2026
08f3066
Fix oxfmt and knip on the OXC Jest transformer
roryabraham Sep 7, 2026
fa9328d
Use esbuild for CJS in the OXC Jest transformer
roryabraham Sep 7, 2026
9dc840b
Hoist jest.mock without Babel in the OXC Jest transformer
roryabraham Sep 7, 2026
01ffa0a
Hoist jest.mock with oxc-parser instead of a scanner
roryabraham Sep 7, 2026
ab90c2f
Fix Jest typecheck after switching API.write spies to jest.mocked
roryabraham Sep 7, 2026
4b26204
Restore Babel loose CJS after OXC for Jest
roryabraham Sep 7, 2026
6f63e8c
Fix Jest ExpoImage load and drop esbuild test rewrites
roryabraham Sep 7, 2026
0d381b3
Mock Expo Store Review in Jest
roryabraham Sep 7, 2026
b3687b4
Keep string SVG sources in the Jest expo-image mock
roryabraham Sep 8, 2026
bffaa33
Force Jest workers to exit after CI shards
roryabraham Sep 8, 2026
01470f2
fix: clean up Jest app timers
roryabraham Sep 9, 2026
c7a8258
Merge remote-tracking branch 'origin/main' into rory/oxc-jest-all-tests
roryabraham Sep 9, 2026
19171ac
ci: reduce OXC Jest worker concurrency
roryabraham Sep 9, 2026
c840274
Merge remote-tracking branch 'origin/main' into rory/oxc-jest-all-tests
roryabraham Sep 9, 2026
3a2f7e6
perf: avoid double-transforming Jest files
roryabraham Sep 9, 2026
f3b9845
Merge remote-tracking branch 'origin/main' into rory/oxc-jest-all-tests
roryabraham Sep 9, 2026
b91cec6
test: verify hybrid Jest transformer routing
roryabraham Sep 9, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ 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
run: npm test -- --silent --shard=${{ fromJSON(matrix.chunk) }}/${{ strategy.job-total }} --maxWorkers=6 --coverage --coverageDirectory=coverage/shard-${{ matrix.chunk }}
run: npm test -- --silent --shard=${{ fromJSON(matrix.chunk) }}/${{ strategy.job-total }} --maxWorkers=4 --coverage --coverageDirectory=coverage/shard-${{ matrix.chunk }}

- name: Upload coverage to Codecov (PRs - tokenless)
if: ${{ github.event_name == 'pull_request' }}
Expand Down
1 change: 0 additions & 1 deletion __mocks__/react-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ jest.doMock('react-native', () => {
const reactNativeMock = Object.setPrototypeOf(
{
NativeModules: {
...ReactNative.NativeModules,
BootSplash: {
hide: jest.fn().mockResolvedValue(undefined),
logoSizeRatio: 1,
Expand Down
67 changes: 52 additions & 15 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 NODE_MODULES_RE = /[/\\]node_modules[/\\]/;
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,10 @@ const REACT_COMPILER_OPTIONS = {
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 @@ -39,27 +49,45 @@ function shouldUseOxc(filename) {
return !NODE_MODULES_RE.test(filename) && !TESTS_RE.test(filename) && !JEST_SETUP_RE.test(filename) && !MOCKS_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 +104,24 @@ module.exports = {
.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
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ 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',
// App sources use OXC for TS/JSX and React Compiler, followed by a small Babel
// CommonJS pass. Test, setup, mock, and Flow files use babel-jest directly to avoid
// paying for both OXC and Babel transforms.
'^.+\\.[jt]sx?$': '<rootDir>/config/babel/oxcJestTransformer.js',
'^.+\\.svg?$': 'jest-transformer-svg',
},
transformIgnorePatterns: [
Expand Down
21 changes: 21 additions & 0 deletions jest/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ jest.mock('expo-task-manager', () => ({
// Add other methods here if you use them
}));

jest.mock('expo-web-browser', () => ({
openAuthSessionAsync: jest.fn(() => Promise.resolve({type: 'dismiss'})),
maybeCompleteAuthSession: jest.fn(),
}));

// jest-expo's haste map uses defaultPlatform 'ios', so `@components/ImageSVG` resolves to
// index.ios.tsx which imports expo-image. expo-image has no jest-expo mock, so requireNativeModule
// throws. Stub the package so UI tests can load Icon/ImageSVG.
jest.mock('expo-image', () => ({
Image: Object.assign(({source}: {source?: unknown}) => (typeof source === 'string' ? source : null), {
clearMemoryCache: jest.fn(() => Promise.resolve(true)),
prefetch: jest.fn(() => Promise.resolve(true)),
}),
}));

jest.mock('expo-store-review', () => ({
hasAction: jest.fn(() => Promise.resolve(false)),
isAvailableAsync: jest.fn(() => Promise.resolve(false)),
requestReview: jest.fn(() => Promise.resolve()),
}));

// Mock expo-location — the jest-expo preset replaces all native module methods with jest.fn(async () => {}),
// which returns undefined instead of a proper PermissionResponse. This causes crashes when code reads .status
// from the result of requestForegroundPermissionsAsync().
Expand Down
6 changes: 5 additions & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@
"shellcheck",
"patch-package",
"diff-so-fancy",
"@babel/plugin-proposal-class-properties"
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-block-scoping",
"@babel/plugin-transform-dynamic-import",
"@babel/plugin-transform-modules-commonjs",
"babel-plugin-jest-hoist"
],
"ignoreBinaries": ["metro-symbolicate", "mkcert", "openssl"]
}
Loading
Loading