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:
@@ -25,6 +25,7 @@ import {
|
||||
type ResolvedTheme,
|
||||
type StylesConfig,
|
||||
} from "@wrnexus/styles";
|
||||
import { getIslandRuntime } from "@wrnexus/react/runtime";
|
||||
import { VALIDATE_RUNTIME } from "@wrnexus/validation";
|
||||
import { I18N_RUNTIME } from "@wrnexus/i18n";
|
||||
import { UPLOAD_RUNTIME, UPLOAD_JS_HREF, UPLOADS_PREFIX, serveStoredFile } from "@wrnexus/uploader";
|
||||
@@ -32,7 +33,7 @@ import type { Mode } from "@wrnexus/core";
|
||||
import type { AssetServer } from "./runtime.ts";
|
||||
import { servePublicAsset } from "./public.ts";
|
||||
import { servePluginAsset, type ServedPluginAsset } from "./plugin-assets.ts";
|
||||
import { serveWrnBrowserArtifact } from "./pipeline.ts";
|
||||
import { serveIslandArtifact, serveWrnBrowserArtifact } from "./pipeline.ts";
|
||||
|
||||
/** Style inputs the dev asset server needs to build `/__wrnexus/styles.css`. */
|
||||
export interface DevStyles {
|
||||
@@ -95,6 +96,10 @@ export function createDevAssetServer(
|
||||
if (pathname.startsWith("/__wrnexus/client/")) {
|
||||
return serveWrnBrowserArtifact(pathname) ?? new Response("Not Found", { status: 404 });
|
||||
}
|
||||
if (pathname.startsWith("/__wrnexus/island/")) {
|
||||
return serveIslandArtifact(pathname) ?? new Response("Not Found", { status: 404 });
|
||||
}
|
||||
if (pathname === "/__wrnexus/islands.js") return jsResponse(getIslandRuntime(true));
|
||||
if (pathname === "/__wrnexus/reactive.js") return jsResponse(getReactiveRuntime(true));
|
||||
if (pathname === "/__wrnexus/controllers.js")
|
||||
return jsResponse(getComponentControllerRuntime(true));
|
||||
|
||||
@@ -57,6 +57,7 @@ export function runMiddleware(
|
||||
const moduleCache = new Map<string, Promise<Record<string, unknown>>>();
|
||||
const moduleVersions = new Map<string, number>();
|
||||
const browserArtifactPaths = new Map<string, string>();
|
||||
const islandArtifactPaths = new Map<string, string>();
|
||||
|
||||
type ImportMode = "legacy" | "compatible" | "explicit";
|
||||
interface CompileImportOptions {
|
||||
@@ -644,6 +645,25 @@ export function serveWrnBrowserArtifact(pathname: string): Response | null {
|
||||
});
|
||||
}
|
||||
|
||||
/** Registers a built island asset for serving under `/__wrnexus/island/`. */
|
||||
export function registerIslandArtifact(pathname: string, artifact: string): void {
|
||||
islandArtifactPaths.set(pathname, artifact);
|
||||
}
|
||||
|
||||
/** Serves a built island bundle, chunk, or the island mount runtime. */
|
||||
export function serveIslandArtifact(pathname: string): Response | null {
|
||||
const artifact = islandArtifactPaths.get(pathname);
|
||||
if (!artifact || !existsSync(artifact)) return null;
|
||||
return new Response(readFileSync(artifact, "utf8"), {
|
||||
headers: {
|
||||
"content-type": "text/javascript; charset=utf-8",
|
||||
"cache-control": "no-store, max-age=0",
|
||||
pragma: "no-cache",
|
||||
expires: "0",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Forget one module and force its next dynamic import to bypass Bun's import cache. */
|
||||
export function invalidateModule(file: string): void {
|
||||
file = resolve(file);
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
getNavRuntime,
|
||||
getRealtimeRuntime,
|
||||
} from "@wrnexus/csr";
|
||||
import { getIslandRuntime } from "@wrnexus/react/runtime";
|
||||
import {
|
||||
loadEnv,
|
||||
resolveProfile,
|
||||
@@ -99,6 +100,7 @@ export interface ProdOptions {
|
||||
controllersPath?: string;
|
||||
/** Absolute directory containing bundled per-WRN browser modules. */
|
||||
clientModulesDir?: string;
|
||||
islandsDir?: string;
|
||||
/** Absolute path to the pre-built theme stylesheet (`theme.css`). */
|
||||
themePath?: string;
|
||||
/** Pre-built active theme/accent stylesheets, loaded on demand. */
|
||||
@@ -341,6 +343,15 @@ function createProdAssetServer(opts: ProdOptions): AssetServer {
|
||||
}
|
||||
return serveFile(join(opts.clientModulesDir, name), JS_HEADERS);
|
||||
}
|
||||
if (pathname.startsWith("/__wrnexus/island/")) {
|
||||
const name = pathname.slice("/__wrnexus/island/".length);
|
||||
if (!opts.islandsDir || !/^[A-Za-z0-9._-]+\.js$/.test(name)) {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
return serveFile(join(opts.islandsDir, name), JS_HEADERS);
|
||||
}
|
||||
if (pathname === "/__wrnexus/islands.js")
|
||||
return new Response(getIslandRuntime(), { headers: JS_HEADERS });
|
||||
if (pathname === "/__wrnexus/reactive.js") {
|
||||
if (opts.reactivePath) {
|
||||
const file = Bun.file(opts.reactivePath);
|
||||
|
||||
Reference in New Issue
Block a user