@thebase/ui
Version:
CDN-installable Owl and Bootstrap 5 UI component library.
269 lines (224 loc) • 8.98 kB
JavaScript
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();