[No QA] Swap babel-loader + babel-plugin-react-compiler for OXC (Rust React Compiler)#93980
Draft
roryabraham wants to merge 27 commits into
Draft
[No QA] Swap babel-loader + babel-plugin-react-compiler for OXC (Rust React Compiler)#93980roryabraham wants to merge 27 commits into
roryabraham wants to merge 27 commits into
Conversation
Two new loaders that together eliminate babel-plugin-react-compiler and the full Babel transform pass from the webpack build: fullstory-annotation-loader.js Standalone replacement for @fullstory/babel-plugin-annotate-react that uses @babel/parser + @babel/traverse in parse-only mode (no code transforms). Injects dataComponent / dataElement / dataSourceFile props onto JSX opening elements so OXC receives annotated JSX and can run its React Compiler before the JSX transform. No Babel transform pipeline needed. oxc-react-compiler-loader.js Thin wrapper around oxc-transform 0.136.0 that runs the Rust port of the React Compiler + JSX transform + TypeScript strip + env lowering in a single native pass. Demotes non-fatal React Compiler diagnostics to webpack warnings rather than hard build errors, matching babel-plugin-react-compiler's default bailout behaviour. Workaround for oxc-project/oxc#23587. Co-authored-by: Cursor <cursoragent@cursor.com>
…C (Rust React Compiler)
Replace the existing webpack transpilation pipeline with a three-pass setup
that uses OXC's native Rust React Compiler and eliminates most of the Babel
transform work.
New pipeline (webpack use[] runs right-to-left):
Pass 1 fullstory-annotation-loader — parse-only JSX annotation; OXC sees
annotated JSX and runs React Compiler before its own JSX transform.
Pass 2 oxc-react-compiler-loader — React Compiler + JSX + TypeScript +
env target in a single Rust pass (oxc-transform 0.136.0, pinned via
package.json overrides; NAPI binding removed in 0.137.0, see
oxc-project/oxc#23590 — re-exposure in progress).
Pass 3 babel-loader (worklets only) — react-native-worklets/plugin only;
no presets needed since OXC already handled types and JSX.
Included node_modules (Rule B) skip the React Compiler but still go through
OXC for JSX/TS transforms. Split into B1 (.ts/.tsx) and B2 (.js/.jsx) to
avoid upgrading .ts generic syntax to TSX lang.
Removed devDependencies superseded by OXC:
@babel/preset-react, @babel/preset-typescript,
@babel/preset-env, @babel/preset-flow, babel-plugin-react-native-web,
@babel/plugin-proposal-export-namespace-from, @babel/plugin-transform-class-properties
Retained (still used by Metro/babel-jest):
babel-plugin-react-compiler, @babel/plugin-proposal-class-properties,
@babel/plugin-transform-export-namespace-from
Build timing (clean / warm-cache):
Baseline (all-Babel): 186.5s / 187.9s
Previous (oxc-loader + Babel): 175.4s / 174.8s
This commit (OXC React Compiler): 71.4s / 71.8s — 62% faster than baseline
Co-authored-by: Cursor <cursoragent@cursor.com>
Closed
50 tasks
|
Hey, I noticed you changed some webpack configuration files. This can break production builds. Did you remember to run a production build locally to verify they still work? |
📲 Install BuildsAndroid
|
Codecov Report✅ All modified and coverable lines are covered by tests. |
- babel.config.js: strip removed packages from the dead webpack branch
(@babel/preset-react, @babel/preset-env, @babel/preset-flow,
@babel/preset-typescript, babel-plugin-react-native-web, and several
class-property/export-namespace transforms). This fixes the Storybook
build and ESLint import-alias rule which both load babel.config.js.
- fullstory-annotation-loader.js: rename annotateJSXNode/annotateComponent
→ applyPropsToJSXNode/applyPropsToComponent to satisfy the
no-negated-variables rule ('annotate' contains 'not' as a substring).
- oxc-react-compiler-loader.js: extract getLang() to eliminate the
no-nested-ternary violation.
- cspell.json: add 'errorbar' (from the fullstory KNOWN_INCOMPATIBLE list).
- package.json: add @babel/generator, get-tsconfig, oxc-transform as
explicit devDependencies (used directly by our loaders) to satisfy knip.
Scope the oxc-transform override to just oxc-loader to avoid conflicts.
Co-authored-by: Cursor <cursoragent@cursor.com>
# Conflicts: # cspell.json # package-lock.json # package.json
Co-authored-by: Cursor <cursoragent@cursor.com>
npm ci fails when a direct dependency uses a semver range (^) but overrides pins an exact version. Pin the devDependency to match the override so setupNode can install dependencies in CI. Co-authored-by: Cursor <cursoragent@cursor.com>
Storybook was still looking for a top-level babel-loader rule; push the OXC transform pipeline rules instead. Rename webpack loaders to .cjs to avoid the new-JS-files typecheck gate. Restore knip's cross-platform oxc-parser optional deps in package-lock.json. Co-authored-by: Cursor <cursoragent@cursor.com>
3 tasks
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # .storybook/webpack.config.ts # babel.config.js # config/rsbuild/fullstory-annotation-loader.cjs # config/rsbuild/oxc-react-compiler-loader.cjs # config/webpack/webpack.common.ts # package-lock.json # package.json
|
@mjasikowski 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] |
…l back cleanly on React Compiler bailouts Two correctness bugs surfaced while re-verifying this branch after rebasing onto the merged Rsbuild migration (#95319): 1. Rsbuild's default 'js' rule (builtin:swc-loader) ran on every .js/.ts/.jsx/.tsx file, including ones already handled by Rule A/B/B2 below, and ran *before* them in the loader chain -- silently stripping JSX/TS before the Fullstory annotation loader or OXC's React Compiler ever saw the original source. Added a `tools.bundlerChain` hook to exclude app source and `includedNodeModules` from the default rule, and forwarded it from `getSharedConfiguration` into `getCommonConfiguration` (which otherwise replaces rather than merges `tools`). 2. oxc-transform returns empty `code` (not just a diagnostic) when React Compiler hits an Error-severity Rules-of-React violation (e.g. accessing `ref.current` inline during render, as in useDebouncedState). The loader was demoting these to warnings but still shipping the empty output, silently breaking every component that imported the affected module (e.g. ThemeProvider crashed on first render in dev). Now retries the transform with `reactCompiler: false` when this happens, matching babel-plugin-react-compiler's default bailout behavior. Also updates knip.json (glob + ignoreDependencies) and regenerates package-lock.json for the merged package.json, both of which were already made locally but not staged before the merge commit. Co-authored-by: Cursor <cursoragent@cursor.com>
oxc-loader is an unofficial community package; oxc-webpack-loader is maintained by the oxc-project itself and is a drop-in replacement with the same TransformOptions passthrough. Also drops the hacky require.resolve indirection into oxc-loader's nested node_modules for resolving oxc-transform, since oxc-transform is already a pinned top-level dependency. Co-authored-by: Cursor <cursoragent@cursor.com>
…test Storybook's --smoke-test treats any webpack/rspack build warning not on its small internal allowlist as a hard failure. oxc-react-compiler-loader deliberately demotes non-fatal React Compiler diagnostics to warnings (matching babel-plugin-react-compiler's default bailout behaviour), which was tripping the CI Storybook tests job. Add them to rsbuild's own ignoreWarnings list, same mechanism already used for the lottie-react-native warning above. Co-authored-by: Cursor <cursoragent@cursor.com>
Rspack natively supports ESM loader modules, so there's no reason for oxc-react-compiler-loader and fullstory-annotation-loader to stay on require()/module.exports. Also disables no-invalid-this (loaders get `this` from the bundler, not a class/object) and the no-negated-variables false positive on "annotation" (matches the "notation" substring) for these loader files now that ESM sourceType makes ESLint newly aware of their top-level function declarations. Co-authored-by: Cursor <cursoragent@cursor.com>
The previous comment said this config "is no longer read by the web build," which reads as the whole file being dead. It's only the Rsbuild/OXC bundler pipeline that stopped reading it — ESLint's @babel/eslint-parser still loads babel.config.js for every file it lints (its caller name is never 'metro'/'babel-jest'), so the `web` branch stays load-bearing for resolving parser syntax plugins. Co-authored-by: Cursor <cursoragent@cursor.com>
Naming the specific presets/plugins that used to live in this branch before OXC took over web transforms violates our no-stale-code-references convention. Describe the current constraint (what ESLint's parser still needs) instead of narrating what was removed from devDependencies. Co-authored-by: Cursor <cursoragent@cursor.com>
e46a87b to
b8638db
Compare
Reapplying after the branch got reset to drop an unrelated commit that had briefly landed here; this fix was lost along with it. Co-authored-by: Cursor <cursoragent@cursor.com>
babel-loader ran a full Babel parse/transform/codegen cycle on every single app source file for react-native-worklets/plugin, even though only ~2.5% of src/ files reference react-native-reanimated, react-native-worklets, or a 'worklet' directive at all (verified by grep) — the rest paid the cost for a no-op pass, including 500KB+ translation files that triggered Babel's codegen deopt warnings. Replace babel-loader with a thin worklets-loader that checks source for those substrings first and only invokes @babel/core when a match is possible, skipping straight through otherwise. Drop babel-loader as a dependency now that nothing uses it. Co-authored-by: Cursor <cursoragent@cursor.com>
Benchmarked 4 approaches against the real src/ corpus (6189 files, 34.67 MiB): case-sensitive String.prototype.includes() beat the original case-insensitive regex by ~15% (3.9us/file vs 4.6us/file median), a toLowerCase()-based case-insensitive includes() was ~2x slower than the regex (allocates a full lowercased copy per file), and shelling out to rg per file was ~2000x slower (9.6ms/file) since process-spawn overhead dwarfs any SIMD search advantage at this scale. Case-sensitivity is also more correct, not just faster: the plugin's own directive check is an exact `=== 'worklet'` comparison, and npm package names are always lowercase, so the previous /i flag only caused false positives (e.g. matching `CLEAR_WORKLET_MAX_RETRIES` or `useWorkletStateMachine`, neither of which the plugin's fixed hook allowlist recognizes) — verified those files produce no worklet markers when actually transformed. Also drops the 'react-native-worklets' alternative, which was always redundant with 'worklet'. Co-authored-by: Cursor <cursoragent@cursor.com>
Benchmarking against the real src/ corpus showed the hand-rolled parse/traverse/conditional-generate pass was ~12% slower than just calling @fullstory/babel-plugin-annotate-react directly through @babel/core, and harder to maintain. Keep only the `<`-presence pre-filter, which is correctness-safe and skips ~28% of files with no JSX at all. Drops the now-unused @babel/generator and @babel/types devDependencies (@babel/traverse is kept — still used directly by .github/actions/javascript/authorChecklist). Co-authored-by: Cursor <cursoragent@cursor.com>
TypeScript's grammar disallows JSX outside .tsx, so this is a
guaranteed-safe skip rather than a heuristic. The existing
source.includes('<') check alone was a poor proxy for "might contain
JSX": 1722 of 3481 .ts files (49.5%) contain a literal '<' from
generics/comparisons (e.g. Record<K, V>) and were false positives,
including all of src/languages/ and most of src/CONST/ -- among the
largest files in the repo, several over the 500KB Babel
codegen-deoptimization threshold.
Benchmarked against the real src/ corpus: cuts Babel invocations from
70.9% to 44.6% of all files (37% fewer calls, ~52% less wall-clock
time in this pass). Fullstory annotation output is unchanged (240
dataComponent occurrences in the main bundle, 2637 total, both before
and after).
.js (non-.jsx) keeps the content check as a fallback, since nothing
in the build enforces that plain .js files can't contain JSX --
verified only 2 such files exist in src/, both generated parsers with
no JSX.
Co-authored-by: Cursor <cursoragent@cursor.com>
cache.type: 'persistent' was only ever wired up inside rsbuild.config.ts's dev-only branch, so `npm run build` (returned straight from getCommonConfiguration, never touching that branch) always ran with no cross-process cache. Moved it into getSharedConfiguration so dev, build, and Storybook all persist the module graph + loader outputs to disk, turning warm `npm run build` from a second cold build (~30s) into an actual warm rebuild (~11-13s) instead. Co-authored-by: Cursor <cursoragent@cursor.com>
Enables the new reactCompiler option in the web loader's plugin config so Fullstory annotations still find JSX return values when React Compiler has already extracted them into cached variables. Co-authored-by: Cursor <cursoragent@cursor.com>
…e false positive Drop the rulesdir/no-negated-variables suppression for config/rsbuild/*-loader.mjs in favor of a seatbelt baseline entry, keeping the config itself simpler. Co-authored-by: Cursor <cursoragent@cursor.com>
Move the three custom Rspack loaders (fullstory-annotation, oxc-react-compiler, worklets) into their own subdirectory and update the referencing paths in rsbuild.common.ts, the ESLint override glob, and the seatbelt baseline. Co-authored-by: Cursor <cursoragent@cursor.com>
… A/B naming Now that oxc-react-compiler-loader runs React Compiler on included node_modules too (with its own per-file Rules-of-React bailout), the separate TS/TSX and JS/JSX rules for that allowlist were identical other than the jsx option, so they're merged into one rule matching all 4 extensions like the app-source rule already does. The worklets-loader + oxc-react-compiler-loader pair shared by both rules is factored into getOxcAndWorkletsLoaders() to avoid repeating the same loader/options twice. Comments no longer use the "Rule A/B/B2" shorthand, and the Fullstory loader comment no longer implies pass ordering matters (the 2.4.0 upgrade's reactCompiler option makes it order-independent). Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Explanation of Change
Replaces the JS/TS transpilation pipeline with a three-pass setup that uses OXC's native Rust port of the React Compiler and eliminates most of the Babel transform work.
New three-pass pipeline (loaders run right-to-left, like Webpack's
use[])Pass 1 —
fullstory-annotation-loader.mjs(runs first on original JSX):@fullstory/babel-plugin-annotate-react(native: true, reactCompiler: true, matchingbabel.config.js'swebconfig plus the new 2.4.0reactCompileroption) directly via@babel/core, skippingbabel-loader's extra layer.tsfiles are skipped unconditionally — TypeScript's grammar disallows JSX outside.tsx, so this is grammar-guaranteed safe, not a heuristic. All other extensions fall back to asource.includes('<')check (a file with no<cannot contain JSX).tsextension check matters because<alone is a weak proxy for "might contain JSX" on.tsfiles: measured against the realsrc/corpus, 1722 of 3481.tsfiles (49.5%) contain a literal<from generics/comparisons (e.g.Record<K, V>) — including all ofsrc/languages/and most ofsrc/CONST/, some of the largest files in the repo. The extension check cuts Babel invocations from 70.9% to 44.6% of all filesPass 2 —
oxc-react-compiler-loader.mjs(runs second):oxc-transformdirectly: React Compiler → JSX transform → TypeScript strip → env target — all in one Rust passoxc-transformpinned to0.136.0(viapackage.json overrides, applied to bothoxc-transformandoxc-webpack-loader's copy): thereactCompilertransform option — the biggest win this PR offers — was removed fromoxc-transform's public API in 0.137.0 and is still absent as of the latest 0.139.0 (oxc-project/oxc#23590). It's on the Q3 roadmap for OXC to add it back. PassingreactCompiler: trueon 0.137+ is silently a no-op (verified: no error, nouseMemoCachein output). Unpinning before that binding is re-exposed would silently disable React Compiler entirely with no build failure to catch itbabel-plugin-react-compiler's default bailout behaviour (workaround for oxc-project/oxc#23587)ref.currentinline during render),oxc-transformreturns empty code for the whole file rather than just skipping the optimization — the loader detects this and re-runs the transform withreactCompiler: falseso the file still ships valid JS, matching Babel's silent-bailout behaviourPass 3 —
worklets-loader.mjs(runs last, worklets only):@babel/coredirectly with only thereact-native-worklets/pluginplugin — serialises'worklet'functions for UI-thread executionsource.includes('react-native-reanimated') || source.includes('worklet')) before invoking Babel at all — only ~2.5% ofsrc/files match (verified by grep), so this skips a full Babel parse/transform/codegen cycle on the other ~97.5%. Case-sensitive is intentional: the plugin's own directive check is an exact=== 'worklet'comparison, so this is more correct as well as fasterNode modules in
includedNodeModules(Rule B/B2) skip the React Compiler but still go through OXC for JSX/TS transforms, split by extension (B:.ts/.tsx, B2:.js/.jsx) to avoid.tsgeneric syntax being confused with JSX. These rules useoxc-webpack-loader, the official oxc-project loader — Pass 2 (app source) doesn't use it directly since it needs the React Compiler diagnostic handling described above, but pins to the sameoxc-transformversion.Also required: Rsbuild's default
builtin:swc-loader('js' rule) matches every.js/.ts/.jsx/.tsxfile and runs before rules added viaaddRules. Without excluding app source andincludedNodeModulesfrom it (via atools.bundlerChainhook), SWC would strip JSX/TS before the Fullstory/OXC loaders ever saw the original source, silently defeating both.All three custom loaders live under
config/rsbuild/loaders/and are plain ESM (.mjs) — Rspack loads loader modules natively, so there's no need to keep them onrequire()/module.exports.Removed devDependencies (superseded by OXC)
@babel/preset-react— JSX transform now handled by OXC@babel/preset-typescript— TypeScript stripping now handled by OXC@babel/preset-env,@babel/preset-flow,babel-plugin-react-native-web,@rsbuild/plugin-babel@babel/plugin-proposal-export-namespace-from,@babel/plugin-transform-class-properties@babel/types— no longer used directlybabel-loader— replaced byworklets-loader.mjs(see Pass 3 above), since it was running a full Babel cycle on every file regardless of contentRetained:
babel-plugin-react-compiler(still used directly by Metro'sbabel.config.jsfor mobile builds),@babel/plugin-proposal-class-properties,@babel/plugin-transform-export-namespace-from(both used by Metro),@babel/core(used directly byworklets-loader.mjsandfullstory-annotation-loader.mjs)Build timing
mainhere already includes #95319 (Rsbuild/Rspack, no OXC — Babel still runs the React Compiler pass via@rsbuild/plugin-babel). Same machine, same day.main(Rsbuild, no OXC)rm -rf dist/ && npm run build)npm run buildagain)npm run web-server, cache cleared)Every row is faster. Cold builds/dev-server starts see the biggest wins (5.85-6.29x) — Babel's React Compiler pass was the single slowest step in the pipeline and OXC's Rust implementation removes it, and the Fullstory/worklets pre-filters (see "Removed devDependencies" above) shave further time off every pass regardless of caching. Persistent Rspack caching (
cache.type: 'persistent') is now wired up fordev,build, and Storybook alike via the sharedgetSharedConfiguration, so warm builds benefit from it too.Correctness verified
npm run web) loads, signs in, and navigates through Inbox/reports/chat without errors — this caught a real bug (below) that static build output alone didn't surfacedataComponentannotations present across bundles (257 in the main app bundle alone, 2727 total)useMemoCachepresent in 18 locations across 2 bundles__workletHashmetadata (injected byreact-native-worklets/plugin) still present in output bundlesnpm run typecheck-tsgo,npm run knip,npx cspell, andnpx eslintall pass clean on changed filesnpm run storybook-buildproduces a workingdist/docs/with the same annotations presentnpm run storybook -- --smoke-test --cijob (this treats any unallowlisted webpack/rspack warning as a failure) initially failed because it saw our demoted React Compiler warnings; fixed by addingoxc-react-compiler-loader:toconfig.ignoreWarningsalongside the existinglottie-react-nativeentryBug caught during this pass: the empty-code-on-Error-severity-bailout issue described above (Pass 2, last bullet) crashed the app on the very first render —
ThemeProvidercalleduseDebouncedState, which OXC had silently compiled to an empty module. It only surfaced by actually loading the app in a browser; the production build itself reported 0 fatal errors and looked complete by bundle size/warning count alone. Fixed by falling back toreactCompiler: falsefor the affected file. This is why theTestssection below asks reviewers to actually load the app rather than just checking the build succeeds.Open items / known limitations
oxc-transformis pinned to0.136.0— required for React Compiler support (see Pass 2 above); do not bump past this version until oxc-project/oxc#23590 restores thereactCompileroption, or React Compiler will silently stop running with no build errorbabel-plugin-react-compilerwas silently skippingRollback: Delete
config/rsbuild/loaders/(the three custom loaders), remove thetools.bundlerChainhooks inconfig/rsbuild/rsbuild.common.ts, and revertpackage.json/babel.config.js/knip.json.Fixed Issues
$
Tests
npm run buildand verify it compiles with 0 fatal errorsnpm run web, load the app in a browser, sign in, and navigate to a report — verify no console errors, particularly no crash on initial rendernpm run storybook-buildand confirm it completes and stories render with no missing componentsOffline tests
N/A — build tooling change only.
QA Steps [No QA]
Build pipeline change only. No user-visible behaviour modified.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
N/A — build tooling change onlyAndroid: mWeb Chrome
N/A — build tooling change onlyiOS: Native
N/A — build tooling change onlyiOS: mWeb Safari
N/A — build tooling change onlyMacOS: Chrome / Safari
N/A — build tooling change only (verified via headless Chromium smoke test — see Correctness section)