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/README.md b/cli/README.md new file mode 100644 index 0000000..49d0aa2 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,83 @@ +# create-voltx + +CLI for creating and managing VoltX.js applications. + +## Usage + +### Create a New Project + +```bash +# Using pnpm (recommended) +pnpm create voltx my-app + +# Using npm +npm create voltx@latest my-app + +# Using npx +npx create-voltx my-app +``` + +### Commands + +#### `init [project-name]` + +Create a new VoltX.js project with interactive template selection. + +```bash +voltx init my-app +``` + +**Templates:** + +- **Minimal** - Basic VoltX.js app with counter +- **With Router** - Multi-page app with routing +- **With Plugins** - All plugins demo +- **Styles Only** - Just HTML + CSS, no framework + +#### `dev` + +Start Vite development server. + +```bash +voltx dev [--port 3000] [--open] +``` + +#### `build` + +Build project for production. + +```bash +voltx build [--out dist] +``` + +#### `download` + +Download VoltX.js assets from CDN. + +```bash +voltx download [--version latest] [--output .] +``` + +## Documentation + +See the [CLI Guide](https://stormlightlabs.github.io/volt/cli) for complete documentation. + +## Development + +```bash +# Install dependencies +pnpm install + +# Build CLI +pnpm build + +# Run tests +pnpm test + +# Type check +pnpm typecheck +``` + +## License + +MIT diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 0000000..9dfb5dc --- /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", "@vitest/coverage-v8": "^3.2.4" }, + "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