fix(repack): lower RN 0.81+ Flow syntax in flow-loader and keep Flow enums in Babel loaders by whydidoo · Pull Request #1492 · callstack/repack · GitHub
Skip to content

fix(repack): lower RN 0.81+ Flow syntax in flow-loader and keep Flow enums in Babel loaders - #1492

Merged
dannyhw merged 10 commits into
callstack:mainfrom
whydidoo:fix/1422-flow-lowering
Oct 21, 2026
Merged

dannyhw merged 10 commits into
callstack:mainfrom
whydidoo:fix/1422-flow-lowering

Conversation

@whydidoo

Copy link
Copy Markdown
Collaborator

Part of #1422 (v5 patch).

Summary

On RN 0.81+, flow-loader (and so getJsTransformRules) emitted invalid JS, because flow-remove-types leaves Flow component/hook/enum/match in place. Separately, @react-native/babel-preset drops Flow enums in babel-loader and babel-swc-loader, so RN's VirtualViewMode.cast() threw at runtime.

flowLoader
  component / hook / enum / match? → RN parser from the preset (hermes / flow-parser)
                                      + flow-strip-types
  otherwise                         → flow-remove-types (as before)
babelLoader (@flow files)           → same RN parser helper, so enums are lowered
  • Enums are lowered to flow-enums-runtime, resolved from the project or react-native, so it works with pnpm.
  • component lowering follows the project's React version: forwardRef on 18, ref as a prop on 19.
  • No new dependencies.
  • Also fixes flow-loader crashing under webpack.

Test plan

  • tests/integration, presets 0.87/0.88 × rspack/webpack: component, hook, enum, match, class fields, source maps, React 18/19, enums in both Babel loaders, getJsTransformRules with a pnpm Flow dependency.
  • New rspack-swc config in tester-app bundles real RN 0.86.3. It failed before this change on View.js.
  • All new tests were red against the old loaders.

…w-loader

flow-remove-types only erases type annotations, so on React Native 0.81+
flow-loader (and getJsTransformRules) emitted invalid JS for `component` /
`hook` declarations, Flow enums and `match` expressions.

Files using them now go through the project's React Native parser
(hermes-parser or flow-parser) and @babel/plugin-transform-flow-strip-types;
all other files still use flow-remove-types. Enums are lowered to
flow-enums-runtime, resolved from the project or from react-native so it
works with isolated installs. Source maps are emitted for lowered files.

Also fix flow-loader crashing under webpack: getOptions() received the
loader context as a schema.

Part of callstack#1422
Flow `component` declarations with a `ref` are lowered to `forwardRef` on
React 18 and pass `ref` as a prop on React 19. Pick the target from the
project's `react` version instead of always assuming React 19.

Part of callstack#1422
@react-native/babel-preset strips Flow enum declarations before its enum
plugin runs, so references such as React Native's VirtualViewMode.cast()
were undefined at runtime. Parse @flow sources with the shared
parseReactNativeSource helper, which lowers enums to flow-enums-runtime
and picks the component lowering target from the project's React version
(hermesParserOverrides still take precedence).

Related to callstack#1422
- detect multi-line `match (...)` in flow-loader's prefilter and cover the
  routing with unit tests
- strip class fields of lowered files like flow-remove-types does: allow
  `declare` fields and honour `ignoreUninitializedFields`
- require @babel/core only when a file needs lowering
- cache flow-enums-runtime and React version lookups per project root
- share the preset lookup and wrap its resolution errors
- pin unused import preservation in a file that is not lowered

Part of callstack#1422
@changeset-bot

changeset-bot Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

@vercel

vercel Bot commented Oct 10, 2026

Copy link
Copy Markdown

@whydidoo is attempting to deploy a commit to the Callstack Team on Vercel.

A member of the Team first needs to authorize it.

Comment thread packages/repack/src/loaders/flowLoader/flowLoader.ts
@CristiCeban

Copy link
Copy Markdown
Contributor

Addin to that @dannyhw already said here:
Tested this on a fresh RN 0.87.1 app with Re.Pack 5.4.2 (Rspack), iOS and Android, Debug and Release. On base, simply rendering unstable_VirtualView throws ReferenceError: Property 'VirtualViewRenderState' doesn't exist. In Debug that's a red box; in Release the app crashes on launch (RCTFatalException on iOS, FATAL EXCEPTION on Android). With this PR it works on all of them 👍

Some notes on the enum part:

The cause is in @react-native/babel-preset, not Re.Pack. In the preset's overrides, @babel/plugin-transform-flow-strip-types runs in the first entry and deletes the EnumDeclaration before babel-plugin-transform-flow-enums (in the second entry) sees it. This is reproducible with plain Babel and no Re.Pack:

babel.transformSync('// @flow\nenum Mode { A = 0 }\nexport const x = Mode.cast(0);', {
  presets: ['@react-native/babel-preset'],
});
// → var x = exports.x = Mode.cast(0);   (the enum is gone)

It isn't Re.Pack-specific either: a Metro bundle of the same app never defines exports.VirtualViewMode in VirtualView.js. The 0.88 preset has the same plugin order. Probably worth reporting upstream as well.

The fix only covers files that go through the hermes-parser path. Enum lowering lives in parseReactNativeSource, so in babel-loader / babel-swc-loader it only runs for files with an @flow pragma (or flow: 'all'). That covers RN core, but an enum in a file without the pragma would still be stripped.

A simpler option to consider: add babel-plugin-transform-flow-enums (already a dependency of the preset) as a top-level plugin in the Babel loaders. Top-level plugins run before preset plugins on each node, so it lowers the enum before flow-strip-types gets to it. That works for every file, and its getRuntime option replaces the hand-built requireCall() ESTree node:

plugins: [
  [
    require.resolve('babel-plugin-transform-flow-enums', { paths: [presetDir] }),
    {
      getRuntime: (t) =>
        t.callExpression(t.identifier('require'), [t.stringLiteral(flowEnumsRuntimePath)]),
    },
  ],
],

I checked that this produces require(".../flow-enums-runtime")({ A: 0, B: 1 }) both with transformSync + preset and with the hermes AST + transformFromAstSync path. For flow-loader, the same idea works without a custom parse step: transformSync with the preset's babel-plugin-syntax-hermes-parser + transform-flow-enums + flow-strip-types, in that order. On the 21 RN files that need lowering it was about as fast as the current approach (~35–45 ms in total).

Not a blocker. The current approach works in my tests, but this would be less code and would cover enums in every file.

@whydidoo

Copy link
Copy Markdown
Collaborator Author

@whydidoo
whydidoo requested a review from dannyhw October 20, 2026 19:09
dannyhw and others added 2 commits October 21, 2026 19:19
Name the injected babel-plugin-transform-flow-enums instance so Babel doesn't
reject it as a duplicate when the project's Babel config already includes the
plugin, a common workaround for enums the preset strips.
@dannyhw
dannyhw merged commit 6536826 into callstack:main Oct 21, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants