UNPKG

mernboot

Version:

A powerful CLI tool for generating full-stack MERN applications...

875 lines (762 loc) • 29.3 kB
#!/usr/bin/env node import { execSync } from "child_process"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import readline from "readline"; import os from "os"; import chalk from "chalk"; import ora from "ora"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const question = (query) => new Promise((resolve) => rl.question(query, resolve)); // ASCII Art Banner const banner = ` __ __ _____ ____ _ _ ____ ___ ___ _____ | \\/ | ____| _ \\| \\ | | __ ) / _ \\ / _ \\_ _| | |\\/| | _| | |_) | \\| | _ \\| | | | | | || | | | | | |___| _ <| |\\ | |_) | |_| | |_| || | |_| |_|_____|_| \\_\\_| \\_|____/ \\___/ \\___/ |_| `; // Display banner with colors console.log(chalk.blue.bold(banner)); async function showMenu() { console.log("\n" + chalk.blue.bold("šŸš€ MERN Stack Project Generator")); console.log(chalk.gray("-----------------------------")); console.log(chalk.white("1. MongoDB Atlas Setup")); console.log(chalk.white("2. Get MongoDB URL")); console.log(chalk.white("3. Backend (Node.js + Express + MongoDB)")); console.log(chalk.white("4. Frontend (React + Vite + Tailwind)")); console.log(chalk.white("5. Full Stack (Backend + Frontend)")); console.log(chalk.white("6. Exit")); const choice = await question(chalk.yellow("\nSelect an option (1-6): ")); return choice; } async function showMongoDBMenu() { console.clear(); console.log(chalk.blue.bold("\n🌟 MongoDB Atlas Setup")); console.log(chalk.gray("-----------------------------")); // Display the note at the top of the menu console.log(chalk.yellow("\nāš ļø Authentication Note:")); console.log(chalk.gray("-----------------------------")); console.log( chalk.white( " * If you encounter authentication errors, use option 2 to logout" ) ); console.log(chalk.white(" * Then try creating a new cluster again")); console.log( chalk.white(" * This will help avoid 'already authenticated' errors") ); console.log(chalk.gray("-----------------------------\n")); console.log(chalk.white("1. Create new MongoDB Atlas cluster")); console.log(chalk.white("2. Logout from MongoDB Atlas")); console.log(chalk.white("3. Back to main menu")); return question(chalk.yellow("\nSelect an option (1-3): ")); } async function createViteReact(location) { const spinner = ora(); try { spinner.start(chalk.blue("Creating Vite React app...")); execSync(`npm create vite@latest ${location} -- --template react`, { stdio: "inherit", }); spinner.succeed(chalk.green("āœ… Vite React app created successfully")); // Change to the client directory and install dependencies const clientPath = path.join(process.cwd(), location); process.chdir(clientPath); spinner.start(chalk.blue("Installing client dependencies...")); execSync("npm install", { stdio: "inherit" }); spinner.succeed(chalk.green("āœ… Client dependencies installed")); spinner.start(chalk.blue("Setting up Tailwind CSS...")); execSync("npm install tailwindcss @tailwindcss/vite", { stdio: "inherit" }); spinner.succeed(chalk.green("āœ… Tailwind CSS installed")); // Update index.css with Tailwind directives const tailwindCss = `@import "tailwindcss";`; fs.writeFileSync(path.join(clientPath, "src", "index.css"), tailwindCss); const envViteContent = `VITE_BACKEND_URL=http://localhost:5000/api`; fs.writeFileSync(path.join(clientPath, ".env"), envViteContent); // Copy template files spinner.start(chalk.blue("Copying template files...")); // Copy infinity.svg to public folder const infinitySvgSource = path.join( __dirname, "configs", "React-Template", "infinity.svg" ); const infinitySvgDest = path.join(clientPath, "public", "infinity.svg"); fs.copyFileSync(infinitySvgSource, infinitySvgDest); // Copy App.jsx to src folder const appJsxSource = path.join( __dirname, "configs", "React-Template", "App.jsx" ); const appJsxDest = path.join(clientPath, "src", "App.jsx"); fs.copyFileSync(appJsxSource, appJsxDest); // Copy App.css to src folder const appCssSource = path.join( __dirname, "configs", "React-Template", "App.css" ); const appCssDest = path.join(clientPath, "src", "App.css"); fs.copyFileSync(appCssSource, appCssDest); // Update index.html to use infinity.svg as favicon and change title const indexHtmlPath = path.join(clientPath, "index.html"); let indexHtmlContent = fs.readFileSync(indexHtmlPath, "utf8"); indexHtmlContent = indexHtmlContent .replace('href="/vite.svg"', 'href="/infinity.svg"') .replace("<title>Vite + React</title>", "<title>MERNBOOT</title>"); fs.writeFileSync(indexHtmlPath, indexHtmlContent); spinner.succeed(chalk.green("Copied template files")); const viteConfigContent = ` import { defineConfig } from 'vite' import tailwindcss from '@tailwindcss/vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [tailwindcss(), react()] })`; fs.writeFileSync( path.join(clientPath, "vite.config.js"), viteConfigContent ); spinner.succeed(chalk.green("Set up Tailwind CSS and configuration")); // Return to the original directory process.chdir(process.cwd()); } catch (error) { spinner.fail( chalk.red(`Failed to create Vite React app: ${error.message}`) ); throw error; } } async function checkAtlasCLI() { try { // Check if atlas command exists and is executable const output = execSync("atlas --version", { encoding: 'utf8' }); if (output && output.trim()) { console.log(chalk.green("\nāœ… MongoDB Atlas CLI is already installed.")); console.log(chalk.gray("Version: " + output.trim())); return true; } return false; } catch (error) { return false; } } async function showAtlasInstallMenu() { console.log("\n" + chalk.blue.bold("šŸ“¦ MongoDB Atlas CLI Installation")); console.log(chalk.gray("-----------------------------")); console.log(chalk.white("1. Automatic Installation (Recommended)")); console.log(chalk.white("2. Manual Installation")); console.log(chalk.white("3. Back to previous menu")); const choice = await question(chalk.yellow("\nSelect an option (1-3): ")); return choice; } async function showManualInstallInstructions() { console.clear(); console.log(chalk.blue.bold("\nšŸ“‹ Manual Installation Instructions")); console.log(chalk.gray("-----------------------------")); console.log(chalk.white("\n1. Visit the official MongoDB Atlas CLI download page:")); console.log(chalk.cyan(" https://www.mongodb.com/try/download/atlas-cli")); console.log(chalk.white("\n2. Download the appropriate version for your operating system")); console.log(chalk.white("\n3. Follow the installation instructions for your platform:")); console.log(chalk.white("\n Windows:")); console.log(chalk.gray(" - Run the downloaded .msi installer")); console.log(chalk.gray(" - Follow the installation wizard")); console.log(chalk.gray(" - Add MongoDB Atlas CLI to your system PATH")); console.log(chalk.white("\n macOS:")); console.log(chalk.gray(" - Use Homebrew: brew install mongodb-atlas-cli")); console.log(chalk.white("\n Linux:")); console.log(chalk.gray(" - Use the package manager for your distribution")); console.log(chalk.gray(" - Or download and extract the binary")); console.log(chalk.white("\n4. Verify installation by running:")); console.log(chalk.cyan(" atlas --version")); console.log(chalk.gray("\n-----------------------------")); console.log(chalk.yellow("\nPress Enter to return to the previous menu...")); await question(""); } async function installAtlasCLI() { try { const installChoice = await showAtlasInstallMenu(); switch (installChoice) { case "1": const spinner = ora("Installing MongoDB Atlas CLI automatically...").start(); try { // Force install with all permissions and accept terms execSync("winget install -e --id MongoDB.MongoDBAtlasCLI --accept-source-agreements --accept-package-agreements --force", { stdio: "inherit" }); spinner.succeed(chalk.green("āœ… MongoDB Atlas CLI installed successfully!")); return true; } catch (error) { spinner.fail(chalk.red("āŒ Failed to install MongoDB Atlas CLI:") + " " + error.message); console.log(chalk.yellow("\nāš ļø Please try manual installation instead.")); return false; } case "2": await showManualInstallInstructions(); return false; case "3": return false; default: console.log(chalk.red("\nāŒ Invalid option. Please try again.")); return false; } } catch (error) { console.error(chalk.red("āŒ Installation process failed:") + " " + error.message); return false; } } async function checkAtlasAuth() { try { // First check atlas config list const configOutput = execSync("atlas config list", { encoding: "utf8" }); const hasProfile = configOutput.includes("default") || configOutput.trim().split("\n").length > 1; if (hasProfile) { // If we have a profile, we're logged in const profileName = configOutput.trim().split("\n")[1] || "default"; console.log(`\nāœ… Already logged in with profile: ${profileName}`); return true; } return false; } catch (error) { return false; } } async function storeAtlasConfig(config) { const userHomeDir = os.homedir(); const configDir = path.join(userHomeDir, ".mern-starter"); const configFile = path.join(configDir, "atlas-config.json"); // Create directory if it doesn't exist if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } // Store the configuration fs.writeFileSync(configFile, JSON.stringify(config, null, 2)); } async function getStoredAtlasConfig() { const userHomeDir = os.homedir(); const configFile = path.join( userHomeDir, ".mern-starter", "atlas-config.json" ); if (fs.existsSync(configFile)) { return JSON.parse(fs.readFileSync(configFile, "utf8")); } return null; } async function checkClusterStatus(projectId, clusterName) { try { const output = execSync( `atlas clusters describe ${clusterName} --projectId ${projectId}`, { encoding: "utf8" } ); const statusMatch = output.match(/StateName: ([A-Z_]+)/); return statusMatch ? statusMatch[1] : null; } catch (error) { return null; } } async function waitForClusterCreation(projectId, clusterName) { console.log("\nā³ Waiting for cluster to be ready..."); console.log("This may take a few minutes. Please wait..."); let status; let attempts = 0; const maxAttempts = 30; // 5 minutes total (10 seconds * 30) while (attempts < maxAttempts) { status = await checkClusterStatus(projectId, clusterName); if (status === "IDLE") { console.log("\nāœ… Cluster is ready!"); return true; } if (status === "CREATING") { process.stdout.write("."); await new Promise((resolve) => setTimeout(resolve, 10000)); // Wait 10 seconds attempts++; } else { console.log("\nāŒ Unexpected cluster status:", status); return false; } } console.log("\nāš ļø Cluster creation is taking longer than expected."); console.log("You can check the status later using 'Get MongoDB URL' option."); return false; } async function handleMongoDBLogout() { const spinner = ora("Logging out from MongoDB Atlas...").start(); try { execSync("atlas auth logout --force", { stdio: "inherit" }); spinner.succeed( chalk.green("āœ… Successfully logged out from MongoDB Atlas") ); console.log(chalk.yellow("\nPlease log in again to continue.")); return true; } catch (error) { spinner.fail(chalk.red("āŒ Failed to logout:") + " " + error.message); return false; } } async function setupMongoDBAtlas() { const spinner = ora(); try { const isAtlasInstalled = await checkAtlasCLI(); if (!isAtlasInstalled) { console.log(chalk.yellow("\nāš ļø MongoDB Atlas CLI is not installed.")); const installed = await installAtlasCLI(); if (!installed) { console.log(chalk.red("\nāŒ Failed to install MongoDB Atlas CLI")); throw new Error("Failed to install MongoDB Atlas CLI"); } // Verify installation after attempting to install const verifyInstall = await checkAtlasCLI(); if (!verifyInstall) { console.log(chalk.red("\nāŒ Installation verification failed")); throw new Error("MongoDB Atlas CLI installation verification failed"); } } while (true) { const mongoChoice = await showMongoDBMenu(); switch (mongoChoice) { case "1": // Check if already logged in using atlas config list const isLoggedIn = await checkAtlasAuth(); if (!isLoggedIn) { // Login to Atlas using browser authentication console.log("\nšŸ” Logging into MongoDB Atlas..."); console.log("Please follow the browser authentication process..."); try { execSync("atlas auth login", { stdio: "inherit" }); } catch (error) { console.error("\nāŒ Login failed:", error.message); continue; } } // Step 1: Create Project console.log("\nšŸ“ Creating MongoDB Atlas Project..."); const projectName = await question("Enter project name: "); const projectOutput = execSync( `atlas projects create "${projectName}"`, { encoding: "utf8" } ); const projectIdMatch = projectOutput.match( /Project '([a-zA-Z0-9]+)' created/ ); if (!projectIdMatch) { console.error("Project creation output:", projectOutput); throw new Error("Failed to get project ID from Atlas CLI output"); } const projectId = projectIdMatch[1]; console.log(`āœ… Project created with ID: ${projectId}`); // Step 2: Create Cluster console.log("\nšŸ”„ Creating MongoDB Atlas Cluster..."); const clusterName = (await question("Enter cluster name (default: myCluster): ")) || "myCluster"; try { execSync( `atlas clusters create ${clusterName} --projectId ${projectId} --provider AWS --region US_EAST_1 --tier M0`, { stdio: "inherit" } ); console.log(`\nāœ… Cluster ${clusterName} creation initiated`); } catch (error) { console.error(`āŒ Failed to create cluster: ${error.message}`); continue; } // Step 3: Create Database User console.log("\nšŸ‘¤ Creating Database User..."); const username = await question("Enter database username: "); const password = await question("Enter database password: "); try { execSync( `atlas dbusers create --username ${username} --password ${password} --projectId ${projectId} --role readWriteAnyDatabase`, { stdio: "inherit" } ); console.log(`āœ… Database user ${username} created successfully`); } catch (error) { console.error( `āŒ Failed to create database user: ${error.message}` ); continue; } // Step 4: Add IP Access List console.log("\n🌐 Adding IP Access List..."); try { execSync( `atlas accessList create 0.0.0.0/0 --comment "Allow access from anywhere" --projectId ${projectId}`, { stdio: "inherit" } ); console.log("āœ… IP Access List created successfully"); } catch (error) { if (error.message.includes("already exists")) { console.log("āœ… IP Access List already exists"); } else { console.error( `āŒ Failed to create IP Access List: ${error.message}` ); continue; } } // Store configuration for future use await storeAtlasConfig({ projectId, clusterName, username, }); // Wait for cluster to be ready const isReady = await waitForClusterCreation(projectId, clusterName); if (isReady) { console.log("\nāœ… MongoDB Atlas setup completed successfully!"); console.log( "\nYou can now use 'Get MongoDB URL' option to get your connection string." ); } else { console.log("\nāš ļø MongoDB Atlas setup is in progress."); console.log( "Please wait a few minutes and use 'Get MongoDB URL' option to check the status." ); } return await showMenu(); case "2": await handleMongoDBLogout(); continue; case "3": return await showMenu(); default: console.log("\nāŒ Invalid option. Please try again."); continue; } } } catch (error) { console.error("\nāŒ MongoDB Atlas setup failed:", error.message); return await showMenu(); } } async function getMongoDBUrlFromConfig() { try { const config = await getStoredAtlasConfig(); if (!config) { console.log( "\nāŒ No MongoDB Atlas configuration found. Please set up MongoDB Atlas first." ); return null; } console.log("\nšŸ” Getting MongoDB URL from existing configuration..."); console.log(`Project ID: ${config.projectId}`); console.log(`Cluster Name: ${config.clusterName}`); // Get fresh connection string let connectionString; try { connectionString = execSync( `atlas clusters connectionString get ${config.clusterName} --projectId ${config.projectId}`, { encoding: "utf8" } ).trim(); } catch (error) { if (error.message.includes("<nil>")) { console.log( "\nā³ Cluster is still being created. Please wait a few minutes and try again." ); console.log("This usually takes 3-5 minutes after cluster creation."); console.log("You can check the status again using option 2."); return null; } console.log( "\nāš ļø Failed to get connection string from existing configuration." ); return null; } // Check if connection string contains <nil> if (connectionString.includes("<nil>")) { console.log( "\nā³ Cluster is still being created. Please wait a few minutes and try again." ); console.log("This usually takes 3-5 minutes after cluster creation."); console.log("You can check the status again using option 2."); return null; } // Get password from user const password = await question("\nEnter your database password: "); // Clean up connection string and construct full MongoDB URI const cleanConnectionString = connectionString .replace("STANDARD CONNECTION STRING\n", "") .trim(); const mongoUri = cleanConnectionString.replace( "mongodb+srv://", `mongodb+srv://${config.username}:${password}@` ) + `/${config.clusterName}?retryWrites=true&w=majority`; console.log("\nāœ… MongoDB URL retrieved successfully!"); console.log("\nšŸ“‹ Your MongoDB URL:"); console.log(mongoUri); // Automatically save to .env file without prompting const envPath = path.join(process.cwd(), ".env"); let envContent = ""; // Read existing .env content if it exists if (fs.existsSync(envPath)) { envContent = fs.readFileSync(envPath, "utf8"); // Remove existing MONGO_URI if it exists envContent = envContent.replace(/MONGO_URI=.*\n?/g, ""); // Ensure content ends with a newline if (!envContent.endsWith("\n")) { envContent += "\n"; } } // Add new MONGO_URI at the beginning envContent = `MONGO_URI=${mongoUri}\n${envContent}`; // Write back to .env fs.writeFileSync(envPath, envContent); console.log("\nāœ… MongoDB URL automatically saved to .env file"); return mongoUri; } catch (error) { console.error("\nāŒ Failed to get MongoDB URL:", error.message); return null; } } async function showDatabaseUrlMenu() { console.log("\n" + chalk.blue.bold("šŸ“¦ Database URL Selection")); console.log(chalk.gray("-----------------------------")); console.log(chalk.white("1. Use recently created MongoDB URL")); console.log(chalk.white("2. Enter custom MongoDB URL")); console.log(chalk.white("3. Create new MongoDB Atlas cluster")); console.log(chalk.white("4. Skip MongoDB setup for now")); console.log(chalk.white("5. Back to main menu")); const choice = await question(chalk.yellow("\nSelect an option (1-5): ")); return choice; } async function getDatabaseUrl() { while (true) { const dbChoice = await showDatabaseUrlMenu(); switch (dbChoice) { case "1": const recentUrl = await getMongoDBUrlFromConfig(); if (recentUrl) { return recentUrl; } console.log( chalk.yellow( "\nāš ļø No recent MongoDB URL found. Please choose another option." ) ); break; case "2": console.log(chalk.yellow("\nšŸ“‹ Please paste your MongoDB URL below:")); console.log(chalk.gray("(You can use right-click to paste)")); const customUrl = await question(chalk.yellow("\nMongoDB URL: ")); if (customUrl && customUrl.trim()) { // Validate the URL format if ( customUrl.startsWith("mongodb://") || customUrl.startsWith("mongodb+srv://") ) { return customUrl.trim(); } else { console.log( chalk.red( "\nāŒ Invalid MongoDB URL format. URL should start with 'mongodb://' or 'mongodb+srv://'" ) ); continue; } } console.log(chalk.red("\nāŒ Invalid URL. Please try again.")); break; case "3": await setupMongoDBAtlas(); const newUrl = await getMongoDBUrlFromConfig(); if (newUrl) { return newUrl; } console.log( chalk.yellow("\nāš ļø Failed to get new MongoDB URL. Please try again.") ); break; case "4": console.log( chalk.yellow( "\nāš ļø Skipping MongoDB setup. You can set it up later by updating the .env file." ) ); return "mongodb://localhost:27017/your-database"; // Default local MongoDB URL case "5": return null; default: console.log(chalk.red("\nāŒ Invalid option. Please try again.")); } } } // Update setupBackend function to handle skipped MongoDB setup async function setupBackend() { try { // Initialize server const spinner = ora("Initializing server...").start(); // Read and write package.json const templatePackageJson = JSON.parse( fs.readFileSync( path.join(__dirname, "configs", "packagejson", "package.json"), "utf8" ) ); fs.writeFileSync( path.join(process.cwd(), "package.json"), JSON.stringify(templatePackageJson, null, 2) ); // Create server.js from template spinner.succeed(chalk.green("āœ… Server initialized")); spinner.start("Creating server.js..."); const serverTemplate = fs.readFileSync( path.join(__dirname, "configs", "server", "server.js"), "utf8" ); fs.writeFileSync(path.join(process.cwd(), "server.js"), serverTemplate); spinner.succeed(chalk.green("āœ… Server.js created")); // Create project structure spinner.start("Creating project structure..."); const folders = [ "controllers", "models", "routes", "config", "middleware", "public", "services", ]; folders.forEach((folder) => { if (!fs.existsSync(path.join(process.cwd(), folder))) { fs.mkdirSync(path.join(process.cwd(), folder)); } }); spinner.succeed(chalk.green("āœ… Project structure created")); // Create MongoDB configuration files spinner.start("Setting up MongoDB configuration..."); const mongooseFolder = path.join(process.cwd(), "config", "mongoose"); if (!fs.existsSync(mongooseFolder)) { fs.mkdirSync(mongooseFolder, { recursive: true }); } // Copy MongoDB configuration file const dbJs = fs.readFileSync( path.join(__dirname, "configs", "mongoose", "db.js"), "utf8" ); fs.writeFileSync(path.join(mongooseFolder, "database.js"), dbJs); spinner.succeed(chalk.green("āœ… MongoDB configuration created")); // Get MongoDB URL const mongoUrl = await getDatabaseUrl(); if (!mongoUrl) { console.log(chalk.yellow("\nāš ļø Backend setup cancelled.")); return; } // Create .env file with MongoDB URL spinner.start("Creating .env file..."); let envContent = `PORT=5000\nNODE_ENV=development\n`; if (mongoUrl === "mongodb://localhost:27017/your-database") { envContent += `# TODO: Replace this with your MongoDB connection string\n`; envContent += `MONGO_URI=${mongoUrl}\n`; console.log( chalk.yellow( "\nāš ļø Using default local MongoDB URL. Remember to update it in .env file later." ) ); } else { envContent += `MONGO_URI=${mongoUrl}\n`; } fs.writeFileSync(path.join(process.cwd(), ".env"), envContent); spinner.succeed(chalk.green("āœ… .env file created")); // Install server dependencies spinner.start("Installing server dependencies..."); execSync("npm install", { stdio: "inherit" }); spinner.succeed(chalk.green("āœ… Server dependencies installed")); console.log(chalk.green("\nāœ… Backend setup completed successfully!")); console.log(chalk.blue("\nšŸš€ To start the server:")); console.log(chalk.white("npm run dev")); } catch (error) { console.error( chalk.red("\nāŒ Backend setup failed:") + " " + error.message ); } } async function setupFrontend() { try { // Ask about React client location console.log("\nšŸ“± React Client Setup"); console.log("-----------------------------"); const defaultLocation = "client"; const clientLocation = await question( `Where would you like to create the React client? (Press Enter for '${defaultLocation}'): ` ); // Create Vite React app await createViteReact(clientLocation || defaultLocation); console.log("\nāœ… Frontend setup completed successfully!"); console.log("\nšŸš€ To start the frontend:"); console.log(`cd ${clientLocation || defaultLocation} && npm run dev`); } catch (error) { console.error("\nāŒ Frontend setup failed:", error.message); } } async function setupProject() { try { let choice = await showMenu(); while (choice !== "6") { switch (choice) { case "1": choice = await setupMongoDBAtlas(); break; case "2": await getMongoDBUrlFromConfig(); choice = await showMenu(); break; case "3": await setupBackend(); choice = await showMenu(); break; case "4": await setupFrontend(); choice = await showMenu(); break; case "5": await setupBackend(); await setupFrontend(); const spinner = ora("Setting up development environment...").start(); // Install concurrently for running both server and client execSync("npm install -D concurrently", { stdio: "inherit" }); spinner.succeed( chalk.green("āœ… Development environment setup completed") ); console.log( chalk.green("\nāœ… Full Stack setup completed successfully!") ); console.log(chalk.blue("\nšŸš€ To start development:")); console.log(chalk.white("1. In one terminal: npm run dev:server")); console.log( chalk.white("2. In another terminal: cd client && npm run dev") ); console.log( chalk.white(" Or simply run: npm run dev (to start both)") ); choice = await showMenu(); break; default: console.log(chalk.red("\nāŒ Invalid option. Please try again.")); choice = await showMenu(); } } console.log(chalk.blue("\nšŸ‘‹ Goodbye!")); process.exit(0); } catch (error) { console.error(chalk.red("\nāŒ Setup failed:") + " " + error.message); } finally { rl.close(); } } setupProject();