UNPKG

@thebase/ui

Version:

CDN-installable Owl and Bootstrap 5 UI component library.

269 lines (224 loc) • 8.98 kB
#!/usr/bin/env node import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { spawnSync } from "node:child_process"; import path from "node:path"; const ownPackageJson = JSON.parse( readFileSync(new URL("../package.json", import.meta.url), "utf8"), ); /** Turns any folder/app name into a valid npm package name (lowercase, dash-separated). */ function toPackageName(name) { return name .trim() .toLowerCase() .replace(/[^a-z0-9-]+/g, "-") .replace(/^-+|-+$/g, "") || "baseui-app"; } /** Builds the fixed set of Vite + BaseUI scaffold files for a project named `appName`. */ function buildTemplateFiles(appName, baseuiVersion) { const title = appName .split(/[-_]/) .filter(Boolean) .map((word) => word[0].toUpperCase() + word.slice(1)) .join(" "); return { "package.json": JSON.stringify( { name: appName, private: true, version: "0.0.1", type: "module", scripts: { dev: "vite", build: "vite build", preview: "vite preview", }, dependencies: { "@thebase/ui": `^${baseuiVersion}`, }, devDependencies: { vite: "^8.2.1", }, }, null, 2, ) + "\n", "vite.config.js": `import { defineConfig } from "vite"; export default defineConfig({ server: { open: true }, }); `, ".gitignore": `node_modules/\ndist/\n`, "index.html": `<!doctype html> <html lang="en" b-theme="light"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>${title}</title> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> </head> <body class="bg-body text-body"> <div id="app"></div> <script type="module" src="/src/main.js"></script> </body> </html> `, "public/favicon.svg": `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <rect width="24" height="24" rx="5" fill="#0f172a" stroke="none" /> <path d="M8 12h8" /> <path d="M12 8v8" /> </svg> `, "src/style.css": `/* App-specific styles layered on top of @thebase/ui/baseui.css go here. */\n`, "src/main.js": `import "@thebase/ui/baseui.css"; import { mount } from "@thebase/ui"; import baseuiTemplates from "@thebase/ui/dist/baseui.templates.xml?raw"; import appTemplates from "./App.xml?raw"; import { App } from "./App.js"; import "./style.css"; /** Both files are full <templates>...</templates> documents; strip each one's outer wrapper * before joining them, otherwise the merged string has two root elements and Owl's XML * parser throws. */ const stripTemplatesWrapper = (xml) => xml.replace(/^\\s*<templates[^>]*>/, "").replace(/<\\/templates>\\s*$/, ""); const templates = \`<templates>\${stripTemplatesWrapper(baseuiTemplates)}\${stripTemplatesWrapper(appTemplates)}</templates>\`; await mount(App, document.getElementById("app"), { templates }); `, "src/App.js": `import { Component, useState } from "@thebase/ui"; import { Button, Card, CardBody, CardHeader, CardTitle } from "@thebase/ui"; /** Root screen of the app: owns top-level state and composes BaseUI components. */ export class App extends Component { static template = "app.Root"; static components = { Button, Card, CardBody, CardHeader, CardTitle }; setup() { this.state = useState({ count: 0 }); } } `, "src/App.xml": `<templates xml:space="preserve"> <t t-name="app.Root"> <div class="container py-4"> <Card> <CardHeader><CardTitle>${title}</CardTitle></CardHeader> <CardBody> <p>Edit <code>src/App.js</code> and <code>src/App.xml</code> to get started.</p> <Button variant="'default'" label="'Count: ' + state.count" onClick="() => state.count++"/> </CardBody> </Card> </div> </t> </templates> `, "README.md": `# ${title} Scaffolded with \`@thebase/ui\`'s CLI (Vite + BaseUI, pure Owl API). \`\`\`sh npm install npm run dev \`\`\` `, }; } /** Writes each template file under targetDir, skipping any that already exist (convert mode is additive/non-destructive). */ function writeFiles(targetDir, files, { overwrite }) { const written = []; const skipped = []; for (const [relPath, content] of Object.entries(files)) { const fullPath = path.join(targetDir, relPath); if (existsSync(fullPath) && !overwrite) { skipped.push(relPath); continue; } mkdirSync(path.dirname(fullPath), { recursive: true }); writeFileSync(fullPath, content); written.push(relPath); } return { written, skipped }; } function runInstall(targetDir) { console.log("\nInstalling dependencies (npm install)..."); const result = spawnSync("npm", ["install"], { cwd: targetDir, stdio: "inherit" }); if (result.status !== 0) { console.warn("\nnpm install did not finish cleanly — run it manually in the project folder."); } } function parseFlags(argv) { const flags = { install: true }; const positional = []; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; if (arg === "--no-install") flags.install = false; else if (arg === "--force") flags.force = true; else if (arg === "--version") flags.version = argv[++i]; else positional.push(arg); } return { flags, positional }; } function cmdCreate(argv) { const { flags, positional } = parseFlags(argv); const rawName = positional[0]; if (!rawName) { console.error("Usage: create-baseui-app create <app-name> [--version <x.y.z>] [--no-install] [--force]"); process.exit(1); } const appName = toPackageName(rawName); const targetDir = path.resolve(process.cwd(), rawName); if (existsSync(targetDir) && readdirSync(targetDir).length > 0 && !flags.force) { console.error(`Directory "${rawName}" already exists and is not empty. Use --force to write into it anyway.`); process.exit(1); } const baseuiVersion = flags.version || ownPackageJson.version; const files = buildTemplateFiles(appName, baseuiVersion); const { written } = writeFiles(targetDir, files, { overwrite: flags.force ?? false }); console.log(`Scaffolded a Vite + BaseUI app in ${targetDir}`); written.forEach((f) => console.log(` create ${f}`)); if (flags.install) runInstall(targetDir); console.log(`\nNext steps:\n cd ${rawName}${flags.install ? "" : "\n npm install"}\n npm run dev`); } function cmdConvert(argv) { const { flags, positional } = parseFlags(argv); const targetDir = path.resolve(process.cwd(), positional[0] || "."); if (!existsSync(targetDir)) { console.error(`Directory "${targetDir}" does not exist.`); process.exit(1); } const existingPackageJsonPath = path.join(targetDir, "package.json"); const appName = toPackageName(path.basename(targetDir)); const baseuiVersion = flags.version || ownPackageJson.version; const files = buildTemplateFiles(appName, baseuiVersion); if (existsSync(existingPackageJsonPath)) { const existing = JSON.parse(readFileSync(existingPackageJsonPath, "utf8")); existing.type ??= "module"; existing.scripts = { dev: "vite", build: "vite build", preview: "vite preview", ...existing.scripts }; existing.dependencies = { ...existing.dependencies, "@thebase/ui": `^${baseuiVersion}` }; existing.devDependencies = { ...existing.devDependencies, vite: existing.devDependencies?.vite || "^8.2.1" }; delete files["package.json"]; writeFileSync(existingPackageJsonPath, JSON.stringify(existing, null, 2) + "\n"); console.log(` merge package.json`); } const { written, skipped } = writeFiles(targetDir, files, { overwrite: false }); console.log(`Converted ${targetDir} into a Vite + BaseUI project.`); written.forEach((f) => console.log(` create ${f}`)); skipped.forEach((f) => console.log(` skip ${f} (already exists)`)); if (skipped.includes("index.html")) { console.log( "\nindex.html already existed and was left untouched — make sure it has a <div id=\"app\"></div> " + 'and a <script type="module" src="/src/main.js"></script> tag so the scaffolded app mounts.', ); } if (flags.install) runInstall(targetDir); console.log(`\nNext steps:\n npm run dev`); } function printHelp() { console.log(`@thebase/ui project CLI Usage: create-baseui-app create <app-name> [--version <x.y.z>] [--no-install] [--force] Scaffold a new Vite + BaseUI project in ./<app-name>. create-baseui-app convert [path] [--version <x.y.z>] [--no-install] Turn an existing (non-npm) folder into a Vite + BaseUI project, in place. Never overwrites files that already exist; only fills in what's missing. `); } const [, , command, ...rest] = process.argv; if (command === "create") cmdCreate(rest); else if (command === "convert") cmdConvert(rest); else printHelp();