Files
WRNexusJS/packages/react/test/island-hmr.test.ts
T
ClintchizandClaude Opus 5 4dd7681bf7 fix(dev): repair the HMR client and keep islands alive across updates
The HMR client script was dead in the browser. HMR_CLIENT_JS is a
TypeScript template literal, so the regex [ \t\r\n] inside it was expanded
into real control characters, producing a regex literal containing a raw
newline — a syntax error that took the whole script down with "Invalid
regular expression: missing /". It now uses \s, and a test asserts the
emitted client parses and holds no control characters inside regex
literals; that test fails if the bug is reintroduced.

HMR also corrupted CSP nonces. A document's nonce is fixed at load, but
morph copied attributes from freshly fetched HTML, overwriting the live
nonce with one the browser will not honour. syncAttrs now leaves nonce
alone, and nodes moved across are re-stamped with the live nonce.

Islands vanished on every HMR update: morph puts the server placeholder
back over the mounted island. The island runtime now remounts on
wrnexus:hmr-updated. Remounting swaps the container for a bare clone —
re-rendering the existing root is a no-op once HMR has wiped the DOM
externally, and unmounting throws asynchronously because the nodes React
wants to remove are already gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:15:21 +05:30

95 lines
2.7 KiB
TypeScript

import { afterEach, expect, test } from "bun:test";
import { Window } from "happy-dom";
import { act, createElement } from "react";
import {
islandRootCount,
mountIslands,
remountIslands,
unmountIslands,
} from "../src/island-runtime.ts";
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
function host(window: Window): ParentNode {
return window.document.body as unknown as ParentNode;
}
function domWith(html: string) {
const window = new Window();
window.document.body.innerHTML = html;
(globalThis as any).window = window;
(globalThis as any).document = window.document;
return window;
}
afterEach(() => {
const doc = (globalThis as any).document;
if (!doc) return;
act(() => {
unmountIslands(doc);
});
});
const marker =
`<div data-wrn-island="Chart" data-wrn-island-strategy="only"` +
` data-wrn-island-props='{}'></div>`;
test("remount replaces island output without leaking roots", async () => {
const window = domWith(marker);
const first = async () => ({ default: () => createElement("span", null, "v1") });
const second = async () => ({ default: () => createElement("span", null, "v2") });
await act(async () => {
await mountIslands(host(window), { loader: first });
});
expect(window.document.body.textContent).toContain("v1");
expect(islandRootCount()).toBe(1);
await act(async () => {
await remountIslands(host(window), { loader: second });
});
expect(window.document.body.textContent).toContain("v2");
expect(window.document.body.textContent).not.toContain("v1");
expect(islandRootCount()).toBe(1);
});
test("repeated remounts stay at one root", async () => {
const window = domWith(marker);
const loader = async () => ({ default: () => createElement("span", null, "x") });
await act(async () => {
await mountIslands(host(window), { loader });
});
for (let i = 0; i < 4; i += 1) {
await act(async () => {
await remountIslands(host(window), { loader });
});
}
expect(islandRootCount()).toBe(1);
});
test("remount re-renders in place instead of creating a second root", async () => {
const window = domWith(marker);
const loader = async () => ({ default: () => createElement("span", null, "v1") });
await act(async () => {
await mountIslands(host(window), { loader });
});
expect(islandRootCount()).toBe(1);
// Simulate HMR morphing server markup back over the mounted island: React's
// rendered DOM is gone, so unmounting it would throw.
const el = window.document.querySelector("[data-wrn-island]")!;
el.innerHTML = "";
await act(async () => {
await remountIslands(host(window), { loader });
});
expect(islandRootCount()).toBe(1);
expect(window.document.body.textContent).toContain("v1");
});