feat(islands): serve the island runtime in dev, prod, and static builds

Adds /__wrnexus/islands.js (the bootstrap) and the /__wrnexus/island/
prefix (mount runtime, island bundles, shared chunks) to all three
serving paths.

Dev reuses the browserArtifactPaths registry pattern from pipeline.ts.
Prod mirrors the clientModulesDir handler, including its filename
allowlist, so island names cannot escape the output directory.

The bootstrap is inert without a data-wrn-island marker, so island-free
pages still download nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 15:28:10 +05:30
co-authored by Claude Opus 5
parent 06df9d66ae
commit a184f1a3be
10 changed files with 135 additions and 12 deletions
+2 -1
View File
@@ -4,7 +4,8 @@ import { Component, type ErrorInfo, type ReactNode } from "react";
export interface IslandErrorBoundaryProps {
name: string;
development: boolean;
children: ReactNode;
/** Optional so createElement(Boundary, props, child) typechecks. */
children?: ReactNode;
}
interface IslandErrorBoundaryState {
+40
View File
@@ -0,0 +1,40 @@
/**
* The island bootstrap served at `/__wrnexus/islands.js`.
*
* Mirrors the `@wrnexus/csr` pattern: this file is only ever fetched when a
* `data-wrn-island` marker is present, so island-free pages download nothing —
* including React.
*/
export function getIslandRuntime(development = false): string {
return `
(function () {
function loader(name) {
return import("/__wrnexus/island/" + encodeURIComponent(name) + ".js");
}
function boot() {
if (!document.querySelector("[data-wrn-island]")) return;
import("/__wrnexus/island/runtime.js").then(function (runtime) {
runtime.mountIslands(document, { loader: loader, development: ${development} });
window.__wrnexusUnmountIslands = function (root) {
runtime.unmountIslands(root || document);
};
window.__wrnexusRemountIslands = function (root) {
return runtime.remountIslands(root || document, {
loader: loader,
development: ${development}
});
};
}).catch(function (error) {
console.error("[wrnexus] failed to load the island runtime", error);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot);
} else {
boot();
}
})();
`;
}
+16 -10
View File
@@ -5,6 +5,12 @@ import { islandRootCount, mountIslands, unmountIslands } from "../src/island-run
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
// happy-dom's element types do not structurally match lib.dom's ParentNode;
// this cast is a test-environment concern, not a runtime one.
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;
@@ -36,7 +42,7 @@ test("mounts an island and passes deserialized props", async () => {
const window = domWith(marker('{"title":"Revenue"}'));
await act(async () => {
await mountIslands(window.document.body, { loader });
await mountIslands(host(window), { loader });
});
expect(window.document.body.textContent).toContain("Revenue");
@@ -47,12 +53,12 @@ test("unmounts roots and leaves no leaked roots behind", async () => {
const window = domWith(marker('{"title":"A"}'));
await act(async () => {
await mountIslands(window.document.body, { loader });
await mountIslands(host(window), { loader });
});
expect(islandRootCount()).toBe(1);
act(() => {
unmountIslands(window.document.body);
unmountIslands(host(window));
});
expect(islandRootCount()).toBe(0);
});
@@ -62,10 +68,10 @@ test("repeated mount/unmount cycles do not accumulate roots", async () => {
for (let i = 0; i < 5; i += 1) {
await act(async () => {
await mountIslands(window.document.body, { loader });
await mountIslands(host(window), { loader });
});
act(() => {
unmountIslands(window.document.body);
unmountIslands(host(window));
});
}
@@ -75,7 +81,7 @@ test("repeated mount/unmount cycles do not accumulate roots", async () => {
test("does nothing when no island markers are present", async () => {
const window = domWith(`<p>plain server html</p>`);
await act(async () => {
await mountIslands(window.document.body, { loader });
await mountIslands(host(window), { loader });
});
expect(islandRootCount()).toBe(0);
});
@@ -84,8 +90,8 @@ test("mounting twice does not create a second root for the same element", async
const window = domWith(marker());
await act(async () => {
await mountIslands(window.document.body, { loader });
await mountIslands(window.document.body, { loader });
await mountIslands(host(window), { loader });
await mountIslands(host(window), { loader });
});
expect(islandRootCount()).toBe(1);
@@ -97,7 +103,7 @@ test("a failing bundle load leaves the placeholder and mounts no root", async ()
console.error = () => {};
await act(async () => {
await mountIslands(window.document.body, {
await mountIslands(host(window), {
loader: async () => {
throw new Error("network down");
},
@@ -115,7 +121,7 @@ test("malformed props JSON falls back to empty props instead of throwing", async
console.error = () => {};
await act(async () => {
await mountIslands(window.document.body, { loader });
await mountIslands(host(window), { loader });
});
console.error = original;
@@ -0,0 +1,25 @@
import { expect, test } from "bun:test";
import { getIslandRuntime } from "../src/runtime-source.ts";
test("emits a runtime that bails out when no island markers exist", () => {
const source = getIslandRuntime(false);
expect(source).toContain("data-wrn-island");
expect(source).toContain("/__wrnexus/island/");
});
test("registers a navigation hook so islands unmount on route change", () => {
expect(getIslandRuntime(false)).toContain("__wrnexusUnmountIslands");
});
test("never references react-dom/server", () => {
expect(getIslandRuntime(true)).not.toContain("react-dom/server");
});
test("threads the development flag into the mount options", () => {
expect(getIslandRuntime(true)).toContain("development: true");
expect(getIslandRuntime(false)).toContain("development: false");
});
test("encodes the island name before using it as a URL path segment", () => {
expect(getIslandRuntime(false)).toContain("encodeURIComponent");
});