test(islands): guard zero-JS routes and single-React bundling
Two guards protect the core promise: a route with no islands emits no assets at all, and a page with several islands keeps React in one shared chunk. buildIslands now writes a generated entry per island instead of passing component sources directly. Two islands sharing a source deduped to a single entrypoint, and output order is not guaranteed to match input order, so island names could bind to the wrong bundle. Island modules are excluded from the editor compiler bundle: it globs packages/compiler/src, and island-bundle.ts calls Bun.build while island-codegen.ts imports @wrnexus/core — neither belongs in a Node-only VS Code artifact. Integration assertions share one build. bun test interferes with Bun.build's module reads after several build calls in one process, while the same calls succeed repeatedly outside the runner; production is unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { afterAll, beforeAll, expect, test } from "bun:test";
|
||||
import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { routeNeedsIslands } from "../src/analysis.ts";
|
||||
import { buildIslands, type IslandBuildResult } from "../src/island-bundle.ts";
|
||||
|
||||
const COUNTER = resolve(import.meta.dir, "../../../examples/basic-app/app/islands/Counter.tsx");
|
||||
|
||||
const created: string[] = [];
|
||||
afterAll(() => {
|
||||
for (const dir of created) rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function outDir(label: string): string {
|
||||
const dir = mkdtempSync(join(process.cwd(), `.island-int-${label}-`));
|
||||
created.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
// Every assertion that needs a real bundle shares this one build.
|
||||
//
|
||||
// Not just for speed: `bun test` interferes with Bun.build's module reads once
|
||||
// several build calls have run across test files in the same process, while the
|
||||
// same calls succeed repeatedly outside the runner. Production is unaffected —
|
||||
// the dev server's rebuild loop was verified separately — but tests must keep
|
||||
// their build count low to stay reliable in the full suite.
|
||||
let dir: string;
|
||||
let result: IslandBuildResult;
|
||||
let bundles: string[];
|
||||
|
||||
beforeAll(async () => {
|
||||
dir = outDir("shared");
|
||||
result = await buildIslands({
|
||||
islands: [
|
||||
{ name: "CounterA", sourcePath: COUNTER },
|
||||
{ name: "CounterB", sourcePath: COUNTER },
|
||||
],
|
||||
outDir: dir,
|
||||
});
|
||||
bundles = readdirSync(dir)
|
||||
.filter((file) => file.endsWith(".js"))
|
||||
.map((file) => readFileSync(join(dir, file), "utf8"));
|
||||
});
|
||||
|
||||
test("a route with no islands ships zero framework JavaScript", async () => {
|
||||
const empty = outDir("nojs");
|
||||
const none = await buildIslands({ islands: [], outDir: empty });
|
||||
|
||||
expect(none.assets).toHaveLength(0);
|
||||
expect(none.sharedChunks).toHaveLength(0);
|
||||
expect(readdirSync(empty)).toHaveLength(0);
|
||||
expect(routeNeedsIslands([])).toBe(false);
|
||||
});
|
||||
|
||||
test("a page with multiple islands ships React exactly once", () => {
|
||||
// React's internals must appear in at most one emitted file — the shared
|
||||
// chunk. If splitting regresses, every island inlines its own copy.
|
||||
const withReactInternals = bundles.filter(
|
||||
(source) => source.includes("REACT_ELEMENT_TYPE") || source.includes("react.development"),
|
||||
);
|
||||
|
||||
expect(result.assets).toHaveLength(2);
|
||||
expect(withReactInternals.length).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("two islands sharing one source get distinct, correctly named assets", () => {
|
||||
// Passing component sources as entrypoints deduped them, so the second island
|
||||
// silently lost its bundle and names could bind to the wrong output.
|
||||
expect(result.assets.map((asset) => asset.name).sort()).toEqual(["CounterA", "CounterB"]);
|
||||
expect(new Set(result.assets.map((asset) => asset.path)).size).toBe(2);
|
||||
});
|
||||
|
||||
test("a real island builds against React and never pulls in the WRNexus renderer", () => {
|
||||
const built = bundles.join("\n");
|
||||
|
||||
expect(built).not.toContain("wrnexus");
|
||||
expect(built).not.toContain("react-dom/server");
|
||||
expect(built).toContain("useState");
|
||||
});
|
||||
|
||||
test("the generated entry directory is not left behind in the output", () => {
|
||||
expect(readdirSync(dir)).not.toContain(".entries");
|
||||
});
|
||||
Reference in New Issue
Block a user