[v6] Rework the SWC setup: native Flow support and a modernized getJsTransformRules
- Dominant language
- TypeScript
- Stars
- 1.9k
- Forks
- 165
- Avg merge
- 11d 16h
- Merged PRs (30d)
- 9
Description
## Motivation
Re.Pack's SWC-native pipeline (`getJsTransformRules` → `getSwcLoaderOptions` + `getFlowTransformRules`) strips Flow types in a separate JavaScript pass (`flow-remove-types` behind `@callstack/repack/flow-loader`) before handing sources to `builtin:swc-loader`. SWC can now parse and strip Flow natively (`jsc.parser.syntax: "flow"`, [docs](https://swc.rs/docs/usage/flow)) — landed via [swc#11685](https://github.com/swc-project/swc/pull/11685), shipped in `@swc/core` 1.15.21 (March 2026), with React Native parity fixes continuing through 1.15.46 (current). That makes the extra pass redundant — and it is already broken on modern React Native.
Part of #1414 (Roadmap to V6), which lists "use swc built in flow removal".
### The current Flow pass is broken on RN ≥ 0.81
`flow-remove-types` only erases type annotations. It does not desugar Flow's `component` / `hook` syntax or Flow enums — the keywords stay behind, producing invalid JavaScript that SWC then refuses to parse. React Native ships `component` syntax in core since 0.81 (`Libraries/Components/View/View.js:25`: `component View(`), so `getJsTransformRules()` cannot build RN 0.81+. Reproduced with the workspace versions (`flow-remove-types@2.324.0`, `@swc/core@1.15.46`):
```
flow-remove-types keeps the component keyword; swc parse of the output:
x Expected ';', '}' or (View.js:25)
swc one-pass { syntax: 'flow', jsx: true, enums: true, components: true }: OK
```
This goes unnoticed because nothing in this repo exercises `getJsTransformRules` — `templates/` and all tester apps use `@callstack/repack/babel-swc-loader`. The [Rspack migration guide](https://re-pack.dev/docs/migration-guides/rspack) does document `getJsTransformRules` + `flow-loader` as the setup to adopt, though (the Metro guide embeds the `babel-swc-loader` templates and is unaffected).
### The extra pass is also the slowest part of the pipeline
Stripping all 455 `.js` files in `react-native/Libraries` (4.6 MB): `flow-remove-types` **659 ms** (strip only, single-threaded JS) vs SWC flow mode **209 ms** serial / **59 ms** parallel — for strip **plus full transform**. Dropping the loader also removes a parse → print → source-map round trip per Flow module and the `flow-remove-types` + `hermes-parser` install footprint.
### What SWC's Flow mode covers
Verified on `@swc/core@1.15.46` with `{ syntax: 'flow', jsx: true, enums: true, components: true }`:
- Annotations, `import type`, `opaque type`, `declare`, `export type *` — stripped (parity with `flow-remove-types`).
- `component` / `hook` — desugared correctly (`flow-remove-types` emits invalid JS).
- Flow enums — transpiled (`flow-remove-types` passes them through verbatim), but **not** to the `flow-enums-runtime` representation — see behaviour notes below.
- `enums` and `components` are **not** default — without them these constructs are parse errors. `requireDirective` must stay off to match our current `all: true` (with it, files without a `@flow` pragma get no stripping at all).
## Blocker: Rspack does not expose `syntax: "flow"`
True on every published version — verified on 1.6.0 and 2.0.0-alpha.1 (`unknown variant 'flow', expected 'ecmascript' or 'typescript'`), and latest 2.1.6 / `main` has no flow variant in `SwcLoaderParserConfig` either. Bumping to Rspack 2 does not fix it. Rspack `main` is already on `swc_core = "72.0.0"`; the gap is a Cargo feature: SWC gates the Flow parser behind `base_flow` (`swc_core` → `swc/flow` → `swc_ecma_parser/flow`), and `rspack_loader_swc` doesn't enable it. The upstream fix is one feature flag, the corresponding `SwcLoaderParserConfig` type, and a `detectSyntax` mapping decision for `.flow` / `.js.flow`. Precedent for this kind of ask: [rspack#13843](https://github.com/web-infra-dev/rspack/issues/13843) (swc_core bump + exposing `jsc.preserveSymlinks`) was accepted and shipped.
Fallback if upstream is slow: route Flow-typed modules through a Re.Pack loader backed by standalone `@swc/core`, which has the feature compiled in. `babelSwcLoader`'s `lazyGetSwc` already resolves `rspack.experiments.swc` → `@rspack/core` → `@swc/core`, so the plumbing exists. Costs the JS↔Rust boundary again, but stays single-pass and works on webpack too.
## Behaviour differences vs Babel
- **Unused value imports are elided by default.** SWC's flow mode applies TypeScript-style import elision (`import Unused from './u'; console.log(1)` → `console.log(1)`). **`jsc.transform.verbatimModuleSyntax: true` fixes this** — verified it preserves unused value imports while still stripping `import type` and `type` specifiers. Set it and pin with a regression test.
- **Flow enums don't use `flow-enums-runtime`.** SWC emits a TS-style object IIFE. Member access matches Flow semantics, but the runtime API (`.members()`, `.cast()`, `.isValid()`, `.getName()`) doesn't exist on the result. RN core doesn't call those methods today; needs a documented caveat (and a check when bumping RN).
- **`exportDefaultFrom` is unavailable under `syntax: "flow"`** (`export v from './v'` → parse error), while `getParserOptions` enables it for all `.js` (RN's preset includes `@babel/plugin-proposal-export-default-from`). So `flow` cannot blanket-replace `ecmascript` for `.js` — keep it scoped to Flow-typed modules until SWC supports the combination.
## Other issues in `getSwcLoaderOptions` / `getJsTransformRules`
Independent of Flow, worth fixing in the same pass:
- **`development` is tied to the JSX runtime, not `mode`.** `getJSCTransformOptions` sets `development: jsxRuntime === 'classic'`, so with the default `automatic` runtime dev builds always get `jsx` instead of `jsxDEV` (verified) — losing `__source`/`__self`, accurate component stacks, and "open in editor". RN's preset adds `jsx-source`/`jsx-self` whenever `dev`. Should follow `mode`.
- **The six-branch `oneOf` collapses under Rspack 2.** `detectSyntax: 'auto'` (new in 2.0.0) infers the parser from the extension, leaving only the sourcemaps-off-for-`node_modules` split; `tsRules`/`tsxRules` are byte-identical today anyway. Unrecognized extensions map to `{ syntax: 'typescript', tsx: true }`, so `.flow` needs an explicit branch either way.
- **`env.targets: { node: 24 }` is a sentinel, not a target** ("assume everything supported, re-add via `env.include`"). It works but drifts silently, and Rspack 2 derives loader targets from the top-level `target` when unset. Re-derive `env.include` (16 entries) from the current RN preset and write down each inclusion/omission — e.g. `transform-react-display-name` is missing from both SWC paths, and the preset's regenerator-gated trio (`optional-catch-binding`, `nullish-coalescing`, `for-of`) is dev+Hermes-only, so omitting it may be correct but should be a written decision.
- **`lazyImports` defaults to `false`**, while RN's preset defaults `lazy` to its `lazy-imports` allowlist. **`isModule` is never set** (`babelSwcLoader` sets it from Babel's `sourceType`; `isModule: 'unknown'` is probably right here).
- **`getSwcParserConfig` (babelSwcLoader) and `getParserOptions` (getSwcLoaderOptions) are near-duplicates** with different answers (`exportDefaultFrom`). Fold into one shared helper.
- **`getCodegenTransformRules` keeps its Babel pass** — `@react-native/babel-plugin-codegen` has no SWC equivalent — but the "must run before Flow stripping" ordering constraint deserves a re-check once SWC parses Flow natively.
## `babel-swc-loader`
Stays as-is — it is the default in `templates/` and every tester app, and it is correct by construction (it reads the project's real Babel config and only offloads transforms it can prove equivalent). Follow-up once Flow support is available: teach `swc.ts` to map `transform-flow-strip-types` / `transform-flow-enums` / `syntax-hermes-parser` to `jsc.parser.syntax: 'flow'` (+ `enums`, `components`), so RN core files go through SWC instead of falling back to Babel — the biggest remaining Babel fallback for a stock RN app.
## Scope
### Upstream
- [ ] Rspack: file the issue/PR enabling `base_flow` in `rspack_loader_swc`, exposing the flow parser options in `SwcLoaderParserConfig`, and deciding the `detectSyntax` mapping for `.flow`
- [ ] SWC: file the issue for `exportDefaultFrom` under `syntax: "flow"`
### Core
- [ ] Switch `getFlowTransformRules` from `flow-loader` to SWC-native Flow (or the `@swc/core`-backed fallback loader), keeping the `FLOW_TYPED_MODULES` include/exclude surface; set `verbatimModuleSyntax: true`
- [ ] Decide the fate of `@callstack/repack/flow-loader` (public export) — deprecate as escape hatch or drop in v6
- [ ] Rewrite `getJsTransformRules` on `detectSyntax: 'auto'`, collapsing the `oneOf`
- [ ] Drive `jsc.transform.react.development` from `mode`
- [ ] Re-derive `env.include` from the current RN preset; align `lazyImports` / `isModule` defaults
- [ ] Fold `getSwcParserConfig` and `getParserOptions` into one helper
- [ ] Drop the `flow-remove-types` dependency once nothing uses it
### v5 patch (independent of v6)
- [ ] Stop `getJsTransformRules` emitting invalid JS for `component` / `hook` / enums on RN ≥ 0.81 — either the SWC path or `babel-plugin-syntax-hermes-parser` + `babel-plugin-transform-flow-enums` in `flow-loader`
### Apps, tests, docs
- [ ] Add a tester-app variant or config-matrix entry that actually builds with `getJsTransformRules` — this class of bug is invisible today
- [ ] `tests/integration` coverage for `component` / `hook` / Flow enums in a `node_modules` dependency; regression test pinning unused-import preservation
- [ ] Update `flow-loader`, `get-flow-transform-rules`, `get-swc-loader-options`, `get-js-transform-rules` docs and the Rspack migration guide; note the minimum Rspack version in the v5 → v6 migration guide
## Open questions
- Wait on upstream Rspack, or ship the `@swc/core`-backed loader first (works today, keeps webpack) and switch to `builtin:swc-loader` when the feature flag lands?
- Does `getJsTransformRules` remain a second-class alternative to `babel-swc-loader`, or does v6 make it the default in `templates/`? #1414 targets Rspack 2+ as a minimum, which makes it viable as a default for the first time — but only if it becomes the path we actually test.
Contributor guide
Assessment
This issue has not been assessed yet.