Skip to content

[No QA] Consolidate on the OXC React Compiler and delete the Babel one - #102659

Open
elirangoshen wants to merge 10 commits into
Expensify:mainfrom
callstack-internal:eliran/101668-remove-babel-react-compiler
Open

elirangoshen wants to merge 10 commits into
Expensify:mainfrom
callstack-internal:eliran/101668-remove-babel-react-compiler

Conversation

@elirangoshen

@elirangoshen elirangoshen commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

Every build now compiles app source with the OXC React Compiler: web through Rsbuild, native through Re.Pack (HybridApp release builds too, since https://github.com/Expensify/Mobile-Expensify/pull/14186), and Jest through oxcJestTransformer (#102081). The Babel React Compiler was left on the BUNDLER=metro fallback, on Jest coverage runs, and in a set of tools that ran both compilers and compared them. This PR moves coverage runs onto OXC, removes babel-plugin-react-compiler completely and deletes the tools.

Jest coverage runs use OXC too. CI runs Jest with --coverage, and those runs used to go through babel-jest, which is why the Babel React Compiler had to stay. Now OXC compiles the file first, and a small Babel pass only adds coverage counters. It runs about as fast as before. So babel-plugin-react-compiler is removed from babel.config.js, package.json and knip.json (it stays in the lockfile only because babel-preset-expo depends on it).

One set of compiler options. config/babel/reactCompilerConfig.js is folded into oxcReactCompilerConfig.js, which every lane (Rsbuild, Re.Pack, Jest and the checker) now uses.

Tools that compared the two compilers deleted. checkBoth, checkWithBabel, check-both-compilers-runner, the divergence audit script and their tests.

Compliance check is OXC only. It keeps the two rules that matter: new files must compile, and modified files must not regress. The divergence rule and the inline Babel analysis are gone. The check's test runs under bun:test now, so it imports the OXC checker directly instead of through a dynamic import.

Lint uses one compiler. ReactCompilerWorker calls checkWithOxc directly, so lint no longer runs a second compiler for each file. The cache fingerprint only lists the files that still exist.

CLAUDE.md, contributingGuides/REACT_COMPILER.md and the compiler coding rule now describe OXC as the only compiler.

Fixed Issues

$ #101668
PROPOSAL:

Automated Tests

  • tests/tooling/ReactCompilerComplianceCheck.test.ts now runs against OXC. The error-location test checks the line that breaks the rule, since OXC reports that line and not the enclosing function.
  • tests/unit/CheckBothCompilersTest.ts is deleted with the code it covered.
  • CI's Jest jobs run with --coverage, so they now test the OXC coverage path. Locally, npx jest tests/ui --coverage passes all 392 suites with TZ=UTC.

Tests

  1. Run npm run react-compiler-compliance-check -- --verbose check src/pages/home/ and verify every file is COMPILED or SKIPPED, none FAILED, and nothing mentions Babel.
  2. Run npm run lint and npm run test:bun, and verify both are green.
  3. Run npx rock bundle --platform ios --dev false --entry-file index.js --bundle-output /tmp/main.jsbundle and verify the bundle contains React Compiler output: grep -o memo_cache_sentinel /tmp/main.jsbundle | wc -l is in the thousands.
  4. Run npm run ios, open Home, then FAB → Create expense, and switch between the Manual and Scan tabs. Verify nothing crashes.
  • Verify that no errors appear in the JS console

Offline tests

N/A. Build and lint tooling only; app source compiles the same way it did before.

QA Steps

N/A. Build and lint tooling only.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If the PR adds or modifies the UI:
    • I asked an AI agent to review the changes for accessibility issues and addressed its findings.
    • I tested with a screen reader (VoiceOver on macOS) and verified all new/changed elements are reachable with a logical focus order.
    • I verified all new/changed elements have meaningful accessible names and roles.
    • I verified state changes are announced (e.g. checked/unchecked, expanded/collapsed, selected).
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari 101668-step3-bundle 101668-step1-compliance

Every build lane now compiles app source with OXC: web through Rsbuild, native
through Re.Pack (HybridApp release builds too, since Mobile-Expensify Expensify#14144)
and Jest through oxcJestTransformer. The Babel React Compiler only remained on
the BUNDLER=metro debug fallback, plus a layer of tooling that ran both
compilers and compared them.

- Drop babel-plugin-react-compiler from babel.config.js, package.json and
  knip.json, and inline reactCompilerConfig.js into oxcReactCompilerConfig.js.
- Delete checkBoth, checkWithBabel, check-both-compilers-runner, the
  divergence audit script and their tests.
- Reduce the compliance check to OXC: new files must compile and modified
  files must not regress. The divergence rule and the inline Babel analysis
  are gone.
- Point the lint worker at checkWithOxc and trim the fingerprint files.
- Update CLAUDE.md, REACT_COMPILER.md and the compiler coding rule.

BUNDLER=metro builds now run without React Compiler memoization. Metro is a
debug escape hatch, not a shipped path.
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ This PR is possibly changing native code and/or updating libraries, it may cause problems with HybridApp. Please check if any patch updates are required in the HybridApp repo and run an AdHoc build to verify that HybridApp will not break. Ask Contributor Plus for help if you are not sure how to handle this. ⚠️

CI runs Jest with --coverage, and oxcJestTransformer sends app source to
babel-jest for those runs so istanbul can instrument in the same pass. That
lane still compiled with babel-plugin-react-compiler through babel.config.js.
Removing the plugin left CI tests unmemoized, and several UI suites looped
until they timed out or ran out of memory.

babel.config.js now applies the plugin only for babel-jest, with the options
from oxcReactCompilerConfig so both compilers compile the same way. Metro and
Re.Pack stay without it. The dependency and its override are restored.
Coverage runs go OXC -> one light Babel pass (istanbul + CJS conversion) using
OXC's source map, replacing the babel-jest coverage lane. Timed ~par in an A/B
(Oct-5); the report shape shifts (type-only modules drop out, branch totals
move), so the Codecov baseline needs team sign-off before merging.
oxcJestTransformer imports babel-plugin-istanbul and the two Babel transform
plugins directly now, but they were only transitive deps. CI's knip comparison
flagged all three as unlisted.
@elirangoshen

elirangoshen commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor Author

ok https://github.com/Expensify/Mobile-Expensify/pull/14186 I suggest to wait few days after https://github.com/Expensify/Mobile-Expensify/pull/14186 will reach prod to make it safer to merge this pr that build upon it.

@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
see 4274 files with indirect coverage changes

Coverage runs compile app source with OXC and only use Babel for istanbul and
the CommonJS conversion, with babel.config.js turned off. The jest block's
babel-plugin-react-compiler only reached mocks, jest setup files and the OXC
fallback, none of which need memoization. Remove it along with the dependency,
its override and the knip ignore, and drop the coverage exception from the docs.

babel-plugin-react-compiler stays in the lockfile as a dependency of
babel-preset-expo, which we only use for inline-env-vars.
@elirangoshen

elirangoshen commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Codecov Report

✅ All modified and coverable lines are covered by tests. see 4291 files with indirect coverage changes

About the Codecov report above: the numbers shift because coverage is now measured on OXC output instead of Babel's, not because any test changed. Type-only files compile to nothing and drop out of the report, so totals move and Codecov shows ~4261 files with indirect changes. CI stays green, since Codecov checks are informational in this repo.

…e-babel-react-compiler

# Conflicts:
#	package.json
@elirangoshen
elirangoshen marked this pull request as ready for review October 23, 2026 08:18
@elirangoshen
elirangoshen requested review from a team and Julesssss as code owners October 23, 2026 08:18
@melvin-bot
melvin-bot Bot requested review from aimane-chnaif and removed request for a team October 23, 2026 08:18
@melvin-bot

melvin-bot Bot commented Oct 23, 2026

Copy link
Copy Markdown

@aimane-chnaif Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-23T08:34:51.038877Z f398d10 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@aimane-chnaif

aimane-chnaif commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified any copy / text that was added to the app is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If the PR adds or modifies the UI:
    • I asked an AI agent to review the changes for accessibility issues and addressed its findings.
    • I tested with a screen reader (VoiceOver on macOS) and verified all new/changed elements are reachable with a logical focus order.
    • I verified all new/changed elements have meaningful accessible names and roles.
    • I verified state changes are announced (e.g. checked/unchecked, expanded/collapsed, selected).
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp
ios.mp4
iOS: mWeb Safari
MacOS: Chrome / Safari
web.mov

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f398d10523

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread config/babel/oxcJestTransformer.js Outdated
Comment on lines +176 to +178
.update(esbuild.version)
.update(oxcVersion)
.update(transformOptions?.instrument ? 'instrument' : '')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hash the coverage transform dependencies in its cache key

When package-lock.json changes one of the newly invoked Babel/Istanbul packages, .github/workflows/test.yml restores an older .jest-cache via its restore-keys entry on line 43, but this custom key only distinguishes the OXC/esbuild versions and the instrumentation bit. Unchanged source files can therefore reuse output and coverage counters generated by an older @babel/core, babel-plugin-istanbul, or CommonJS/dynamic-import plugin, mixing stale instrumentation with the current dependencies; include those package versions (or a lockfile fingerprint) in the instrumented key.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread babel.config.js Outdated
}

return ['metro', 'babel-jest'].includes(runningIn) ? metro : {};
return runningIn === 'babel-jest' || runningIn === 'metro' ? metro : {};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BUNDLER=metro now builds without React Compiler

The metro lane in babel.config.js lost babel-plugin-react-compiler and nothing replaces it, since OXC only runs in the Re.Pack loader. BUNDLER=metro is still advertised as the fallback (here and in Mobile-Expensify's build.gradle and react-native-xcode.sh), so anyone using it would ship an unmemoized bundle with no error or warning.

App/rock.config.mjs

Lines 8 to 11 in f398d10

// `BUNDLER=metro` reverts the native build to Metro. Keep it while HybridApp still bundles with
// Metro: its release builds go through the Gradle plugin's own Metro task, so deleting Metro here is
// blocked on moving hybrid bundling onto Rock in the wrapper repo first.
const useMetro = process.env.BUNDLER === 'metro';

  1. On this PR run BUNDLER=metro npx rock bundle --platform ios --dev false --entry-file index.js --bundle-output /tmp/main.jsbundle
  2. Run grep -o memo_cache_sentinel /tmp/main.jsbundle | wc -l

Expected: thousands, or the build refuses to run
Actual: 0

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread config/babel/oxcJestTransformer.js Outdated
.update(REACT_COMPILER_CONFIG_KEY)
.update(esbuild.version)
.update(oxcVersion)
.update(transformOptions?.instrument ? 'instrument' : '')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage cache key ignores Babel and istanbul versions

The instrument lane output depends on @babel/core, babel-plugin-istanbul and @babel/plugin-transform-modules-commonjs, but none of their versions are hashed. After a bump, a local .jest-cache keeps serving stale instrumented output.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

Comment thread config/babel/oxcJestTransformer.js Outdated
Comment on lines 103 to 133
function processWithOxcForCoverage(sourceText, sourcePath, transformOptions) {
const {transformSync} = getOxcPipeline();
const oxcResult = transformSync(sourcePath, sourceText, {
lang: getLang(sourcePath),
sourcemap: true,
jsx: {runtime: 'automatic', development: true},
reactCompiler: REACT_COMPILER_OPTIONS,
});

if (oxcResult.fatal || !oxcResult.code) {
return null;
}
return !NODE_MODULES_RE.test(filename) && !TESTS_RE.test(filename) && !JEST_SETUP_RE.test(filename) && !MOCKS_RE.test(filename) && !TEST_FILE_RE.test(filename);

const result = require('@babel/core').transformSync(oxcResult.code, {
filename: sourcePath,
configFile: false,
babelrc: false,
inputSourceMap: oxcResult.map,
sourceMaps: true,
auxiliaryCommentBefore: ' istanbul ignore next ',
plugins: [
[require.resolve('babel-plugin-istanbul'), {cwd: transformOptions.config.cwd, exclude: []}],
[require.resolve('@babel/plugin-transform-modules-commonjs'), {strictMode: false}],
require.resolve('@babel/plugin-transform-dynamic-import'),
],
});

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

function processWithOxc(sourceText, sourcePath) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated OXC call

processWithOxcForCoverage repeats the transformSync call and fatal check from processWithOxc. A shared helper keeps the two lanes from drifting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

@MelvinBot

Copy link
Copy Markdown
Contributor

⚠️ A setup step failed before I could start. This is usually a temporary issue — try again in a few minutes, and if it persists post in #melvinbot-ai.

Run details

@aimane-chnaif

Copy link
Copy Markdown
Contributor

@MelvinBot review

@MelvinBot

Copy link
Copy Markdown
Contributor

Review

The PR looks correct apart from one issue to fix before merge. CI is green except for the checklist checks. I agree with all three of your inline comments and found nothing new beyond one stale comment.

1. Fix before merge: BUNDLER=metro silently ships without React Compiler (your inline comment)

I confirmed it. The metro lane in babel.config.js:26-29 no longer runs any compiler, and OXC only runs in the Re.Pack loader. rock.config.mjs:8-11 still offers BUNDLER=metro and says HybridApp release builds use Metro. That contradicts the PR description. Either:

  • Make the metro caller fail loudly in babel.config.js (throw when runningIn === 'metro'), or
  • Remove the BUNDLER=metro fallback and update that comment.

Either way, merge only after HybridApp stops bundling through Metro, as elirangoshen already suggested.

2. Should fix: coverage cache key misses Babel and istanbul versions (your inline comment)

oxcJestTransformer.js:178 hashes only the word instrument. Because test.yml restores .jest-cache through restore-keys, a bump to @babel/core, babel-plugin-istanbul or the CommonJS plugins can reuse stale instrumented output. Add their versions to the key in the instrument case.

3. Nit: duplicated OXC call (your inline comment)

processWithOxcForCoverage and processWithOxc repeat the same transformSync call and fatal check. A shared compileWithOxc(sourceText, sourcePath) helper would keep them in sync.

What I checked and found fine
  • Coverage source maps: babel-plugin-istanbul reads the OXC map through inputSourceMap, so coverage maps back to the original lines.
  • Coverage fallback: a fatal OXC result still falls back to babel-jest, which instruments the file itself.
  • Leftover references: no workflow, script or config references the deleted files (reactCompilerConfig.js, checkBoth, checkWithBabel, the divergence audit or the runner).
  • Lockfile: babel-plugin-react-compiler@1.0.0 stays only as a dependency of babel-preset-expo. The repo uses only that preset's inline-env-vars plugin and doesn't enable reactCompiler, so it has no effect.
  • Lint and compliance check: ReactCompilerWorker and ReactCompilerFilter are a clean rename from bothMemoized to memoized. The lint fingerprint still covers package-lock.json, so an OXC version bump still invalidates the cache. The compliance check keeps rules 1 and 2 and still uses the resolved base commit.
  • Jest cache keys: the jest-perf keys hash config/babel/**, so they change with this PR.

view run

@elirangoshen

Copy link
Copy Markdown
Contributor Author

Review

The PR looks correct apart from one issue to fix before merge. CI is green except for the checklist checks. I agree with all three of your inline comments and found nothing new beyond one stale comment.

1. Fix before merge: BUNDLER=metro silently ships without React Compiler (your inline comment)

I confirmed it. The metro lane in babel.config.js:26-29 no longer runs any compiler, and OXC only runs in the Re.Pack loader. rock.config.mjs:8-11 still offers BUNDLER=metro and says HybridApp release builds use Metro. That contradicts the PR description. Either:

  • Make the metro caller fail loudly in babel.config.js (throw when runningIn === 'metro'), or
  • Remove the BUNDLER=metro fallback and update that comment.

Either way, merge only after HybridApp stops bundling through Metro, as elirangoshen already suggested.

2. Should fix: coverage cache key misses Babel and istanbul versions (your inline comment)

oxcJestTransformer.js:178 hashes only the word instrument. Because test.yml restores .jest-cache through restore-keys, a bump to @babel/core, babel-plugin-istanbul or the CommonJS plugins can reuse stale instrumented output. Add their versions to the key in the instrument case.

3. Nit: duplicated OXC call (your inline comment)

processWithOxcForCoverage and processWithOxc repeat the same transformSync call and fatal check. A shared compileWithOxc(sourceText, sourcePath) helper would keep them in sync.

What I checked and found fine

  • Coverage source maps: babel-plugin-istanbul reads the OXC map through inputSourceMap, so coverage maps back to the original lines.
  • Coverage fallback: a fatal OXC result still falls back to babel-jest, which instruments the file itself.
  • Leftover references: no workflow, script or config references the deleted files (reactCompilerConfig.js, checkBoth, checkWithBabel, the divergence audit or the runner).
  • Lockfile: babel-plugin-react-compiler@1.0.0 stays only as a dependency of babel-preset-expo. The repo uses only that preset's inline-env-vars plugin and doesn't enable reactCompiler, so it has no effect.
  • Lint and compliance check: ReactCompilerWorker and ReactCompilerFilter are a clean rename from bothMemoized to memoized. The lint fingerprint still covers package-lock.json, so an OXC version bump still invalidates the cache. The compliance check keeps rules 1 and 2 and still uses the resolved base commit.
  • Jest cache keys: the jest-perf keys hash config/babel/**, so they change with this PR.

view run

fixed

@elirangoshen

Copy link
Copy Markdown
Contributor Author

I addressed all comments @aimane-chnaif

@aimane-chnaif

Copy link
Copy Markdown
Contributor

1. Import comments contradict each other

The worker says Bun resolves the .mjs "without the extension" while the compliance check says "with the extension". Both import it with the extension, so the worker comment is wrong. In the compliance check the comment also sits on the second import, while the first one has the same eslint-disable unexplained.

// The compiler helper is ESM (.mjs); Bun resolves it without the extension.
// eslint-disable-next-line import/extensions
import checkReactCompilerWithOxc from '../../../config/reactCompiler/checkWithOxc.mjs';

// eslint-disable-next-line import/extensions
import type {CompilationResult, CompilerError} from '../config/reactCompiler/checkWithOxc.mjs';
// The checker is ESM (.mjs); Bun resolves it with the extension.
// eslint-disable-next-line import/extensions
import checkReactCompilerCompliance from '../config/reactCompiler/checkWithOxc.mjs';

Fix: one wording in both files, placed above the first import.

2. metro naming and comments in babel.config.js are stale

Metro is now refused, so the metro object only serves Jest and the repack base. The comments "For react-native (iOS/Android) caller will be 'metro'" and "metro.config.js sets it for the Metro process" still describe Metro as a live caller.

App/babel.config.js

Lines 3 to 4 in e0c996a

* Expo SDK 56 replaces global fetch with expo/fetch unless this is set at transform time.
* metro.config.js sets it for the Metro process; babel needs it here so release bundles inline the value.

const metro = {

// For `react-native` (iOS/Android) caller will be "metro"

Fix: rename it (e.g. base) and reword those comments, or leave it for the PR that removes metro.config.js.

3. istanbul cwd differs from babel-jest

babel-jest passes cwd: config.rootDir to babel-plugin-istanbul, this passes config.cwd. Same today, but they diverge if Jest runs from another directory, and istanbul skips files outside cwd. strictMode: false mirrors @react-native/babel-preset without saying so.

[require.resolve('babel-plugin-istanbul'), {cwd: transformOptions.config.cwd, exclude: []}],
[require.resolve('@babel/plugin-transform-modules-commonjs'), {strictMode: false}],

Fix: use rootDir and add a short note on strictMode: false.

4. Coverage lane still loads and hashes esbuild

Coverage runs never call esbuild, but getCacheKey loads it and hashes its version, so an esbuild bump needlessly misses the coverage cache.

const {esbuild, oxcVersion} = getOxcPipeline();
return crypto
.createHash('sha1')
.update(sourceText)
.update('\0', 'utf8')
.update(sourcePath)
.update(TRANSFORMER_SOURCE)
.update(REACT_COMPILER_CONFIG_KEY)
.update(esbuild.version)
.update(oxcVersion)
.update(transformOptions?.instrument ? `instrument:${getCoverageVersionsKey()}` : '')

Fix: hash esbuild.version only on the non-instrument lane.

5. Redundant type annotation

const mainResult: CompilationResult | undefined = ... is what TypeScript already infers.

const mainResult: CompilationResult | undefined = mainSource !== undefined ? checkReactCompilerCompliance(mainSource, mainPath) : undefined;

Fix: drop the annotation.

@MelvinBot

Copy link
Copy Markdown
Contributor

Re-review at e0c996a

The latest commit fixes all three earlier findings. I see no blocking issues, and CI is green except for the checklist checks. Of aimane-chnaif's five new comments, four are valid nits and one (#3) is not.

Earlier findings: fixed

  • Metro: babel.config.js:184-187 now throws for the metro caller, so a Metro build fails instead of shipping without the compiler. npm start runs rock start, which uses Re.Pack unless BUNDLER=metro is set, so local dev is unaffected. I can't check the HybridApp side. Keep your plan to merge only after the HybridApp change reaches production.
  • Cache key: getCoverageVersionsKey hashes all four coverage packages. None of them declares an exports field, so each require('<name>/package.json') resolves.
  • Duplication: compileWithOxc is now shared by both lanes.

New review comments

# Comment Verdict
1 Import comments contradict each other ✅ Valid. ReactCompilerWorker.ts:1 says "without the extension" above an import that uses .mjs.
2 Stale metro naming and comments ✅ Valid. babel.config.js:4 and :171 still describe Metro as a live caller. You can defer the rename.
3 istanbul cwd should be rootDir ❌ Not needed. The installed babel-jest 29.7.0 passes cwd: transformOptions.config.cwd to babel-plugin-istanbul, not rootDir, so oxcJestTransformer.js:144 already matches it. A short comment on strictMode: false would still help.
4 Coverage lane hashes esbuild.version ✅ Valid nit. oxcJestTransformer.js:190 hashes it on both lanes, so an esbuild bump causes an extra cache miss on coverage runs. getOxcPipeline still loads esbuild in both lanes.
5 Redundant CompilationResult | undefined annotation ✅ Valid nit. TypeScript infers the same type.

view run

@aimane-chnaif

Copy link
Copy Markdown
Contributor

Some minor nits above but not blockers

@MelvinBot

Copy link
Copy Markdown
Contributor

Code quality review at e0c996a

The code is clean and I found no bugs. Below are four small cleanups that are not already in aimane-chnaif's five comments or my earlier review. None of them blocks merge.

1. The compliance check re-exports the OXC checker under a second name

react-compiler-compliance-check.ts:21 imports checkWithOxc.mjs as checkReactCompilerCompliance, while the worker calls the same function checkReactCompilerWithOxc. The script then re-exports it at line 232 only so the test can import it, and nothing imports the re-exported types.

Fix: have ReactCompilerComplianceCheck.test.ts import checkWithOxc.mjs directly, since it runs under bun:test now. Then drop both exports and use one name in both files. With the annotation dropped (comment 5), the script needs only CompilerError from the type import.

2. oxcJestTransformer.js can compile once and branch

processWithOxcForCoverage and processWithOxc each call compileWithOxc and repeat the null check. Other small issues:

  • @babel/core and the three require.resolve calls run on every file at lines 136-146. The rest of the file loads its dependencies lazily, once, through getOxcPipeline.
  • Lines 79-81 call oxcReactCompilerConfig() twice. JSON.stringify(REACT_COMPILER_OPTIONS) gives the same key.

Fix: in process, call compileWithOxc once, then pass its result to an instrument step or an esbuild step. Load the Babel plugins in a lazy getCoveragePipeline() that also builds the version key, which replaces getCoverageVersionsKey. This also addresses comment 4.

3. oxcReactCompilerConfig.js has two JSDoc blocks and a stale sentence

Lines 1-17 stack two JSDoc blocks. The first describes history ("Each used to spell out its own"). The second explains why isDev is missing, but isDev was a Babel option, and Babel is gone, so no one will look for it.

Fix: merge the blocks into one, and keep only why eslintSuppressionRules: [] is set and which overrides are allowed.

4. The lint cache fingerprint still includes rsbuild.common.ts

ReactCompilerFilter.ts:15 hashes config/rsbuild/rsbuild.common.ts. However, checkWithOxc.mjs reads only oxcReactCompilerConfig.js, so any Rsbuild edit clears the lint compiler cache for nothing.

Fix: remove that entry.

Optional follow-up

oxcReactCompilerConfig.js is no longer used by Babel, so config/reactCompiler/ would be a better home than config/babel/. If you move it, update the hashFiles('config/babel/**') cache keys in reassurePerformanceTests.yml and seedJestPerfCache.yml. Otherwise, a change to the compiler options would stop invalidating the Reassure cache. That makes it a better fit for a separate PR.


view run

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants