I was shipping a mobile consumer Expo app. Reanimated and react-native-worklets run work on the UI thread so gestures stay smooth on product screens. Shoppers never heard of SHA-1. They wanted a TestFlight build that existed. EAS went red instead.
Metro died while it hashed the worklet graph. CI could not ship a binary. Production stayed on last week's build, which is a product decision I did not mean to make.
Local expo start looked fine. My laptop never ran the same transform EAS runs in the cloud. A green local bundler still leaves you with no TestFlight file. The shopper keeps last week's app on their phone while you debug a bundler that never ran the EAS transform.
What Bundle Mode actually does
Worklets Bundle Mode is not a Metro toggle. It is bundleMode: true on react-native-worklets/plugin in babel.config.js. With the flag on, the plugin compiles worklets into generated modules under .worklets/ instead of inlining them into the main app bundle. Metro has to discover those files, add them to the dependency graph, and compute a SHA-1 for each one before EAS can finish the eager bundle step.
The official Bundle Mode setup also expects Metro wrapped with getBundleModeMetroConfig, plus patches to metro and metro-runtime. Without that wiring, the graph Metro hashes on EAS can be incomplete even when Babel emitted the worklet files. Missing index entries show up as Failed to get SHA-1 for ... in the cloud log. Local dev often skips the same code path, so the error feels like it came from nowhere.
Why Bundle Mode showed up without opting in
I opened Metro config first because the EAS log talks about Metro and SHA-1. Wrong file. The switch that created the extra graph lives on react-native-worklets/plugin in babel.config.js.
I never set bundleMode: true. The plugin default is false. Expo 54 likes eager imports, and eager imports were enough to set globalThis._WORKLETS_BUNDLE_MODE as if I had opted in. Tree-shaking thought it was helping. The cloud job then hashed a worklet graph I never asked for, without the Metro config the docs assume when you turn Bundle Mode on for real.
That gap is the trap. expo start on my machine can bundle happily while EAS runs a stricter transform-and-hash pass over a graph I never fully opted into. Babel alone is not enough to enable Bundle Mode safely. You need the plugin flag and the Metro wrapper and patches together. I had neither on purpose, and still got half of the behavior.
Pin the flag in babel.config.js
I needed a build. I made the default explicit in babel.config.js so Expo tree-shaking and eager imports could not flip it behind my back.
// babel.config.js
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: [['react-native-worklets/plugin', { bundleMode: false }]],
};
};If you are not using worklets at all, babel-preset-expo can take worklets: false so the plugin never loads. I still wanted Reanimated. I just did not want the extra bundle Metro had to hash on EAS.
After the flag I ran npx expo start --clear and reran EAS. Skip --clear and Metro can keep serving the old graph that already failed SHA-1. A green expo start on my laptop did not mean EAS would hash a clean graph. Once the cloud step finished without the .worklets/ side bundle, I had a binary again.
Reanimated stayed
Reanimated stayed. Only the separate worklet bundle went away. If I ever turn Bundle Mode on for real, I will enable the plugin flag and apply the Metro config and runtime patches in the same change, not Babel alone.
When EAS dies hashing worklets and local start looks innocent, I open babel.config.js before I rewrite Metro. The plugin default being false did not save me once Expo 54 eager imports set globalThis._WORKLETS_BUNDLE_MODE. I write the flag down now. Then I go get the TestFlight build the shopper was waiting for.
Happy coding! Sander