Files
WRNexusJS/packages/react/test/island-hmr.test.ts
T
ClintchizandClaude Opus 5 17aa3b98eb feat(islands): wire islands end to end
The island pieces existed but nothing connected .wrn compilation to island
emission. Now:

- codegen emits a data-wrn-island placeholder for component tags bound to
  .tsx imports, keeping .wrn components on the normal mount path
- the dev pipeline and static build resolve island imports, thread the
  names into codegen, and build the bundles
- collectScripts adds /__wrnexus/islands.js only when island markup is
  present, so island-free pages still ship nothing
- island routes classify as static-interactive via hasIslands

Three bugs found by driving a real page in the browser:

1. The mount runtime was never built anywhere, so the bootstrap 404'd and
   no island mounted.
2. Building the runtime separately from the islands gave each its own copy
   of React: "Cannot read properties of null (reading 'useState')". The
   runtime is now an entrypoint of the same build so React stays in one
   shared chunk. The existing single-React test only compared bundles
   within one build and could not see across build boundaries.
3. Island props arrived as attribute strings, so start={3} was "3" and
   incrementing produced "31" then "311". Props now follow JSX semantics:
   {…} parses as JSON, quoted values stay strings, and a runtime
   expression is a WRN-ISLAND-PROPS build error rather than a silent
   wrong value.

island-codegen.ts no longer imports @wrnexus/core. Compiler modules are
bundled into the Node-only VS Code extension, which contains no other
packages, so a runtime import of core broke the editor compiler; the two
helpers are implemented locally and the core dependency is dropped again.

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

73 lines
2.0 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);
});