Files
WRNexusJS/packages/cli/src/system.ts
T
ClintchizandClaude Opus 5 17aa3b98eb 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>
2026-08-18 16:16:03 +05:30

81 lines
5.0 KiB
TypeScript

import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
function pascal(value: string): string {
return value
.split(/[^A-Za-z0-9]+/)
.filter(Boolean)
.map((part) => part[0]!.toUpperCase() + part.slice(1))
.join("");
}
function camel(value: string): string {
const name = pascal(value);
return name ? name[0]!.toLowerCase() + name.slice(1) : name;
}
function safe(value: string): string {
const name = value
.trim()
.toLowerCase()
.replace(/^@wrnexus\//, "")
.replace(/[^a-z0-9-]+/g, "-")
.replace(/^-+|-+$/g, "");
if (!name) throw new Error("System name is required");
return name;
}
/** Scaffold the standard package/component/runtime/test layout for a WRNexus system. */
export function generateSystem(rootDir: string, input: string): string[] {
const root = resolve(rootDir);
const name = safe(input);
const directory = join(root, "packages", name);
if (existsSync(directory)) throw new Error(`Package already exists: packages/${name}`);
const className = pascal(name);
const functionName = camel(name);
const files: Record<string, string> = {
"package.json":
JSON.stringify(
{
name: `@wrnexus/${name}`,
version: "0.4.0",
type: "module",
main: "./src/index.ts",
exports: { ".": "./src/index.ts", "./plugin": "./src/plugin.ts" },
files: ["src", "components", "assets", "README.md"],
scripts: {
test: "bun test",
typecheck: "tsc --noEmit",
check: "bun run typecheck && bun run test",
},
dependencies: {
"@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*",
},
devDependencies: { "@types/bun": "^1.3.14", typescript: "^6.0.3" },
wrnexus: {
plugin: { plugin: "./src/plugin.ts", export: "default", factory: true },
},
},
null,
2,
) + "\n",
"src/index.ts": `export interface ${className}Options {\n enabled?: boolean;\n}\n\nexport function create${className}(options: ${className}Options = {}) {\n return { enabled: options.enabled !== false };\n}\n`,
"src/plugin.ts": `import { dirname, join } from "node:path";\nimport { fileURLToPath } from "node:url";\nimport { definePlugin } from "@wrnexus/plugin";\n\nconst root = dirname(dirname(fileURLToPath(import.meta.url)));\n\nexport function ${functionName}Plugin() {\n return definePlugin({\n name: "@wrnexus/${name}",\n version: "0.4.0",\n componentDirs: [join(root, "components")],\n clientRuntimes: [\n {\n id: "${name}",\n entry: join(root, "assets", "client", "runtime.js"),\n type: "script",\n load: "defer",\n singleton: true,\n bundle: false,\n },\n ],\n styleSources: [{ id: "${name}-components", source: join(root, "components") }],\n });\n}\n\nexport default ${functionName}Plugin;\n`,
[`components/${className}.wrn`]: `component ${className} {\n props {\n class = ""\n color = "primary"\n size = "normal"\n }\n\n view {\n <div\n {...attrs}\n data-wrnexus-runtime="${name}"\n class='{class}'\n >\n ${className}\n </div>\n }\n}\n`,
"assets/client/runtime.js": `(function () {\n var runtimeId = ${JSON.stringify(name)};\n\n function mount(root) {\n (root || document)\n .querySelectorAll('[data-wrnexus-runtime="${name}"]')\n .forEach(function (node) {\n if (node.dataset.wrnexusMounted === runtimeId) return;\n node.dataset.wrnexusMounted = runtimeId;\n });\n }\n\n function unmount(root) {\n (root || document)\n .querySelectorAll('[data-wrnexus-runtime="${name}"]')\n .forEach(function (node) {\n if (node.dataset.wrnexusMounted === runtimeId) delete node.dataset.wrnexusMounted;\n });\n }\n\n window.__wrnexusRuntimes = window.__wrnexusRuntimes || {};\n window.__wrnexusRuntimes[runtimeId] = { mount: mount, unmount: unmount };\n\n if (document.readyState === "loading") {\n document.addEventListener("DOMContentLoaded", function () { mount(document); }, { once: true });\n } else {\n mount(document);\n }\n})();\n`,
"test/system.test.ts": `import { expect, test } from "bun:test";\nimport { create${className} } from "../src/index.ts";\n\ntest("${name} system initializes", () => {\n expect(create${className}().enabled).toBe(true);\n});\n`,
"README.md": `# @wrnexus/${name}\n\nFramework-native WRNexusJS system package generated by \`wrnexus generate system ${name}\`.\n\nThe component directory and browser runtime are discovered automatically when the package is present in an application's dependencies. No public asset copy or manual script tag is required.\n`,
};
const written: string[] = [];
for (const [file, content] of Object.entries(files)) {
const target = join(directory, file);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, content, "utf8");
written.push(target);
}
return written;
}