81 lines
5.0 KiB
TypeScript
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": "latest", typescript: "^5.9.2" },
|
|
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;
|
|
}
|