From 843db2815f77bd4b03a1bbb46ffe3f9006bd1a25 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Tue, 18 Aug 2026 16:26:58 +0530 Subject: [PATCH] fix(islands): rebuild on .tsx edits and support islands inside components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs found by driving the dev server rather than reading code: 1. An island used inside a .wrn component still emitted a component mount — only the page and nested-page render paths were covered. 2. Editing an island .tsx never rebuilt in dev. The bundle cache was keyed on source path alone, and page modules are cached after the first request so no compile runs to notice the change. The cache key now includes mtime, and the file watcher rebuilds islands whose .tsx changed. 3. A .wrn cache hit skipped island building entirely, so after a restart with a warm cache no island bundle was ever produced. Island inputs are now persisted beside the other artifacts and rebuilt on a cache hit. The islands manifest is deliberately excluded from the artifact completeness check: only the async compile path writes it, so requiring it made the sync path miss the cache on every call. Co-Authored-By: Claude Opus 5 --- editors/vscode/src/compiler.cjs | 6 +- editors/vscode/src/extension.bundle.cjs | 2 +- packages/compiler/src/codegen.ts | 4 ++ packages/compiler/test/island-emit.test.ts | 9 +++ packages/dev-server/src/index.ts | 13 ++++ packages/dev-server/src/pipeline.ts | 69 ++++++++++++++++++++-- 6 files changed, 97 insertions(+), 6 deletions(-) diff --git a/editors/vscode/src/compiler.cjs b/editors/vscode/src/compiler.cjs index 3c71ba67..befc7ac7 100644 --- a/editors/vscode/src/compiler.cjs +++ b/editors/vscode/src/compiler.cjs @@ -1,6 +1,6 @@ "use strict"; // Generated by scripts/build-editor-compiler.mjs. Do not edit directly. -// WRN editor compiler source hash: 631914b0f75e27a95908eb3252604e0948abf27b50533ab01f0dd12b532a1020 +// WRN editor compiler source hash: fd183ab8c54df72c779d099d7625ce0068e49bea458052335c77cbf31ccf9179 // WRN editor compiler generator hash: a54ca847c758bc98d8e353ad6d70088df31de1820f6cf9d1c3462505f563e6b8 // Generated with TypeScript: 6.0.3 const __nodeRequire = require; @@ -1385,6 +1385,10 @@ function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindin return `
${inner}
`; } function renderNestedComponentInvocation(node, ctx) { + // Islands work inside .wrn components too, not just pages. + const island = islandMarkerFor(node); + if (island) + return island; let bindIndex = 0; const attrs = node.attrs .filter((attr) => attr.name !== "data-component") diff --git a/editors/vscode/src/extension.bundle.cjs b/editors/vscode/src/extension.bundle.cjs index cc870af8..48fa082d 100644 --- a/editors/vscode/src/extension.bundle.cjs +++ b/editors/vscode/src/extension.bundle.cjs @@ -1,4 +1,4 @@ -// WRN editor extension source hash: 67bc974ee8e647ebce3dfc0ceaff2a1be56384fc16ed0334b89ab32d6bd590a8 +// WRN editor extension source hash: c9938fa5f643ca435ead2c8dd5b545c6906c37c0024cf3ea788666236c28ddb6 // WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728 "use strict"; var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); diff --git a/packages/compiler/src/codegen.ts b/packages/compiler/src/codegen.ts index 8cd4a543..464ba1cb 100644 --- a/packages/compiler/src/codegen.ts +++ b/packages/compiler/src/codegen.ts @@ -773,6 +773,10 @@ function renderNestedComponentInvocation( node: Extract, ctx: CompCtx, ): string { + // Islands work inside .wrn components too, not just pages. + const island = islandMarkerFor(node); + if (island) return island; + let bindIndex = 0; const attrs = node.attrs diff --git a/packages/compiler/test/island-emit.test.ts b/packages/compiler/test/island-emit.test.ts index 4d1fd997..2902af57 100644 --- a/packages/compiler/test/island-emit.test.ts +++ b/packages/compiler/test/island-emit.test.ts @@ -51,3 +51,12 @@ test("a runtime expression prop fails the build with WRN-ISLAND-PROPS", () => { /WRN-ISLAND-PROPS/, ); }); + +test("an island inside a .wrn component also emits a marker", () => { + const source = `component Panel { + view { } +}`; + const out = generate(parse(source), { islands: new Set(["Chart"]) }); + expect(out).toContain("data-wrn-island="); + expect(out).not.toContain('data-component="Chart"'); +}); diff --git a/packages/dev-server/src/index.ts b/packages/dev-server/src/index.ts index 4c4223cc..dc8ef39c 100644 --- a/packages/dev-server/src/index.ts +++ b/packages/dev-server/src/index.ts @@ -43,6 +43,7 @@ import { loadWrnServerModule, setCompileCacheDir, setCompileImportOptions, + rebuildChangedIslands, setDevCompilerPipeline, wrnBrowserArtifactUrlAsync, } from "./pipeline.ts"; @@ -685,6 +686,18 @@ export async function startServer(opts: ServeOptions): Promise { console.log(`[wrnexus] hot update — ${files.join(", ")}`); await pluginRunner.hook("hmrUpdate", files); + // Island .tsx sources are not .wrn files, so nothing below would rebuild + // them; page modules are cached, so no compile runs on the next request. + const islandFiles = files + .map((changed) => (isAbsolute(changed) ? changed : resolve(appDir, changed))) + .filter((changed) => changed.endsWith(".tsx")); + if (islandFiles.length > 0) { + try { + await rebuildChangedIslands(islandFiles); + } catch (error) { + console.warn("[wrnexus] island rebuild failed", error); + } + } const storeUpdates: Array<{ name: string; url: string; kind: string }> = []; for (const changed of files) { const absolute = isAbsolute(changed) ? changed : resolve(appDir, changed); diff --git a/packages/dev-server/src/pipeline.ts b/packages/dev-server/src/pipeline.ts index a74fb2e0..829a2818 100644 --- a/packages/dev-server/src/pipeline.ts +++ b/packages/dev-server/src/pipeline.ts @@ -130,6 +130,9 @@ function importOptionsHash(file: string): string { /** Island bundles already built this dev session, keyed by resolved source. */ const builtIslands = new Map(); +/** Island name -> resolved .tsx source, so the watcher can rebuild on edit. */ +const islandSources = new Map(); +let islandAppRoot: string | null = null; let islandRuntimeBuilt = false; /** @@ -143,8 +146,21 @@ async function ensureIslandArtifacts( ): Promise { if (islands.length === 0) return; const outDir = join(appRoot, ".wrnexus", "island"); + islandAppRoot = appRoot; + for (const island of islands) islandSources.set(island.name, island.sourcePath); - const pending = islands.filter((island) => builtIslands.get(island.name) !== island.sourcePath); + // Keyed by source AND mtime: keying on the path alone serves a stale bundle + // forever once an island .tsx is edited. + const stamp = (island: { name: string; sourcePath: string }) => { + let mtime: number; + try { + mtime = statSync(island.sourcePath).mtimeMs; + } catch { + mtime = 0; + } + return `${island.sourcePath}:${mtime}`; + }; + const pending = islands.filter((island) => builtIslands.get(island.name) !== stamp(island)); if (pending.length === 0 && islandRuntimeBuilt) return; // The runtime is built alongside the islands so they share one React copy. @@ -157,8 +173,8 @@ async function ensureIslandArtifacts( islandRuntimeBuilt = true; for (const asset of result.assets) { registerIslandArtifact(`/__wrnexus/island/${asset.name}.js`, asset.path); - const source = pending.find((island) => island.name === asset.name)?.sourcePath; - if (source) builtIslands.set(asset.name, source); + const rebuilt = pending.find((island) => island.name === asset.name); + if (rebuilt) builtIslands.set(asset.name, stamp(rebuilt)); } for (const chunk of result.sharedChunks) { registerIslandArtifact(`/__wrnexus/island/${basename(chunk)}`, chunk); @@ -169,6 +185,23 @@ async function ensureIslandArtifacts( * Island imports in this file: names so codegen emits placeholders instead of * component mounts, and sources so the bundles can be built. */ +/** + * Rebuilds islands whose .tsx source changed. + * + * Page modules are cached after the first request, so no compile runs on a + * later request and nothing else would notice an island edit. + */ +export async function rebuildChangedIslands(changed: string[]): Promise { + if (!islandAppRoot) return false; + const touched = new Set(changed.map((file) => resolve(file))); + const affected = [...islandSources] + .filter(([, sourcePath]) => touched.has(resolve(sourcePath))) + .map(([name, sourcePath]) => ({ name, sourcePath })); + if (affected.length === 0) return false; + await ensureIslandArtifacts(affected, islandAppRoot); + return true; +} + function islandsForFile( ast: PageAst, importer: string, @@ -461,6 +494,8 @@ export interface WrnCompileArtifacts { declarations: string; contract: string; rpc: string; + /** Island inputs for this file, so a cache hit can still build islands. */ + islands: string; } export interface WrnCompileMetrics { @@ -551,11 +586,14 @@ export function compileWrnArtifactsAsync(file: string, version = 0): Promise statSync(path).isFile())) { + // The islands manifest is written only by the async compile path, so it + // is not part of the completeness check — a missing manifest means "no + // islands known for this file", not a stale cache. + const requiredArtifacts = Object.entries(artifacts) + .filter(([key]) => key !== "islands") + .map(([, path]) => path); + if (requiredArtifacts.every((path) => statSync(path).isFile())) { compileMetrics.hits++; browserArtifactPaths.set(`/__wrnexus/client/${stem}.mjs`, artifacts.browser); + // A cached .wrn still needs its island bundles: the .tsx may have changed + // since, and after a restart with a warm cache nothing else would build them. + let cachedIslands: Array<{ name: string; sourcePath: string }> = []; + try { + cachedIslands = JSON.parse(readFileSync(artifacts.islands, "utf8")) as Array<{ + name: string; + sourcePath: string; + }>; + } catch { + cachedIslands = []; + } + // This variant is synchronous, so the rebuild is kicked off rather than + // awaited. The async compile path awaits it before serving a page. + void ensureIslandArtifacts(cachedIslands, projectRootForFile(file)).catch((error) => { + console.warn("[wrnexus] island rebuild failed", error); + }); return artifacts; } } catch {