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 {