Skip to content

fix(compiler,vxrn): run codegen on React Native's Flow .js specs#735

Merged
natew merged 2 commits into
onejs:mainfrom
YevheniiKotyrlo:fix/compiler-flow-js-codegen
Jul 16, 2026
Merged

fix(compiler,vxrn): run codegen on React Native's Flow .js specs#735
natew merged 2 commits into
onejs:mainfrom
YevheniiKotyrlo:fix/compiler-flow-js-codegen

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

On a native dev build (new architecture / bridgeless), React Native logs a LogBox warning on every render + hot reload:

Codegen didn't run for SafeAreaView. This will be an error in the future. Make sure you are using @react-native/babel-preset when building your JavaScript code.

Non-fatal today, but RN has slated it to become a hard error. SafeAreaView is RN core's own deprecated Flow spec (RCTSafeAreaViewNativeComponent.js) — its codegenNativeComponent<Props>(…) call is never replaced with a static view-config at build time, so the runtime codegenNativeComponent executes and warns.

Root cause

Two layers, both about Flow .js never reaching codegen with its type intact:

1. @vxrn/compiler has no Flow parser for .js. transformBabel adds @babel/preset-typescript only for .ts/.tsx and nothing for .js:

presets: [isTS ? ['@babel/preset-typescript', { isTSX, allExtensions: isTSX }] : '', ...(options.presets || [])].filter(Boolean),

RN's own component specs are Flow .js (import type, codegenNativeComponent<T>(…), (expr: Type) casts). @react-native/babel-plugin-codegen declares no manipulateOptions/inherits, so it doesn't enable Flow parsing itself — its visitor needs babel to have already parsed the TypeCastExpression/CallExpression. With no Flow syntax plugin, babel's parser throws on the first line (Unexpected token … import type); transformBabel only silences "Could not find component config", so it logs and returns undefined, and the file falls back to the SWC path (which strips Flow but does not run codegen). The raw codegenNativeComponent("SafeAreaView", …) survives into the bundle → the runtime warning. (safe-area-context's own spec is a .ts file, so preset-typescript parses it and codegen runs — which is why RNCSafeAreaView works but SafeAreaView doesn't.)

2. On the native bundler, Flow is stripped before codegen can run. getNativePlugins runs flowStripPlugin before the compiler. It matches RN files (node_modules/(react-native|@react-native)/**/*.js) and strips Flow via fast-flow-transform, so codegenNativeComponent<Props>(…) becomes codegenNativeComponent(…) (type erased) before the (now Flow-capable) compiler — which already owns codegen — ever sees it. So fixing (1) alone isn't enough on native: the type argument is already gone.

Fix

Two coordinated changes that keep codegen a single responsibility of @vxrn/compiler.

@vxrn/compiler — parse Flow for non-TS files. Append @babel/plugin-transform-flow-strip-types (which inherits @babel/plugin-syntax-flow, so it both parses and strips) to the plugin list for non-TS files, mirroring @react-native/babel-preset (TS → preset-typescript, JS → this plugin). The general fix — any Flow .js through babel now parses, so the codegen plugin the compiler already runs works on RN specs. The TS path is byte-identical (empty slice).

vxrn — run the compiler before flowStripPlugin. getNativePlugins now orders vxrnCompilerPlugin ahead of flowStripPlugin, so RN's Flow .js specs reach the compiler's codegen with their type argument intact; flowStripPlugin then strips any remaining Flow downstream as the guaranteed safety net before rolldown's oxc core parse. Because the safety net now sits after the transform instead of before it, it can't leave un-stripped Flow for oxc to choke on (the failure mode of the naive "skip specs in flowStrip" alternative).

Dependency: @babel/plugin-transform-flow-strip-types was only present transitively — added to @vxrn/compiler's deps.

Test plan

  • Part 1, deterministic (driving the real exported getBabelOptions + transformBabel): the RN core Flow .js spec RCTSafeAreaViewNativeComponent.jsbefore, transformBabel returns undefined (SyntaxError on import type) and codegen does not run; after, codegen runs, replacing codegenNativeComponent('SafeAreaView', …) with NativeComponentRegistry.get('RCTSafeAreaView', () => __INTERNAL_VIEW_CONFIG). A .ts spec is unchanged (no regression); the TS path is byte-identical.
  • On device (Android emulator, Hermes, new arch): the served bundle's raw codegenNativeComponent("SafeAreaView") count goes 1 → 0; the Codegen didn't run warning is gone (initial render + hot reload); app renders + hot-reloads; build healthy. The pipeline reorder is behavior-changing, so an on-device re-smoke is worthwhile.
  • No committed unit test: @vxrn/compiler has no test harness today and this entry pulls unbuilt workspace deps, so testing it would need new infrastructure plus a build-before-test step CI doesn't run — out of scope here; the behavior is covered on-device.

Notes

  • POSIX-safe — extension/content branches + plugin order, no path logic.
  • Single codegen owner: rather than run @react-native/babel-plugin-codegen in two places (the compiler and the strip plugin), the compiler owns it and the pipeline order lets it see the Flow type. This drops a second, divergent codegen gate and a silent catch {} that would otherwise have hidden a future codegen regression behind the exact warning this fixes.
  • Architecture: transformBabel owns how to parse a file (dialect by extension: TS vs Flow); the pipeline owns when each transform runs. The bug was that the parse layer only knew TypeScript and the strip ran before the transform — this fixes both.

RN's own component specs are Flow .js (e.g. RCTSafeAreaViewNativeComponent.js).
@react-native/babel-plugin-codegen must replace codegenNativeComponent<Props>(...)
with a static view-config at build time, but it never runs on them — so the
runtime codegenNativeComponent executes and RN logs "Codegen didn't run for
SafeAreaView. This will be an error in the future." on every render/hot reload.

Two layers, both about Flow .js never reaching codegen with its type intact:

1. @vxrn/compiler's transformBabel adds a parser only for TS (.ts/.tsx via
   preset-typescript) and nothing for .js, so babel can't parse RN's Flow specs
   (import type, codegenNativeComponent<T>(...), casts). It throws, transformBabel
   returns undefined, and the file falls back to the SWC path (strips Flow, no
   codegen). Fix: parse Flow for non-TS files by appending
   @babel/plugin-transform-flow-strip-types (parses + strips), mirroring
   @react-native/babel-preset (TS -> preset-typescript, JS -> this plugin). The TS
   path is byte-identical.

2. On the native bundler, flowStripPlugin runs before the compiler and strips Flow
   from RN files, erasing the type argument codegen needs. Fix: in flowStripPlugin,
   when a file uses codegenNativeComponent, run @react-native/babel-plugin-codegen
   with a Flow parser first (while the type is present), then strip Flow. Graceful:
   any error falls through to the plain strip, never a broken build.

Adds @babel/plugin-transform-flow-strip-types to @vxrn/compiler's deps (was only
transitive) and @react-native/babel-plugin-codegen to vxrn's deps (now referenced
by flowStripPlugin).
…rop the duplicate

Running @react-native/babel-plugin-codegen inside flowStripPlugin was a layering
violation — codegen ran in two packages with two divergent gates plus a silent catch.
Instead, run vxrnCompilerPlugin BEFORE flowStripPlugin: now that the compiler parses
Flow, react-native's Flow .js specs reach the codegen the compiler already owns with
their type argument intact, and flowStripPlugin strips any remaining Flow downstream
as the guaranteed safety net before oxc's core parse.

Removes the flowStripPlugin codegen block, its silent catch, and vxrn's now-unused
@react-native/babel-plugin-codegen dependency.
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as draft July 15, 2026 17:35
@natew
natew marked this pull request as ready for review July 16, 2026 00:57
@natew
natew merged commit 7847183 into onejs:main Jul 16, 2026
2 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.

2 participants