From b80899ad6a525be2e0ed6609ef6d7ad5efa4c63e Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Sat, 22 Nov 2025 03:03:42 -0600 Subject: [PATCH 1/2] feat: add cli package for project scaffolding --- ROADMAP.md | 14 + cli/package.json | 25 ++ cli/src/commands/build.ts | 39 +++ cli/src/commands/dev.ts | 47 +++ cli/src/commands/download.ts | 40 +++ cli/src/commands/init.ts | 156 ++++++++++ cli/src/index.ts | 82 ++++++ cli/src/templates/minimal.ts | 149 ++++++++++ cli/src/templates/styles.ts | 194 +++++++++++++ cli/src/templates/with-plugins.ts | 278 ++++++++++++++++++ cli/src/templates/with-router.ts | 235 +++++++++++++++ cli/src/tests/download.test.ts | 27 ++ cli/src/tests/files.test.ts | 64 ++++ cli/src/tests/templates.test.ts | 146 ++++++++++ cli/src/utils/download.ts | 56 ++++ cli/src/utils/echo.ts | 40 +++ cli/src/utils/files.ts | 28 ++ cli/tsconfig.json | 25 ++ cli/tsdown.config.ts | 12 + cli/vitest.config.ts | 13 + package.json | 2 + pnpm-lock.yaml | 467 +++++++++++++++++++++++++----- pnpm-workspace.yaml | 1 + 23 files changed, 2069 insertions(+), 71 deletions(-) create mode 100644 cli/package.json create mode 100644 cli/src/commands/build.ts create mode 100644 cli/src/commands/dev.ts create mode 100644 cli/src/commands/download.ts create mode 100644 cli/src/commands/init.ts create mode 100644 cli/src/index.ts create mode 100644 cli/src/templates/minimal.ts create mode 100644 cli/src/templates/styles.ts create mode 100644 cli/src/templates/with-plugins.ts create mode 100644 cli/src/templates/with-router.ts create mode 100644 cli/src/tests/download.test.ts create mode 100644 cli/src/tests/files.test.ts create mode 100644 cli/src/tests/templates.test.ts create mode 100644 cli/src/utils/download.ts create mode 100644 cli/src/utils/echo.ts create mode 100644 cli/src/utils/files.ts create mode 100644 cli/tsconfig.json create mode 100644 cli/tsdown.config.ts create mode 100644 cli/vitest.config.ts diff --git a/ROADMAP.md b/ROADMAP.md index 4c8c62f..aad8a2a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -255,6 +255,20 @@ directive parsing, and network operations, making it easier to debug apps withou ## Parking Lot +### IIFE Build Support + +Provide an IIFE (Immediately Invoked Function Expression) build target for VoltX.js to support direct `` +- Ensure plugins work with IIFE build +- Add IIFE examples to documentation + ### Evaluator & Binder Hardening All expression evaluation now flows through a cached `new Function` compiler guarded by a hardened scope proxy, with the binder slimmed into a directive registry so plugins self-register while tests verify the sandboxed error surfaces. diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 0000000..1b3602a --- /dev/null +++ b/cli/package.json @@ -0,0 +1,25 @@ +{ + "name": "create-voltx", + "version": "0.1.0", + "description": "CLI for creating and managing VoltX.js applications", + "type": "module", + "author": "Owais Jamil", + "license": "MIT", + "repository": { "type": "git", "url": "https://github.com/stormlightlabs/volt.git", "directory": "cli" }, + "bin": { "create-voltx": "./dist/index.js", "voltx": "./dist/index.js" }, + "files": ["dist", "templates"], + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { ".": "./dist/index.js", "./package.json": "./package.json" }, + "scripts": { + "build": "tsdown", + "dev": "tsdown --watch", + "test": "vitest", + "test:run": "vitest run", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { "tsdown": "^0.15.6" }, + "dependencies": { "chalk": "^5.6.2", "commander": "^14.0.1", "@inquirer/prompts": "^8.0.1" }, + "keywords": ["voltx", "reactive", "framework", "cli", "scaffold", "create"] +} diff --git a/cli/src/commands/build.ts b/cli/src/commands/build.ts new file mode 100644 index 0000000..ea99145 --- /dev/null +++ b/cli/src/commands/build.ts @@ -0,0 +1,39 @@ +import { echo } from "$utils/echo.js"; +import { spawn } from "node:child_process"; + +/** + * Builds the VoltX.js project for production using Vite. + */ +export async function buildCommand(options: { outDir?: string } = {}): Promise { + const outDir = options.outDir || "dist"; + + echo.title("\n⚡ Building VoltX.js project for production...\n"); + + try { + const { existsSync } = await import("node:fs"); + if (!existsSync("index.html")) { + echo.warn("Warning: No index.html found in current directory"); + echo.info("Are you in a VoltX.js project?\n"); + } + + const viteArgs = ["vite", "build", "--outDir", outDir]; + const viteProcess = spawn("npx", viteArgs, { stdio: "inherit", shell: true }); + + viteProcess.on("error", (error) => { + echo.err("Failed to build project:", error); + process.exit(1); + }); + + viteProcess.on("exit", (code) => { + if (code === 0) { + echo.success(`\n✓ Build completed successfully!\n`); + echo.info(`Output directory: ${outDir}\n`); + } else if (code !== null) { + process.exit(code); + } + }); + } catch (error) { + echo.err("Error building project:", error); + process.exit(1); + } +} diff --git a/cli/src/commands/dev.ts b/cli/src/commands/dev.ts new file mode 100644 index 0000000..ce838fe --- /dev/null +++ b/cli/src/commands/dev.ts @@ -0,0 +1,47 @@ +import { echo } from "$utils/echo.js"; +import { spawn } from "node:child_process"; + +/** + * Starts a Vite development server for the current project. + */ +export async function devCommand(options: { port?: number; open?: boolean } = {}): Promise { + const port = options.port || 3000; + const shouldOpen = options.open || false; + + echo.title("\n⚡ Starting VoltX.js development server...\n"); + + try { + const { existsSync } = await import("node:fs"); + if (!existsSync("index.html")) { + echo.warn("Warning: No index.html found in current directory"); + echo.info("Are you in a VoltX.js project?\n"); + } + + const viteArgs = ["vite", "--port", port.toString(), "--host"]; + + if (shouldOpen) { + viteArgs.push("--open"); + } + + const viteProcess = spawn("npx", viteArgs, { stdio: "inherit", shell: true }); + + viteProcess.on("error", (error) => { + echo.err("Failed to start dev server:", error); + process.exit(1); + }); + + viteProcess.on("exit", (code) => { + if (code !== 0 && code !== null) { + process.exit(code); + } + }); + + process.on("SIGINT", () => { + viteProcess.kill("SIGINT"); + process.exit(0); + }); + } catch (error) { + echo.err("Error starting dev server:", error); + process.exit(1); + } +} diff --git a/cli/src/commands/download.ts b/cli/src/commands/download.ts new file mode 100644 index 0000000..69899bc --- /dev/null +++ b/cli/src/commands/download.ts @@ -0,0 +1,40 @@ +import { downloadFile, getCDNUrls } from "$utils/download.js"; +import { echo } from "$utils/echo.js"; +import path from "node:path"; + +/** + * Downloads VoltX.js assets (JS and/or CSS) from the CDN. + */ +export async function downloadCommand( + options: { version?: string; js?: boolean; css?: boolean; output?: string } = {}, +): Promise { + const version = options.version || "latest"; + const downloadJS = options.js !== false; + const downloadCSS = options.css !== false; + const outputDir = options.output || "."; + + echo.title("\n⚡ Downloading VoltX.js assets...\n"); + + try { + const urls = getCDNUrls(version); + + if (downloadJS) { + const jsPath = path.join(outputDir, "voltx.min.js"); + echo.info(`Downloading voltx.min.js (${version})...`); + await downloadFile(urls.js, jsPath); + echo.ok(`✓ Downloaded: ${jsPath}`); + } + + if (downloadCSS) { + const cssPath = path.join(outputDir, "voltx.min.css"); + echo.info(`Downloading voltx.min.css (${version})...`); + await downloadFile(urls.css, cssPath); + echo.ok(`✓ Downloaded: ${cssPath}`); + } + + echo.success("\n✓ Download completed successfully!\n"); + } catch (error) { + echo.err("Failed to download assets:", error); + process.exit(1); + } +} diff --git a/cli/src/commands/init.ts b/cli/src/commands/init.ts new file mode 100644 index 0000000..dae3925 --- /dev/null +++ b/cli/src/commands/init.ts @@ -0,0 +1,156 @@ +import { + generateMinimalCSS, + generateMinimalHTML, + generateMinimalPackageJSON, + generateMinimalREADME, +} from "$templates/minimal.js"; +import { + generateStylesCSS, + generateStylesHTML, + generateStylesPackageJSON, + generateStylesREADME, +} from "$templates/styles.js"; +import { + generatePluginsCSS, + generatePluginsHTML, + generatePluginsPackageJSON, + generatePluginsREADME, +} from "$templates/with-plugins.js"; +import { + generateRouterCSS, + generateRouterHTML, + generateRouterPackageJSON, + generateRouterREADME, +} from "$templates/with-router.js"; +import { downloadFile, getCDNUrls } from "$utils/download.js"; +import { echo } from "$utils/echo.js"; +import { createFile, isEmptyOrMissing } from "$utils/files.js"; +import { input, select } from "@inquirer/prompts"; +import path from "node:path"; + +type Template = "minimal" | "with-router" | "with-plugins" | "styles"; + +/** + * Download VoltX.js assets to the project directory. + */ +async function downloadAssets(projectDir: string, template: Template): Promise { + const urls = getCDNUrls(); + + echo.info("Downloading VoltX.js assets..."); + + const cssPath = path.join(projectDir, "voltx.min.css"); + await downloadFile(urls.css, cssPath); + echo.ok(` Downloaded: voltx.min.css`); + + if (template !== "styles") { + const jsPath = path.join(projectDir, "voltx.min.js"); + await downloadFile(urls.js, jsPath); + echo.ok(` Downloaded: voltx.min.js`); + } +} + +/** + * Generate project files based on the selected template. + */ +async function generateProjectFiles(projectDir: string, projectName: string, template: Template): Promise { + echo.info("Generating project files..."); + + let htmlContent: string; + let cssContent: string; + let packageJsonContent: string; + let readmeContent: string; + + switch (template) { + case "minimal": + htmlContent = generateMinimalHTML(projectName); + cssContent = generateMinimalCSS(); + packageJsonContent = generateMinimalPackageJSON(projectName); + readmeContent = generateMinimalREADME(projectName); + break; + + case "styles": + htmlContent = generateStylesHTML(projectName); + cssContent = generateStylesCSS(); + packageJsonContent = generateStylesPackageJSON(projectName); + readmeContent = generateStylesREADME(projectName); + break; + + case "with-router": + htmlContent = generateRouterHTML(projectName); + cssContent = generateRouterCSS(); + packageJsonContent = generateRouterPackageJSON(projectName); + readmeContent = generateRouterREADME(projectName); + break; + + case "with-plugins": + htmlContent = generatePluginsHTML(projectName); + cssContent = generatePluginsCSS(); + packageJsonContent = generatePluginsPackageJSON(projectName); + readmeContent = generatePluginsREADME(projectName); + break; + } + + await createFile(path.join(projectDir, "index.html"), htmlContent); + echo.ok(` Created: index.html`); + + await createFile(path.join(projectDir, "styles.css"), cssContent); + echo.ok(` Created: styles.css`); + + await createFile(path.join(projectDir, "package.json"), packageJsonContent); + echo.ok(` Created: package.json`); + + await createFile(path.join(projectDir, "README.md"), readmeContent); + echo.ok(` Created: README.md`); +} + +/** + * Init command implementation. + * + * Creates a new VoltX.js project with the selected template. + */ +export async function initCommand(projectName?: string): Promise { + echo.title("\n⚡ Create VoltX.js App\n"); + + if (!projectName) { + projectName = await input({ message: "Project name:", default: "my-voltx-app" }); + + if (!projectName) { + echo.err("Project name is required"); + process.exit(1); + } + } + + const projectDir = path.resolve(process.cwd(), projectName); + + if (!(await isEmptyOrMissing(projectDir))) { + echo.err(`Directory ${projectName} already exists and is not empty`); + process.exit(1); + } + + const template = await select