callstack / callstack/repack

[v6] Use the Rust React Compiler via jsc.transform.reactCompiler

Open
#1,423 1 comment 0 reactions 0 assignees View on GitHub
area:repack type:feature
Dominant language
TypeScript
Stars
1.9k
Forks
165
Avg merge
11d 16h
Merged PRs (30d)
9

Description

## Motivation

The React Compiler has been ported to Rust and is exposed through `builtin:swc-loader` as `jsc.transform.reactCompiler` since [Rspack 2.1](https://rspack.rs/blog/announcing-2-1). Rspack measures the Rust implementation at **7–13x** the speed of `babel-plugin-react-compiler`.

Re.Pack has no React Compiler support of any kind today — no mention in `packages/`, `website/src` or the templates. Users who want it add `babel-plugin-react-compiler` to their own `babel.config.js`, and because of how `babel-swc-loader` partitions work, that plugin then runs on the **Babel** side for every component file in the app.

Part of #1414 (Roadmap to V6), which lists "rust react compiler support" as a deliverable. Adjacent to #1422 (SWC setup rework) — both touch `getSwcLoaderOptions`, and the config surface should be designed once.

### Why it lands on the Babel side today

[`babelSwcLoader`](packages/repack/src/loaders/babelSwcLoader/babelSwcLoader.ts) reads the project's Babel config, partitions the detected plugins into "SWC can do this" vs "Babel must do this", then runs Babel with `excludePlugins: supportedSwcTransforms` followed by SWC:

```ts
const { includedSwcTransforms, supportedSwcTransforms, swcConfig } =
partitionTransforms(this.resourcePath, detectedBabelTransforms);
const babelResult = await transform(source, baseBabelConfig, {
excludePlugins: supportedSwcTransforms,
...
});
```

`babel-plugin-react-compiler` appears in none of `SWC_SUPPORTED_NORMAL_RULES` / `SWC_SUPPORTED_CONFIGURABLE_RULES` / `SWC_SUPPORTED_CUSTOM_RULES` in [`swc.ts`](packages/repack/src/loaders/babelSwcLoader/swc.ts), so it is never excluded and always runs through Babel. React Compiler is by far the most expensive Babel plugin in a typical RN app's config — it does whole-function dataflow analysis on every component. Mapping it into `jsc.transform.reactCompiler` is the single biggest remaining Babel offload available to us.

## Verified behaviour

All of the below was checked against `@rspack/core@2.1.6` via `experiments.swc.transformSync`.

**It works, and composes with the RN-shaped config we already emit:**

| config | result |
| --- | --- |
| `reactCompiler: true` | memoized, emits `react/compiler-runtime` |
| `+ env.include` (RN-style transform list from `getSwcLoaderOptions`) | memoized |
| `+ module.type: 'commonjs'` | memoized, emits `require("react/compiler-runtime")` |

The last row matters — `getSwcLoaderOptions` emits `module.type: 'commonjs'` by default, and the compiler runtime import survives the module transform correctly.

**The accepted option surface is narrower than the Babel plugin's.** Probed exhaustively:

| option | accepted values |
| --- | --- |
| `target` | `'17'`, `'18'`, `'19'` — `'20'` rejected |
| `compilationMode` | `'infer'`, `'annotation'`, `'all'`, `'syntax'` |
| `panicThreshold` | `'none'`, `'critical_errors'`, `'all_errors'` (lowercase only) |
| anything else | **rejected** — `expected boolean or object` |

Notably `sources` is rejected. The Babel plugin uses it to scope which files get compiled; here that has to be expressed with the loader rule's `include` / `exclude` instead. Same for the `enable*` escape hatches (e.g. `enableTreatRefLikeIdentifiersAsRefs`) — projects depending on those cannot move off Babel yet.

**Version floor.** `@rspack/core >= 2.1.0`. #1414 currently targets "Rspack 2+", which is not sufficient for this; the floor needs to be 2.1 if this ships as a supported feature (or the option needs a version guard, which Re.Pack already has machinery for — see `getRspackMajorVersion` / `isRspack2` in `src/helpers/rspackVersion.ts`).

**React version.** The workspace catalog is on `react: 19.2.3`, where `react/compiler-runtime` is built in and `target` can be omitted. React 17/18 projects need an explicit `target` **and** the separate `react-compiler-runtime` package installed — that's a user-facing install step we have to document, not something we can do for them.

## Proposed direction

Two independent surfaces, both opt-in and off by default:

1. **`babel-swc-loader` (the default path).** Add `babel-plugin-react-compiler` to the custom-transform map in `swc.ts` so a project that already has it in `babel.config.js` gets it lifted to SWC automatically, with its Babel options translated to the `reactCompiler` shape. Because the surface is narrower, the mapping must **detect unsupported options and leave the plugin on the Babel side** rather than silently dropping them — a `sources` or `enable*` option present means "keep using Babel for this project".

2. **`getSwcLoaderOptions` / `getJsTransformRules` (the SWC-native path).** Add a `reactCompiler?: boolean | { target?, compilationMode?, panicThreshold? }` option that maps to `jsc.transform.reactCompiler`, defaulting to off.

### Ordering caveat to verify

In `babelSwcLoader`, Babel runs *first* and SWC second, so lifting React Compiler to SWC moves it from "before everything" to "after the remaining Babel plugins". JSX survives the Babel pass (`transform-react-jsx` is in the SWC-supported set, so it is excluded from Babel), which is the main thing React Compiler needs to see intact — but this needs a real test, not an assumption. Projects with worklet-style plugins that rewrite function bodies (`react-native-worklets/plugin` is in `apps/tester-app/babel.config.js`, and Reanimated is a first-party integration in `packages/plugin-reanimated`) are the interesting case: those stay on the Babel side and would now run *before* the compiler instead of after it.

## Scope

### Core

- [ ] Add `reactCompiler` to `getSwcLoaderOptions` (+ thread through `getJsTransformRules`), off by default, mapping to `jsc.transform.reactCompiler`
- [ ] Guard the option behind Rspack >= 2.1 with a clear error/warning on older majors and on webpack (`builtin:swc-loader` is Rspack-only)
- [ ] Map `babel-plugin-react-compiler` in `swc.ts`'s `SWC_SUPPORTED_CUSTOM_RULES`, translating `target` / `compilationMode` / `panicThreshold`
- [ ] Bail out of the mapping (leave the plugin on Babel) when the project passes options SWC does not accept — `sources`, `enable*`, anything outside the three above
- [ ] Decide whether `target` is inferred from the resolved `react` version or left explicit

### Apps and tests

- [ ] Verify the Babel-then-SWC ordering is safe, specifically alongside `react-native-worklets/plugin` and `packages/plugin-reanimated`
- [ ] Enable React Compiler in `apps/tester-app` (or a variant) so the path is exercised on device, not just in unit tests
- [ ] Unit tests for the `swc.ts` mapping, including the unsupported-option bail-out
- [ ] Snapshot coverage in `getSwcLoaderOptions` tests

### Docs

- [ ] New guide page for React Compiler — how to enable it on both loader paths, the Rspack >= 2.1 requirement, and the React 17/18 `target` + `react-compiler-runtime` install step
- [ ] Document the option gap vs `babel-plugin-react-compiler` (`sources` → use rule `include`/`exclude`) and when a project should stay on Babel
- [ ] Update `website/src/latest/api/utils/get-swc-loader-options.md` and `get-js-transform-rules.md`

## Open questions

- Does v6 raise the Rspack floor to 2.1 (making this unconditional), or keep 2.0 and version-guard the option?
- Should Re.Pack enable React Compiler by default for new projects in `templates/`, follow React Native's own default, or leave it entirely opt-in?
- Is auto-lifting from `babel.config.js` the right default for `babel-swc-loader`, or should it require an explicit loader option? Auto-lifting is the bigger win but silently changes where a plugin runs in the pipeline.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.