callstack / callstack/repack

[v6] Rework the SWC setup: native Flow support and a modernized getJsTransformRules

Offen
#1,422 2 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
area:repack type:feature
Vorherrschende Sprache
TypeScript
Sterne
1.9k
Forks
165
Ø Merge
11 T. 16 Std.
Gemergte PRs (30 T.)
9

Beschreibung

## 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.

Beitragsleitfaden

Beitragsleitfaden öffnen

Rechercherichtung

Lesen Sie die aktuellen Einstiegspunkte getJsTransformRules, getSwcLoaderOptions und getFlowTransformRules, und prüfen Sie anschließend die hier beschriebene Parser-Einschränkung von Rspack. Verwenden Sie tests/integration sowie die vorgeschlagene tester-app- oder config-matrix-Abdeckung als Ausgangspunkte für die Validierung. Als abgeschlossen gilt die Arbeit, wenn ein abgestimmter v6-Flow-Pfad, aktualisierte Regeln und Optionen, Dokumentation sowie Abdeckung für Komponenten, Hooks, Enums und ungenutzte Importe vorhanden sind.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
react-native, typescript
Bereich
build-system, tooling
Issue-Typ
Refactoring
Schwierigkeit
5/5
Geschätzter Aufwand
Über eine Woche
Aktivitätsstatus
Aktiv
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
30/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.