feat(react): remount islands on hot module replacement
Disposes and re-creates island roots after a source change. Island state resets by design; Fast Refresh needs a Babel/SWC transform plus a runtime and is out of scope for v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,3 +6,4 @@ export { IslandErrorBoundary } from "./error-boundary.tsx";
|
|||||||
export type { IslandErrorBoundaryProps } from "./error-boundary.tsx";
|
export type { IslandErrorBoundaryProps } from "./error-boundary.tsx";
|
||||||
export { islandRootCount, mountIslands, unmountIslands } from "./island-runtime.ts";
|
export { islandRootCount, mountIslands, unmountIslands } from "./island-runtime.ts";
|
||||||
export type { MountOptions } from "./island-runtime.ts";
|
export type { MountOptions } from "./island-runtime.ts";
|
||||||
|
export { remountIslands } from "./island-runtime.ts";
|
||||||
|
|||||||
@@ -100,3 +100,16 @@ export function unmountIslands(root: ParentNode): void {
|
|||||||
roots.delete(element);
|
roots.delete(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dev-only: dispose and re-create island roots after a source change.
|
||||||
|
*
|
||||||
|
* Island state resets by design — Fast Refresh needs a Babel/SWC transform plus
|
||||||
|
* a runtime and is out of scope. `unmountIslands` clears each element from the
|
||||||
|
* root registry, so the following `mountIslands` is not short-circuited by the
|
||||||
|
* already-mounted guard.
|
||||||
|
*/
|
||||||
|
export async function remountIslands(root: ParentNode, options: MountOptions): Promise<void> {
|
||||||
|
unmountIslands(root);
|
||||||
|
await mountIslands(root, options);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user