In Tauri mobile dev the page origin doesn't match the dev server, so
Next.js's `getSocketUrl` builds an unreachable HMR URL (`wss://localhost`
on iOS, `ws://tauri.localhost` on Android), the HMR client never connects,
and the page stays blank.
Inject a tiny script in `<head>` (dev + Tauri only) that subclasses
`window.WebSocket` and rewrites the broken URL to the actual dev server.
`TAURI_DEV_HOST` is forwarded from the build env so `pnpm tauri {ios,android}
dev --host <ip>` also routes HMR through the LAN address.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,12 +72,67 @@ export const viewport: Viewport = {
|
||||
// unrecognized key on every page load, so we keep it out of SSR.
|
||||
};
|
||||
|
||||
// In Tauri mobile dev the page origin doesn't match the dev server, so
|
||||
// Next.js's `getSocketUrl` builds an unreachable HMR URL (see
|
||||
// `next/dist/client/dev/hot-reloader/get-socket-url.js`):
|
||||
// - iOS sim: page at `tauri://localhost` → `wss://localhost/_next/...`
|
||||
// (no port, non-http scheme falls through to `wss:`)
|
||||
// - Android emul.: page at `http://tauri.localhost` → `ws://tauri.localhost/_next/...`
|
||||
// (`tauri.localhost` is intercepted by Tauri's asset handler, but
|
||||
// WebSocket frames bypass the interceptor and the dev server is on the
|
||||
// host machine, reachable from the emulator as `10.0.2.2`)
|
||||
// Rewrite the WebSocket constructor before the HMR client runs.
|
||||
// When `--host <ip>` is passed, tauri-cli exports `TAURI_DEV_HOST=<ip>`
|
||||
// before invoking `beforeDevCommand`, so we forward that as `devHost` and
|
||||
// use it for the rewrite (the dev server must also bind to the same address
|
||||
// — typically `next dev -H 0.0.0.0`).
|
||||
function patchTauriHmrWebSocket(devHost?: string) {
|
||||
const isIosTauriProxy = location.protocol === 'tauri:' && location.hostname === 'localhost';
|
||||
const isAndroidTauriProxy =
|
||||
location.protocol === 'http:' && location.hostname === 'tauri.localhost';
|
||||
if (!isIosTauriProxy && !isAndroidTauriProxy) return;
|
||||
|
||||
// Priority: explicit --host > platform default loopback alias.
|
||||
// iOS Simulator can reach the host's localhost directly.
|
||||
// Android emulator reaches the host machine via 10.0.2.2.
|
||||
const hmrHost = devHost
|
||||
? `${devHost}:3000`
|
||||
: isIosTauriProxy
|
||||
? 'localhost:3000'
|
||||
: '10.0.2.2:3000';
|
||||
const brokenHostPattern = /^wss?:\/\/(localhost|tauri\.localhost)(?=\/_next\/)/;
|
||||
|
||||
const OriginalWebSocket = window.WebSocket;
|
||||
class PatchedWebSocket extends OriginalWebSocket {
|
||||
constructor(url: string | URL, protocols?: string | string[]) {
|
||||
const urlStr = url instanceof URL ? url.href : url;
|
||||
const rewritten =
|
||||
typeof urlStr === 'string' && brokenHostPattern.test(urlStr)
|
||||
? urlStr.replace(brokenHostPattern, `ws://${hmrHost}`)
|
||||
: url;
|
||||
super(rewritten, protocols);
|
||||
}
|
||||
}
|
||||
window.WebSocket = PatchedWebSocket;
|
||||
}
|
||||
|
||||
const shouldInjectDevHmrPatch =
|
||||
process.env['NODE_ENV'] === 'development' && process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'tauri';
|
||||
const devHmrPatchScript = `(${patchTauriHmrWebSocket.toString()})(${JSON.stringify(
|
||||
process.env['TAURI_DEV_HOST'],
|
||||
)});`;
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html
|
||||
lang='en'
|
||||
className={process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'tauri' ? 'edge-to-edge' : ''}
|
||||
>
|
||||
{shouldInjectDevHmrPatch ? (
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: devHmrPatchScript }} />
|
||||
</head>
|
||||
) : null}
|
||||
<body>
|
||||
<ViewTransitions>
|
||||
<EnvProvider>
|
||||
|
||||
Reference in New Issue
Block a user