diff --git a/index.js b/index.js index e04659a..38a4d23 100644 --- a/index.js +++ b/index.js @@ -33,7 +33,10 @@ async function askStackQuestions() { { name: chalk.bold.cyan("MEVN") + " → MongoDB + Express + Vue.js + Node.js", value: "mevn" }, { name: chalk.bold.yellow("MEVN") + " + Tailwind + Auth", value: "mevn+tailwind+auth" }, { name: chalk.bold.yellow("Next.js") + " + tRPC + Prisma + Tailwind + Auth", value: "t3-stack" }, - { name: chalk.bold.red("Hono") + " → Hono + Prisma + React", value: "hono" } + { name: chalk.bold.red("Hono") + " → Hono + Prisma + React", value: "hono" }, + { name: chalk.bold.blue("Express TS Pro") + " → Production-ready TypeScript Express backend", value: "express-ts-pro" } + ,{ name: chalk.bold.cyan("Next.js + Express") + " → Separate backend with Express.js", value: "next-express" } + ,{ name: chalk.bold.green("MERN Turborepo") + " → Monorepo with Turbo (apps/client, apps/server)", value: "mern-turbo" } ], pageSize: 10, diff --git a/templates/express-ts-pro/server/README.md b/templates/express-ts-pro/server/README.md new file mode 100644 index 0000000..a5d4a1f --- /dev/null +++ b/templates/express-ts-pro/server/README.md @@ -0,0 +1,35 @@ +Express TS Pro (Celtrix) + +Production-ready TypeScript + Express backend template with: + +- Logging (Winston) with JSON logs and request IDs +- Security: Helmet, CORS, rate limiting +- Role-based auth (JWT-based; roles via claims) +- Basic notification service (email via Nodemailer) +- File utilities: read XLSX and PDF +- Clean project structure and error handling + +Quick Start + +``` +cp .env.example .env +npm install +npm run dev +``` + +Health check: GET `/health` + +Scripts +- npm run dev: Run with ts-node + nodemon +- npm run build: Type-check and transpile to dist +- npm start: Run compiled app + +Env +- PORT (default 4000) +- JWT_SECRET (required for auth) +- SMTP vars for notifications (optional) + +Notes +- Upload endpoint: POST `/files/parse` (field: `file`) parses XLSX or PDF. +- Protected example: GET `/admin/stats` requires role `admin`. + diff --git a/templates/express-ts-pro/server/package.json b/templates/express-ts-pro/server/package.json new file mode 100644 index 0000000..94dbfbd --- /dev/null +++ b/templates/express-ts-pro/server/package.json @@ -0,0 +1,38 @@ +{ + "name": "express-ts-pro-server", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "nodemon --watch src --ext ts --exec ts-node src/server.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/server.js" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "express-rate-limit": "^7.4.0", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "nodemailer": "^6.9.14", + "pdf-parse": "^1.1.1", + "socket.io": "^4.8.1", + "useragent": "^2.3.0", + "uuid": "^9.0.1", + "winston": "^3.13.0", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", + "@types/multer": "^1.4.12", + "@types/node": "^22.7.4", + "nodemon": "^3.1.7", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } +} + diff --git a/templates/express-ts-pro/server/src/app.ts b/templates/express-ts-pro/server/src/app.ts new file mode 100644 index 0000000..c982ceb --- /dev/null +++ b/templates/express-ts-pro/server/src/app.ts @@ -0,0 +1,28 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import { rateLimiter } from './setup/rateLimiter'; +import { requestLogger } from './setup/requestLogger'; +import { errorHandler, notFoundHandler } from './setup/errorHandlers'; +import { router as apiRouter } from './setup/routes'; + +export function createApp() { + const app = express(); + + app.use(helmet()); + app.use(cors()); + app.use(express.json({ limit: '2mb' })); + app.use(requestLogger); + app.use(rateLimiter); + + app.get('/health', (_req, res) => res.json({ status: 'ok' })); + + app.use('/api/v1', apiRouter); + + app.use(notFoundHandler); + app.use(errorHandler); + + return app; +} + + diff --git a/templates/express-ts-pro/server/src/server.ts b/templates/express-ts-pro/server/src/server.ts new file mode 100644 index 0000000..f70cf8f --- /dev/null +++ b/templates/express-ts-pro/server/src/server.ts @@ -0,0 +1,27 @@ +import 'dotenv/config'; +import { createApp } from './app'; +import { logger } from './setup/logger'; +import { createServer } from 'http'; +import { Server } from 'socket.io'; +import { MonitoringHub } from './telemetry/monitoringHub'; + +const app = createApp(); +const httpServer = createServer(app); +const io = new Server(httpServer, { cors: { origin: '*' } }); +MonitoringHub.attach(io); + +const port = Number(process.env.PORT) || 4000; +const server = httpServer.listen(port, () => { + logger.info('server_started', { port, env: process.env.NODE_ENV || 'development' }); +}); + +process.on('unhandledRejection', (reason: any) => { + logger.error('unhandled_rejection', { reason: reason?.message || String(reason) }); +}); + +process.on('uncaughtException', (err) => { + logger.error('uncaught_exception', { message: err.message, stack: err.stack }); + server.close(() => process.exit(1)); +}); + + diff --git a/templates/express-ts-pro/server/src/setup/errorHandlers.ts b/templates/express-ts-pro/server/src/setup/errorHandlers.ts new file mode 100644 index 0000000..5ae3996 --- /dev/null +++ b/templates/express-ts-pro/server/src/setup/errorHandlers.ts @@ -0,0 +1,14 @@ +import { Request, Response, NextFunction } from 'express'; +import { logger } from './logger'; + +export function notFoundHandler(_req: Request, res: Response) { + res.status(404).json({ error: 'Not found' }); +} + +export function errorHandler(err: any, _req: Request, res: Response, _next: NextFunction) { + logger.error('unhandled_error', { message: err?.message, stack: err?.stack }); + const status = typeof err?.status === 'number' ? err.status : 500; + res.status(status).json({ error: err?.message || 'Internal Server Error' }); +} + + diff --git a/templates/express-ts-pro/server/src/setup/index.ts b/templates/express-ts-pro/server/src/setup/index.ts new file mode 100644 index 0000000..fa592ec --- /dev/null +++ b/templates/express-ts-pro/server/src/setup/index.ts @@ -0,0 +1,7 @@ +export * from './errorHandlers'; +export * from './logger'; +export * from './rateLimiter'; +export * from './requestLogger'; +export * from './routes'; + + diff --git a/templates/express-ts-pro/server/src/setup/logger.ts b/templates/express-ts-pro/server/src/setup/logger.ts new file mode 100644 index 0000000..ba5b8fe --- /dev/null +++ b/templates/express-ts-pro/server/src/setup/logger.ts @@ -0,0 +1,37 @@ +import { createLogger, format, transports } from 'winston'; +import path from 'path'; +import fs from 'fs'; +import { v4 as uuid } from 'uuid'; +import { Request, Response, NextFunction } from 'express'; + +const logDir = path.join(process.cwd(), 'src', 'log'); +if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true }); + +export const logger = createLogger({ + level: 'info', + format: format.combine( + format.timestamp(), + format.errors({ stack: true }), + format.json(), + ), + transports: [ + new transports.Console({ + format: format.combine( + format.colorize(), + format.printf(({ level, message, timestamp, ...meta }) => { + return `${timestamp} ${level}: ${message} ${Object.keys(meta).length ? JSON.stringify(meta) : ''}`; + }) + ) + }), + new transports.File({ filename: path.join(logDir, 'error.log'), level: 'error' }), + new transports.File({ filename: path.join(logDir, 'warning.log'), level: 'warn' }), + new transports.File({ filename: path.join(logDir, 'success.log'), level: 'info' }) + ], +}); + +export function withRequestContext(req: Request, _res: Response, next: NextFunction) { + (req as any).requestId = (req.headers['x-request-id'] as string) || uuid(); + next(); +} + + diff --git a/templates/express-ts-pro/server/src/setup/rateLimiter.ts b/templates/express-ts-pro/server/src/setup/rateLimiter.ts new file mode 100644 index 0000000..1b3b9b1 --- /dev/null +++ b/templates/express-ts-pro/server/src/setup/rateLimiter.ts @@ -0,0 +1,10 @@ +import rateLimit from 'express-rate-limit'; + +export const rateLimiter = rateLimit({ + windowMs: 60 * 1000, + limit: 100, + standardHeaders: true, + legacyHeaders: false, +}); + + diff --git a/templates/express-ts-pro/server/src/setup/requestLogger.ts b/templates/express-ts-pro/server/src/setup/requestLogger.ts new file mode 100644 index 0000000..df61f21 --- /dev/null +++ b/templates/express-ts-pro/server/src/setup/requestLogger.ts @@ -0,0 +1,30 @@ +import { Request, Response, NextFunction } from 'express'; +import { logger } from './logger'; +import useragent from 'useragent'; +import { MonitoringHub } from '../telemetry/monitoringHub'; + +export function requestLogger(req: Request, res: Response, next: NextFunction) { + const start = Date.now(); + res.on('finish', () => { + const durationMs = Date.now() - start; + const agent = useragent.parse(req.headers['user-agent'] || ''); + const payload = { + method: req.method, + path: req.originalUrl, + status: res.statusCode, + durationMs, + ip: req.ip, + ua: { + family: agent.family, + os: agent.os?.toString(), + device: agent.device?.toString() + }, + timestamp: new Date().toISOString() + }; + logger.info('http_request', payload); + MonitoringHub.emitEvent('http_request', payload); + }); + next(); +} + + diff --git a/templates/express-ts-pro/server/src/setup/routes.ts b/templates/express-ts-pro/server/src/setup/routes.ts new file mode 100644 index 0000000..1b1f8ef --- /dev/null +++ b/templates/express-ts-pro/server/src/setup/routes.ts @@ -0,0 +1,17 @@ +import { Router } from 'express'; +import { authenticate, requireRoles } from '../system/auth'; +import { router as filesRouter } from '../system/files'; +import { router as notifyRouter } from '../system/notify'; +import { dashboardRouter } from '../telemetry/dashboard'; + +export const router = Router(); + +router.use('/files', authenticate, filesRouter); +router.use('/notify', authenticate, notifyRouter); +router.use('/monitor', dashboardRouter); + +router.get('/admin/stats', authenticate, requireRoles('admin'), (_req, res) => { + res.json({ users: 0, uptime: process.uptime() }); +}); + + diff --git a/templates/express-ts-pro/server/src/system/auth.ts b/templates/express-ts-pro/server/src/system/auth.ts new file mode 100644 index 0000000..68dc2e6 --- /dev/null +++ b/templates/express-ts-pro/server/src/system/auth.ts @@ -0,0 +1,40 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; + +export type JwtUser = { + sub: string; + roles?: string[]; +}; + +declare global { + namespace Express { + interface Request { + user?: JwtUser; + } + } +} + +export function authenticate(req: Request, res: Response, next: NextFunction) { + const authHeader = req.headers.authorization || ''; + const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : undefined; + if (!token) return res.status(401).json({ error: 'Missing token' }); + try { + const secret = process.env.JWT_SECRET || 'dev_secret'; + const decoded = jwt.verify(token, secret) as JwtUser; + req.user = decoded; + next(); + } catch { + return res.status(401).json({ error: 'Invalid token' }); + } +} + +export function requireRoles(...roles: string[]) { + return (req: Request, res: Response, next: NextFunction) => { + const userRoles = req.user?.roles || []; + const ok = roles.every((r) => userRoles.includes(r)); + if (!ok) return res.status(403).json({ error: 'Forbidden' }); + next(); + }; +} + + diff --git a/templates/express-ts-pro/server/src/system/files.ts b/templates/express-ts-pro/server/src/system/files.ts new file mode 100644 index 0000000..e50009a --- /dev/null +++ b/templates/express-ts-pro/server/src/system/files.ts @@ -0,0 +1,52 @@ +import { Router } from 'express'; +import multer from 'multer'; +import path from 'path'; +import fs from 'fs'; +import xlsx from 'xlsx'; +import pdfParse from 'pdf-parse'; + +export const router = Router(); + +const diskStorage = multer.diskStorage({ + destination: (_req, file, cb) => { + const mime = file.mimetype; + let folder = 'documents'; + if (mime.startsWith('image/')) folder = 'images'; + else if (mime.startsWith('video/')) folder = 'videos'; + const uploadDir = path.join(process.cwd(), 'uploads', folder); + fs.mkdirSync(uploadDir, { recursive: true }); + cb(null, uploadDir); + }, + filename: (_req, file, cb) => { + const unique = Date.now() + '-' + Math.round(Math.random() * 1e9); + cb(null, unique + path.extname(file.originalname)); + } +}); + +const upload = multer({ storage: diskStorage, limits: { fileSize: 10 * 1024 * 1024 } }); + +router.post('/parse', upload.single('file'), async (req, res, next) => { + try { + if (!req.file) return res.status(400).json({ error: 'file is required' }); + const mime = req.file.mimetype; + if (mime === 'application/pdf') { + const data = await pdfParse(req.file.buffer); + return res.json({ type: 'pdf', text: data.text.slice(0, 5000) }); + } + if ( + mime === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || + mime === 'application/vnd.ms-excel' || + mime === 'application/octet-stream' + ) { + const wb = xlsx.readFile(req.file.path); + const first = wb.SheetNames[0]; + const rows = xlsx.utils.sheet_to_json(wb.Sheets[first], { header: 1 }); + return res.json({ type: 'xlsx', rows }); + } + return res.status(415).json({ error: 'Unsupported file type' }); + } catch (e) { + next(e); + } +}); + + diff --git a/templates/express-ts-pro/server/src/system/notify.ts b/templates/express-ts-pro/server/src/system/notify.ts new file mode 100644 index 0000000..d435bf9 --- /dev/null +++ b/templates/express-ts-pro/server/src/system/notify.ts @@ -0,0 +1,56 @@ +import { Router } from 'express'; +import nodemailer from 'nodemailer'; +// Stubs for future channels (Twilio, Telegram, Slack, Discord, Meta) +// Implement actual clients and secrets via env vars when available. + +export const router = Router(); + +const transporter = nodemailer.createTransport({ + host: process.env.NOTIFY_SMTP_HOST, + port: Number(process.env.NOTIFY_SMTP_PORT || 587), + secure: false, + auth: process.env.NOTIFY_SMTP_USER + ? { user: process.env.NOTIFY_SMTP_USER, pass: process.env.NOTIFY_SMTP_PASS } + : undefined, +}); + +router.post('/email', async (req, res, next) => { + try { + const { to, subject, text, html } = req.body || {}; + if (!to || !subject || (!text && !html)) { + return res.status(400).json({ error: 'to, subject, and text|html are required' }); + } + const from = process.env.NOTIFY_FROM || 'no-reply@example.com'; + const info = await transporter.sendMail({ from, to, subject, text, html }); + res.json({ messageId: info.messageId }); + } catch (e) { + next(e); + } +}); + +router.post('/sms', async (_req, res) => { + // TODO: Integrate Twilio + res.json({ status: 'queued', channel: 'sms' }); +}); + +router.post('/whatsapp', async (_req, res) => { + // TODO: Integrate Twilio WhatsApp + res.json({ status: 'queued', channel: 'whatsapp' }); +}); + +router.post('/telegram', async (_req, res) => { + // TODO: Integrate Telegram bot + res.json({ status: 'queued', channel: 'telegram' }); +}); + +router.post('/slack', async (_req, res) => { + // TODO: Integrate Slack webhook + res.json({ status: 'queued', channel: 'slack' }); +}); + +router.post('/discord', async (_req, res) => { + // TODO: Integrate Discord webhook + res.json({ status: 'queued', channel: 'discord' }); +}); + + diff --git a/templates/express-ts-pro/server/src/telemetry/dashboard.ts b/templates/express-ts-pro/server/src/telemetry/dashboard.ts new file mode 100644 index 0000000..db6b302 --- /dev/null +++ b/templates/express-ts-pro/server/src/telemetry/dashboard.ts @@ -0,0 +1,49 @@ +import { Router } from 'express'; +import { authenticate, requireRoles } from '../system/auth'; +import { MonitoringHub } from './monitoringHub'; + +export const dashboardRouter = Router(); + +dashboardRouter.get('/events', authenticate, requireRoles('admin'), (_req, res) => { + res.json({ events: MonitoringHub.getRecent() }); +}); + +dashboardRouter.get('/', authenticate, requireRoles('admin'), (_req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.send(` + + + + Realtime Monitoring + + + + + +

Real-time API Monitor

+
+ + `); +}); + + diff --git a/templates/express-ts-pro/server/src/telemetry/monitoringHub.ts b/templates/express-ts-pro/server/src/telemetry/monitoringHub.ts new file mode 100644 index 0000000..7eb6948 --- /dev/null +++ b/templates/express-ts-pro/server/src/telemetry/monitoringHub.ts @@ -0,0 +1,33 @@ +import { Server } from 'socket.io'; + +type HttpEvent = { + method: string; + path: string; + status: number; + durationMs: number; + ip: string; + ua?: { family?: string; os?: string; device?: string }; + timestamp: string; +}; + +export class MonitoringHub { + private static io: Server | null = null; + private static buffer: HttpEvent[] = []; + private static maxBuffer = 1000; + + static attach(io: Server) { + MonitoringHub.io = io; + } + + static emitEvent(event: 'http_request', data: HttpEvent) { + MonitoringHub.buffer.push(data); + if (MonitoringHub.buffer.length > MonitoringHub.maxBuffer) MonitoringHub.buffer.shift(); + if (MonitoringHub.io) MonitoringHub.io.emit(event, data); + } + + static getRecent(): HttpEvent[] { + return MonitoringHub.buffer.slice(-200); + } +} + + diff --git a/templates/express-ts-pro/server/src/types/global.d.ts b/templates/express-ts-pro/server/src/types/global.d.ts new file mode 100644 index 0000000..ae146e6 --- /dev/null +++ b/templates/express-ts-pro/server/src/types/global.d.ts @@ -0,0 +1,7 @@ +declare namespace Express { + export interface Request { + requestId?: string; + } +} + + diff --git a/templates/express-ts-pro/server/tsconfig.json b/templates/express-ts-pro/server/tsconfig.json new file mode 100644 index 0000000..134a1c9 --- /dev/null +++ b/templates/express-ts-pro/server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "moduleResolution": "Node", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} + diff --git a/utils/installer.js b/utils/installer.js index 2e07232..e810e3d 100644 --- a/utils/installer.js +++ b/utils/installer.js @@ -29,6 +29,128 @@ export function installDependencies(projectPath, config, projectName,server=true } } +export function writeDockerArtifacts(projectPath, config){ + try{ + const hasServer = fs.existsSync(path.join(projectPath,'server')); + const hasClient = fs.existsSync(path.join(projectPath,'client')); + const hasT3 = fs.existsSync(path.join(projectPath,'t3-app')); + + // Server Dockerfile + if(hasServer){ + const serverDockerfile = path.join(projectPath,'server','Dockerfile'); + if(!fs.existsSync(serverDockerfile)){ + const serverContent = `FROM node:20-alpine\nWORKDIR /app\nCOPY package*.json ./\nRUN npm install --omit=dev=false\nCOPY . .\n# Build if TypeScript project\nRUN [ -f tsconfig.json ] && npm run build || echo "no build step"\nENV NODE_ENV=production\nEXPOSE 4000\nCMD [ "sh", "-c", "[ -d dist ] && npm start || npm run dev" ]\n`; + fs.writeFileSync(serverDockerfile, serverContent); + } + } + + // Client Dockerfile + if(hasClient){ + const clientDockerfile = path.join(projectPath,'client','Dockerfile'); + if(!fs.existsSync(clientDockerfile)){ + const clientContent = `FROM node:20-alpine\nWORKDIR /app\nCOPY package*.json ./\nRUN npm install\nCOPY . .\nEXPOSE 5173\nCMD [ "npm", "run", "dev", "--", "--host" ]\n`; + fs.writeFileSync(clientDockerfile, clientContent); + } + } + + // Next.js Dockerfile (t3-app) + if(hasT3){ + const nextDockerfile = path.join(projectPath,'t3-app','Dockerfile'); + if(!fs.existsSync(nextDockerfile)){ + const nextContent = `FROM node:20-alpine AS deps\nWORKDIR /app\nCOPY package*.json ./\nRUN npm install --omit=dev=false\n\nFROM node:20-alpine AS builder\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY . .\nRUN npm run build\n\nFROM node:20-alpine AS runner\nWORKDIR /app\nENV NODE_ENV=production\nCOPY --from=builder /app/.next ./.next\nCOPY --from=builder /app/node_modules ./node_modules\nCOPY --from=builder /app/package*.json ./\nEXPOSE 3000\nCMD [ "npm", "start" ]\n`; + fs.writeFileSync(nextDockerfile, nextContent); + } + } + + // docker-compose.yml at root + const composePath = path.join(projectPath,'docker-compose.yml'); + if(!fs.existsSync(composePath)){ + const clientService = hasT3 ? `\n client:\n build:\n context: ./t3-app\n ports:\n - "3000:3000"\n environment:\n - NODE_ENV=development\n volumes:\n - ./t3-app:/app\n - /app/node_modules\n` : (hasClient ? `\n client:\n build:\n context: ./client\n ports:\n - "5173:5173"\n environment:\n - NODE_ENV=development\n volumes:\n - ./client:/app\n - /app/node_modules\n` : ''); + + const serverService = hasServer ? `\n server:\n build:\n context: ./server\n ports:\n - "4000:4000"\n environment:\n - NODE_ENV=development\n - PORT=4000\n volumes:\n - ./server:/app\n - /app/node_modules\n` : ''; + + const compose = `version: "3.9"\nservices:${clientService}${serverService}`; + fs.writeFileSync(composePath, compose); + } + + logger.info("🐳 Docker artifacts generated"); + }catch(error){ + logger.error("❌ Failed to write Docker artifacts"); + throw error; + } +} + +export function turboMernSetup(projectPath, config, projectName){ + try{ + logger.info("⚡ Setting up MERN Turborepo..."); + // root package.json with workspaces + const rootPkg = { + name: projectName, + private: true, + version: "0.1.0", + packageManager: "npm@10", + workspaces: ["apps/*", "packages/*"], + scripts: { + dev: "turbo run dev", + build: "turbo run build", + lint: "turbo run lint" + }, + devDependencies: { + turbo: "^2.1.2" + } + }; + fs.writeFileSync(path.join(projectPath,'package.json'), JSON.stringify(rootPkg, null, 2)); + fs.mkdirSync(path.join(projectPath,'apps'), { recursive: true }); + + // scaffold client via Vite React + if(config.language==='typescript'){ + execSync(`npm create vite@latest client -- --t react-ts --no-rolldown --no-interactive`, { cwd: path.join(projectPath,'apps'), stdio: 'inherit', shell: true }); + }else{ + execSync(`npm create vite@latest client -- --t react --no-rolldown --no-interactive`, { cwd: path.join(projectPath,'apps'), stdio: 'inherit', shell: true }); + } + // move to apps/client already created by vite + + // scaffold server from express-ts-pro template + const serverDir = path.join(projectPath,'apps','server'); + fs.mkdirSync(serverDir, { recursive: true }); + const fromServer = path.join(process.cwd(), 'templates','express-ts-pro','server'); + fs.cpSync(fromServer, serverDir, { recursive: true }); + + // turbo.json + const turbo = { + $schema: "https://turbo.build/schema.json", + pipeline: { + build: { + dependsOn: ["^build"], + outputs: ["dist/**", ".next/**"] + }, + dev: { + cache: false + }, + lint: {} + } + }; + fs.writeFileSync(path.join(projectPath,'turbo.json'), JSON.stringify(turbo, null, 2)); + + // client package add turbo scripts + const clientPkgPath = path.join(projectPath, 'apps','client','package.json'); + if(fs.existsSync(clientPkgPath)){ + const pkg = JSON.parse(fs.readFileSync(clientPkgPath,'utf-8')); + pkg.scripts = pkg.scripts || {}; + pkg.scripts.dev = pkg.scripts.dev || 'vite'; + pkg.scripts.build = pkg.scripts.build || 'vite build'; + pkg.scripts.lint = pkg.scripts.lint || 'echo "no lint"'; + fs.writeFileSync(clientPkgPath, JSON.stringify(pkg, null, 2)); + } + + // server package already present + + logger.info("✅ MERN Turborepo created (apps/client, apps/server)"); + }catch(error){ + logger.error("❌ Failed to set up MERN Turborepo"); + throw error; + } +} export function angularSetup(projectPath, config, projectName) { logger.info("⚡ Setting up Angular..."); @@ -412,4 +534,25 @@ export function mevnSetup(projectPath,config,projectName){ logger.error("❌ Failed to set up MEVN"); throw error; } +} + +export function nextExpressSetup(projectPath, config, projectName){ + try{ + logger.info("⚡ Setting up Next.js + Express..."); + const isTs = config.language === 'typescript'; + const tmpl = isTs ? 'next-app --ts' : 'next-app'; + execSync(`npx create-next-app@latest client --${isTs ? 'ts' : ''} --eslint --app --tailwind --src-dir --import-alias @/* --no-git --yes`, { cwd: projectPath, stdio: "inherit", shell: true }); + + // prepare express server using existing template + execSync(`mkdir server`, { cwd: projectPath, shell: true }); + // copy from our templates/express-ts-pro/server into project server + const from = path.join(process.cwd(), 'templates','express-ts-pro','server'); + const to = path.join(projectPath, 'server'); + fs.cpSync(from, to, { recursive: true }); + + logger.info("✅ Next.js + Express setup complete"); + }catch(error){ + logger.error("❌ Failed to set up Next.js + Express"); + throw error; + } } \ No newline at end of file diff --git a/utils/project.js b/utils/project.js index 58a68e3..fc0b483 100644 --- a/utils/project.js +++ b/utils/project.js @@ -4,7 +4,7 @@ import chalk from "chalk"; import boxen from "boxen"; import { logger } from "./logger.js"; import { copyTemplates } from "./templateManager.js"; -import { HonoReactSetup,mernTailwindSetup, installDependencies, mernSetup, serverAuthSetup, serverSetup, mevnSetup } from "./installer.js"; +import { HonoReactSetup,mernTailwindSetup, installDependencies, mernSetup, serverAuthSetup, serverSetup, mevnSetup, nextExpressSetup, writeDockerArtifacts, turboMernSetup } from "./installer.js"; import { angularSetup, angularTailwindSetup } from "./installer.js"; export async function setupProject(projectName, config) { @@ -36,9 +36,10 @@ export async function setupProject(projectName, config) { ); // --- Copy & Install --- - if(config.stack !== "mean" && config.stack !== "mean+tailwind+auth" && config.stack!=="hono"){ + if(config.stack !== "mean" && config.stack !== "mean+tailwind+auth" && config.stack!=="hono" && config.stack!=="next-express"){ copyTemplates(projectPath, config); installDependencies(projectPath, config, projectName); + writeDockerArtifacts(projectPath, config); } if(config.stack==="mern+tailwind+auth"){ @@ -86,6 +87,22 @@ export async function setupProject(projectName, config) { installDependencies(projectPath, config, projectName,false,[]) } + if(config.stack === 'next-express'){ + nextExpressSetup(projectPath,config,projectName); + // install deps: client and server + installDependencies(projectPath, config, projectName, true); + writeDockerArtifacts(projectPath, config); + } + + if(config.stack === 'mern-turbo'){ + turboMernSetup(projectPath, config, projectName); + logger.info("📦 Installing workspace dependencies..."); + // Root install to hoist turbo, then app-level installs handled by turbo or per-app + try { + installDependencies(projectPath, config, projectName, false); + } catch {} + } + // --- Success + Next Steps --- console.log(chalk.gray("-------------------------------------------")) console.log(`${chalk.greenBright(`✅ Project ${chalk.bold.yellow(`${projectName}`)} created successfully! 🎉`)}`); diff --git a/utils/templateManager.js b/utils/templateManager.js index ec0f56b..0320bbb 100644 --- a/utils/templateManager.js +++ b/utils/templateManager.js @@ -34,6 +34,14 @@ export function copyTemplates(projectPath, config) { fs.copySync(backendTemplate, serverPath); } + else if(stack === "express-ts-pro"){ + const backendTemplate = path.join(__dirname, "..", "templates", "express-ts-pro", "server"); + const serverPath = path.join(projectPath, "server"); + + logger.info("📂 Copying backend template files..."); + fs.copySync(backendTemplate, serverPath); + } + else if(stack !== "mean" && stack !== "mean+tailwind+auth" && stack !== "t3-stack"){ const frontendTemplate = path.join(__dirname, "..", "templates", stack, config.language, "client"); const backendTemplate = path.join(__dirname, "..", "templates", stack, config.language, "server"); @@ -62,4 +70,6 @@ export function copyTemplates(projectPath, config) { logger.info("📂 Copying template files..."); fs.copySync(frontendTemplate, clientPath); } + + // next-express: handled by installer copying express server directly; no template copy here }