feat(islands): wire islands end to end
The island pieces existed but nothing connected .wrn compilation to island
emission. Now:
- codegen emits a data-wrn-island placeholder for component tags bound to
.tsx imports, keeping .wrn components on the normal mount path
- the dev pipeline and static build resolve island imports, thread the
names into codegen, and build the bundles
- collectScripts adds /__wrnexus/islands.js only when island markup is
present, so island-free pages still ship nothing
- island routes classify as static-interactive via hasIslands
Three bugs found by driving a real page in the browser:
1. The mount runtime was never built anywhere, so the bootstrap 404'd and
no island mounted.
2. Building the runtime separately from the islands gave each its own copy
of React: "Cannot read properties of null (reading 'useState')". The
runtime is now an entrypoint of the same build so React stays in one
shared chunk. The existing single-React test only compared bundles
within one build and could not see across build boundaries.
3. Island props arrived as attribute strings, so start={3} was "3" and
incrementing produced "31" then "311". Props now follow JSX semantics:
{…} parses as JSON, quoted values stay strings, and a runtime
expression is a WRN-ISLAND-PROPS build error rather than a silent
wrong value.
island-codegen.ts no longer imports @wrnexus/core. Compiler modules are
bundled into the Node-only VS Code extension, which contains no other
packages, so a runtime import of core broke the editor compiler; the two
helpers are implemented locally and the core dependency is dropped again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,16 @@ export function assertReactAvailable(
|
||||
export async function buildIslands(input: {
|
||||
islands: IslandInput[];
|
||||
outDir: string;
|
||||
/**
|
||||
* App root used to resolve the island mount runtime. When given, the runtime
|
||||
* is emitted as `runtime.js` in the SAME build as the islands.
|
||||
*
|
||||
* This is not a convenience: building the runtime separately gives it its own
|
||||
* copy of React, and a component rendered by one copy while importing hooks
|
||||
* from another fails with "Cannot read properties of null (reading
|
||||
* 'useState')". One build with splitting keeps React in a single shared chunk.
|
||||
*/
|
||||
appRoot?: string;
|
||||
}): Promise<IslandBuildResult> {
|
||||
if (input.islands.length === 0) return { assets: [], sharedChunks: [] };
|
||||
|
||||
@@ -100,9 +110,22 @@ export async function buildIslands(input: {
|
||||
writeFileSync(join(entryDir, `${island.name}.tsx`), generateIslandEntry(island), "utf8");
|
||||
}
|
||||
|
||||
const entrypoints = input.islands.map((island) => join(entryDir, `${island.name}.tsx`));
|
||||
if (input.appRoot) {
|
||||
const resolveFrom = createRequire(join(input.appRoot, "package.json"));
|
||||
const runtimeEntry = join(entryDir, "runtime.ts");
|
||||
writeFileSync(
|
||||
runtimeEntry,
|
||||
`export * from ${JSON.stringify(resolveFrom.resolve("@wrnexus/react/browser"))};
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
entrypoints.push(runtimeEntry);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await Bun.build({
|
||||
entrypoints: input.islands.map((island) => join(entryDir, `${island.name}.tsx`)),
|
||||
entrypoints,
|
||||
outdir: input.outDir,
|
||||
target: "browser",
|
||||
format: "esm",
|
||||
@@ -123,6 +146,7 @@ export async function buildIslands(input: {
|
||||
// Bun names an entry's output after its entry file, so the basename
|
||||
// identifies the island unambiguously.
|
||||
const stem = basename(output.path).replace(/\.js$/, "");
|
||||
if (stem === "runtime") continue;
|
||||
if (!islandNames.has(stem)) continue;
|
||||
assets.push({
|
||||
name: stem,
|
||||
|
||||
Reference in New Issue
Block a user