Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions packages/vxrn/src/runtime/native-prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,6 @@ if (typeof globalThis.URLSearchParams === 'undefined') {
};
}

// suppress HMRClient.setup() error from native side
// native calls HMRClient.setup() during bundle load but we don't use Metro HMR
// this intercept catches the error and prevents the red screen
var __origErrorHandler = globalThis.ErrorUtils && globalThis.ErrorUtils.getGlobalHandler && globalThis.ErrorUtils.getGlobalHandler();
if (globalThis.ErrorUtils && globalThis.ErrorUtils.setGlobalHandler) {
globalThis.ErrorUtils.setGlobalHandler(function(error, isFatal) {
if (error && error.message && error.message.indexOf('HMRClient') !== -1) {
// suppress HMRClient errors silently
return;
}
if (__origErrorHandler) __origErrorHandler(error, isFatal);
});
}

// ensure console exists before any library prelude touches it in release bundles
globalThis.console = globalThis.console || {};
var console = globalThis.console;
Expand Down
53 changes: 53 additions & 0 deletions packages/vxrn/src/utils/createNativeDevEngine.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
getNativeTransformConfig,
hmrClientNoopPlugin,
wrapNativeBundleModuleScope,
} from './createNativeDevEngine'

Expand Down Expand Up @@ -66,3 +67,55 @@ describe('wrapNativeBundleModuleScope', () => {
expect(wrapNativeBundleModuleScope(input)).toBe(input)
})
})

describe('hmrClientNoopPlugin', () => {
const plugin = hmrClientNoopPlugin()
const resolveId = plugin.resolveId as unknown as (source: string) => any
const load = plugin.load as unknown as (id: string) => any
const VIRTUAL_ID = '\0vxrn-hmr-client-noop'

it.each([
'react-native/Libraries/Utilities/HMRClient',
'../Utilities/HMRClient',
'../../Utilities/HMRClient.js',
// native Windows ids use backslashes
'..\\Utilities\\HMRClient.js',
'../Utilities/HMRClient.ts',
'../Utilities/HMRClient.tsx',
'../Utilities/HMRClient.cjs',
])('aliases RN HMRClient specifier %j to the no-op virtual module', (source) => {
expect(resolveId(source)).toEqual({ id: VIRTUAL_ID, external: false })
})

it.each([
// trailing letters (no boundary) must not match
'react-native/Libraries/Utilities/HMRClientRegistry',
// Utilities must sit at a path boundary
'some/MyUtilities/HMRClient',
// unrelated RN modules
'react-native/Libraries/Core/setUpDeveloperTools',
'react',
])('does not touch unrelated specifier %j', (source) => {
expect(resolveId(source)).toBeUndefined()
})

it('loads a no-op module exposing every HMRClient method RN calls', () => {
const result = load(VIRTUAL_ID)
expect(result?.moduleType).toBe('js')
for (const method of [
'setup',
'enable',
'disable',
'registerBundle',
'log',
'isEnabled',
]) {
expect(result!.code).toContain(method)
}
expect(result!.code).toContain('export default HMRClient')
})

it('does not load unrelated ids', () => {
expect(load('\0some-other-virtual')).toBeUndefined()
})
})
51 changes: 43 additions & 8 deletions packages/vxrn/src/utils/createNativeDevEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ function getNativePlugins(
serverFileExclusionPlugin(),
// guard server-only / client-only / web-only / native-only imports
environmentGuardPlugin(),
// alias RN's Metro HMR client to a no-op — vxrn drives HMR itself (the
// rolldown-runtime WebSocket); RN's client otherwise opens a /hot socket and
// red-boxes "unknown-message [object Object]" on every edit (new arch)
hmrClientNoopPlugin(),
// stub CSS imports — native doesn't support CSS and rolldown removed CSS bundling
cssStubPlugin(),
// handle import.meta.glob (used by One's route system)
Expand Down Expand Up @@ -470,14 +474,6 @@ try {
// skipped by the per-file SWC plugin) so old Hermes can parse them
code = await downlevelClassFieldsInBundle(code)

// register a no-op HMRClient so RN's native side doesn't error when calling HMRClient.setup()
// our actual HMR is handled via the outro WebSocket connection
const hmrClientStub = `registerCallableModule("HMRClient",{setup:function(){},enable:function(){},disable:function(){},registerBundle:function(){},log:function(){}})`
code = code.replace(
/registerCallableModule\s*\(\s*["']AppRegistry["']/,
(match) => hmrClientStub + ',' + match
)

// wrap module code in a function scope so top-level `var`s (e.g. RN
// fetch.js's `Headers`/`Request`) don't leak as non-configurable
// globals and break RN's polyfillGlobal (dev-only redbox). see fn doc.
Expand Down Expand Up @@ -792,6 +788,45 @@ function environmentGuardPlugin(): Plugin {
}
}

/**
* Alias react-native's Metro HMR client (`Libraries/Utilities/HMRClient`) to a
* no-op module.
*
* vxrn drives Fast Refresh itself over the rolldown-runtime WebSocket and never
* speaks Metro's `/hot` protocol. On the new architecture, react-native
* `registerCallableModule('HMRClient', require('./HMRClient'))`s its real client
* eagerly at startup — before vxrn's late override runs — and `emplace` keeps
* that first registration. RN's client then opens a `MetroHMRClient` socket that
* receives vxrn's `hmr:*` frames it can't parse and red-boxes
* `unknown-message [object Object]` on every edit.
*
* Neutralizing the module at its source means RN registers *this* no-op as the
* one-and-only `HMRClient` (working WITH `emplace`, so it's arch-agnostic) and
* the stray socket is never opened. The class-shaped surface
* (`setup`/`enable`/`disable`/`registerBundle`/`log`/`isEnabled`) mirrors the
* methods RN calls on it.
*/
export function hmrClientNoopPlugin(): Plugin {
// Match RN's HMRClient by module path, tolerating either separator (native
// Windows ids use `\`) and an optional js/ts extension.
const RN_HMR_CLIENT_RE = /(^|[\\/])Utilities[\\/]HMRClient(\.[cm]?[jt]sx?)?$/
return {
name: 'vxrn:hmr-client-noop',
resolveId(source) {
if (RN_HMR_CLIENT_RE.test(source))
return { id: '\0vxrn-hmr-client-noop', external: false }
},
load(id) {
if (id === '\0vxrn-hmr-client-noop') {
return {
code: `const HMRClient = { setup() {}, enable() {}, disable() {}, registerBundle() {}, log() {}, isEnabled() { return false } }\nexport default HMRClient`,
moduleType: 'js' as any,
}
}
},
}
}

/**
* Stub CSS imports for native builds.
* Native doesn't support CSS and rolldown removed CSS bundling support.
Expand Down