UNPKG

kickstart-express

Version:

A powerful CLI tool to quickly scaffold Express.js projects with modern tooling and best practices

333 lines (293 loc) 10.4 kB
import inquirer from "inquirer"; import fs from "fs-extra"; import path from "path"; import { exec as execCallback } from "child_process"; import { promisify } from "util"; import ora from "ora"; import chalk from "chalk"; import { fileURLToPath } from "url"; import { getGitignoreContent } from "./lib/constants.js"; const exec = promisify(execCallback); export class Scaffolder { constructor(cliOptions = {}) { this.projectName = cliOptions.projectName || ""; this.options = { src: cliOptions.src !== undefined ? cliOptions.src : null, structuredSrc: cliOptions.structuredSrc !== undefined ? cliOptions.structuredSrc : null, docker: cliOptions.docker !== undefined ? cliOptions.docker : null, language: cliOptions.language || "ts", // Default to TypeScript }; this.cliOptions = cliOptions; const __dirname = path.dirname(fileURLToPath(import.meta.url)); this.templatePath = path.resolve(__dirname, "../templates"); // Track project creation state for cleanup this.projectCreationInProgress = false; this.projectPath = null; } async run() { await this.promptUser(); this.projectPath = path.join(process.cwd(), this.projectName); this.projectCreationInProgress = true; await this.generateProject(); console.log("⚙️ Initializing Git and installing dependencies..."); await this.runCommands(this.projectPath); this.projectCreationInProgress = false; console.log(`\n✅ Project ${this.projectName} scaffolded successfully!`); console.log(`\nNavigate to your project and start developing:\n`); console.log(` cd ${this.projectName}`); console.log(` pnpm dev\n`); } async runCommands(projectPath) { const opts = { cwd: projectPath }; const spinner = ora(); try { await exec("git init", opts); await exec("git add -A", opts); await exec("git commit -am 'Initial commit'", opts); console.log("✓ Git repository initialized."); spinner.start("Installing dependencies with pnpm..."); try { await exec("pnpm --version", opts); await exec("pnpm install", opts); spinner.succeed("Dependencies installed with pnpm."); } catch { spinner.start("pnpm not found, falling back to npm..."); await exec("npm install", opts); spinner.succeed("Dependencies installed with npm."); } } catch (error) { if (spinner.isSpinning) { spinner.fail("Failed to install dependencies."); } console.error("\n❌ Error during setup:", error.stderr || error.message); } } async promptUser() { const prompts = []; // Only prompt for project name if not provided via CLI if (!this.projectName) { prompts.push({ name: "projectName", message: "Project name?", default: "my-express-app", }); } // Only prompt for language if not provided via CLI if (!this.cliOptions.language) { prompts.push({ name: "language", type: "list", message: "Which language do you want to use?", choices: ["ts", "js"], default: "ts", }); } // Only prompt for docker if not provided via CLI if (this.options.docker === null) { prompts.push({ name: "docker", type: "confirm", message: "Include Dockerfile?", }); } // Ask all questions at once if any are needed if (prompts.length > 0) { const answers = await inquirer.prompt(prompts); if (answers.projectName) this.projectName = answers.projectName; if (answers.language) this.options.language = answers.language; if (answers.docker !== undefined) this.options.docker = answers.docker; } // Only prompt for src folder if not provided via CLI if (this.options.src === null) { const { src } = await inquirer.prompt([ { name: "src", type: "confirm", message: "Do you want a src folder?", default: true, }, ]); this.options.src = src; // Show suggestion if user chooses no src folder if (!src) { console.log( "\n" + chalk.blue("💡 Suggestion:"), chalk.yellow( "For best use of Kickstart.express, consider using a 'src' folder with structured architecture." ) ); console.log( " ", chalk.gray( "This provides better organization with controllers, services, and router separation." ) ); console.log( " ", chalk.gray("You can still proceed with your current choice.\n") ); } } // Only prompt for structured src if src folder is enabled and structured option not provided via CLI if (this.options.src && this.options.structuredSrc === null) { const { structured } = await inquirer.prompt([ { name: "structured", type: "confirm", message: "Use structured src/ (router, controllers, services)?", }, ]); this.options.structuredSrc = structured; } // If src folder is false, ensure structuredSrc is also false if (!this.options.src) { this.options.structuredSrc = false; } } async generateProject() { const dest = this.projectPath; if (this.options.language) { const languageFolder = this.options.language; await fs.copy( path.join(this.templatePath, "base", languageFolder), dest, { globOptions: { dot: true } } ); } const envContent = "PORT=3000"; const envPath = path.join(dest, ".env"); await fs.writeFile(envPath, envContent); if (this.options.src) { const destSrcPath = path.join(dest, "src"); if (this.options.structuredSrc) { // CASE: Structured src const sourceDir = path.join( this.templatePath, "modules", "src-structured", this.options.language ); await fs.copy(sourceDir, destSrcPath); } else { // CASE: Simple src await fs.ensureDir(destSrcPath); const sourceFile = path.join( this.templatePath, "modules", "src", `index.${this.options.language}` ); const destFile = path.join( destSrcPath, `index.${this.options.language}` ); await fs.copy(sourceFile, destFile); } } else { // CASE: No src folder await fs.copy(path.join(this.templatePath, "modules", "no-src"), dest); } if (this.options.docker) { const language = this.options.language; // Copy Dockerfile const dockerfileSource = path.join( this.templatePath, "modules", "docker", language ); await fs.copy(dockerfileSource, dest); } // Ensure .gitignore exists (runtime creation as backup) const gitignorePath = path.join(dest, ".gitignore"); const gitignoreExists = await fs.pathExists(gitignorePath); if (!gitignoreExists) { console.log("📝 Creating .gitignore file..."); await fs.writeFile(gitignorePath, getGitignoreContent()); } // Update package.json name const pkgPath = path.join(dest, "package.json"); const pkg = await fs.readJson(pkgPath); if (this.projectName === ".") { pkg.name = path.basename(process.cwd()); } else { pkg.name = this.projectName; } // Update scripts based on src folder choice if (this.options.src) { if (this.options.language === "js") { pkg.scripts.dev = "nodemon src/index.js"; pkg.scripts.start = "node src/index.js"; } else if (this.options.language === "ts") { pkg.scripts.dev = "tsx watch src/index.ts"; } } else { // No src folder - update TypeScript tsconfig.json if needed if (this.options.language === "ts") { const tsconfigPath = path.join(dest, "tsconfig.json"); try { // Read the tsconfig file as text and try to parse it const tsconfigContent = await fs.readFile(tsconfigPath, "utf8"); // Simple text replacement for the specific values we need to change const updatedContent = tsconfigContent .replace('"rootDir": "./src"', '"rootDir": "./"') .replace('"include": ["src/**/*.ts"]', '"include": ["*.ts"]'); await fs.writeFile(tsconfigPath, updatedContent); } catch (error) { console.warn("Could not update tsconfig.json:", error.message); } } } await fs.writeJson(pkgPath, pkg, { spaces: 2 }); // Generate components.json to track user setup const componentsConfig = this.generateComponentsJson(); const componentsPath = path.join(dest, "components.json"); await fs.writeJson(componentsPath, componentsConfig, { spaces: 2 }); console.log( "📋 Generated components.json to track your setup configuration." ); } // Generate components.json to track user setup generateComponentsJson() { const config = { name: this.projectName, language: this.options.language, architecture: { src: this.options.src, structured: this.options.structuredSrc, docker: this.options.docker, }, generated: new Date().toISOString(), version: "1.3.3", }; // Add suggestion if user chose single index file structure if (!this.options.src) { config.suggestion = { recommended: "src-structured", reason: "Better organization and scalability for Express.js applications. Consider using 'src' folder with structured architecture (controllers, services, router) for best use of Kickstart.express.", }; } return config; } // Methods for graceful error handling isProjectCreationInProgress() { return this.projectCreationInProgress; } async cleanup() { if (this.projectPath && (await fs.pathExists(this.projectPath))) { try { await fs.remove(this.projectPath); console.log( `🗑️ Removed partially created project: ${this.projectName}` ); } catch (error) { console.log(`⚠️ Could not remove project directory: ${error.message}`); console.log(`📁 Please manually remove: ${this.projectPath}`); } } } }