1
0 Comments

A dependency version I never thought about silently produced bundles that couldn't compile

My production build failed at the Gradle bundling step with hundreds of these:

index.android.bundle:3761:5: error: private properties are not supported <unknown>:0: error: too many errors emitted

The offending code was in React Native's own internal DOM shims — DOMRectReadOnly, EventEmitter, Performance. All using ES2022 private class fields (#x). Nothing I wrote.

Those fields are supposed to be transpiled away before reaching Hermes. On this React Native version, Hermes can't compile them to bytecode.

Cause: I had babel-preset-expo pinned to ^57.0.4 in devDependencies. My project is Expo SDK 54, and expo bundles its own nested babel-preset-expo@54.0.12 for internal use.

My babel.config.js says:

presets: ['babel-preset-expo']

Normal Node resolution finds the top-level copy. The wrong one. Version 57 targets a newer Hermes that supports private fields natively, so it stops transforming them. My actual Hermes couldn't handle them.

How it got there: I added babel-preset-expo while setting up Jest, and grabbed the latest version without thinking about it. It looked like a test-only dependency. It isn't — Metro uses the same babel.config.js for real production bundling. A "test dependency" was silently corrupting my shipping build.

Why nothing caught it: tsc passed. 161 Vitest tests passed. 12 Jest component tests passed. None of them exercise Metro or Hermes. The only thing that fails is a real build.

Fix: pin it to your SDK's line.

"babel-preset-expo": "^54.0.0"

Reproducing this locally instead of burning cloud builds. This is the internal command the RN Gradle plugin runs, so it reproduces the exact failure:

npx expo export:embed --entry-file index.ts --platform android \ --dev false --minify false --unstable-transform-profile hermes \ --bundle-output /tmp/b/index.android.bundle \ --assets-dest /tmp/b/assets --reset-cache

Then compile it with the Hermes binary that ships in your node_modules:

./node_modules/react-native/sdks/hermesc/<platform>-bin/hermesc \ -emit-binary -out /tmp/b/index.hbc /tmp/b/index.android.bundle

Exit code 0 means CI will pass. That turned a 20-minute round trip into 40 seconds. (Plain npx react-native bundle won't work in an Expo project — @react-native-community/cli isn't installed.)

The generalizable lesson: if you add a tooling dependency that has a same-named counterpart already bundled by your framework, match the framework's version line. Don't take latest. A mismatch there can silently corrupt your real build pipeline while every test stays green, and you won't find out until you try to ship.


A note on all three: these are drafted from what actually happened, but I'd read each one over before posting and cut anything that doesn't sound like you. Also double-check the Hermes path in post 3 against your own node_modules before publishing — it's win64-bin on your machine, but you'll want the generic phrasing for other readers, which is what I've used.

posted toAvatar for product Norvayo
Norvayo