UNPKG

nodly

Version:

Nodly is an interactive CLI for scaffolding modern Node.js/Express.js backend projects with instant setup for databases, mailers, queues, sockets, and more. Skip boilerplate and start building features fast.

618 lines (540 loc) 18.2 kB
#!/usr/bin/env node import fs from "fs"; import path from "path"; import chalk from "chalk"; import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const args = process.argv.slice(2); const templatesDir = path.join(__dirname, "templates"); // --- Add Middleware --- if (args[0] === "add" && args[1] === "middleware" && args[2]) { // Check if middlewares folder exists in current directory const middlewaresDir = path.join(process.cwd(), "middlewares"); if (!fs.existsSync(middlewaresDir)) { console.log(chalk.red("✖ No 'middlewares' folder found. Please run this command from your Nodly project root.")); process.exit(1); } const name = args[2]; const fileName = `${name}.middleware.js`; const middlewarePath = path.join(middlewaresDir, fileName); // Check if file already exists if (fs.existsSync(middlewarePath)) { console.log(chalk.red(`✖ Middleware already exists: middlewares/${fileName}`)); process.exit(1); } const template = `// middlewares/${fileName} import ApiResponse from "../utils/apiresponses.js"; export const ${name}Middleware = (req, res, next) => { try { // Your middleware logic here next(); } catch (error) { console.error("${name} Middleware Error:", error); return ApiResponse.error(res, "${name} middleware failed"); } }; `; fs.writeFileSync(middlewarePath, template); console.log(chalk.green(`✔ Created middleware: middlewares/${fileName}`)); process.exit(0); } // --- Add Controller --- if (args[0] === "add" && args[1] === "controller" && args[2]) { const name = args[2]; const pascalName = name.charAt(0).toUpperCase() + name.slice(1); const fileName = `${name}.controller.js`; const controllerPath = path.join(process.cwd(), "controllers", fileName); // Check if file already exists if (fs.existsSync(controllerPath)) { console.log(chalk.red(`✖ Controller already exists: controllers/${fileName}`)); process.exit(1); } const template = `// controllers/${fileName} import ApiResponse from "../utils/apiresponses.js"; export const get${pascalName} = (req, res) => { return ApiResponse.success(res, "${pascalName} fetched successfully"); }; export const create${pascalName} = (req, res) => { // Example: const data = req.body; return ApiResponse.success(res, "${pascalName} created successfully"); }; // Add more controller methods as needed `; fs.writeFileSync(controllerPath, template); console.log(chalk.green(`✔ Created controller: controllers/${fileName}`)); process.exit(0); } // --- Add Router --- if (args[0] === "add" && args[1] === "router" && args[2]) { const name = args[2]; const pascalName = name.charAt(0).toUpperCase() + name.slice(1); const fileName = `${name}.routes.js`; const routerPath = path.join(process.cwd(), "routes", fileName); // Check if file already exists if (fs.existsSync(routerPath)) { console.log(chalk.red(`✖ Router already exists: routes/${fileName}`)); process.exit(1); } const template = `// routes/${fileName} import { Router } from "express"; import { get${pascalName}, create${pascalName} } from "../controllers/${name}.controller.js"; const router = Router(); router.get("/", get${pascalName}); router.post("/", create${pascalName}); // Add more routes as needed export default router; `; fs.writeFileSync(routerPath, template); console.log(chalk.green(`✔ Created router: routes/${fileName}`)); process.exit(0); } // Only import prompts, ora, and run project creation logic BELOW this line! import prompts from "prompts"; import ora from "ora"; import { execSync } from "child_process"; const answers = await prompts([ { type: "text", name: "projectName", message: "Enter project name:", initial: "my-nodly-app", }, { type: "multiselect", name: "databases", message: "Which database(s) do you want to use?", choices: [ { title: "MySQL2", value: "mysql2" }, { title: "PostgreSQL", value: "postgresql" }, { title: "MongoDB (Mongoose)", value: "mongoose" }, { title: "Redis", value: "redis" }, { title: "Prisma", value: "prisma" }, { title: "Supabase", value: "supabase" }, { title: "TypeORM", value: "typeorm" }, { title: "Drizzle ORM", value: "drizzle" }, { title: "Firebase", value: "firebase" }, ], }, { type: "multiselect", name: "queues", message: "Which message queue(s) do you want to use?", choices: [ { title: "BullMQ", value: "bullmq" }, { title: "RabbitMQ", value: "rabbitmq" }, { title: "Kafka", value: "kafka" }, ], }, { type: "confirm", name: "useMailer", message: "Do you want to include Nodemailer?", initial: true, }, { type: "confirm", name: "useSocketIO", message: "Do you want to include Socket.IO?", initial: true, }, { type: "toggle", name: "useJWT", message: "Do you want to include JWT authentication?", initial: true, active: "yes", inactive: "no", }, { type: prev => prev ? "text" : null, name: "jwtSecret", message: "Enter a JWT secret (leave blank for default):", initial: "supersecretkey", }, ]); // 1. Create project directory const targetDir = path.join(process.cwd(), answers.projectName); fs.mkdirSync(targetDir, { recursive: true }); // 2. Copy common files (Dockerfile, server.js, etc.) ["Dockerfile", "server.js", ".env"].forEach((file) => { fs.cpSync(path.join(templatesDir, file), path.join(targetDir, file)); }); // Copy essential folders (routes, controllers, middlewares, utils) ["routes", "controllers", "middlewares", "utils"].forEach((folder) => { const srcFolder = path.join(templatesDir, folder); const destFolder = path.join(targetDir, folder); if (fs.existsSync(srcFolder)) { fs.cpSync(srcFolder, destFolder, { recursive: true }); } }); // Conditionally copy mailers folder and sendMail.js if (answers.useMailer) { const mailersSrc = path.join(templatesDir, "config", "mailers"); const mailersDest = path.join(targetDir, "config", "mailers"); if (fs.existsSync(mailersSrc)) { fs.cpSync(mailersSrc, mailersDest, { recursive: true }); } const sendMailSrc = path.join(templatesDir, "utils", "sendMail.js"); const sendMailDest = path.join(targetDir, "utils", "sendMail.js"); if (fs.existsSync(sendMailSrc)) { fs.cpSync(sendMailSrc, sendMailDest); } console.log(chalk.yellow("✔ Nodemailer files added.")); console.log(chalk.green("Nodemailer mailer integration enabled!")); } // Conditionally copy sockets folder if (answers.useSocketIO) { const socketsSrc = path.join(templatesDir, "config", "sockets"); const socketsDest = path.join(targetDir, "config", "sockets"); if (fs.existsSync(socketsSrc)) { fs.cpSync(socketsSrc, socketsDest, { recursive: true }); } console.log(chalk.yellow("✔ Socket.IO files added.")); } // Conditionally copy nodemailer.js and socketIO.js in config logic const allConfigs = [ "redis", "mysql2", "mongoose", "prisma", "postgresql", "drizzle", "supabase", "typeorm", "firebase", "bullmq", "rabbitmq", "kafka", ]; allConfigs.forEach((cfg) => { let src, dest; if (["bullmq", "rabbitmq"].includes(cfg)) { src = path.join(templatesDir, "config", "queues", cfg, `${cfg}.js`); dest = path.join(targetDir, "config", "queues", cfg, `${cfg}.js`); if (answers.queues && answers.queues.includes(cfg)) { fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.cpSync(src, dest); console.log(chalk.green(`Queue integration enabled: ${cfg}`)); } } else if (cfg === "kafka") { // Copy both producer and consumer files if Kafka is selected if (answers.queues && answers.queues.includes("kafka")) { const kafkaSrcDir = path.join(templatesDir, "config", "queues", "kafka"); const kafkaDestDir = path.join(targetDir, "config", "queues", "kafka"); fs.mkdirSync(kafkaDestDir, { recursive: true }); fs.cpSync( path.join(kafkaSrcDir, "producer.js"), path.join(kafkaDestDir, "producer.js") ); fs.cpSync( path.join(kafkaSrcDir, "consumer.js"), path.join(kafkaDestDir, "consumer.js") ); console.log(chalk.green(`Queue integration enabled: ${cfg}`)); } } else { src = path.join(templatesDir, "config", "databases", `${cfg}.js`); dest = path.join(targetDir, "config", "databases", `${cfg}.js`); if (answers.databases.includes(cfg)) { fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.cpSync(src, dest); console.log(chalk.green(`Database integration enabled: ${cfg}`)); } } if (cfg === "kafka") { // Copy both producer and consumer files if Kafka is selected if (answers.queues && answers.queues.includes("kafka")) { const kafkaSrcDir = path.join(templatesDir, "config", "queues", "kafka"); const kafkaDestDir = path.join(targetDir, "config", "queues", "kafka"); fs.mkdirSync(kafkaDestDir, { recursive: true }); fs.cpSync( path.join(kafkaSrcDir, "producer.js"), path.join(kafkaDestDir, "producer.js") ); fs.cpSync( path.join(kafkaSrcDir, "consumer.js"), path.join(kafkaDestDir, "consumer.js") ); } } }); // 4. Write .env file (already well-structured in your template) fs.cpSync(path.join(templatesDir, ".env"), path.join(targetDir, ".env")); // 5. Install dependencies const baseDeps = [ "express", "dotenv", "cors", "morgan", "express-rate-limit", "jsonwebtoken", "chalk", "prompts", "ora", ]; // Add nodemon as a dev dependency const devDeps = ["nodemon"]; const deps = [...baseDeps]; if (answers.useMailer) deps.push("nodemailer"); if (answers.useSocketIO) deps.push("socket.io"); if (answers.databases.includes("mongoose")) deps.push("mongoose"); if (answers.databases.includes("redis")) deps.push("ioredis"); if (answers.databases.includes("mysql2")) deps.push("mysql2"); if (answers.databases.includes("postgresql")) deps.push("pg"); if (answers.databases.includes("prisma")) deps.push("prisma", "@prisma/client"); if (answers.databases.includes("typeorm")) deps.push("typeorm", "reflect-metadata"); if (answers.databases.includes("drizzle")) deps.push("drizzle-orm", "pg"); if (answers.databases.includes("supabase")) deps.push("@supabase/supabase-js"); if (answers.databases.includes("firebase")) deps.push("firebase"); if (answers.queues && answers.queues.includes("bullmq")) deps.push("bullmq"); if (answers.queues && answers.queues.includes("kafka")) deps.push("kafkajs"); if (answers.queues && answers.queues.includes("rabbitmq")) deps.push("amqplib"); // Show the user which packages will be installed console.log(chalk.cyan("\nThe following npm packages will be installed:")); console.log(chalk.green(deps.join(" "))); console.log(chalk.cyan("The following dev packages will be installed:")); console.log(chalk.green(devDeps.join(" "))); console.log(); const spinner = ora("Installing dependencies...").start(); try { execSync(`npm init -y`, { stdio: "inherit", cwd: targetDir }); execSync(`npm install ${deps.join(" ")}`, { stdio: "inherit", cwd: targetDir, }); execSync(`npm install -D ${devDeps.join(" ")}`, { stdio: "inherit", cwd: targetDir, }); spinner.succeed("Dependencies installed!"); } catch (err) { spinner.fail("Failed to install dependencies."); console.error(err); } // 6. Add start script and type:module const pkgPath = path.join(targetDir, "package.json"); const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); pkg.scripts = pkg.scripts || {}; pkg.scripts.start = "node server.js"; pkg.scripts.dev = "nodemon server.js"; fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); // Patch server.js only if Socket.IO is selected const serverPath = path.join(targetDir, "server.js"); let content = fs.readFileSync(serverPath, "utf8"); if (answers.useSocketIO) { content = content.replace( "// SOCKET.IO_PLACEHOLDER", `import http from "http";\nimport { initSocketIO } from "./config/sockets/socketIO.js";` ); content = content.replace( "// SOCKET.IO_BOOTSTRAP", `const socketServer = http.createServer(server);\n initSocketIO(socketServer);\n;` ); console.log(chalk.yellow("✔ Socket.IO code added to server.js.")); } else { // Remove both placeholders and use basic app.listen content = content.replace("// SOCKET.IO_PLACEHOLDER", ""); content = content.replace("// SOCKET.IO_BOOTSTRAP", ""); } fs.writeFileSync(serverPath, content); // Initialize Prisma if selected if (answers.databases.includes("prisma")) { try { execSync(`npx prisma init`, { stdio: "inherit", cwd: targetDir }); console.log( chalk.yellow( "✔ Prisma initialized (prisma/schema.prisma and .env created)." ) ); } catch (err) { console.error("Failed to initialize Prisma:", err); } } // TypeORM reminder if (answers.databases.includes("typeorm")) { console.log( chalk.blue( "👉 Add your entities to the TypeORM config and run migrations as needed." ) ); } // Drizzle reminder if (answers.databases.includes("drizzle")) { console.log( chalk.blue( "👉 Set up your Drizzle models and run migrations as needed." ) ); } // 7. Write default .env file const envVars = { // App PORT: "5000", NODE_ENV: "development", JWT_SECRET: "supersecretkey", // Mail MAIL_HOST: "smtp.yourprovider.com", MAIL_PORT: "587", MAIL_SECURE: "false", MAIL_USER: "your@email.com", MAIL_PASS: "yourpassword", // MYSQL2 MYSQL_HOST: "localhost", MYSQL_PORT: "3306", MYSQL_USER: "root", MYSQL_PASSWORD: "yourpassword", MYSQL_DATABASE: "nodly_db", // POSTGRESQL POSTGRES_URL: "postgresql://user:password@localhost:5432/nodly_db", // MONGODB MONGO_URI: "mongodb://localhost:27017/nodly_db", // REDIS REDIS_HOST: "127.0.0.1", REDIS_PORT: "6379", REDIS_PASSWORD: "yourpassword", // PRISMA DATABASE_URL: "postgresql://user:password@localhost:5432/nodly_db", // TYPEORM TYPEORM_DATABASE_URL: "postgresql://user:password@localhost:5432/nodly_db", // DRIZZLE DRIZZLE_DATABASE_URL: "postgresql://user:password@localhost:5432/nodly_db", // SUPABASE SUPABASE_URL: "https://your-supabase-url.supabase.co", SUPABASE_ANON_KEY: "your-anon-key", // FIREBASE FIREBASE_API_KEY: "your-firebase-api-key", FIREBASE_AUTH_DOMAIN: "your-app.firebaseapp.com", FIREBASE_PROJECT_ID: "your-project-id", FIREBASE_STORAGE_BUCKET: "your-app.appspot.com", FIREBASE_MESSAGING_SENDER_ID: "your-messaging-sender-id", FIREBASE_APP_ID: "your-app-id", // QUEUES RABBITMQ_URL: "amqp://localhost", KAFKA_BROKER: "localhost:9092", }; const envPath = path.join(targetDir, ".env"); let envContent = `#App PORT=5000 NODE_ENV=development `; if (answers.useJWT) { envContent += `JWT_SECRET=${answers.jwtSecret || "supersecretkey"}\n`; } if (answers.useMailer) { envContent += ` # Mail MAIL_HOST=${envVars.MAIL_HOST} MAIL_PORT=${envVars.MAIL_PORT} MAIL_SECURE=${envVars.MAIL_SECURE} MAIL_USER=${envVars.MAIL_USER} MAIL_PASS=${envVars.MAIL_PASS} `; } if (answers.databases.includes("mysql2")) { envContent += ` # MYSQL2 MYSQL_HOST=${envVars.MYSQL_HOST} MYSQL_PORT=${envVars.MYSQL_PORT} MYSQL_USER=${envVars.MYSQL_USER} MYSQL_PASSWORD=${envVars.MYSQL_PASSWORD} MYSQL_DATABASE=${envVars.MYSQL_DATABASE} `; } if (answers.databases.includes("postgresql")) { envContent += ` # POSTGRESQL POSTGRES_URL=${envVars.POSTGRES_URL} `; } if (answers.databases.includes("mongoose")) { envContent += ` # MONGODB MONGO_URI=${envVars.MONGO_URI} `; } if (answers.databases.includes("redis")) { envContent += ` # REDIS REDIS_HOST=${envVars.REDIS_HOST} REDIS_PORT=${envVars.REDIS_PORT} REDIS_PASSWORD=${envVars.REDIS_PASSWORD} `; } if (answers.databases.includes("prisma")) { envContent += ` # PRISMA DATABASE_URL=${envVars.DATABASE_URL} `; } if (answers.databases.includes("typeorm")) { envContent += ` # TYPEORM TYPEORM_DATABASE_URL=${envVars.TYPEORM_DATABASE_URL} `; } if (answers.databases.includes("drizzle")) { envContent += ` # DRIZZLE DRIZZLE_DATABASE_URL=${envVars.DRIZZLE_DATABASE_URL} `; } if (answers.databases.includes("supabase")) { envContent += ` # SUPABASE SUPABASE_URL=${envVars.SUPABASE_URL} SUPABASE_ANON_KEY=${envVars.SUPABASE_ANON_KEY} `; } if (answers.databases.includes("firebase")) { envContent += ` # FIREBASE FIREBASE_API_KEY=${envVars.FIREBASE_API_KEY} FIREBASE_AUTH_DOMAIN=${envVars.FIREBASE_AUTH_DOMAIN} FIREBASE_PROJECT_ID=${envVars.FIREBASE_PROJECT_ID} FIREBASE_STORAGE_BUCKET=${envVars.FIREBASE_STORAGE_BUCKET} FIREBASE_MESSAGING_SENDER_ID=${envVars.FIREBASE_MESSAGING_SENDER_ID} FIREBASE_APP_ID=${envVars.FIREBASE_APP_ID} `; } if (answers.queues && answers.queues.includes("rabbitmq")) { envContent += ` # RABBITMQ RABBITMQ_URL=${envVars.RABBITMQ_URL} `; } if (answers.queues && answers.queues.includes("kafka")) { envContent += ` # KAFKA KAFKA_BROKER=${envVars.KAFKA_BROKER} `; } // Write the .env file fs.writeFileSync(path.join(targetDir, ".env"), envContent.trim()); // JWT utility file if (answers.useJWT) { const jwtUtilSrc = path.join(templatesDir, "utils", "jsonwebtoken.js"); const jwtUtilDest = path.join(targetDir, "utils", "jsonwebtoken.js"); if (fs.existsSync(jwtUtilSrc)) { fs.cpSync(jwtUtilSrc, jwtUtilDest); } else { // fallback: create a minimal file if template missing fs.writeFileSync(jwtUtilDest, `import jwt from "jsonwebtoken"; export const signToken = (payload, secret = process.env.JWT_SECRET, options = {}) => jwt.sign(payload, secret, options); export const verifyToken = (token, secret = process.env.JWT_SECRET) => jwt.verify(token, secret); `); } } console.log(chalk.green(`\n✅ Nodly App setup complete at: ${targetDir}\n`)); console.log( chalk.blue(`Run your app with: cd ${answers.projectName} && npm start`) ); console.log(chalk.blue(`Don't forget to configure your .env file!`)); console.log(chalk.blue(`Happy coding! 🚀\n`)); console.log(chalk.green("\n✔ All selected integrations have been set up!"));