Skip to content

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

Open
whydidoo wants to merge 8 commits into
callstack:mainfrom
whydidoo:fix/1422-flow-lowering
Open

whydidoo wants to merge 8 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

🦋 Changeset detected

Latest commit: 1c62da2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@callstack/repack Patch
@callstack/repack-plugin-expo-modules Patch
@callstack/repack-plugin-nativewind Patch
@callstack/repack-plugin-reanimated Patch
@callstack/repack-dev-server Patch
@callstack/repack-init Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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

try {
if (needsFlowLowering(source)) {
const { code, map } = await lowerFlowSyntax(this, source, options);
callback(null, code, map as RawSourceMap | undefined);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a file goes through the lowering path, this passes Babel's source map to the next loader as an object. babel-swc-loader (babelSwcLoader.ts:111) and babel-loader (babelLoader.ts:146) both run JSON.parse(sourceMap) on it, which throws SyntaxError: "[object Object]" is not valid JSON. So the new lowering breaks whenever flow-loader runs before one of the Babel loaders with source maps on, and source maps are on by default in dev.

How I reproduced it in apps/tester-app on this PR's head (rspack, babel-swc-loader config, iOS simulator):

  1. Added the flow-loader rule from the Flow support docs (test: /\.jsx?$/, exclude: /node_modules/, options: { all: true }).
  2. Added a // @flow file under src/ that uses component syntax and imported it from App.tsx.
  3. webpack-bundle --dev=true fails on that module, and so does the dev server. On the device the app shows a red box: Module build failed: SyntaxError: "[object Object]" is not valid JSON at Object.babelSwcLoader (babelSwcLoader.js:86:43).

With devtool: false it builds. I also got the same error with babel-loader in a standalone project. Without the parallel worker, the error happens before the loader's try, so it takes down the Node process instead of showing up as a module error.

This isn't a regression: on the base commit the same setup already failed, with Missing semicolon, because flow-remove-types leaves component in place. It does mean the fix doesn't take effect for anyone using flow-loader with a Babel loader. getJsTransformRules (builtin swc-loader) accepts the object and works fine.

I checked a one-line change in babelSwcLoader.ts that fixes it. The bundle builds and the component renders on the simulator:

const inputSourceMap: InputSourceMap = sourceMap
  ? typeof sourceMap === 'string'
    ? JSON.parse(sourceMap)
    : sourceMap
  : undefined;

Could you do the same in babelLoader.ts? That also covers the existing pretty: true path, which already returns an object from generateMap(). Alternatively flow-loader could pass JSON.stringify(map), but accepting both seems more robust. A test that chains flow-loader into babel-swc-loader with devtool set would catch this.

@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

@dannyhw @CristiCeban thanks, both addressed:

  • Source maps: both Babel loaders now accept string or object maps; tests chain flow-loader into them with devtool set. 28c9b63
  • Enums: switched to the preset's transform-flow-enums ahead of the preset, so files without @flow work too; requireCall is gone. flow-loader still calls the parser directly through the helper shared with the Babel loaders rather than the preset's syntax plugin — same parser, one code path. aa169f8

This branch has not been deployed

No deployments
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