Skip to content

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

Description

@dannyhw

Motivation

Re.Pack's SWC-native pipeline (getJsTransformRulesgetSwcLoaderOptions + 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) — landed via swc#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 <eof>  (View.js:25)
swc one-pass { syntax: 'flow', jsx: true, enums: true, components: true }: OK

This goes unnoticed because nothing in this repo exercises getJsTransformRulestemplates/ and all tester apps use @callstack/repack/babel-swc-loader. The Rspack migration guide 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_coreswc/flowswc_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 (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/? Roadmap to V6 #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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:repackThe issue is about logic/code inside of Re.Pack.type:featureProposal or idea for a new feature or enhancement.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions