Skip to content

fix(vxrn): include transform-classes in native dev bundles (Hermes)#733

Merged
natew merged 2 commits into
onejs:mainfrom
YevheniiKotyrlo:fix/vxrn-transform-classes-dev-hermes
Jul 16, 2026
Merged

fix(vxrn): include transform-classes in native dev bundles (Hermes)#733
natew merged 2 commits into
onejs:mainfrom
YevheniiKotyrlo:fix/vxrn-transform-classes-dev-hermes

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

With native.bundler: 'vite', a dev native build compiles, installs, and launches, then Hermes red-screens at init, before React mounts:

[runtime not ready]: TypeError: Cannot read property 'prototype' of undefined
  anonymous@461:59   ← new ReactNativeDevRuntime() in the injected dev runtime

The app never renders. Production (one build android) is unaffected. The same JS runs clean under Node/JSC — it is Hermes-specific.

Root cause

vxrn downlevels the dev bundle for Hermes with SWC, but gates transform-classes behind !dev in two places (packages/vxrn/src/utils/createNativeDevEngine.ts), so dev builds transpile class fields while leaving class extends as modern ES6 — an inconsistent, half-transpiled class hierarchy Hermes chokes on when constructing the class:

  1. downlevelClassFieldsInBundle (the injected runtime prelude) — include had transform-class-properties/-static-block/-private-* but no transform-classes.
  2. hermesCompatSWCPlugin (app modules) — ...(!dev ? ['transform-classes', 'transform-async-to-generator'] : []), i.e. transform-classes only in production.

Bisected on device: adding transform-classes to (1) advanced the crash from vxrn's own runtime into RN's modules (BatchedBridge/MessageQueue); adding it to (2) cleared those too — pinpointing the !dev gate as the whole cause.

Fix

Make transform-classes unconditional in both spots so the entire native dev bundle is consistently ES5-classed and Hermes-safe (matching the fully-classed production bundle); transform-async-to-generator stays production-only.

The two include lists were hand-copied — which is exactly how transform-classes came to be missing from one of them — so this also extracts the shared set into a single named constant (HERMES_CLASS_TRANSFORMS) plus the prod-only addend (HERMES_PROD_TRANSFORMS), both consumed through getHermesSWCIncludes(dev). The "these class transforms move together" invariant is now a compile-time fact instead of two lists that can silently drift apart again.

Test plan

  • Unit test (createNativeDevEngine.test.ts): getHermesSWCIncludes always contains the full class-transform set for both dev and prod (the regression guard — transform-classes was the missing member) and adds transform-async-to-generator only in prod.
  • Transform (deterministic): running vxrn's exact SWC options, the dev include (without transform-classes) keeps class X extends Y as ES6 while lowering the fields (half-transpile); adding transform-classes emits full ES5 (_inherits/_call_super/_class_call_check).
  • Hermes toolchain: the half-transpiled (ES6-class) output is rejected by the Hermes compiler (invalid statement encountered), while the fully-lowered ES5 output compiles to valid bytecode.
  • Node/V8: runs both outputs fine (confirms the defect is Hermes-specific).
  • On device (Android emulator, Hermes, new arch): app went red-screen (prototype of undefined at new ReactNativeDevRuntime()) → renders; e2e-check.sh native-render FAIL→PASS. Durable across the whole session.

Notes

Not Windows-specific — this is a Hermes + dev-mode issue; any bundler: 'vite' native dev target hits it. The !dev gate reads like a dev-speed optimization (skip the heavier class transform in dev), but a half-transpiled class hierarchy is not Hermes-safe, so keeping transform-classes unconditional is the correct trade-off.

transform-classes was gated behind !dev in two spots, so dev native
bundles transpiled class fields but left `class extends` as modern ES6.
Hermes chokes on that half-transpiled class hierarchy at `new Subclass()`
(TypeError: Cannot read property 'prototype' of undefined), red-screening
the app at init on the Vite-native path. Make transform-classes
unconditional (matching the fully-classed production bundle); keep only
async-to-generator production-only.
…t drift

The Hermes SWC downlevel include list was hand-copied in two call sites, and the
original bug was transform-classes missing from one of them. Define the class set
once (HERMES_CLASS_TRANSFORMS) plus the prod-only addend (HERMES_PROD_TRANSFORMS),
consumed by both sites via getHermesSWCIncludes(dev). Behavior-identical; makes the
"these move together" invariant a compile-time fact rather than a review-time hope.
Unit-tested.
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as draft July 15, 2026 17:17
@natew
natew marked this pull request as ready for review July 16, 2026 00:57
@natew
natew merged commit 8391f61 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