Skip to content

[Worklets] Bundle Mode: react-native shim breaks with resolvers that remap react-native (e.g. uniwind) → infinite NativeModules recursion #9817

Description

@ottob

Summary

When Worklets Bundle Mode is enabled alongside any Metro resolver that remaps the react-native specifier — e.g. uniwind, which rewrites import 'react-native'uniwind/components — the app crashes at startup with an infinite recursion:

ERROR  [runtime not ready]: RangeError: Maximum call stack size exceeded (native stack depth)
ERROR  [runtime not ready]: Invariant Violation: "main" has not been registered.

It's a get NativeModules recursion during module initialization, before AppRegistry.registerComponent runs.

Root cause

Bundle Mode's resolver redirects every react-native import to bundleMode/shims/reactNativeShim.js, and the shim obtains the real module with:

module.exports = require('react-native');

This re-requests the bare react-native specifier, which re-enters the resolver chain. Bundle Mode's own guard (context.originModulePath !== reactNativeShimPath) prevents self-redirection, but it can't stop another resolver from intercepting the request. uniwind's resolver (which wraps Bundle Mode's) rewrites the shim's require('react-native')uniwind/components; and uniwind/components' internal require('react-native') is then redirected by Bundle Mode back to the shim. The result is a shim ↔ uniwind/components require cycle that recurses through NativeModules at runtime.

(uniwind does have carve-outs for RN-internal origins, but they key on the path segment /react-native/; the shim lives under /react-native-worklets/, so it isn't recognized. The bug isn't uniwind-specific though — any resolver that remaps the bare react-native specifier triggers it.)

Environment

  • react-native-worklets 0.10.0
  • react-native 0.86.0, Expo SDK 57, Metro 0.84.4, Hermes
  • uniwind 1.10.0 (any react-native-remapping resolver reproduces it)

Repro

  1. Expo app with reanimated/worklets + Bundle Mode enabled (per the setup guide).
  2. Add uniwind and withUniwindConfig(...) in metro.config.js.
  3. Add any worklet, run on a simulator/device → startup recursion crash.

Proposed fix

Have the shim acquire the real module through a worklets-internal sentinel specifier instead of the bare react-native, and resolve that sentinel to the real RN path inside Bundle Mode's resolver. No other resolver matches the sentinel, and Bundle Mode returns a concrete filePath (never re-entering resolution), so the cycle can't form — regardless of resolver ordering. This hardens Bundle Mode against any react-native-remapping setup, not just uniwind.

packages/react-native-worklets/bundleMode/index.js:

@@
   'module',
   'index.js'
 );
+const realReactNativeModuleName = path.posix.join(
+  workletsPackageName,
+  'bundleMode',
+  'realReactNative'
+);
 
 function bundleModeResolveRequest(
   /** @type {any} */ context,
   /** @type {string} */ moduleName,
   /** @type {any} */ platform,
-  /** @type {any} */ userConfigResolveRequest
+  /** @type {any} */ userConfigResolveRequest,
+  /** @type {string} */ realReactNativePath
 ) {
   if (moduleName.startsWith(workletsDirPath)) {
     const fullModuleName = path.join(workletsPackageParentDir, moduleName);
     return { type: 'sourceFile', filePath: fullModuleName };
   }
+  // The react-native shim requires the real react-native through this internal
+  // sentinel specifier instead of the bare 'react-native' specifier, so that
+  // resolvers which remap 'react-native' (e.g. uniwind -> uniwind/components)
+  // cannot intercept it and form a shim <-> replacement require cycle.
+  if (moduleName === realReactNativeModuleName) {
+    return { type: 'sourceFile', filePath: realReactNativePath };
+  }
   if (
     moduleName === 'react-native' &&
     context.originModulePath !== reactNativeShimPath
@@ function getBundleModeMetroConfig(config) {
   config.serializer.createModuleIdFactory = bundleModeCreateModuleIdFactory;
 
+  const realReactNativePath = require.resolve('react-native', {
+    paths: [config.projectRoot || process.cwd()],
+  });
+
   const currentResolveRequest = config?.resolver?.resolveRequest;
   config.resolver.resolveRequest = (context, moduleName, platform) =>
     bundleModeResolveRequest(
       context,
       moduleName,
       platform,
-      currentResolveRequest
+      currentResolveRequest,
+      realReactNativePath
     );

packages/react-native-worklets/bundleMode/shims/reactNativeShim.js:

-module.exports = require('react-native');
+module.exports = require('react-native-worklets/bundleMode/realReactNative');

(The same change would apply to the bundleModeMetroConfig object used by non-Expo projects.)

Validation

Applied this patch to the installed react-native-worklets@0.10.0 in a real Expo 57 + uniwind app: the startup recursion is gone, the app boots, worklets run, and uniwind styling is preserved (app code's react-nativeuniwind/components rewrite is untouched — only the shim's own react-native bypasses it).

Happy to send a PR if this approach looks right.

Metadata

Metadata

Assignees

Labels

Missing infoThe user didn't precise the problem enoughRepro providedA reproduction with a snippet of code, snack or repo is provided

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions