You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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) — 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 getJsTransformRules — templates/ 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-types659 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).
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 (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.
Motivation
Re.Pack's SWC-native pipeline (
getJsTransformRules→getSwcLoaderOptions+getFlowTransformRules) strips Flow types in a separate JavaScript pass (flow-remove-typesbehind@callstack/repack/flow-loader) before handing sources tobuiltin:swc-loader. SWC can now parse and strip Flow natively (jsc.parser.syntax: "flow", docs) — landed via swc#11685, shipped in@swc/core1.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-typesonly erases type annotations. It does not desugar Flow'scomponent/hooksyntax or Flow enums — the keywords stay behind, producing invalid JavaScript that SWC then refuses to parse. React Native shipscomponentsyntax in core since 0.81 (Libraries/Components/View/View.js:25:component View(), sogetJsTransformRules()cannot build RN 0.81+. Reproduced with the workspace versions (flow-remove-types@2.324.0,@swc/core@1.15.46):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 does documentgetJsTransformRules+flow-loaderas the setup to adopt, though (the Metro guide embeds thebabel-swc-loadertemplates and is unaffected).The extra pass is also the slowest part of the pipeline
Stripping all 455
.jsfiles inreact-native/Libraries(4.6 MB):flow-remove-types659 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 theflow-remove-types+hermes-parserinstall footprint.What SWC's Flow mode covers
Verified on
@swc/core@1.15.46with{ syntax: 'flow', jsx: true, enums: true, components: true }:import type,opaque type,declare,export type *— stripped (parity withflow-remove-types).component/hook— desugared correctly (flow-remove-typesemits invalid JS).flow-remove-typespasses them through verbatim), but not to theflow-enums-runtimerepresentation — see behaviour notes below.enumsandcomponentsare not default — without them these constructs are parse errors.requireDirectivemust stay off to match our currentall: true(with it, files without a@flowpragma 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 /mainhas no flow variant inSwcLoaderParserConfigeither. Bumping to Rspack 2 does not fix it. Rspackmainis already onswc_core = "72.0.0"; the gap is a Cargo feature: SWC gates the Flow parser behindbase_flow(swc_core→swc/flow→swc_ecma_parser/flow), andrspack_loader_swcdoesn't enable it. The upstream fix is one feature flag, the correspondingSwcLoaderParserConfigtype, and adetectSyntaxmapping decision for.flow/.js.flow. Precedent for this kind of ask: rspack#13843 (swc_core bump + exposingjsc.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'slazyGetSwcalready resolvesrspack.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
import Unused from './u'; console.log(1)→console.log(1)).jsc.transform.verbatimModuleSyntax: truefixes this — verified it preserves unused value imports while still strippingimport typeandtypespecifiers. Set it and pin with a regression test.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).exportDefaultFromis unavailable undersyntax: "flow"(export v from './v'→ parse error), whilegetParserOptionsenables it for all.js(RN's preset includes@babel/plugin-proposal-export-default-from). Soflowcannot blanket-replaceecmascriptfor.js— keep it scoped to Flow-typed modules until SWC supports the combination.Other issues in
getSwcLoaderOptions/getJsTransformRulesIndependent of Flow, worth fixing in the same pass:
developmentis tied to the JSX runtime, notmode.getJSCTransformOptionssetsdevelopment: jsxRuntime === 'classic', so with the defaultautomaticruntime dev builds always getjsxinstead ofjsxDEV(verified) — losing__source/__self, accurate component stacks, and "open in editor". RN's preset addsjsx-source/jsx-selfwheneverdev. Should followmode.oneOfcollapses under Rspack 2.detectSyntax: 'auto'(new in 2.0.0) infers the parser from the extension, leaving only the sourcemaps-off-for-node_modulessplit;tsRules/tsxRulesare byte-identical today anyway. Unrecognized extensions map to{ syntax: 'typescript', tsx: true }, so.flowneeds an explicit branch either way.env.targets: { node: 24 }is a sentinel, not a target ("assume everything supported, re-add viaenv.include"). It works but drifts silently, and Rspack 2 derives loader targets from the top-leveltargetwhen unset. Re-deriveenv.include(16 entries) from the current RN preset and write down each inclusion/omission — e.g.transform-react-display-nameis 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.lazyImportsdefaults tofalse, while RN's preset defaultslazyto itslazy-importsallowlist.isModuleis never set (babelSwcLoadersets it from Babel'ssourceType;isModule: 'unknown'is probably right here).getSwcParserConfig(babelSwcLoader) andgetParserOptions(getSwcLoaderOptions) are near-duplicates with different answers (exportDefaultFrom). Fold into one shared helper.getCodegenTransformRuleskeeps its Babel pass —@react-native/babel-plugin-codegenhas no SWC equivalent — but the "must run before Flow stripping" ordering constraint deserves a re-check once SWC parses Flow natively.babel-swc-loaderStays 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: teachswc.tsto maptransform-flow-strip-types/transform-flow-enums/syntax-hermes-parsertojsc.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
base_flowinrspack_loader_swc, exposing the flow parser options inSwcLoaderParserConfig, and deciding thedetectSyntaxmapping for.flowexportDefaultFromundersyntax: "flow"Core
getFlowTransformRulesfromflow-loaderto SWC-native Flow (or the@swc/core-backed fallback loader), keeping theFLOW_TYPED_MODULESinclude/exclude surface; setverbatimModuleSyntax: true@callstack/repack/flow-loader(public export) — deprecate as escape hatch or drop in v6getJsTransformRulesondetectSyntax: 'auto', collapsing theoneOfjsc.transform.react.developmentfrommodeenv.includefrom the current RN preset; alignlazyImports/isModuledefaultsgetSwcParserConfigandgetParserOptionsinto one helperflow-remove-typesdependency once nothing uses itv5 patch (independent of v6)
getJsTransformRulesemitting invalid JS forcomponent/hook/ enums on RN ≥ 0.81 — either the SWC path orbabel-plugin-syntax-hermes-parser+babel-plugin-transform-flow-enumsinflow-loaderApps, tests, docs
getJsTransformRules— this class of bug is invisible todaytests/integrationcoverage forcomponent/hook/ Flow enums in anode_modulesdependency; regression test pinning unused-import preservationflow-loader,get-flow-transform-rules,get-swc-loader-options,get-js-transform-rulesdocs and the Rspack migration guide; note the minimum Rspack version in the v5 → v6 migration guideOpen questions
@swc/core-backed loader first (works today, keeps webpack) and switch tobuiltin:swc-loaderwhen the feature flag lands?getJsTransformRulesremain a second-class alternative tobabel-swc-loader, or does v6 make it the default intemplates/? 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.