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 = { "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 \n ${className}\n \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; }