fix(vxrn): alias RN's Metro HMR client to a no-op on native dev (new arch)#734
Merged
Merged
Conversation
On the new architecture, RN registers its real HMRClient via registerCallableModule during setUpBatchedBridge — before vxrn's bundle-splice stub runs — and the underlying std::unordered_map::emplace keeps the first registration. So RN's HMRClient.setup() survives, opens a MetroHMRClient to /hot, and red-boxes "unknown-message [object Object]" on every edit (metro's client can't parse vxrn's hmr:* frames). Alias react-native's Utilities/HMRClient to a no-op module at resolve time so RN registers the no-op as the first-and-only HMRClient — working with emplace, arch-agnostically. MetroHMRClient is never constructed (and tree-shakes out of the bundle), so the socket never opens and the error cannot occur. vxrn's own HMR (the rolldown-runtime WebSocket) is untouched. Also removes two now-dead workarounds that only chased the symptom: the emplace-losing bundle-splice stub, and a global ErrorUtils handler that matched 'HMRClient' (the thrown text is "unknown-message [object Object]", so it never caught this error).
YevheniiKotyrlo
marked this pull request as draft
July 15, 2026 16:42
Cover resolveId (aliases RN Utilities/HMRClient across both separators and optional js/ts/cjs extension; leaves near-misses like HMRClientRegistry alone) and load (no-op module exposing every method RN calls). Export the plugin factory so the test drives it directly.
natew
marked this pull request as ready for review
July 16, 2026 00:57
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On a native dev build (
bundler: 'vite'), the new architecture / bridgeless, every source edit pops a red LogBox:It's non-fatal (the app keeps rendering and vxrn's own Fast Refresh still runs), but it fires on every change and papers over the dev experience. It's React Native's built-in Metro HMR client choking on vxrn's
hmr:*WebSocket protocol — a client vxrn believes it has disabled, but hasn't on the new arch.Root cause — the no-op stub loses an
emplaceracevxrn drives Fast Refresh itself over the rolldown-runtime WebSocket and never speaks Metro's
/hotprotocol. To keep RN's MetroHMRClientout of the way, it splices a no-opregisterCallableModule("HMRClient", …)into the bundle right beforeregisterCallableModule("AppRegistry", …). On the new arch that registration is silently dropped:Libraries/Core/setUpBatchedBridge.jsdoesregisterCallableModule('HMRClient', () => require('../Utilities/HMRClient').default), which runs duringsetUpBatchedBridge— before theAppRegistryregistration the stub is spliced in front of.registerCallableModule→global.RN$registerCallableModule→ReactInstance.cppcallableModules_.emplace(...).std::unordered_map::emplaceis a no-op when the key already exists — first registration wins. (On the old arch,MessageQueueassignsthis._lazyCallableModules[name] = …— last-wins — which is why the splice worked there.)HMRClient.setup()survives.setup()unconditionally opens aMetroHMRClientsocket to/hot(theenable/disable/isEnabledflags only gate whether refreshes are applied, not whether the socket opens).hmr:*to every/hotclient, including the Metro one. metro-runtime'sWebSocketHMRClientcan't parsehmr:*, hits itsdefaultbranch, and RN's handler formats`${data.type} ${data.message}`="unknown-message [object Object]"and throws it → the red LogBox.Fix
Alias
react-native/…/Utilities/HMRClientto a no-op module via a resolver plugin (hmrClientNoopPlugin), instead of the emplace-order-dependent bundle-regex splice. RN's ownrequire('../Utilities/HMRClient')then resolves to the no-op, so RN registers the no-op as the first-and-onlyHMRClient— working withemplace, arch-agnostically.MetroHMRClientis never constructed (and tree-shakes out of the bundle), so it never connects to/hotand the error cannot occur. vxrn's real HMR path (the rolldown-runtime WebSocket) is untouched.This also lets two now-dead workarounds go, both of which only ever chased the symptom:
createNativeDevEngine.ts) — this is the registration that loses theemplacerace, i.e. the root cause itself. Redundant once RN registers the no-op directly.ErrorUtilshandler innative-prelude.tsthat swallowed any error whose message contained'HMRClient'. The thrown text isunknown-message [object Object]— it contains no'HMRClient', so this never caught this error; all it did was risk hiding unrelated errors that happen to mention HMRClient. With the alias, no HMRClient error is thrown at all.Why alias the module (vs. the alternatives)
/hot) would stop the cross-talk, but leaves RN's real HMRClient instantiated and an idle/hotsocket open — it routes around the Metro client instead of removing it, and stays fragile to any future RN/metro that expects a/hothandshake.hmr:*on connect; Metro's sendsregister-entrypoints) — so it's not impossible — but it's racy (a broadcast before the first frame leaks) and still symptom-level: the Metro client stays alive.setup/enable/disable/registerBundle/log), verified against RN's whole HMRClient import graph (setUpBatchedBridge,loadBundleFromServer,setUpDeveloperTools).Test plan
unknown-message [object Object]LogBox that appeared on every edit is gone; the app still renders and vxrn's own Fast Refresh still applies.unknown-messagestring count goes 2 → 0 andnew MetroHMRClient→ 0 (MetroHMRClient is no longer reachable and tree-shakes out — bundle ~21KB smaller).hmr:*frame to a metroWebSocketHMRClientyields exactly{type:'unknown-message', message: …}→"unknown-message [object Object]".createNativeDevEngine.test.ts):hmrClientNoopPluginaliases every RN HMRClient specifier (both separators, optional.js/.ts/.cjsext) to the virtual no-op and leaves near-misses alone (HMRClientRegistry,MyUtilities/HMRClient,setUpDeveloperTools); the loaded module exposes every method RN calls.Notes
MessageQueuepath made the original splice work, which is likely why this regressed silently as the ecosystem moved to bridgeless./hotsocket): this one is purely why a red error appears.runtime/hmr-client.tsthat is now dead (nothing imports it) and incompatible with the current outro-driven runtime (itshmr:updatehandler is a no-op) — a separate deletion, not part of this PR.