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>
209 lines
6.7 KiB
TypeScript
209 lines
6.7 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { format } from "prettier";
|
|
import { scaffoldApp, scaffoldFrameworkRange } from "../src/create.ts";
|
|
import { currentCliVersion } from "../src/update-notifier.ts";
|
|
|
|
test("scaffoldApp creates a comprehensive .gitignore", () => {
|
|
const parent = mkdtempSync(join(tmpdir(), "wrnexus-create-"));
|
|
const root = join(parent, "demo-app");
|
|
|
|
try {
|
|
scaffoldApp(root, "demo-app");
|
|
|
|
const file = join(root, ".gitignore");
|
|
expect(existsSync(file)).toBe(true);
|
|
const contents = readFileSync(file, "utf8");
|
|
for (const entry of [
|
|
"node_modules/",
|
|
"dist/",
|
|
".wrnexus/",
|
|
".env.*",
|
|
"!.env.example",
|
|
"!.env.*.example",
|
|
"*.log",
|
|
"*.db",
|
|
"uploads/",
|
|
"coverage/",
|
|
"mobile/android/",
|
|
".vscode/",
|
|
"*.tsbuildinfo",
|
|
]) {
|
|
expect(contents).toContain(entry);
|
|
}
|
|
} finally {
|
|
rmSync(parent, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("scaffoldApp includes production build and start scripts", () => {
|
|
const parent = mkdtempSync(join(tmpdir(), "wrnexus-create-"));
|
|
const root = join(parent, "production-app");
|
|
|
|
try {
|
|
scaffoldApp(root, "production-app");
|
|
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
|
|
expect(pkg.scripts.build).toBe("wrnexus build .");
|
|
expect(pkg.scripts.start).toBe("bun dist/server.js");
|
|
expect(pkg.scripts.production).toBe("bun run build && bun run start");
|
|
expect(pkg.scripts.typecheck).toBe("tsc --noEmit");
|
|
expect(pkg.scripts.test).toBe("wrnexus test .");
|
|
expect(pkg.scripts.check).toBe(
|
|
"bun run typecheck && bun run lint && bun run test && bun run format:check",
|
|
);
|
|
} finally {
|
|
rmSync(parent, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("scaffoldApp includes the complete v0.8 configuration and starter structure", () => {
|
|
const parent = mkdtempSync(join(tmpdir(), "wrnexus-create-"));
|
|
const root = join(parent, "complete-app");
|
|
|
|
try {
|
|
scaffoldApp(root, "complete-app");
|
|
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
const config = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
|
|
const documentLayout = readFileSync(join(root, "app/layouts/document.wrn"), "utf8");
|
|
|
|
expect(documentLayout).toContain('runtime = "server"');
|
|
expect(documentLayout).not.toContain("cookies =");
|
|
|
|
for (const packageName of [
|
|
"@wrnexus/auth",
|
|
"@wrnexus/captcha",
|
|
"@wrnexus/db",
|
|
"@wrnexus/encryption",
|
|
"@wrnexus/i18n",
|
|
"@wrnexus/image",
|
|
"@wrnexus/jwt",
|
|
"@wrnexus/observability",
|
|
"@wrnexus/realtime",
|
|
"@wrnexus/security",
|
|
"@wrnexus/store",
|
|
"@wrnexus/ui",
|
|
"@wrnexus/uploader",
|
|
"@wrnexus/validation",
|
|
]) {
|
|
expect(pkg.dependencies[packageName]).toBe(scaffoldFrameworkRange);
|
|
}
|
|
|
|
for (const block of [
|
|
"plugins:",
|
|
"imports:",
|
|
"types:",
|
|
"stores:",
|
|
"compatibility:",
|
|
"performance:",
|
|
"observability:",
|
|
"tenancy:",
|
|
"build:",
|
|
"navigation:",
|
|
"devToolbar:",
|
|
"theme:",
|
|
"i18n:",
|
|
"db:",
|
|
"databases:",
|
|
"storage:",
|
|
"realtime:",
|
|
"profiles:",
|
|
]) {
|
|
expect(config).toContain(block);
|
|
}
|
|
|
|
for (const relative of [
|
|
".env.example",
|
|
".env.test.example",
|
|
"app/locales/en.json",
|
|
"app/db/migrations/0001_init.sql",
|
|
"app/db/seed.ts",
|
|
"app/schemas/contact.ts",
|
|
"app/realtime/chat.ts",
|
|
"test/smoke.test.ts",
|
|
]) {
|
|
expect(existsSync(join(root, relative))).toBe(true);
|
|
}
|
|
} finally {
|
|
rmSync(parent, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("scaffoldApp uses compatible framework packages and pins the current CLI", () => {
|
|
const parent = mkdtempSync(join(tmpdir(), "wrnexus-create-"));
|
|
const root = join(parent, "current-app");
|
|
|
|
try {
|
|
scaffoldApp(root, "current-app");
|
|
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
const version = currentCliVersion();
|
|
|
|
expect(pkg.wrnexus.version).toBe(version);
|
|
expect(pkg.dependencies["@wrnexus/helpers"]).toBe(scaffoldFrameworkRange);
|
|
for (const dependency of Object.values(pkg.dependencies) as string[]) {
|
|
expect(dependency).toBe(scaffoldFrameworkRange);
|
|
}
|
|
expect(pkg.devDependencies["@wrnexus/cli"]).toBe(version);
|
|
expect(pkg.devDependencies["@iconify/tailwind4"]).toBe("^1.2.3");
|
|
expect(pkg.devDependencies["@iconify-json/lucide"]).toBe("^1.2.123");
|
|
const globalCss = readFileSync(join(root, "app", "styles", "global.css"), "utf8");
|
|
expect(globalCss).toContain('@plugin "@iconify/tailwind4";');
|
|
expect(globalCss).toContain('"Plus Jakarta Sans"');
|
|
expect(globalCss).toContain("--wrn-font-sans");
|
|
const appConfig = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
|
|
expect(appConfig).toContain('family: "Plus Jakarta Sans"');
|
|
expect(appConfig).toContain("weights: [400, 500, 600, 700]");
|
|
expect(existsSync(join(root, "public", "llms.txt"))).toBe(true);
|
|
expect(existsSync(join(root, "llms.txt"))).toBe(false);
|
|
expect(readFileSync(join(root, ".prettierignore"), "utf8")).toContain("CLAUDE.md");
|
|
expect(readFileSync(join(root, ".vscode", "settings.json"), "utf8")).toContain(
|
|
'"editor.formatOnSave": true',
|
|
);
|
|
expect(readFileSync(join(root, ".vscode", "settings.json"), "utf8")).toContain(
|
|
'"editor.defaultFormatter": "wrnexus.wrnexus"',
|
|
);
|
|
expect(readFileSync(join(root, ".vscode", "extensions.json"), "utf8")).toContain(
|
|
"wrnexus.wrnexus",
|
|
);
|
|
expect(readFileSync(join(root, "app", "pages", "index.wrn"), "utf8")).toContain(
|
|
'href="/api/hello"',
|
|
);
|
|
expect(existsSync(join(root, "app", "pages", "about.wrn"))).toBe(true);
|
|
} finally {
|
|
rmSync(parent, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("scaffoldApp emits Prettier-compliant editor and framework files", async () => {
|
|
const parent = mkdtempSync(join(tmpdir(), "wrnexus-create-"));
|
|
const root = join(parent, "formatted-app");
|
|
|
|
try {
|
|
scaffoldApp(root, "formatted-app");
|
|
for (const relative of [
|
|
"app/styles/global.css",
|
|
"eslint.config.js",
|
|
".vscode/settings.json",
|
|
".vscode/extensions.json",
|
|
]) {
|
|
const file = join(root, relative);
|
|
const source = readFileSync(file, "utf8");
|
|
const formatted = await format(source, {
|
|
filepath: file,
|
|
printWidth: 100,
|
|
tabWidth: 2,
|
|
useTabs: false,
|
|
semi: true,
|
|
singleQuote: false,
|
|
trailingComma: "all",
|
|
endOfLine: "lf",
|
|
});
|
|
expect(formatted).toBe(source);
|
|
}
|
|
} finally {
|
|
rmSync(parent, { recursive: true, force: true });
|
|
}
|
|
});
|