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
- Expo app with reanimated/worklets + Bundle Mode enabled (per the setup guide).
- Add uniwind and
withUniwindConfig(...) in metro.config.js.
- 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-native → uniwind/components rewrite is untouched — only the shim's own react-native bypasses it).
Happy to send a PR if this approach looks right.
Summary
When Worklets Bundle Mode is enabled alongside any Metro resolver that remaps the
react-nativespecifier — e.g. uniwind, which rewritesimport 'react-native'→uniwind/components— the app crashes at startup with an infinite recursion:It's a
get NativeModulesrecursion during module initialization, beforeAppRegistry.registerComponentruns.Root cause
Bundle Mode's resolver redirects every
react-nativeimport tobundleMode/shims/reactNativeShim.js, and the shim obtains the real module with:This re-requests the bare
react-nativespecifier, 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'srequire('react-native')→uniwind/components; anduniwind/components' internalrequire('react-native')is then redirected by Bundle Mode back to the shim. The result is ashim ↔ uniwind/componentsrequire cycle that recurses throughNativeModulesat 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 barereact-nativespecifier triggers it.)Environment
react-native-worklets0.10.0react-native0.86.0, Expo SDK 57, Metro 0.84.4, Hermesuniwind1.10.0 (anyreact-native-remapping resolver reproduces it)Repro
withUniwindConfig(...)inmetro.config.js.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 concretefilePath(never re-entering resolution), so the cycle can't form — regardless of resolver ordering. This hardens Bundle Mode against anyreact-native-remapping setup, not just uniwind.packages/react-native-worklets/bundleMode/index.js:packages/react-native-worklets/bundleMode/shims/reactNativeShim.js:(The same change would apply to the
bundleModeMetroConfigobject used by non-Expo projects.)Validation
Applied this patch to the installed
react-native-worklets@0.10.0in a real Expo 57 + uniwind app: the startup recursion is gone, the app boots, worklets run, and uniwind styling is preserved (app code'sreact-native→uniwind/componentsrewrite is untouched — only the shim's ownreact-nativebypasses it).Happy to send a PR if this approach looks right.