UNPKG

create-bun-stack

Version:

Rails-inspired fullstack application generator for Bun

432 lines (377 loc) 14.2 kB
#!/usr/bin/env bun import { existsSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { stdin as input, stdout as output } from "node:process"; import * as readline from "node:readline/promises"; import { copyTemplateDirectory, getExcludePatterns } from "./utils/template"; // Parse command line arguments function parseArgs() { const args = process.argv.slice(2); const options: { name?: string; db?: string; skipDbSetup?: boolean; skipInstall?: boolean; quiet?: boolean; } = {}; for (let i = 0; i < args.length; i++) { const arg = args[i]; switch (arg) { case "--name": case "-n": options.name = args[++i]; break; case "--db": case "-d": options.db = args[++i]; break; case "--skip-db-setup": options.skipDbSetup = true; break; case "--skip-install": options.skipInstall = true; break; case "--quiet": case "-q": options.quiet = true; break; case "--help": case "-h": console.log(` Usage: create-bun-stack [options] Options: -n, --name <name> Project name -d, --db <type> Database type: postgres, sqlite, or auto (default: auto) --skip-db-setup Skip database setup --skip-install Skip dependency installation -q, --quiet Suppress output -h, --help Show help `); process.exit(0); } } return options; } // Create readline interface const rl = readline.createInterface({ input, output }); async function main() { const cliOptions = parseArgs(); const isNonInteractive = cliOptions.name !== undefined; if (!cliOptions.quiet) { console.log(` ╔═══════════════════════════════════════════════════════════════╗ ║ ║ ║ ██████╗ ██╗ ██╗███╗ ██╗ ║ ║ ██╔══██╗██║ ██║████╗ ██║ ║ ║ ██████╔╝██║ ██║██╔██╗ ██║ ║ ║ ██╔══██╗██║ ██║██║╚██╗██║ ║ ║ ██████╔╝╚██████╔╝██║ ╚████║ ║ ║ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ║ ║ ║ ║ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ ║ ║ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ ║ ║ ███████╗ ██║ ███████║██║ █████╔╝ ║ ║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ ║ ║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗ ║ ║ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ║ ║ ║ ╚═══════════════════════════════════════════════════════════════╝ 🚀 Welcome to Create Bun Stack! ✨ Using Bun - The all-in-one JavaScript runtime & toolkit `); } try { // Check if Bun is installed const bunVersion = Bun.version; if (!cliOptions.quiet) { console.log(`✅ Using Bun ${bunVersion}\n`); } // Get project name let projectName: string; if (isNonInteractive) { projectName = cliOptions.name as string; if (!cliOptions.quiet) { console.log(`📝 Project name: ${projectName}`); } } else { projectName = await rl.question("📝 Project name: "); } if (!projectName || projectName.trim().length === 0) { console.error("❌ Project name is required"); process.exit(1); } // Validate project name format const validProjectName = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/i; if (!validProjectName.test(projectName)) { console.error("❌ Project name must contain only letters, numbers, and hyphens"); console.error(" It must start and end with a letter or number"); console.error(" Example: my-awesome-app"); process.exit(1); } // Check for common reserved names const reservedNames = ["node_modules", "test", "tests", "src", "dist", "build", "public"]; if (reservedNames.includes(projectName.toLowerCase())) { console.error(`❌ "${projectName}" is a reserved name. Please choose a different name.`); process.exit(1); } const projectPath = join(process.cwd(), projectName); // Check if directory already exists if (existsSync(projectPath)) { console.error(`❌ Directory ${projectName} already exists`); process.exit(1); } // Database choice let dbProvider = "auto"; let dbInstructions = ""; if (isNonInteractive) { // Use CLI option for database const dbOption = cliOptions.db || "auto"; // Validate database option const validDbOptions = ["postgres", "sqlite", "auto"]; if (!validDbOptions.includes(dbOption)) { console.error(`❌ Invalid database option: ${dbOption}`); console.error(` Valid options are: ${validDbOptions.join(", ")}`); process.exit(1); } switch (dbOption) { case "postgres": dbProvider = "postgres"; dbInstructions = ` To use PostgreSQL: 1. Make sure PostgreSQL is installed and running 2. Create a database: createdb ${projectName}_dev 3. Set DATABASE_URL in your .env file: DATABASE_URL=postgres://username:password@localhost:5432/${projectName}_dev `; break; case "sqlite": dbProvider = "sqlite"; dbInstructions = "\nSQLite will be used (no additional setup required)."; break; default: dbProvider = "auto"; dbInstructions = "\nThe app will try PostgreSQL first, then fallback to SQLite if not available."; } if (!cliOptions.quiet) { console.log(`\n📊 Database: ${dbProvider}`); } } else { console.log("\n📊 Database Configuration:"); console.log("1. PostgreSQL (recommended for production)"); console.log("2. SQLite (perfect for development)"); console.log("3. Auto-detect (PostgreSQL with SQLite fallback)"); const dbChoice = (await rl.question("\nChoose database option (1-3) [default: 3]: ")) || "3"; switch (dbChoice) { case "1": dbProvider = "postgres"; dbInstructions = ` To use PostgreSQL: 1. Make sure PostgreSQL is installed and running 2. Create a database: createdb ${projectName}_dev 3. Set DATABASE_URL in your .env file: DATABASE_URL=postgres://username:password@localhost:5432/${projectName}_dev `; break; case "2": dbProvider = "sqlite"; dbInstructions = "\nSQLite will be used (no additional setup required)."; break; default: dbProvider = "auto"; dbInstructions = "\nThe app will try PostgreSQL first, then fallback to SQLite if not available."; } } if (!cliOptions.quiet) { console.log("\n🔨 Creating your Bun Stack app...\n"); } // Copy template files const templateDir = join(import.meta.dir, "templates", "default"); const templateVariables = { projectName: projectName, dbProvider: dbProvider, }; await copyTemplateDirectory(templateDir, projectPath, templateVariables, getExcludePatterns()); if (!cliOptions.quiet) { console.log("✅ Project structure created"); } // Change to project directory process.chdir(projectPath); // Create database directory mkdirSync("db", { recursive: true }); // Create .gitignore if it doesn't exist const gitignorePath = join(projectPath, ".gitignore"); try { await Bun.file(gitignorePath).text(); } catch { // .gitignore doesn't exist, create it automatically const gitignoreContent = `node_modules .DS_Store *.log .env .env.local dist/ db/*.sqlite db/*.sqlite-journal db/*.db db/*.db-journal db/test.db db/test.db-journal drizzle/ # Build output public/styles.css public/main.js public/main.js.map # HMR .hmr-timestamp # Production build dist/ `; await Bun.write(gitignorePath, gitignoreContent); if (!cliOptions.quiet) { console.log("✅ Created .gitignore file"); } } // Prompt for git init let shouldInitGit = false; if (!isNonInteractive) { const gitInitAnswer = await rl.question("\n🐙 Initialize git repository? (Y/n): "); shouldInitGit = gitInitAnswer.toLowerCase() !== "n"; } if (shouldInitGit) { const gitInitProc = Bun.spawn(["git", "init"], { stdout: cliOptions.quiet ? "ignore" : "inherit", stderr: cliOptions.quiet ? "ignore" : "inherit", }); await gitInitProc.exited; if (gitInitProc.exitCode === 0) { if (!cliOptions.quiet) { console.log("✅ Initialized git repository"); } } else { if (!cliOptions.quiet) { console.log("⚠️ Failed to initialize git repository"); } } } // Install dependencies if (!cliOptions.skipInstall) { if (!cliOptions.quiet) { console.log("\n📦 Installing dependencies..."); } const installProc = Bun.spawn(["bun", "install"], { stdout: cliOptions.quiet ? "ignore" : "inherit", stderr: cliOptions.quiet ? "ignore" : "inherit", }); await installProc.exited; if (installProc.exitCode !== 0) { console.error("❌ Failed to install dependencies"); process.exit(1); } if (!cliOptions.quiet) { console.log("✅ Dependencies installed"); } } // Copy .env.example to .env const envProc = Bun.spawn(["cp", ".env.example", ".env"], { stdout: "inherit", stderr: "inherit", }); await envProc.exited; // Setup database let shouldSetupDb = false; if (!cliOptions.skipDbSetup) { if (isNonInteractive) { shouldSetupDb = true; } else { const dbSetupAnswer = await rl.question("\n🗄️ Setup database now? (Y/n): "); shouldSetupDb = dbSetupAnswer.toLowerCase() !== "n"; } } if (shouldSetupDb) { if (!cliOptions.quiet) { console.log("\n🔧 Setting up database..."); } const dbProc = Bun.spawn(["bun", "run", "db:push"], { stdout: cliOptions.quiet ? "ignore" : "inherit", stderr: cliOptions.quiet ? "ignore" : "inherit", }); await dbProc.exited; if (dbProc.exitCode === 0) { if (!cliOptions.quiet) { console.log("✅ Database setup complete"); } // Offer to seed (skip in non-interactive mode) if (!isNonInteractive) { const shouldSeed = await rl.question("\n🌱 Seed database with sample data? (y/N): "); if (shouldSeed.toLowerCase() === "y") { const seedProc = Bun.spawn(["bun", "run", "db:seed"], { stdout: cliOptions.quiet ? "ignore" : "inherit", stderr: cliOptions.quiet ? "ignore" : "inherit", }); await seedProc.exited; if (seedProc.exitCode === 0 && !cliOptions.quiet) { console.log("✅ Database seeded"); } } } } else { if (!cliOptions.quiet) { console.log("⚠️ Database setup failed - you can run 'bun run db:push' later"); } } } // Build CSS if (!cliOptions.quiet) { console.log("\n🎨 Building CSS..."); } const cssProc = Bun.spawn(["bun", "run", "build:css"], { stdout: cliOptions.quiet ? "ignore" : "inherit", stderr: cliOptions.quiet ? "ignore" : "inherit", }); await cssProc.exited; if (cssProc.exitCode === 0) { if (!cliOptions.quiet) { console.log("✅ CSS built successfully"); } } else { if (!cliOptions.quiet) { console.log("⚠️ CSS build failed - you can run 'bun run build:css' later"); } } // Success message if (!cliOptions.quiet) { console.log(` ╔═══════════════════════════════════════════════════════════════╗ ║ 🎉 SUCCESS! 🎉 ║ ╚═══════════════════════════════════════════════════════════════╝ Your Bun Stack app is ready! 📁 Project created at: ${projectPath} ${dbInstructions} 🚀 Get started: cd ${projectName} bun run dev 📚 Available commands: bun run dev - Start development server bun test - Run tests bun run db:studio - Open database GUI bun run build - Build for production 🔗 Resources: Documentation: https://github.com/jasencarroll/create-bun-stack Bun Docs: https://bun.sh Happy coding! 🚀 `); } } catch (error) { console.error("\n❌ Error:", error); process.exit(1); } finally { rl.close(); } } // Run the CLI main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });