Repository navigation
Conversation
🦋 Changeset detectedLatest commit: 9ef4b18 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
@DanNizy is attempting to deploy a commit to the Callstack Team on Vercel. A member of the Team first needs to authorize it. |
2105858 to
05ed50c
Compare
|
Crash: worklet in a lazy chunk kills the app in Release (iOS + Android) I tested this PR in a fresh RN 0.86.3 app (Re.Pack 5.4.2 + Rspack, reanimated 4.7.1, worklets 0.13.0) on the iOS simulator and the Android emulator, in Debug and Release. Basic bundle mode works on both: worker → scheduleOnUI → RN, and forwarded imports inside worklets. Worklets that live in a lazy chunk break. Repro // App.tsx
const LazyScreen = React.lazy(() => import('./src/LazyScreen'));
// src/LazyScreen.tsx
import { lazyHelper } from './lazyOnlyHelper'; // only imported here, forwarded via importForwarding.relativePaths
scheduleOnRuntime(createWorkletRuntime('lazy'), () => {
'worklet';
const value = lazyHelper(21);
scheduleOnUI(() => { 'worklet'; scheduleOnRN(report, value); });
});What happens
Cause Suggested fix stack trace: |
| const deferredHashes: number[] = []; | ||
|
|
||
| for (const module of compilation.modules) { | ||
| const resource = getModuleResource(module); | ||
| if (typeof resource !== 'string' || entryModules.has(module)) { | ||
| continue; | ||
| } | ||
| const workletsModuleId = getWorkletsModuleId(resource, workletsRoot); | ||
| if ( | ||
| workletsModuleId !== undefined && | ||
| workletsModuleId !== WORKLETS_ENTRY_MODULE_ID | ||
| ) { | ||
| deferredHashes.push(workletsModuleId); | ||
| } | ||
| } |
There was a problem hiding this comment.
Issue: The build stays silent in case of the crash -> because the check only looks at the .worklets modules themselves.
Evidence: The repro build had zero bundle-mode warnings
Suggestion: Also warn when a factory's dependencies sit outside the entry chunk
| const workletModuleIds: Record<number, string | number | null> = {}; | ||
| let polyfillModuleId: string | number | null | undefined; | ||
|
|
||
| if (winner) { | ||
| workletModuleIds[WORKLETS_ENTRY_MODULE_ID] = chunkGraph.getModuleId( | ||
| winner.module | ||
| ); | ||
| } | ||
|
|
||
| for (const module of chunkModules) { | ||
| const resource = getModuleResource(module); | ||
| if (typeof resource !== 'string') { | ||
| continue; | ||
| } | ||
| if (toPosixPath(path.normalize(resource)) === polyfillPath) { | ||
| polyfillModuleId = chunkGraph.getModuleId(module); | ||
| continue; | ||
| } | ||
| const workletsModuleId = getWorkletsModuleId(resource, workletsRoot); | ||
| if ( | ||
| workletsModuleId !== undefined && | ||
| workletsModuleId !== WORKLETS_ENTRY_MODULE_ID | ||
| ) { | ||
| workletModuleIds[workletsModuleId] = chunkGraph.getModuleId(module); | ||
| } | ||
| } |
There was a problem hiding this comment.
Issue: createSerializable.__bundleData.source comes from require.resolveWeak('react-native-worklets'). Metro returns -2 there; webpack returns its own module id,
which isn't in the __r map.
Evidence: Dev, on device: Exception in HostFunction: Value is a string, expected a number. Prod, in the emitted bundle: source: 32355 but the map only has "-2":
32355.
Suggestion: Rewrite that resolveWeak to -2 (parser hook or loader on memory/serializable.native.*), or map the entry's real id too
| `if (globalThis.nativeModuleProxy === undefined) {`, | ||
| Template.asString([ | ||
| `globalThis.nativeModuleProxy = {`, | ||
| Template.asString([ | ||
| `__workletsModuleProxy: globalThis.__workletsModuleProxy,`, | ||
| ]), | ||
| `};`, | ||
| ]), | ||
| `}`, |
There was a problem hiding this comment.
Issue: Only partial Metro parity. Metro redirects react-native to bundleMode/shims/reactNativeShim.js, which sets __RUNTIME_KIND = 1 and, in DEV, gives a clear
"Perhaps you tried to access a Turbo Module?" error. Webpack also hoists requires where Metro inlines them, so forwarded import graphs run eagerly on the UI
runtime.
Evidence: With broad importForwarding, Reanimated internals hit TurboModuleRegistry.getEnforcing(...): 'PlatformConstants' could not be found every frame. Not
checked against Metro.
Suggestion: Alias react-native to the shim like Metro does; document the eager-evaluation difference
| export function detectWorkletsBundleMode(context: string): boolean { | ||
| const babel = require('@babel/core') as typeof import('@babel/core'); | ||
| try { | ||
| // `loadPartialConfig` is used instead of `loadOptions`, because only the former | ||
| // keeps the request the plugin was configured with. Once Babel has resolved the | ||
| // config, the Worklets plugin is only identified by the name it returns | ||
| // ('worklets'), which says nothing about where it came from. | ||
| const config = babel.loadPartialConfig({ | ||
| caller: { name: '@callstack/repack' }, | ||
| filename: path.join(context, 'index.js'), | ||
| root: context, | ||
| envName: process.env.BABEL_ENV || process.env.NODE_ENV || 'development', | ||
| }); | ||
| const plugins = (config?.options?.plugins ?? | ||
| []) as WorkletsBabelPluginItem[]; | ||
| return plugins.some((plugin) => { | ||
| const request = [plugin?.name, plugin?.file?.request] | ||
| .filter((value): value is string => typeof value === 'string') | ||
| .join(' '); | ||
| return ( | ||
| WORKLETS_BABEL_PLUGIN_RE.test(request) && | ||
| plugin?.options?.bundleMode === true | ||
| ); | ||
| }); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
Issue: Detection can disagree with the Babel config actually used: envName falls back to 'development', the filename is fixed to index.js, and a preset that adds
the worklets plugin itself (e.g. Expo's) isn't seen. Babel then flips the flags but no runtime bridge is emitted.
Evidence: Code reading
Suggestion: Detect from the loader's actual options, or let users force it with an explicit plugin option
|
|
||
| This will disable adding now redundant `babel-loader` rules, and defer the transformation to Babel through `babel-swc-loader`. | ||
|
|
||
| ### Enabling Bundle Mode |
There was a problem hiding this comment.
Issue: The docs only show the Babel snippet. Missing: worklets ≥ 0.13 requirement; importForwarding (without it, imports in worklets become remote functions);
lazy-chunk caveat; full reload needed after JS changes; ScriptManager resolver for lazy chunks.
Evidence: My first device run failed on exactly the importForwarding point
Suggestion: Add a "Limitations" section
Summary
Introduces bundle mode support to the
@callstack/repack-plugin-reanimated.The implementation mimics the behaviour of what
react-native-worklets/bundleModedoes to metro config but it is aligned for rspack/webpack. More about implementation is available inagent_context/worklets-bundle-mode-oct2026/design.mdEnabling bundle mode
Assuming reanimated is fully installed and configured according to this doc
Not implemented yet
Testing
tests/integration/src/plugins/__fixtures__/worklets-bundle-modescheduleOnUi()from worker runtime (which is not possible without bundle mode).