Skip to content

[No QA] Swap babel-loader + babel-plugin-react-compiler for OXC (Rust React Compiler)#93980

Draft
roryabraham wants to merge 27 commits into
mainfrom
rory-oxc-loader-v2
Draft

[No QA] Swap babel-loader + babel-plugin-react-compiler for OXC (Rust React Compiler)#93980
roryabraham wants to merge 27 commits into
mainfrom
rory-oxc-loader-v2

Conversation

@roryabraham

@roryabraham roryabraham commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

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

  • Thin wrapper that calls @fullstory/babel-plugin-annotate-react (native: true, reactCompiler: true, matching babel.config.js's web config plus the new 2.4.0 reactCompiler option) directly via @babel/core, skipping babel-loader's extra layer
  • Pre-filters before invoking Babel at all: .ts files 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 a source.includes('<') check (a file with no < cannot contain JSX)
  • The .ts extension check matters because < alone is a weak proxy for "might contain JSX" on .ts files: measured against the real src/ corpus, 1722 of 3481 .ts files (49.5%) contain a literal < from generics/comparisons (e.g. Record<K, V>) — including all of src/languages/ and most of src/CONST/, some of the largest files in the repo. The extension check cuts Babel invocations from 70.9% to 44.6% of all files

Pass 2 — oxc-react-compiler-loader.mjs (runs second):

  • Calls oxc-transform directly: React Compiler → JSX transform → TypeScript strip → env target — all in one Rust pass
  • React Compiler runs before OXC's own JSX transform, as required
  • Requires oxc-transform pinned to 0.136.0 (via package.json overrides, applied to both oxc-transform and oxc-webpack-loader's copy): the reactCompiler transform option — the biggest win this PR offers — was removed from oxc-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. Passing reactCompiler: true on 0.137+ is silently a no-op (verified: no error, no useMemoCache in output). Unpinning before that binding is re-exposed would silently disable React Compiler entirely with no build failure to catch it
  • Demotes non-fatal React Compiler diagnostics to build warnings instead of hard errors, matching babel-plugin-react-compiler's default bailout behaviour (workaround for oxc-project/oxc#23587)
  • When an Error-severity React Compiler diagnostic occurs (a genuine Rules-of-React violation, e.g. reading ref.current inline during render), oxc-transform returns empty code for the whole file rather than just skipping the optimization — the loader detects this and re-runs the transform with reactCompiler: false so the file still ships valid JS, matching Babel's silent-bailout behaviour

Pass 3 — worklets-loader.mjs (runs last, worklets only):

  • Calls @babel/core directly with only the react-native-worklets/plugin plugin — serialises 'worklet' functions for UI-thread execution
  • No presets needed — OXC already stripped types and transformed JSX
  • Pre-filters on raw source (source.includes('react-native-reanimated') || source.includes('worklet')) before invoking Babel at all — only ~2.5% of src/ 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 faster

Node 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 .ts generic syntax being confused with JSX. These rules use oxc-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 same oxc-transform version.

Also required: Rsbuild's default builtin:swc-loader ('js' rule) matches every .js/.ts/.jsx/.tsx file and runs before rules added via addRules. Without excluding app source and includedNodeModules from it (via a tools.bundlerChain hook), 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 on require()/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 directly
  • babel-loader — replaced by worklets-loader.mjs (see Pass 3 above), since it was running a full Babel cycle on every file regardless of content

Retained: babel-plugin-react-compiler (still used directly by Metro's babel.config.js for mobile builds), @babel/plugin-proposal-class-properties, @babel/plugin-transform-export-namespace-from (both used by Metro), @babel/core (used directly by worklets-loader.mjs and fullstory-annotation-loader.mjs)

Build timing

main here already includes #95319 (Rsbuild/Rspack, no OXC — Babel still runs the React Compiler pass via @rsbuild/plugin-babel). Same machine, same day.

Measurement main (Rsbuild, no OXC) This PR (Rsbuild + OXC) Speed
App build, cold (rm -rf dist/ && npm run build) 180.1s ~28.6s 6.29x faster
App build, warm (npm run build again) 20.0s ~11.5s 1.74x faster
Storybook build 28.4s ~22.5s 1.26x faster
Dev server, cold (npm run web-server, cache cleared) 116.5s ~19.9s 5.85x faster
Dev server, warm (HMR rebuild after touching 1 file) 0.30s ~0.26s 1.14x faster (both sub-second, PR ranged 0.22-0.30s across 4 samples)

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 for dev, build, and Storybook alike via the shared getSharedConfiguration, so warm builds benefit from it too.

Correctness verified

  • Fresh dev server (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 surface
  • Fullstory dataComponent annotations present across bundles (257 in the main app bundle alone, 2727 total)
  • React Compiler useMemoCache present in 18 locations across 2 bundles
  • __workletHash metadata (injected by react-native-worklets/plugin) still present in output bundles
  • npm run typecheck-tsgo, npm run knip, npx cspell, and npx eslint all pass clean on changed files
  • npm run storybook-build produces a working dist/docs/ with the same annotations present
  • CI's npm run storybook -- --smoke-test --ci job (this treats any unallowlisted webpack/rspack warning as a failure) initially failed because it saw our demoted React Compiler warnings; fixed by adding oxc-react-compiler-loader: to config.ignoreWarnings alongside the existing lottie-react-native entry

Bug 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 — ThemeProvider called useDebouncedState, 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 to reactCompiler: false for the affected file. This is why the Tests section below asks reviewers to actually load the app rather than just checking the build succeeds.

Open items / known limitations

  • oxc-transform is pinned to 0.136.0 — required for React Compiler support (see Pass 2 above); do not bump past this version until oxc-project/oxc#23590 restores the reactCompiler option, or React Compiler will silently stop running with no build error
  • React Compiler warnings are expected bailouts on components that violate Rules of React — the same components babel-plugin-react-compiler was silently skipping

Rollback: Delete config/rsbuild/loaders/ (the three custom loaders), remove the tools.bundlerChain hooks in config/rsbuild/rsbuild.common.ts, and revert package.json / babel.config.js / knip.json.

Fixed Issues

$

Tests

  1. Run npm run build and verify it compiles with 0 fatal errors
  2. Run npm run web, load the app in a browser, sign in, and navigate to a report — verify no console errors, particularly no crash on initial render
  3. Run npm run storybook-build and confirm it completes and stories render with no missing components

Offline tests

N/A — build tooling change only.

QA Steps [No QA]

Build pipeline change only. No user-visible behaviour modified.

  • 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 any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • 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 shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • 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)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • 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)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • 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 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 a new page is added, I verified it is using the ScrollView component to make it scrollable when more elements are added to the page.
  • 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 N/A — build tooling change only
Android: mWeb Chrome N/A — build tooling change only
iOS: Native N/A — build tooling change only
iOS: mWeb Safari N/A — build tooling change only
MacOS: Chrome / Safari N/A — build tooling change only (verified via headless Chromium smoke test — see Correctness section)

roryabraham and others added 2 commits June 18, 2026 13:02
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>
@melvin-bot

melvin-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

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?

@sentry

sentry Bot commented Jun 18, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
Expensify org.me.mobiexpensifyg 9.4.15-1 (509041501) Release

⚙️ app Build Distribution Settings

@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

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

roryabraham and others added 5 commits June 18, 2026 14:32
- 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>
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
@roryabraham roryabraham marked this pull request as ready for review July 11, 2026 00:09
@roryabraham roryabraham requested a review from a team as a code owner July 11, 2026 00:09
@melvin-bot melvin-bot Bot requested review from mjasikowski and removed request for a team July 11, 2026 00:10
@melvin-bot

melvin-bot Bot commented Jul 11, 2026

Copy link
Copy Markdown

@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]

chatgpt-codex-connector[bot]

This comment was marked as resolved.

…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>
@roryabraham roryabraham requested a review from chuckdries July 11, 2026 00:49
@roryabraham roryabraham marked this pull request as draft July 11, 2026 00:53
roryabraham and others added 2 commits July 10, 2026 18:03
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>
@roryabraham roryabraham changed the title perf(webpack): swap babel-loader + babel-plugin-react-compiler for OXC (Rust React Compiler) perf(rsbuild): swap babel-loader + babel-plugin-react-compiler for OXC (Rust React Compiler) Jul 11, 2026
roryabraham and others added 3 commits July 10, 2026 21:55
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>
@roryabraham roryabraham changed the title perf(rsbuild): swap babel-loader + babel-plugin-react-compiler for OXC (Rust React Compiler) [Spike] Facet (facetpack) Metro OXC transformer Jul 11, 2026
@roryabraham roryabraham changed the title [Spike] Facet (facetpack) Metro OXC transformer perf(rsbuild): swap babel-loader + babel-plugin-react-compiler for OXC (Rust React Compiler) Jul 11, 2026
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>
@roryabraham roryabraham changed the title perf(rsbuild): swap babel-loader + babel-plugin-react-compiler for OXC (Rust React Compiler) [No QA] Swap babel-loader + babel-plugin-react-compiler for OXC (Rust React Compiler) Jul 11, 2026
roryabraham and others added 12 commits July 11, 2026 00:06
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>
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.

1 participant