From 01a3b3b4e9182b476d74c5397bb6870f119b2e02 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Tue, 18 Aug 2026 15:30:19 +0530 Subject: [PATCH] feat(compiler): classify island routes as static-interactive A route mounting an island ships JavaScript, so reporting it as zero-JS static would make the framework's performance accounting wrong. Adds a separate needsIslandRuntime flag rather than reusing needsClientRuntime: an island needs the island runtime, not WRNexus's reactive runtime, and conflating them would ship the wrong bundle. analyzeRuntimeRequirements takes island presence as an optional second argument, so existing callers are unaffected. Co-Authored-By: Claude Opus 5 --- packages/compiler/src/analysis.ts | 23 +++++++- .../test/island-classification.test.ts | 56 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/island-classification.test.ts diff --git a/packages/compiler/src/analysis.ts b/packages/compiler/src/analysis.ts index da0a4f16..0401d3f5 100644 --- a/packages/compiler/src/analysis.ts +++ b/packages/compiler/src/analysis.ts @@ -1,4 +1,5 @@ import type { PageAst, ViewNode } from "@wrnexus/syntax"; +import type { ResolvedImport } from "./import-resolver.ts"; export type RouteExecutionKind = | "static" @@ -12,6 +13,8 @@ export interface RuntimeRequirements { kind: RouteExecutionKind; canPrerender: boolean; needsClientRuntime: boolean; + /** True when the route mounts a React island and must ship the island runtime. */ + needsIslandRuntime: boolean; needsServerRuntime: boolean; hydrationStrategy: string | null; reasons: string[]; @@ -210,8 +213,21 @@ function hasEvent(nodes: ViewNode[]): boolean { return false; } -export function analyzeRuntimeRequirements(ast: PageAst): RuntimeRequirements { +/** + * A route containing a React island ships JavaScript and can no longer be + * classified as zero-JS static, so island presence must reach the classifier. + */ +export function routeNeedsIslands(imports: ResolvedImport[]): boolean { + return imports.some((entry) => entry.kind === "island"); +} + +export function analyzeRuntimeRequirements( + ast: PageAst, + options: { hasIslands?: boolean } = {}, +): RuntimeRequirements { + const hasIslands = options.hasIslands ?? false; const reasons: string[] = []; + if (hasIslands) reasons.push("react island"); const clientFunctions = ast.runtimeFunctions.some((fn) => fn.runtime !== "server"); const clientState = ast.states.some((state) => state.runtime !== "server"); const interactive = @@ -260,12 +276,17 @@ export function analyzeRuntimeRequirements(ast: PageAst): RuntimeRequirements { reasons.push("partial-static shell with streamed dynamic regions"); } + // An island ships JavaScript, so a would-be zero-JS static route must be + // reported as static-interactive. Explicit render modes still win above. + if (hasIslands && kind === "static") kind = "static-interactive"; + const clientDisabled = ast.renderMode === "static" || ast.renderMode === "server"; const serverDisabled = ast.renderMode === "client"; return { kind, canPrerender: kind === "static" || kind === "static-interactive", + needsIslandRuntime: hasIslands, needsClientRuntime: !clientDisabled && (interactive || ast.renderMode === "client") && diff --git a/packages/compiler/test/island-classification.test.ts b/packages/compiler/test/island-classification.test.ts new file mode 100644 index 00000000..868b4b90 --- /dev/null +++ b/packages/compiler/test/island-classification.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from "bun:test"; +import { parse } from "@wrnexus/syntax"; +import { analyzeRuntimeRequirements, routeNeedsIslands } from "../src/analysis.ts"; + +test("a route with an island import needs client JavaScript", () => { + expect( + routeNeedsIslands([ + { declaration: { source: "./a" } as any, resolved: "/app/a.ts" }, + { declaration: { source: "./Chart" } as any, resolved: "/app/Chart.tsx", kind: "island" }, + ]), + ).toBe(true); +}); + +test("a route with no island imports stays zero-JS", () => { + expect(routeNeedsIslands([{ declaration: { source: "./a" } as any, resolved: "/app/a.ts" }])).toBe( + false, + ); +}); + +test("an empty import list stays zero-JS", () => { + expect(routeNeedsIslands([])).toBe(false); +}); + +test("an unresolved import does not count as an island", () => { + expect( + routeNeedsIslands([ + { + declaration: { source: "./missing" } as any, + diagnostic: { code: "WRN-IMPORT-NOT-FOUND", message: "nope", severity: "warning" }, + }, + ]), + ).toBe(false); +}); + +test("an island promotes a static route to static-interactive", () => { + const source = `page Home { view {
hello
} }`; + const ast = parse(source); + + const plain = analyzeRuntimeRequirements(ast); + expect(plain.kind).toBe("static"); + expect(plain.needsIslandRuntime).toBe(false); + + const withIsland = analyzeRuntimeRequirements(ast, { hasIslands: true }); + expect(withIsland.kind).toBe("static-interactive"); + expect(withIsland.needsIslandRuntime).toBe(true); + expect(withIsland.reasons).toContain("react island"); +}); + +test("an island does not turn on the WRNexus reactive runtime", () => { + const ast = parse(`page Home { view {
hello
} }`); + const withIsland = analyzeRuntimeRequirements(ast, { hasIslands: true }); + + // Islands ship the island runtime, not WRNexus's own client runtime. + expect(withIsland.needsClientRuntime).toBe(false); + expect(withIsland.needsIslandRuntime).toBe(true); +});