diff --git a/.changeset/easy-pans-wink.md b/.changeset/easy-pans-wink.md new file mode 100644 index 0000000..3aa5569 --- /dev/null +++ b/.changeset/easy-pans-wink.md @@ -0,0 +1,7 @@ +--- +"create-express-forge": major +"@repo/lint-config": minor +"@repo/typescript-config": minor +--- + +### 🚀 Core CLI & Codebase Modernization diff --git a/.changeset/shy-papers-peel.md b/.changeset/shy-papers-peel.md new file mode 100644 index 0000000..db38820 --- /dev/null +++ b/.changeset/shy-papers-peel.md @@ -0,0 +1,5 @@ +--- +"create-express-forge": major +--- + +### 🚀 Core CLI & Codebase Modernization- **Unified Tooling**: Migrated the entire monorepo to **Biome 2.4**, achieving sub-100ms linting and formatting.- **Generator Refactor**: Re-architected the generator logic into modular, testable components (Base, Structure, Features).- **Pro Testing Suite**: - Separated internal logic tests (`main.test.ts`) from CLI E2E tests (`smoke.integration.test.ts`). - Disabled test caching in Turborepo to ensure 100% reliable smoke runs.- **Modern Templates**: - Updated to **Express 5** (native async error handling) and **Prisma 6**. - Implemented professional **multi-stage Docker builds** (Node 20 Alpine). - Integrated **Biome** as the default linter/formatter for all generated projects.- **Production Hardening**: Added graceful shutdown handlers (SIGTERM/SIGINT) and fail-fast database connection checks on startup.- **Infrastructure**: Renamed and modernized shared monorepo configs (`@repo/lint-config`). diff --git a/.changeset/tidy-mammals-stay.md b/.changeset/tidy-mammals-stay.md new file mode 100644 index 0000000..54be252 --- /dev/null +++ b/.changeset/tidy-mammals-stay.md @@ -0,0 +1,6 @@ +--- +"@repo/lint-config": minor +"@repo/typescript-config": minor +--- + +Internal refactor diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 133f41b..28fe266 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,4 +1,10 @@ module.exports = { root: true, - ignorePatterns: ["dist", "node_modules", "packages/**/*", "docs/**/*", "examples/**/*"], + ignorePatterns: [ + "dist", + "node_modules", + "packages/**/*", + "docs/**/*", + "examples/**/*", + ], }; diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..31e20cd --- /dev/null +++ b/biome.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.13/schema.json", + "extends": ["./packages/lint-config/biome.base.json"], + "files": { + "includes": [ + "!**/dist", + "!**/node_modules", + "!**/pnpm-lock.yaml", + "!.turbo", + "!docs/.vitepress/cache", + "!docs/.vitepress/dist" + ] + } +} diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index a336e19..1633c34 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -11,11 +11,31 @@ export default defineConfig({ head: [ ["link", { rel: "icon", href: `${base}logo.svg` }], - ["meta", { name: "keywords", content: "express, typescript, nodejs, backend, api, generator, scaffold, prisma, sequelize, architecture, mvc, modular, rest-api, server-boilerplate" }], + [ + "meta", + { + name: "keywords", + content: + "express, typescript, nodejs, backend, api, generator, scaffold, prisma, sequelize, architecture, mvc, modular, rest-api, server-boilerplate", + }, + ], ["meta", { name: "author", content: "Yatharth Lakhate" }], ["meta", { property: "og:type", content: "website" }], - ["meta", { property: "og:title", content: "Express Forge | The Ultimate Express + TypeScript Generator" }], - ["meta", { property: "og:description", content: "Scaffold production-ready Express.js TypeScript backends in seconds with built-in Auth, ORM, and OpenAPI support." }], + [ + "meta", + { + property: "og:title", + content: "Express Forge | The Ultimate Express + TypeScript Generator", + }, + ], + [ + "meta", + { + property: "og:description", + content: + "Scaffold production-ready Express.js TypeScript backends in seconds with built-in Auth, ORM, and OpenAPI support.", + }, + ], ], themeConfig: { @@ -29,7 +49,10 @@ export default defineConfig({ { text: "Guide", link: "/guide/getting-started" }, { text: "Features", link: "/guide/features" }, { text: "Reference", link: "/reference/cli-options" }, - { text: "⭐ Star on GitHub", link: "https://github.com/CODE-Y02/express-cli" }, + { + text: "⭐ Star on GitHub", + link: "https://github.com/CODE-Y02/express-cli", + }, ], sidebar: [ diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index beaa8ab..f2f2249 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -8,18 +8,26 @@ --vp-c-brand-2: #4f46e5; /* Indigo 600 */ --vp-c-brand-3: #818cf8; /* Indigo 400 */ --vp-c-brand-next: #06b6d4; /* Cyan 500 */ - + --vp-c-brand-soft: rgba(99, 102, 241, 0.12); /* Typography Visibility */ --vp-c-text-1: #1e293b; /* Slate 800 */ --vp-c-text-2: #475569; /* Slate 600 */ - + /* Hero Gradient */ --vp-home-hero-name-color: transparent; - --vp-home-hero-name-background: linear-gradient(135deg, #6366f1 0%, #06b6d4 100%); - - --vp-home-hero-image-background-image: radial-gradient(circle, rgba(99, 102, 241, 0.2) 0%, rgba(6, 182, 212, 0.15) 100%); + --vp-home-hero-name-background: linear-gradient( + 135deg, + #6366f1 0%, + #06b6d4 100% + ); + + --vp-home-hero-image-background-image: radial-gradient( + circle, + rgba(99, 102, 241, 0.2) 0%, + rgba(6, 182, 212, 0.15) 100% + ); --vp-home-hero-image-filter: blur(60px); } @@ -27,10 +35,10 @@ --vp-c-brand-1: #818cf8; --vp-c-brand-2: #6366f1; --vp-c-brand-3: #a5b4fc; - + --vp-c-text-1: #f8fafc; /* Slate 50 */ --vp-c-text-2: #94a3b8; /* Slate 400 */ - + --vp-c-bg: #0f172a; /* Slate 900 - Deep Obsidian */ --vp-c-bg-soft: #1e293b; /* Slate 800 */ --vp-c-bg-mute: #1e293b; @@ -38,97 +46,105 @@ /* Hero Section Enhancements */ .VPHero { - margin-top: calc(var(--vp-nav-height) + 32px) !important; - padding: 0 32px !important; + margin-top: calc(var(--vp-nav-height) + 32px); + padding: 0 32px; } @media (min-width: 640px) { .VPHero { - padding: 0 48px !important; + padding: 0 48px; } } @media (min-width: 960px) { .VPHero { - padding: 0 64px !important; + padding: 0 64px; } } .name { - font-weight: 900 !important; - letter-spacing: -0.02em !important; + font-weight: 900; + letter-spacing: -0.02em; } /* Feature Cards */ .VPFeatures { - padding: 64px 32px !important; + padding: 64px 32px; } .VPFeature { - border: 1px solid var(--vp-c-divider) !important; - background-color: var(--vp-c-bg-soft) !important; - transition: all 0.4s cubic-bezier(0.2, 1, 0.2, 1) !important; - border-radius: 16px !important; + border: 1px solid var(--vp-c-divider); + background-color: var(--vp-c-bg-soft); + transition: all 0.4s cubic-bezier(0.2, 1, 0.2, 1); + border-radius: 16px; } .VPFeature:hover { - border-color: var(--vp-c-brand-1) !important; + border-color: var(--vp-c-brand-1); transform: translateY(-8px) scale(1.02); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12); - background-color: var(--vp-c-bg) !important; + background-color: var(--vp-c-bg); } /* Dark Mode Overrides */ .dark .VPFeature { - background-color: rgba(30, 41, 59, 0.5) !important; + background-color: rgba(30, 41, 59, 0.5); backdrop-filter: blur(8px); } .dark .VPFeature:hover { - background-color: #1e293b !important; + background-color: #1e293b; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); } /* Typography & Content Visibility */ :root { - --vp-font-family-base: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + --vp-font-family-base: + "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, + Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; } -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { letter-spacing: -0.02em; - font-weight: 800 !important; + font-weight: 800; color: var(--vp-c-text-1); } .VPHero .tagline { - color: var(--vp-c-text-2) !important; - font-weight: 500 !important; - font-size: 1.25rem !important; - max-width: 600px !important; - margin-left: auto !important; - margin-right: auto !important; + color: var(--vp-c-text-2); + font-weight: 500; + font-size: 1.25rem; + max-width: 600px; + margin-left: auto; + margin-right: auto; } /* Code Block Visibility */ -.vp-doc [class*='language-'] { - border-radius: 12px !important; - border: 1px solid var(--vp-c-divider) !important; +.vp-doc [class*="language-"] { + border-radius: 12px; + border: 1px solid var(--vp-c-divider); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); } -.dark .vp-doc [class*='language-'] { - background-color: #0f172a !important; +.dark .vp-doc [class*="language-"] { + background-color: #0f172a; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } /* Hero Buttons */ .VPHero .actions { - justify-content: center !important; - gap: 12px !important; + justify-content: center; + gap: 12px; } /* Overflow & Clipping Prevention */ -.vp-doc p, .vp-doc li { +.vp-doc p, +.vp-doc li { overflow-wrap: break-word; } @@ -138,7 +154,7 @@ h1, h2, h3, h4, h5, h6 { } .VPSidebarItem.level-0 > .item > .text { - font-weight: 700 !important; + font-weight: 700; } /* Table Responsiveness */ @@ -152,22 +168,22 @@ h1, h2, h3, h4, h5, h6 { } /* Code Block Wrapping */ -.vp-doc [class*='language-'] pre { - white-space: pre-wrap !important; - word-break: break-all !important; +.vp-doc [class*="language-"] pre { + white-space: pre-wrap; + word-break: break-all; } /* Hero Section Responsiveness */ @media (max-width: 640px) { .VPHero .name { - font-size: 2.5rem !important; - line-height: 1.1 !important; + font-size: 2.5rem; + line-height: 1.1; } .VPHero .text { - font-size: 1.5rem !important; + font-size: 1.5rem; } .VPHero .tagline { - font-size: 1rem !important; + font-size: 1rem; padding: 0 16px; } } diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index bdba88a..7913a43 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -3,7 +3,7 @@ import "./custom.css"; export default { extends: DefaultTheme, - enhanceApp({}) { + enhanceApp() { // register your custom global components }, }; diff --git a/package.json b/package.json index 31d0e6e..40c05b3 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "build": "turbo run build", "dev": "turbo run dev", "lint": "turbo run lint", - "format": "prettier --write \"**/*.{ts,tsx,md,json}\"", + "format": "biome format --write .", "check-types": "turbo run check-types", "test": "turbo run test", "changeset": "changeset", @@ -17,10 +17,9 @@ "prepare": "husky" }, "devDependencies": { + "@biomejs/biome": "^2.4.13", "@changesets/cli": "^2.27.7", - "eslint": "^8.57.0", "husky": "^9.1.7", - "prettier": "^3.3.3", "turbo": "^2.0.9", "typescript": "5.5.3" }, diff --git a/packages/create-express-forge/package.json b/packages/create-express-forge/package.json index b7e40dd..ebd2305 100644 --- a/packages/create-express-forge/package.json +++ b/packages/create-express-forge/package.json @@ -37,6 +37,7 @@ }, "files": [ "dist", + "templates", "README.md", "CHANGELOG.md" ], @@ -47,7 +48,7 @@ "dev": "tsx src/index.ts", "build": "tsup", "check-types": "tsc --noEmit", - "lint": "eslint . --max-warnings 0", + "lint": "biome lint .", "test": "vitest run", "test:smoke": "vitest run tests/smoke.integration.test.ts" }, @@ -55,14 +56,15 @@ "@inquirer/prompts": "^7.1.0", "chalk": "^5.3.0", "commander": "^12.1.0", + "eta": "^3.5.0", "execa": "^9.3.0", "fs-extra": "^11.2.0", - "ora": "^8.1.1" + "ora": "^8.1.1", + "pkg-types": "^1.2.0" }, "devDependencies": { - "@repo/eslint-config": "workspace:*", + "@repo/lint-config": "workspace:*", "@repo/typescript-config": "workspace:*", - "eslint": "^8.57.0", "@types/fs-extra": "^11.0.4", "@types/node": "^20.14.0", "tsup": "^8.1.0", diff --git a/packages/create-express-forge/src/generator/base.ts b/packages/create-express-forge/src/generator/base.ts index da9e787..8ad0c38 100644 --- a/packages/create-express-forge/src/generator/base.ts +++ b/packages/create-express-forge/src/generator/base.ts @@ -1,69 +1,83 @@ -import path from 'path'; -import type { CliOptions } from '../types.js'; -import { writeFile, writeJson } from '../utils/file.js'; +import path from "node:path"; +import type { CliOptions } from "../types.js"; +import type { TemplateManager } from "../utils/template-manager.js"; -function dbExampleUrl(db: string): string { - if (db === 'mysql') return 'mysql://user:password@localhost:3306/mydb'; - if (db === 'sqlite') return 'file:./dev.db'; - return 'postgresql://user:password@localhost:5432/mydb'; -} +export async function generateBaseFiles( + _opts: CliOptions, + dir: string, + tmpl: TemplateManager, +): Promise { + const src = path.join(dir, "src"); -export async function generateBaseFiles(opts: CliOptions, dir: string): Promise { - const { orm, database, logger, testing, docker, projectName } = opts; - const hasDb = orm !== 'none' && database !== 'none'; + // Root files + await tmpl.renderTemplateFile( + "base/tsconfig.json.eta", + path.join(dir, "tsconfig.json"), + ); + await tmpl.renderTemplateFile( + "base/.gitignore.eta", + path.join(dir, ".gitignore"), + ); + await tmpl.renderTemplateFile( + "base/.env.example.eta", + path.join(dir, ".env.example"), + ); + await tmpl.renderTemplateFile( + "base/.env.example.eta", + path.join(dir, ".env"), + ); + await tmpl.renderTemplateFile( + "base/README.md.eta", + path.join(dir, "README.md"), + ); - await writeJson(path.join(dir, 'tsconfig.json'), { - compilerOptions: { - target: 'ES2022', - module: 'NodeNext', - moduleResolution: 'NodeNext', - outDir: './dist', - rootDir: './src', - strict: true, - esModuleInterop: true, - skipLibCheck: true, - declaration: true, - sourceMap: true, - ...(opts.importAlias ? { baseUrl: '.', paths: { '@/*': ['src/*'] } } : {}), - ...(orm === 'sequelize' ? { experimentalDecorators: true, emitDecoratorMetadata: true } : {}), - }, - include: ['src/**/*'], - exclude: ['node_modules', 'dist', '**/*.test.ts'], - }); + // Core src files + await tmpl.renderTemplateFile( + "base/src/app.ts.eta", + path.join(src, "app.ts"), + ); + await tmpl.renderTemplateFile( + "base/src/server.ts.eta", + path.join(src, "server.ts"), + ); - await writeFile( - path.join(dir, '.gitignore'), - `node_modules/\ndist/\n.env\n*.log\nlogs/\ncoverage/\n.DS_Store\n${orm === 'prisma' ? 'prisma/migrations/\n*.db\n' : ''}`, + // Config + await tmpl.renderTemplateFile( + "base/src/config/env.ts.eta", + path.join(src, "config", "env.ts"), ); - const envLines = [ - '# Application', - 'NODE_ENV=development', - 'PORT=3000', - '', - '# CORS', - 'CORS_ORIGIN=http://localhost:3000', - '', - '# Rate limiter', - 'RATE_LIMIT_WINDOW_MS=900000', - 'RATE_LIMIT_MAX=100', - '', - ...(hasDb ? ['# Database', `DATABASE_URL="${dbExampleUrl(database)}"`, ''] : []), - ...(opts.auth === 'jwt' - ? ['# JWT Auth', 'JWT_SECRET=your_super_secret_jwt_key_change_me', 'JWT_EXPIRES_IN=1d', ''] - : []), - ...(opts.auth === 'session' - ? ['# Session Auth', 'SESSION_SECRET=your_session_secret_key_change_me', ''] - : []), - ...(opts.cache === 'redis' - ? ['# Redis Cache', 'REDIS_URL="redis://localhost:6379"', ''] - : []), - ]; - await writeFile(path.join(dir, '.env.example'), envLines.join('\n')); - await writeFile(path.join(dir, '.env'), envLines.join('\n')); + // Utils + await tmpl.renderTemplateFile( + "base/src/utils/ApiError.ts.eta", + path.join(src, "utils", "ApiError.ts"), + ); + await tmpl.renderTemplateFile( + "base/src/utils/ApiResponse.ts.eta", + path.join(src, "utils", "ApiResponse.ts"), + ); + + // Middleware + await tmpl.renderTemplateFile( + "base/src/middleware/errorHandler.ts.eta", + path.join(src, "middleware", "errorHandler.ts"), + ); + await tmpl.renderTemplateFile( + "base/src/middleware/notFound.ts.eta", + path.join(src, "middleware", "notFound.ts"), + ); + await tmpl.renderTemplateFile( + "base/src/middleware/rateLimiter.ts.eta", + path.join(src, "middleware", "rateLimiter.ts"), + ); + await tmpl.renderTemplateFile( + "base/src/middleware/validate.ts.eta", + path.join(src, "middleware", "validate.ts"), + ); - await writeFile( - path.join(dir, 'README.md'), - `# ${projectName}\n\n> Scaffolded with [create-express-forge](https://github.com/CODE-Y02/create-express-forge)\n\n## Stack\n\n- TypeScript + Express.js\n- Zod validation\n${orm === 'prisma' ? `- Prisma ORM + ${database}\n` : ''}${orm === 'sequelize' ? `- Sequelize ORM + ${database}\n` : ''}${logger !== 'none' ? `- ${logger} logger\n` : ''}${testing !== 'none' ? `- ${testing} tests\n` : ''}${docker ? '- Docker + docker-compose\n' : ''}\n## Quick Start\n\n\`\`\`bash\ncp .env.example .env\n${orm === 'prisma' ? `${opts.packageManager === 'npm' ? 'npm run' : opts.packageManager} db:migrate\n` : ''}${opts.packageManager} run dev\n\`\`\`\n`, + // Types + await tmpl.renderTemplateFile( + "base/src/types/express.d.ts.eta", + path.join(src, "types", "express.d.ts"), ); } diff --git a/packages/create-express-forge/src/generator/features/auth.ts b/packages/create-express-forge/src/generator/features/auth.ts index dc09b1e..62472aa 100644 --- a/packages/create-express-forge/src/generator/features/auth.ts +++ b/packages/create-express-forge/src/generator/features/auth.ts @@ -1,61 +1,25 @@ -import path from 'path'; -import fs from 'fs-extra'; -import type { CliOptions } from '../../types.js'; - -export async function generateAuth(opts: CliOptions, targetDir: string): Promise { - if (opts.auth === 'none') return; - - const authDir = path.join(targetDir, 'src/middleware'); - await fs.ensureDir(authDir); - - if (opts.auth === 'jwt') { - const isCookie = opts.jwtStorage === 'cookie'; - await fs.writeFile( - path.join(authDir, 'auth.middleware.ts'), - `import type { Request, Response, NextFunction } from 'express'; -import jwt from 'jsonwebtoken'; -import { ApiError } from '../utils/ApiError.js'; -import { env } from '../config/env.js'; - -export const auth = (req: Request, _res: Response, next: NextFunction) => { - let token: string | undefined; - - ${ - isCookie - ? "token = req.cookies?.token;" - : `const authHeader = req.headers.authorization; - if (authHeader?.startsWith('Bearer ')) { - token = authHeader.split(' ')[1]; - }` - } - - if (!token) { - return next(ApiError.unauthorized('No token provided')); - } - - try { - const decoded = jwt.verify(token, env.JWT_SECRET); - req.user = decoded as any; - next(); - } catch (err) { - next(ApiError.unauthorized('Invalid or expired token')); - } -}; -` +import path from "node:path"; +import type { CliOptions } from "../../types.js"; +import type { TemplateManager } from "../../utils/template-manager.js"; + +export async function generateAuth( + opts: CliOptions, + targetDir: string, + tmpl: TemplateManager, +): Promise { + if (opts.auth === "none") return; + + const authDir = path.join(targetDir, "src/middleware"); + + if (opts.auth === "jwt") { + await tmpl.renderTemplateFile( + "features/auth/jwt-auth.middleware.ts.eta", + path.join(authDir, "auth.middleware.ts"), ); - } else if (opts.auth === 'session') { - await fs.writeFile( - path.join(authDir, 'auth.middleware.ts'), - `import type { Request, Response, NextFunction } from 'express'; -import { ApiError } from '../utils/ApiError.js'; - -export const auth = (req: Request, _res: Response, next: NextFunction) => { - if (!req.session || !(req.session as any).user) { - return next(ApiError.unauthorized('Session expired or invalid')); - } - next(); -}; -` + } else if (opts.auth === "session") { + await tmpl.renderTemplateFile( + "features/auth/session-auth.middleware.ts.eta", + path.join(authDir, "auth.middleware.ts"), ); } } diff --git a/packages/create-express-forge/src/generator/features/cache.ts b/packages/create-express-forge/src/generator/features/cache.ts index afcbba2..0a8f2f6 100644 --- a/packages/create-express-forge/src/generator/features/cache.ts +++ b/packages/create-express-forge/src/generator/features/cache.ts @@ -1,67 +1,25 @@ -import path from 'path'; -import fs from 'fs-extra'; -import type { CliOptions } from '../../types.js'; - -export async function generateCache(opts: CliOptions, targetDir: string): Promise { - if (opts.cache === 'none') return; - - const cacheDir = path.join(targetDir, 'src/cache'); - await fs.ensureDir(cacheDir); - - if (opts.cache === 'redis') { - await fs.writeFile( - path.join(cacheDir, 'index.ts'), - `import { createClient } from 'redis'; -${opts.logger === 'none' ? '' : "import { logger } from '../logger/index.js';"} - -const client = createClient({ - url: process.env.REDIS_URL || 'redis://localhost:6379' -}); - -client.on('error', (err) => ${opts.logger === 'none' ? "console.error('Redis Client Error', err)" : "logger.error('Redis Client Error', err)"}); - -export const connectRedis = async () => { - await client.connect(); - ${opts.logger === 'none' ? "console.info('🔴 Redis connected successfully');" : "logger.info('🔴 Redis connected successfully');"} -}; - -export const cache = { - get: async (key: string) => client.get(key), - set: async (key: string, value: string, ttlSeconds?: number) => { - if (ttlSeconds) { - await client.set(key, value, { EX: ttlSeconds }); - } else { - await client.set(key, value); - } - }, - del: async (key: string) => client.del(key), -}; -` +import path from "node:path"; +import type { CliOptions } from "../../types.js"; +import type { TemplateManager } from "../../utils/template-manager.js"; + +export async function generateCache( + opts: CliOptions, + targetDir: string, + tmpl: TemplateManager, +): Promise { + if (opts.cache === "none") return; + + const cacheDir = path.join(targetDir, "src/cache"); + + if (opts.cache === "redis") { + await tmpl.renderTemplateFile( + "features/cache/redis.ts.eta", + path.join(cacheDir, "index.ts"), ); - } else if (opts.cache === 'node-cache') { - await fs.writeFile( - path.join(cacheDir, 'index.ts'), - `import NodeCache from 'node-cache'; -${opts.logger === 'none' ? '' : "import { logger } from '../logger/index.js';"} - -const nodeCache = new NodeCache({ stdTTL: 100, checkperiod: 120 }); - -${opts.logger === 'none' ? "console.info('💾 In-memory cache initialized');" : "logger.info('💾 In-memory cache initialized');"} - -export const cache = { - get: async (key: string) => nodeCache.get(key) as string | undefined, - set: async (key: string, value: string, ttlSeconds?: number) => { - if (ttlSeconds) { - nodeCache.set(key, value, ttlSeconds); - } else { - nodeCache.set(key, value); - } - }, - del: async (key: string) => { - nodeCache.del(key); - }, -}; -` + } else if (opts.cache === "node-cache") { + await tmpl.renderTemplateFile( + "features/cache/node-cache.ts.eta", + path.join(cacheDir, "index.ts"), ); } } diff --git a/packages/create-express-forge/src/generator/features/docker.ts b/packages/create-express-forge/src/generator/features/docker.ts index a54bc53..eac15b4 100644 --- a/packages/create-express-forge/src/generator/features/docker.ts +++ b/packages/create-express-forge/src/generator/features/docker.ts @@ -1,135 +1,22 @@ -import path from 'path'; -import type { CliOptions } from '../../types.js'; -import { writeFile } from '../../utils/file.js'; - -export async function generateDocker(opts: CliOptions, dir: string): Promise { - const hasDb = opts.orm !== 'none' && opts.database !== 'none' && opts.database !== 'sqlite'; - - const pm = opts.packageManager; - const pmInstallGlobal = pm === 'pnpm' ? 'RUN npm install -g pnpm\n' : pm === 'bun' ? 'RUN npm install -g bun\n' : ''; - const pmInstallCmd = pm === 'pnpm' ? 'RUN pnpm install --frozen-lockfile' : pm === 'yarn' ? 'RUN yarn install --frozen-lockfile' : pm === 'bun' ? 'RUN bun install --frozen-lockfile' : 'RUN npm ci'; - const pmProdInstallCmd = pm === 'pnpm' ? 'RUN pnpm install --frozen-lockfile --prod' : pm === 'yarn' ? 'RUN yarn install --production --frozen-lockfile' : pm === 'bun' ? 'RUN bun install --frozen-lockfile --production' : 'RUN npm ci --omit=dev && npm cache clean --force'; - const pmRunBuild = pm === 'pnpm' ? 'pnpm run build' : pm === 'yarn' ? 'yarn run build' : pm === 'bun' ? 'bun run build' : 'npm run build'; - - await writeFile(path.join(dir, 'Dockerfile'), - `# ── Stage 1: Builder ───────────────────────────────────────────────────────── -FROM node:20-alpine AS builder -WORKDIR /app -${pmInstallGlobal}COPY package.json pnpm-lock.yaml* yarn.lock* bun.lock* package-lock.json* ./ -${pmInstallCmd} -COPY . . -RUN ${pmRunBuild} -${opts.orm === 'prisma' ? `RUN ${pm === 'bun' ? 'bunx' : pm === 'pnpm' ? 'pnpx' : 'npx'} prisma generate\n` : ''} -# ── Stage 2: Production ─────────────────────────────────────────────────────── -FROM node:20-alpine AS production -WORKDIR /app -ENV NODE_ENV=production -${pmInstallGlobal}COPY package.json pnpm-lock.yaml* yarn.lock* bun.lock* package-lock.json* ./ -${pmProdInstallCmd} -COPY --from=builder /app/dist ./dist -${opts.orm === 'prisma' ? 'COPY --from=builder /app/prisma ./prisma\nCOPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma\n' : ''} -EXPOSE 3000 -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\ - CMD wget -qO- http://localhost:3000/api/health || exit 1 - -CMD ["node", "dist/server.js"] -`); - - await writeFile(path.join(dir, '.dockerignore'), - `node_modules\ndist\n.env\n*.log\nlogs/\ncoverage\n.git\n.turbo\n`); - - const dbService = buildDbService(opts.database); - const redisService = opts.cache === 'redis' ? buildRedisService() : ''; - - const dependsOn: string[] = []; - if (hasDb) dependsOn.push('db'); - if (opts.cache === 'redis') dependsOn.push('redis'); - - const dependsOnTpl = dependsOn.length > 0 - ? `\n depends_on:${dependsOn.map(s => `\n ${s}:\n condition: service_healthy`).join('')}` - : ''; - - const volumes: string[] = []; - if (hasDb) volumes.push(' db_data:'); - if (opts.cache === 'redis') volumes.push(' redis_data:'); - - const volumesTpl = volumes.length > 0 - ? `\nvolumes:\n${volumes.join('\n')}\n` - : ''; - - await writeFile(path.join(dir, 'docker-compose.yml'), - `version: '3.9' - -services: - app: - build: . - restart: unless-stopped - ports: - - "\${PORT:-3000}:3000" - env_file: .env - environment: - NODE_ENV: \${NODE_ENV:-development}${dependsOnTpl} -${dbService}${redisService}${volumesTpl}`); -} - -function buildDbService(db: string): string { - if (db === 'postgresql') { - return ` - db: - image: postgres:16-alpine - restart: unless-stopped - environment: - POSTGRES_USER: \${POSTGRES_USER:-user} - POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-password} - POSTGRES_DB: \${POSTGRES_DB:-mydb} - ports: - - "5432:5432" - volumes: - - db_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-user}"] - interval: 10s - timeout: 5s - retries: 5 -`; - } - if (db === 'mysql') { - return ` - db: - image: mysql:8.0 - restart: unless-stopped - environment: - MYSQL_ROOT_PASSWORD: \${MYSQL_ROOT_PASSWORD:-root} - MYSQL_DATABASE: \${MYSQL_DATABASE:-mydb} - MYSQL_USER: \${MYSQL_USER:-user} - MYSQL_PASSWORD: \${MYSQL_PASSWORD:-password} - ports: - - "3306:3306" - volumes: - - db_data:/var/lib/mysql - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] - interval: 10s - timeout: 5s - retries: 5 -`; - } - return ''; -} - -function buildRedisService(): string { - return ` - redis: - image: redis:7-alpine - restart: unless-stopped - ports: - - "6379:6379" - volumes: - - redis_data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 -`; +import path from "node:path"; +import type { CliOptions } from "../../types.js"; +import type { TemplateManager } from "../../utils/template-manager.js"; + +export async function generateDocker( + _opts: CliOptions, + dir: string, + tmpl: TemplateManager, +): Promise { + await tmpl.renderTemplateFile( + "features/docker/Dockerfile.eta", + path.join(dir, "Dockerfile"), + ); + await tmpl.renderTemplateFile( + "features/docker/docker-compose.yml.eta", + path.join(dir, "docker-compose.yml"), + ); + + const dockerIgnore = `node_modules\ndist\n.env\n*.log\nlogs/\ncoverage\n.git\n.turbo\n`; + const { writeFile } = await import("../../utils/file.js"); + await writeFile(path.join(dir, ".dockerignore"), dockerIgnore); } diff --git a/packages/create-express-forge/src/generator/features/logger.ts b/packages/create-express-forge/src/generator/features/logger.ts index d7f715e..685f54d 100644 --- a/packages/create-express-forge/src/generator/features/logger.ts +++ b/packages/create-express-forge/src/generator/features/logger.ts @@ -1,16 +1,27 @@ -import path from 'path'; -import type { CliOptions } from '../../types.js'; -import { writeFile } from '../../utils/file.js'; +import path from "node:path"; +import type { CliOptions } from "../../types.js"; +import type { TemplateManager } from "../../utils/template-manager.js"; -export async function generateLogger(opts: CliOptions, dir: string): Promise { - const loggerDir = path.join(dir, 'src', 'logger'); - if (opts.logger === 'winston') { - await writeFile(path.join(loggerDir, 'index.ts'), - `import winston from 'winston';\nimport 'winston-daily-rotate-file';\nconst { combine, timestamp, printf, colorize, errors } = winston.format;\nconst devFmt = combine(colorize({ all: true }), timestamp({ format: 'HH:mm:ss' }), errors({ stack: true }), printf(({ level, message, timestamp: ts, stack }) => \`\${ts as string} [\${level}]: \${(stack as string | undefined) ?? (message as string)}\`));\nconst prodFmt = combine(timestamp(), errors({ stack: true }), winston.format.json());\nexport const logger = winston.createLogger({\n level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',\n format: process.env.NODE_ENV === 'production' ? prodFmt : devFmt,\n transports: [new winston.transports.Console(), ...(process.env.NODE_ENV === 'production' ? [new winston.transports.DailyRotateFile({ filename: 'logs/app-%DATE%.log', datePattern: 'YYYY-MM-DD', zippedArchive: true, maxSize: '20m', maxFiles: '14d' })] : [])],\n exceptionHandlers: [new winston.transports.Console()],\n rejectionHandlers: [new winston.transports.Console()],\n});\n`); - } else if (opts.logger === 'pino') { - await writeFile(path.join(loggerDir, 'index.ts'), - `import pino from 'pino';\nexport const logger = pino({\n level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',\n ...(process.env.NODE_ENV !== 'production' && { transport: { target: 'pino-pretty', options: { colorize: true, translateTime: 'HH:MM:ss Z', ignore: 'pid,hostname' } } }),\n});\n`); - await writeFile(path.join(dir, 'src', 'middleware', 'httpLogger.ts'), - `import pinoHttp from 'pino-http';\nimport { logger } from '../logger/index.js';\nexport const httpLogger = pinoHttp({ logger });\n`); +export async function generateLogger( + opts: CliOptions, + dir: string, + tmpl: TemplateManager, +): Promise { + const loggerDir = path.join(dir, "src", "logger"); + + if (opts.logger === "winston") { + await tmpl.renderTemplateFile( + "features/logger/winston.ts.eta", + path.join(loggerDir, "index.ts"), + ); + } else if (opts.logger === "pino") { + await tmpl.renderTemplateFile( + "features/logger/pino.ts.eta", + path.join(loggerDir, "index.ts"), + ); + await tmpl.renderTemplateFile( + "features/logger/pino.ts.eta", + path.join(dir, "src", "middleware", "httpLogger.ts"), + ); } } diff --git a/packages/create-express-forge/src/generator/features/openapi.ts b/packages/create-express-forge/src/generator/features/openapi.ts index 3abb264..9ed455a 100644 --- a/packages/create-express-forge/src/generator/features/openapi.ts +++ b/packages/create-express-forge/src/generator/features/openapi.ts @@ -1,63 +1,17 @@ -import path from 'path'; -import fs from 'fs-extra'; -import type { CliOptions } from '../../types.js'; +import path from "node:path"; +import type { CliOptions } from "../../types.js"; +import type { TemplateManager } from "../../utils/template-manager.js"; -export async function generateOpenApi(opts: CliOptions, targetDir: string): Promise { +export async function generateOpenApi( + opts: CliOptions, + targetDir: string, + tmpl: TemplateManager, +): Promise { if (!opts.openapi) return; - const docsDir = path.join(targetDir, 'src/docs'); - await fs.ensureDir(docsDir); - - await fs.writeFile( - path.join(docsDir, 'swagger.ts'), - `import swaggerJsdoc from 'swagger-jsdoc'; -import swaggerUi from 'swagger-ui-express'; -import type { Express } from 'express'; - -const options: swaggerJsdoc.Options = { - definition: { - openapi: '3.0.0', - info: { - title: '${opts.projectName} API', - version: '1.0.0', - description: 'API documentation for ${opts.projectName}', - }, - servers: [ - { - url: 'http://localhost:3000', - description: 'Development server', - }, - ], - components: { - securitySchemes: { - ${ - opts.auth === 'jwt' - ? opts.jwtStorage === 'cookie' - ? "cookieAuth: { type: 'apiKey', in: 'cookie', name: 'token' }" - : "bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }" - : '' - } - } - }, - ${opts.auth === 'jwt' ? `security: [{ ${opts.jwtStorage === 'cookie' ? "'cookieAuth'" : "'bearerAuth'"}: [] }],` : ''} - }, - apis: ['./src/app.ts', './src/modules/**/*.routes.ts', './src/routes/**/*.ts'], -}; - -const specs = swaggerJsdoc(options); - -export const setupSwagger = (app: Express) => { - // Always expose the OpenAPI spec as JSON for debugging or external tools - app.get('/docs.json', (_req, res) => res.json(specs)); - - ${ - opts.openapiUI - ? `app.use('/docs', swaggerUi.serve, swaggerUi.setup(specs)); - console.log('📜 API Docs available at http://localhost:3000/docs'); - console.log('📜 API Spec available at http://localhost:3000/docs.json');` - : `console.log('📜 API Spec available at http://localhost:3000/docs.json (UI disabled)');` - } -}; -` + const docsDir = path.join(targetDir, "src/docs"); + await tmpl.renderTemplateFile( + "features/openapi/swagger.ts.eta", + path.join(docsDir, "swagger.ts"), ); } diff --git a/packages/create-express-forge/src/generator/features/prisma.ts b/packages/create-express-forge/src/generator/features/prisma.ts index 660f16b..66d4fe2 100644 --- a/packages/create-express-forge/src/generator/features/prisma.ts +++ b/packages/create-express-forge/src/generator/features/prisma.ts @@ -1,110 +1,25 @@ -import path from 'path'; -import { execa } from 'execa'; -import fs from 'fs-extra'; -import type { CliOptions } from '../../types.js'; -import { writeFile } from '../../utils/file.js'; - -export async function generatePrisma(opts: CliOptions, dir: string): Promise { - const provider = opts.database === 'mysql' ? 'mysql' : opts.database === 'sqlite' ? 'sqlite' : 'postgresql'; - const dbUrl = opts.database === 'sqlite' ? 'file:./dev.db' : opts.database === 'mysql' ? 'mysql://user:password@localhost:3306/mydb' : 'postgresql://user:password@localhost:5432/mydb'; - - try { - // Initialize prisma folder using latest CLI pattern - const npxCmd = opts.packageManager === 'bun' ? 'bunx' : 'npx'; - await execa(npxCmd, ['prisma', 'init', '--datasource-provider', provider], { cwd: dir }); - } catch (err) { - // Fallback - await fs.ensureDir(path.join(dir, 'prisma')); - } - - await writeFile(path.join(dir, 'prisma', 'schema.prisma'), - `generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "${provider}" - url = env("DATABASE_URL") -} -${opts.auth !== 'none' ? ` -model User { - id String @id @default(uuid()) - name String - email String @unique - password String - role Role @default(USER) - todos Todo[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@map("users") -} - -model Todo { - id String @id @default(cuid()) - title String - description String? - completed Boolean @default(false) - userId String - user User @relation(fields: [userId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@map("todos") -} - -enum Role { - USER - ADMIN -}` : ''} -`); - - await writeFile(path.join(dir, 'prisma', 'seed.ts'), - `import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - console.log('🌱 Seeding database...'); - - const admin = await prisma.user.upsert({ - where: { email: 'admin@example.com' }, - update: {}, - create: { - email: 'admin@example.com', - name: 'Admin User', - password: 'password123', // In real app, hash this! - role: 'ADMIN', - }, - }); - - console.log({ admin }); - console.log('✅ Seeding completed.'); -} - -main() - .catch((e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); -`); - - await writeFile(path.join(dir, 'src', 'config', 'database.ts'), - `import { PrismaClient } from '@prisma/client';\n\nexport const prisma = new PrismaClient({\n log: process.env.NODE_ENV === 'development' ? ['query', 'warn', 'error'] : ['error'],\n});\n`); - - // Patch .env with correct DATABASE_URL if init didn't do it right or we want to ensure our format - const { readFile, writeFile: fsWrite } = await import('fs/promises'); - for (const f of [path.join(dir, '.env'), path.join(dir, '.env.example')]) { - try { - const c = await readFile(f, 'utf-8'); - if (c.includes('DATABASE_URL=')) { - await fsWrite(f, c.replace(/DATABASE_URL=.*/, `DATABASE_URL="${dbUrl}"`), 'utf-8'); - } else { - await fsWrite(f, c + `\nDATABASE_URL="${dbUrl}"\n`, 'utf-8'); - } - } catch { /* ignore */ } - } +import path from "node:path"; +import type { CliOptions } from "../../types.js"; +import type { TemplateManager } from "../../utils/template-manager.js"; + +export async function generatePrisma( + _opts: CliOptions, + dir: string, + tmpl: TemplateManager, +): Promise { + // We no longer need to run `npx prisma init` during scaffolding which slows it down. + // We just copy the configured files directly. + + await tmpl.renderTemplateFile( + "features/prisma/schema.prisma.eta", + path.join(dir, "prisma", "schema.prisma"), + ); + await tmpl.renderTemplateFile( + "features/prisma/seed.ts.eta", + path.join(dir, "prisma", "seed.ts"), + ); + await tmpl.renderTemplateFile( + "features/prisma/database.ts.eta", + path.join(dir, "src", "config", "database.ts"), + ); } diff --git a/packages/create-express-forge/src/generator/features/sequelize.ts b/packages/create-express-forge/src/generator/features/sequelize.ts index 7485ea7..488eba1 100644 --- a/packages/create-express-forge/src/generator/features/sequelize.ts +++ b/packages/create-express-forge/src/generator/features/sequelize.ts @@ -1,33 +1,29 @@ -import path from 'path'; -import type { CliOptions } from '../../types.js'; -import { writeFile } from '../../utils/file.js'; +import path from "node:path"; +import type { CliOptions } from "../../types.js"; +import type { TemplateManager } from "../../utils/template-manager.js"; -export async function generateSequelize(opts: CliOptions, dir: string): Promise { - const dialect = opts.database === 'mysql' ? 'mysql' : opts.database === 'sqlite' ? 'sqlite' : 'postgres'; - - await writeFile(path.join(dir, '.sequelizerc'), - `const path = require('path'); -module.exports = { - config: path.resolve('src', 'config', 'sequelize.cjs'), - 'models-path': path.resolve('src', 'models'), - 'seeders-path': path.resolve('db', 'seeders'), - 'migrations-path': path.resolve('db', 'migrations'), -}; -`); +export async function generateSequelize( + opts: CliOptions, + dir: string, + tmpl: TemplateManager, +): Promise { + await tmpl.renderTemplateFile( + "features/sequelize/.sequelizerc.eta", + path.join(dir, ".sequelizerc"), + ); + await tmpl.renderTemplateFile( + "features/sequelize/sequelize.cjs.eta", + path.join(dir, "src", "config", "sequelize.cjs"), + ); + await tmpl.renderTemplateFile( + "features/sequelize/database.ts.eta", + path.join(dir, "src", "config", "database.ts"), + ); - await writeFile(path.join(dir, 'src', 'config', 'sequelize.cjs'), - `module.exports = { - development: { use_env_variable: 'DATABASE_URL', dialect: '${dialect}', logging: console.log }, - test: { use_env_variable: 'DATABASE_URL', dialect: '${dialect}', logging: false }, - production: { use_env_variable: 'DATABASE_URL', dialect: '${dialect}', logging: false }, -}; -`); - - await writeFile(path.join(dir, 'src', 'config', 'database.ts'), - `import { Sequelize } from 'sequelize-typescript';\nimport { env } from './env.js';\n${opts.auth !== 'none' ? "import { User } from '../models/User.js';\n" : ""}export const sequelize = new Sequelize(env.DATABASE_URL, { dialect: '${dialect}' as const, logging: env.NODE_ENV === 'development' ? console.log : false, models: [${opts.auth !== 'none' ? 'User' : ''}] });\nexport async function connectDB() { await sequelize.authenticate(); console.log('✅ Database connected'); }\n`); - - if (opts.auth !== 'none') { - await writeFile(path.join(dir, 'src', 'models', 'User.ts'), - `import { Table, Column, Model, DataType, CreatedAt, UpdatedAt, PrimaryKey, Default, Unique } from 'sequelize-typescript';\n@Table({ tableName: 'users', timestamps: true })\nexport class User extends Model {\n @PrimaryKey @Default(DataType.UUIDV4) @Column(DataType.UUID) declare id: string;\n @Column({ type: DataType.STRING, allowNull: false }) declare name: string;\n @Unique @Column({ type: DataType.STRING, allowNull: false }) declare email: string;\n @Column({ type: DataType.STRING, allowNull: false }) declare password: string;\n @CreatedAt declare createdAt: Date;\n @UpdatedAt declare updatedAt: Date;\n}\n`); + if (opts.auth !== "none") { + await tmpl.renderTemplateFile( + "features/sequelize/User.ts.eta", + path.join(dir, "src", "models", "User.ts"), + ); } } diff --git a/packages/create-express-forge/src/generator/features/testing.ts b/packages/create-express-forge/src/generator/features/testing.ts index 6d106e7..f12aa66 100644 --- a/packages/create-express-forge/src/generator/features/testing.ts +++ b/packages/create-express-forge/src/generator/features/testing.ts @@ -1,20 +1,31 @@ -import path from 'path'; -import type { CliOptions } from '../../types.js'; -import { writeFile } from '../../utils/file.js'; +import path from "node:path"; +import type { CliOptions } from "../../types.js"; +import type { TemplateManager } from "../../utils/template-manager.js"; -export async function generateTesting(opts: CliOptions, dir: string): Promise { - const testDir = path.join(dir, 'src', '__tests__'); - const healthPath = opts.pattern === 'modular' ? '/api/health' : '/api/v1/health'; +export async function generateTesting( + opts: CliOptions, + dir: string, + tmpl: TemplateManager, +): Promise { + const testDir = path.join(dir, "src", "__tests__"); - if (opts.testing === 'vitest') { - await writeFile(path.join(dir, 'vitest.config.ts'), - `import { defineConfig } from 'vitest/config';\nexport default defineConfig({ test: { globals: true, environment: 'node', coverage: { provider: 'v8', reporter: ['text', 'html', 'lcov'] } } });\n`); - await writeFile(path.join(testDir, 'health.test.ts'), - `import { describe, it, expect } from 'vitest';\nimport request from 'supertest';\nimport { app } from '../app.js';\n\ndescribe('Health', () => {\n it('GET ${healthPath} → 200', async () => {\n const res = await request(app).get('${healthPath}');\n expect(res.status).toBe(200);\n expect(res.body.success).toBe(true);\n expect(res.body.data.status).toBe('ok');\n });\n});\n`); - } else if (opts.testing === 'jest') { - await writeFile(path.join(dir, 'jest.config.ts'), - `import type { Config } from 'jest';\nconst config: Config = { preset: 'ts-jest/presets/default-esm', testEnvironment: 'node', extensionsToTreatAsEsm: ['.ts'], moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1' } };\nexport default config;\n`); - await writeFile(path.join(testDir, 'health.test.ts'), - `import request from 'supertest';\nimport { app } from '../app.js';\n\ndescribe('Health', () => {\n it('GET ${healthPath} → 200', async () => {\n const res = await request(app).get('${healthPath}');\n expect(res.status).toBe(200);\n expect(res.body.success).toBe(true);\n });\n});\n`); + if (opts.testing === "vitest") { + await tmpl.renderTemplateFile( + "features/testing/vitest.config.ts.eta", + path.join(dir, "vitest.config.ts"), + ); + await tmpl.renderTemplateFile( + "features/testing/smoke.test.ts.eta", + path.join(testDir, "smoke.test.ts"), + ); + } else if (opts.testing === "jest") { + await tmpl.renderTemplateFile( + "features/testing/jest.config.js.eta", + path.join(dir, "jest.config.js"), + ); + await tmpl.renderTemplateFile( + "features/testing/smoke.test.ts.eta", + path.join(testDir, "smoke.test.ts"), + ); } } diff --git a/packages/create-express-forge/src/generator/index.ts b/packages/create-express-forge/src/generator/index.ts index 77ba35b..c536ad6 100644 --- a/packages/create-express-forge/src/generator/index.ts +++ b/packages/create-express-forge/src/generator/index.ts @@ -1,31 +1,36 @@ -import ora from 'ora'; -import chalk from 'chalk'; -import path from 'path'; -import { execa } from 'execa'; -import fs from 'fs-extra'; -import type { CliOptions } from '../types.js'; -import { writeJson } from '../utils/file.js'; -import { buildPackageJson } from '../utils/package-builder.js'; -import { displaySuccess, displayError } from '../utils/display.js'; -import { generateBaseFiles } from './base.js'; -import { generateModularStructure } from './structure/modular.js'; -import { generateMvcStructure } from './structure/mvc.js'; -import { generatePrisma } from './features/prisma.js'; -import { generateSequelize } from './features/sequelize.js'; -import { generateLogger } from './features/logger.js'; -import { generateTesting } from './features/testing.js'; -import { generateDocker } from './features/docker.js'; -import { generateAuth } from './features/auth.js'; -import { generateCache } from './features/cache.js'; -import { generateOpenApi } from './features/openapi.js'; - -export async function generateProject(opts: CliOptions, targetDir: string): Promise { +import path from "node:path"; +import chalk from "chalk"; +import { execa } from "execa"; +import fs from "fs-extra"; +import ora from "ora"; +import type { CliOptions } from "../types.js"; +import { displayError, displaySuccess } from "../utils/display.js"; +import { writeJson } from "../utils/file.js"; +import { buildPackageJson } from "../utils/package-builder.js"; +import { generateBaseFiles } from "./base.js"; +import { generateAuth } from "./features/auth.js"; +import { generateCache } from "./features/cache.js"; +import { generateDocker } from "./features/docker.js"; +import { generateLogger } from "./features/logger.js"; +import { generateOpenApi } from "./features/openapi.js"; +import { generatePrisma } from "./features/prisma.js"; +import { generateSequelize } from "./features/sequelize.js"; +import { generateTesting } from "./features/testing.js"; +import { generateModularStructure } from "./structure/modular.js"; +import { generateMvcStructure } from "./structure/mvc.js"; + +export async function generateProject( + opts: CliOptions, + targetDir: string, +): Promise { const spinner = ora(); if (await fs.pathExists(targetDir)) { const files = await fs.readdir(targetDir); if (files.length > 0) { - displayError(`Directory "${opts.projectName}" already exists and is not empty.`); + displayError( + `Directory "${opts.projectName}" already exists and is not empty.`, + ); process.exit(1); } } @@ -34,90 +39,135 @@ export async function generateProject(opts: CliOptions, targetDir: string): Prom console.log(); try { - spinner.start(chalk.dim('Creating package.json...')); - await writeJson(path.join(targetDir, 'package.json'), buildPackageJson(opts)); - spinner.succeed(chalk.green('package.json')); + spinner.start(chalk.dim("Creating package.json...")); + await writeJson( + path.join(targetDir, "package.json"), + buildPackageJson(opts), + ); + spinner.succeed(chalk.green("package.json")); - spinner.start(chalk.dim('Setting up base config...')); - await generateBaseFiles(opts, targetDir); - spinner.succeed(chalk.green('Base config files')); + const { TemplateManager } = await import("../utils/template-manager.js"); + const tmpl = new TemplateManager(opts); + + spinner.start(chalk.dim("Setting up base config...")); + await generateBaseFiles(opts, targetDir, tmpl); + spinner.succeed(chalk.green("Base config files")); spinner.start(chalk.dim(`Scaffolding ${opts.pattern} architecture...`)); - if (opts.pattern === 'modular') { - await generateModularStructure(opts, targetDir); + if (opts.pattern === "modular") { + await generateModularStructure(opts, targetDir, tmpl); } else { - await generateMvcStructure(opts, targetDir); + await generateMvcStructure(opts, targetDir, tmpl); } spinner.succeed(chalk.green(`${opts.pattern.toUpperCase()} architecture`)); - if (opts.orm === 'prisma') { - spinner.start(chalk.dim('Configuring Prisma...')); - await generatePrisma(opts, targetDir); - spinner.succeed(chalk.green('Prisma')); - } else if (opts.orm === 'sequelize') { - spinner.start(chalk.dim('Configuring Sequelize...')); - await generateSequelize(opts, targetDir); - spinner.succeed(chalk.green('Sequelize')); + if (opts.orm === "prisma") { + spinner.start(chalk.dim("Configuring Prisma...")); + await generatePrisma(opts, targetDir, tmpl); + spinner.succeed(chalk.green("Prisma")); + } else if (opts.orm === "sequelize") { + spinner.start(chalk.dim("Configuring Sequelize...")); + await generateSequelize(opts, targetDir, tmpl); + spinner.succeed(chalk.green("Sequelize")); } - if (opts.logger !== 'none') { + if (opts.logger !== "none") { spinner.start(chalk.dim(`Configuring ${opts.logger}...`)); - await generateLogger(opts, targetDir); + await generateLogger(opts, targetDir, tmpl); spinner.succeed(chalk.green(`${opts.logger} logger`)); } - if (opts.testing !== 'none') { + if (opts.testing !== "none") { spinner.start(chalk.dim(`Configuring ${opts.testing}...`)); - await generateTesting(opts, targetDir); + await generateTesting(opts, targetDir, tmpl); spinner.succeed(chalk.green(opts.testing)); } if (opts.docker) { - spinner.start(chalk.dim('Adding Docker files...')); - await generateDocker(opts, targetDir); - spinner.succeed(chalk.green('Docker + docker-compose')); + spinner.start(chalk.dim("Adding Docker files...")); + await generateDocker(opts, targetDir, tmpl); + spinner.succeed(chalk.green("Docker + docker-compose")); } - if (opts.auth !== 'none') { + if (opts.auth !== "none") { spinner.start(chalk.dim(`Configuring ${opts.auth} authentication...`)); - await generateAuth(opts, targetDir); + await generateAuth(opts, targetDir, tmpl); spinner.succeed(chalk.green(`${opts.auth.toUpperCase()} Auth`)); } - if (opts.cache !== 'none') { + if (opts.cache !== "none") { spinner.start(chalk.dim(`Configuring ${opts.cache} caching...`)); - await generateCache(opts, targetDir); + await generateCache(opts, targetDir, tmpl); spinner.succeed(chalk.green(`${opts.cache.toUpperCase()} Cache`)); } if (opts.openapi) { - spinner.start(chalk.dim('Generating OpenAPI docs...')); - await generateOpenApi(opts, targetDir); - spinner.succeed(chalk.green('OpenAPI (Swagger)')); + spinner.start(chalk.dim("Generating OpenAPI docs...")); + await generateOpenApi(opts, targetDir, tmpl); + spinner.succeed(chalk.green("OpenAPI (Swagger)")); + } + + if (!opts.skipPolish) { + // Final polish: Format everything with Biome + spinner.start(chalk.dim("Polishing code with Biome...")); + try { + await execa( + "npx", + ["--yes", "@biomejs/biome", "format", "--write", "."], + { cwd: targetDir }, + ); + spinner.succeed(chalk.green("Code polished and formatted")); + } catch (_err) { + spinner.warn(chalk.yellow("Polishing skipped (Biome failed)")); + } } if (opts.installDeps) { - spinner.start(chalk.dim(`Installing dependencies with ${opts.packageManager}...`)); - await execa(opts.packageManager, ['install'], { cwd: targetDir }); - spinner.succeed(chalk.green('Dependencies installed')); + spinner.start( + chalk.dim(`Installing dependencies with ${opts.packageManager}...`), + ); + try { + await execa(opts.packageManager, ["install"], { + cwd: targetDir, + env: { ...process.env, NODE_ENV: undefined }, // Ensure we install devDeps + }); + spinner.succeed(chalk.green("Dependencies installed")); + } catch (err) { + spinner.fail( + chalk.red( + `Failed to install dependencies with ${opts.packageManager}`, + ), + ); + console.error(chalk.dim((err as Error).message)); + throw err; + } - if (opts.orm === 'prisma') { - spinner.start(chalk.dim('Generating Prisma client...')); + if (opts.orm === "prisma") { + spinner.start(chalk.dim("Generating Prisma client...")); try { - const npxCmd = opts.packageManager === 'bun' ? 'bunx' : opts.packageManager === 'pnpm' ? 'pnpm' : 'npx'; - const args = opts.packageManager === 'pnpm' ? ['exec', 'prisma', 'generate'] : ['prisma', 'generate']; - await execa(npxCmd, args, { cwd: targetDir }); - spinner.succeed(chalk.green('Prisma client generated')); - } catch (err) { - spinner.warn(chalk.yellow('Prisma client generation skipped (network/env issue)')); - console.log(chalk.dim(' You can run "npx prisma generate" manually once your network is stable.')); + // Use npx --yes to ensure it runs without prompts and is reliable across environments + await execa("npx", ["--yes", "prisma", "generate"], { + cwd: targetDir, + }); + spinner.succeed(chalk.green("Prisma client generated")); + } catch (_err) { + spinner.warn( + chalk.yellow( + "Prisma client generation skipped (manual run required)", + ), + ); + console.log( + chalk.dim( + ' You can run "npx prisma generate" manually once your network is stable.', + ), + ); } } } displaySuccess(opts.projectName, opts.packageManager, opts.installDeps); } catch (err) { - spinner.fail(chalk.red('Scaffolding failed')); + spinner.fail(chalk.red("Scaffolding failed")); displayError((err as Error).message); process.exit(1); } diff --git a/packages/create-express-forge/src/generator/structure/modular.ts b/packages/create-express-forge/src/generator/structure/modular.ts index 4ddfd4b..c24795c 100644 --- a/packages/create-express-forge/src/generator/structure/modular.ts +++ b/packages/create-express-forge/src/generator/structure/modular.ts @@ -1,96 +1,61 @@ -import path from 'path'; -import type { CliOptions } from '../../types.js'; -import { writeFile } from '../../utils/file.js'; -import { - tplEnvConfig, tplApiError, tplApiResponse, tplAsyncHandler, - tplErrorHandler, tplNotFound, tplRateLimiter, tplValidate, - tplExpressTypes, tplServerTs, tplAppTs, -} from '../templates.js'; -import { TemplateManager } from '../../utils/template-manager.js'; - -export async function generateModularStructure(opts: CliOptions, dir: string): Promise { - const src = path.join(dir, 'src'); - const tm = new TemplateManager(opts); - - await writeFile(path.join(src, 'config', 'env.ts'), tplEnvConfig(opts)); - await writeFile(path.join(src, 'types', 'express.d.ts'), tplExpressTypes()); - await writeFile(path.join(src, 'utils', 'ApiError.ts'), tplApiError()); - await writeFile(path.join(src, 'utils', 'ApiResponse.ts'), tplApiResponse()); - await writeFile(path.join(src, 'utils', 'asyncHandler.ts'), tplAsyncHandler()); - await writeFile(path.join(src, 'middleware', 'errorHandler.ts'), tplErrorHandler()); - await writeFile(path.join(src, 'middleware', 'notFound.ts'), tplNotFound()); - await writeFile(path.join(src, 'middleware', 'rateLimiter.ts'), tplRateLimiter()); - await writeFile(path.join(src, 'middleware', 'validate.ts'), tplValidate()); - if (opts.auth !== 'none') { - await writeFile(path.join(src, 'middleware', 'auth.middleware.ts'), tm.renderAuthMiddleware(1)); +import path from "node:path"; +import type { CliOptions } from "../../types.js"; +import type { TemplateManager } from "../../utils/template-manager.js"; + +export async function generateModularStructure( + opts: CliOptions, + dir: string, + tmpl: TemplateManager, +): Promise { + const src = path.join(dir, "src"); + + if (opts.auth !== "none") { + await tmpl.renderTemplateFile( + "features/auth/jwt-auth.middleware.ts.eta", + path.join(src, "middleware", "auth.middleware.ts"), + ); } // Health module - await writeFile(path.join(src, 'modules', 'health', 'health.routes.ts'), - `import { Router } from 'express';\nimport { getHealth } from './health.controller.js';\nconst router = Router();\n/**\n * @openapi\n * /api/health:\n * get:\n * tags:\n * - Health\n * description: Responds if the app is up and running\n * responses:\n * 200:\n * description: App is up and running\n */\nrouter.get('/', getHealth);\nexport { router as healthRouter };\n`); - await writeFile(path.join(src, 'modules', 'health', 'health.controller.ts'), - `import type { Request, Response } from 'express';\nimport { ApiResponse } from '../../utils/ApiResponse.js';\nexport const getHealth = (_req: Request, res: Response) =>\n ApiResponse.success(res, { status: 'ok', timestamp: new Date().toISOString() }, 'Service healthy');\n`); + await tmpl.renderTemplateFile( + "structure/modular/modules/health/health.routes.ts.eta", + path.join(src, "modules", "health", "health.routes.ts"), + ); + await tmpl.renderTemplateFile( + "structure/modular/modules/health/health.controller.ts.eta", + path.join(src, "modules", "health", "health.controller.ts"), + ); // Todos module - await writeFile(path.join(src, 'modules', 'todos', 'todos.schema.ts'), - `import { z } from 'zod';\n\nexport const createTodoSchema = z.object({\n body: z.object({\n title: z.string().min(1).max(100),\n description: z.string().max(500).optional(),\n }),\n});\n\nexport const updateTodoSchema = z.object({\n params: z.object({ id: z.string() }),\n body: z.object({\n title: z.string().min(1).max(100).optional(),\n description: z.string().max(500).optional(),\n completed: z.boolean().optional(),\n }),\n});\n\nexport type CreateTodoDto = z.infer['body'];\nexport type UpdateTodoDto = z.infer['body'];\n`); - - await writeFile(path.join(src, 'modules', 'todos', 'todos.service.ts'), - `import { ApiError } from '../../utils/ApiError.js';\n\nexport interface Todo { id: string; title: string; description?: string; completed: boolean; ${opts.auth === 'none' ? '' : 'userId: string; '}}\n\nconst store: Todo[] = [];\n\nexport const TodosService = {\n findAll: async (${opts.auth === 'none' ? '' : 'userId: string'}) => store${opts.auth === 'none' ? '' : '.filter(t => t.userId === userId)'},\n findById: async (id: string${opts.auth === 'none' ? '' : ', userId: string'}) => {\n const todo = store.find((t) => t.id === id${opts.auth === 'none' ? '' : ' && t.userId === userId'});\n if (!todo) throw ApiError.notFound('Todo not found');\n return todo;\n },\n create: async (data: Omit) => {\n const todo = { id: Math.random().toString(36).substring(7), ...data };\n store.push(todo);\n return todo;\n },\n update: async (id: string, ${opts.auth === 'none' ? '' : 'userId: string, '}data: Partial) => {\n const todo = store.find((t) => t.id === id${opts.auth === 'none' ? '' : ' && t.userId === userId'});\n if (!todo) throw ApiError.notFound('Todo not found');\n Object.assign(todo, data);\n return todo;\n },\n remove: async (id: string${opts.auth === 'none' ? '' : ', userId: string'}) => {\n const idx = store.findIndex((t) => t.id === id${opts.auth === 'none' ? '' : ' && t.userId === userId'});\n if (idx === -1) throw ApiError.notFound('Todo not found');\n store.splice(idx, 1);\n },\n};\n`); - - await writeFile(path.join(src, 'modules', 'todos', 'todos.controller.ts'), - `import type { Request, Response } from 'express';\nimport { asyncHandler } from '../../utils/asyncHandler.js';\nimport { ApiResponse } from '../../utils/ApiResponse.js';\nimport { TodosService } from './todos.service.js';\nimport type { CreateTodoDto, UpdateTodoDto } from './todos.schema.js';\n\nexport const getTodos = asyncHandler(async (req, res: Response) => {\n const todos = await TodosService.findAll(${opts.auth === 'none' ? '' : "req.user?.id || 'guest'"});\n return ApiResponse.success(res, todos, 'Todos fetched');\n});\n\nexport const getTodoById = asyncHandler(async (req: Request, res: Response) => {\n const todo = await TodosService.findById(req.params.id || ''${opts.auth === 'none' ? '' : ", req.user?.id || 'guest'"});\n return ApiResponse.success(res, todo);\n});\n\nexport const createTodo = asyncHandler(async (req: Request, res: Response) => {\n const todo = await TodosService.create({ ...(req.body as CreateTodoDto), completed: false${opts.auth === 'none' ? '' : ", userId: req.user?.id || 'guest'" } });\n return ApiResponse.created(res, todo, 'Todo created');\n});\n\nexport const updateTodo = asyncHandler(async (req: Request, res: Response) => {\n const todo = await TodosService.update(req.params.id || '', ${opts.auth === 'none' ? '' : "req.user?.id || 'guest', "}req.body as UpdateTodoDto);\n return ApiResponse.success(res, todo, 'Todo updated');\n});\n\nexport const deleteTodo = asyncHandler(async (req: Request, res: Response) => {\n await TodosService.remove(req.params.id || ''${opts.auth === 'none' ? '' : ", req.user?.id || 'guest'"});\n return ApiResponse.noContent(res);\n});\n`); - - await writeFile(path.join(src, 'modules', 'todos', 'todos.routes.ts'), - `import { Router } from 'express';\nimport { getTodos, getTodoById, createTodo, updateTodo, deleteTodo } from './todos.controller.js';\nimport { validate } from '../../middleware/validate.js';\nimport { createTodoSchema, updateTodoSchema } from './todos.schema.js';\n${opts.auth !== 'none' ? "import { auth } from '../../middleware/auth.middleware.js';\n" : ""} -const router = Router(); -${opts.auth !== 'none' ? 'router.use(auth);' : ''} - -/** - * @openapi - * /api/v1/todos: - * get: - * tags: - * - Todos - * responses: - * 200: - * description: Success - * post: - * tags: - * - Todos - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [title] - * properties: - * title: - * type: string - * description: - * type: string - * responses: - * 201: - * description: Created - */ -router.get('/', getTodos); -router.get('/:id', getTodoById); -router.post('/', validate(createTodoSchema), createTodo); -router.patch('/:id', validate(updateTodoSchema), updateTodo); -router.delete('/:id', deleteTodo); -export { router as todosRouter };\n`); - - if (opts.auth === 'jwt') { - await writeFile(path.join(src, 'modules', 'auth', 'auth.schema.ts'), - `import { z } from 'zod';\nexport const loginSchema = z.object({ body: z.object({ email: z.string().email(), password: z.string().min(8) }) });\n`); - await writeFile(path.join(src, 'modules', 'auth', 'auth.controller.ts'), - `import type { Request, Response } from 'express';\nimport jwt from 'jsonwebtoken';\nimport { asyncHandler } from '../../utils/asyncHandler.js';\nimport { ApiResponse } from '../../utils/ApiResponse.js';\nimport { env } from '../../config/env.js';\n\nexport const login = asyncHandler(async (req, res) => {\n // Demo logic: accept any valid email/password\n const token = jwt.sign({ email: req.body.email }, env.JWT_SECRET, { expiresIn: env.JWT_EXPIRES_IN as jwt.SignOptions['expiresIn'] });\n ${opts.jwtStorage === 'cookie' ? "res.cookie('token', token, { httpOnly: true, secure: env.NODE_ENV === 'production' });\n return ApiResponse.success(res, { token }, 'Logged in successfully');" : "return ApiResponse.success(res, { token }, 'Logged in successfully');"}\n});\n`); - await writeFile(path.join(src, 'modules', 'auth', 'auth.routes.ts'), - `import { Router } from 'express';\nimport { login } from './auth.controller.js';\nimport { validate } from '../../middleware/validate.js';\nimport { loginSchema } from './auth.schema.js';\nconst router = Router();\n/**\n * @openapi\n * /api/v1/auth/login:\n * post:\n * tags:\n * - Auth\n * requestBody:\n * required: true\n * content:\n * application/json:\n * schema:\n * type: object\n * required: [email, password]\n * properties:\n * email:\n * type: string\n * password:\n * type: string\n * responses:\n * 200:\n * description: Logged in successfully\n */\nrouter.post('/login', validate(loginSchema), login);\nexport { router as authRouter };\n`); + await tmpl.renderTemplateFile( + "structure/modular/modules/todos/todos.schema.ts.eta", + path.join(src, "modules", "todos", "todos.schema.ts"), + ); + await tmpl.renderTemplateFile( + "structure/modular/modules/todos/todos.service.ts.eta", + path.join(src, "modules", "todos", "todos.service.ts"), + ); + await tmpl.renderTemplateFile( + "structure/modular/modules/todos/todos.controller.ts.eta", + path.join(src, "modules", "todos", "todos.controller.ts"), + ); + await tmpl.renderTemplateFile( + "structure/modular/modules/todos/todos.routes.ts.eta", + path.join(src, "modules", "todos", "todos.routes.ts"), + ); + + if (opts.auth === "jwt") { + await tmpl.renderTemplateFile( + "structure/modular/modules/auth/auth.schema.ts.eta", + path.join(src, "modules", "auth", "auth.schema.ts"), + ); + await tmpl.renderTemplateFile( + "structure/modular/modules/auth/auth.controller.ts.eta", + path.join(src, "modules", "auth", "auth.controller.ts"), + ); + await tmpl.renderTemplateFile( + "structure/modular/modules/auth/auth.routes.ts.eta", + path.join(src, "modules", "auth", "auth.routes.ts"), + ); } - - await writeFile(path.join(src, 'app.ts'), tplAppTs(opts)); - - await writeFile(path.join(src, 'server.ts'), tplServerTs(opts.logger)); } diff --git a/packages/create-express-forge/src/generator/structure/mvc.ts b/packages/create-express-forge/src/generator/structure/mvc.ts index 227e2d0..44f9a04 100644 --- a/packages/create-express-forge/src/generator/structure/mvc.ts +++ b/packages/create-express-forge/src/generator/structure/mvc.ts @@ -1,61 +1,62 @@ -import path from 'path'; -import type { CliOptions } from '../../types.js'; -import { writeFile } from '../../utils/file.js'; -import { - tplEnvConfig, tplApiError, tplApiResponse, tplAsyncHandler, - tplErrorHandler, tplNotFound, tplRateLimiter, tplValidate, - tplExpressTypes, tplServerTs, tplAppTs, -} from '../templates.js'; -import { TemplateManager } from '../../utils/template-manager.js'; - -export async function generateMvcStructure(opts: CliOptions, dir: string): Promise { - const src = path.join(dir, 'src'); - const tm = new TemplateManager(opts); - - await writeFile(path.join(src, 'config', 'env.ts'), tplEnvConfig(opts)); - await writeFile(path.join(src, 'types', 'express.d.ts'), tplExpressTypes()); - await writeFile(path.join(src, 'utils', 'ApiError.ts'), tplApiError()); - await writeFile(path.join(src, 'utils', 'ApiResponse.ts'), tplApiResponse()); - await writeFile(path.join(src, 'utils', 'asyncHandler.ts'), tplAsyncHandler()); - await writeFile(path.join(src, 'middleware', 'errorHandler.ts'), tplErrorHandler()); - await writeFile(path.join(src, 'middleware', 'notFound.ts'), tplNotFound()); - await writeFile(path.join(src, 'middleware', 'rateLimiter.ts'), tplRateLimiter()); - await writeFile(path.join(src, 'middleware', 'validate.ts'), tplValidate()); - if (opts.auth !== 'none') { - await writeFile(path.join(src, 'middleware', 'auth.middleware.ts'), tm.renderAuthMiddleware(1)); +import path from "node:path"; +import type { CliOptions } from "../../types.js"; +import type { TemplateManager } from "../../utils/template-manager.js"; + +export async function generateMvcStructure( + opts: CliOptions, + dir: string, + tmpl: TemplateManager, +): Promise { + const src = path.join(dir, "src"); + + if (opts.auth !== "none") { + await tmpl.renderTemplateFile( + "features/auth/jwt-auth.middleware.ts.eta", + path.join(src, "middleware", "auth.middleware.ts"), + ); } - await writeFile(path.join(src, 'schemas', 'todo.schema.ts'), - `import { z } from 'zod';\nexport const createTodoSchema = z.object({ body: z.object({ title: z.string().min(1).max(100), description: z.string().max(500).optional() }) });\nexport const updateTodoSchema = z.object({ params: z.object({ id: z.string() }), body: z.object({ title: z.string().min(1).max(100).optional(), description: z.string().max(500).optional(), completed: z.boolean().optional() }) });\nexport type CreateTodoDto = z.infer['body'];\nexport type UpdateTodoDto = z.infer['body'];\n`); - - await writeFile(path.join(src, 'services', 'todo.service.ts'), - `import { ApiError } from '../utils/ApiError.js';\nexport interface Todo { id: string; title: string; description?: string; completed: boolean; ${opts.auth === 'none' ? '' : 'userId: string; '}}\nconst store: Todo[] = [];\nexport const TodoService = {\n findAll: async (${opts.auth === 'none' ? '' : 'userId: string'}) => store${opts.auth === 'none' ? '' : '.filter(t => t.userId === userId)'},\n findById: async (id: string${opts.auth === 'none' ? '' : ', userId: string'}) => { const t = store.find((t) => t.id === id${opts.auth === 'none' ? '' : ' && t.userId === userId'}); if (!t) throw ApiError.notFound('Todo not found'); return t; },\n create: async (data: Omit) => { const t = { id: Math.random().toString(36).substring(7), ...data }; store.push(t); return t; },\n update: async (id: string, ${opts.auth === 'none' ? '' : 'userId: string, '}data: Partial) => { const t = store.find((t) => t.id === id${opts.auth === 'none' ? '' : ' && t.userId === userId'}); if (!t) throw ApiError.notFound('Todo not found'); Object.assign(t, data); return t; },\n remove: async (id: string${opts.auth === 'none' ? '' : ', userId: string'}) => { const i = store.findIndex((t) => t.id === id${opts.auth === 'none' ? '' : ' && t.userId === userId'}); if (i === -1) throw ApiError.notFound('Todo not found'); store.splice(i, 1); },\n};\n`); - - await writeFile(path.join(src, 'controllers', 'health.controller.ts'), - `import type { Request, Response } from 'express';\nimport { ApiResponse } from '../utils/ApiResponse.js';\nexport const getHealth = (_req: Request, res: Response) => ApiResponse.success(res, { status: 'ok', timestamp: new Date().toISOString() }, 'Service healthy');\n`); - - await writeFile(path.join(src, 'controllers', 'todo.controller.ts'), - `import type { Request, Response } from 'express';\nimport { asyncHandler } from '../utils/asyncHandler.js';\nimport { ApiResponse } from '../utils/ApiResponse.js';\nimport { TodoService } from '../services/todo.service.js';\nimport type { CreateTodoDto, UpdateTodoDto } from '../schemas/todo.schema.js';\nexport const getTodos = asyncHandler(async (req, res: Response) => ApiResponse.success(res, await TodoService.findAll(${opts.auth === 'none' ? '' : "req.user?.id || 'guest'"}), 'Todos fetched'));\nexport const getTodoById = asyncHandler(async (req: Request, res: Response) => ApiResponse.success(res, await TodoService.findById(req.params.id || ''${opts.auth === 'none' ? '' : ", req.user?.id || 'guest'"})));\nexport const createTodo = asyncHandler(async (req: Request, res: Response) => ApiResponse.created(res, await TodoService.create({ ...(req.body as CreateTodoDto), completed: false${opts.auth === 'none' ? '' : ", userId: req.user?.id || 'guest'"} }), 'Todo created'));\nexport const updateTodo = asyncHandler(async (req: Request, res: Response) => ApiResponse.success(res, await TodoService.update(req.params.id || '', ${opts.auth === 'none' ? '' : "req.user?.id || 'guest', "}req.body as UpdateTodoDto), 'Todo updated'));\nexport const deleteTodo = asyncHandler(async (req: Request, res: Response) => { await TodoService.remove(req.params.id || ''${opts.auth === 'none' ? '' : ", req.user?.id || 'guest'"}); return ApiResponse.noContent(res); });\n`); - - await writeFile(path.join(src, 'routes', 'index.ts'), - `import { Router } from 'express';\nimport { healthRouter } from './health.routes.js';\nimport { todoRouter } from './todo.routes.js';\n${opts.auth === 'jwt' ? "import { authRouter } from './auth.routes.js';\n" : ""}const router = Router();\nrouter.use('/health', healthRouter);\nrouter.use('/todos', todoRouter);\n${opts.auth === 'jwt' ? "router.use('/auth', authRouter);\n" : ""}export { router };\n`); - - await writeFile(path.join(src, 'routes', 'health.routes.ts'), - `import { Router } from 'express';\nimport { getHealth } from '../controllers/health.controller.js';\nconst router = Router();\n/**\n * @openapi\n * /api/health:\n * get:\n * tags:\n * - Health\n * description: Responds if the app is up and running\n * responses:\n * 200:\n * description: App is up and running\n */\nrouter.get('/', getHealth);\nexport { router as healthRouter };\n`); - - await writeFile(path.join(src, 'routes', 'todo.routes.ts'), - `import { Router } from 'express';\nimport { getTodos, getTodoById, createTodo, updateTodo, deleteTodo } from '../controllers/todo.controller.js';\nimport { validate } from '../middleware/validate.js';\nimport { createTodoSchema, updateTodoSchema } from '../schemas/todo.schema.js';\n${opts.auth !== 'none' ? "import { auth } from '../middleware/auth.middleware.js';\n" : ""}const router = Router();\n${opts.auth !== 'none' ? 'router.use(auth);\n' : ''}/**\n * @openapi\n * /api/v1/todos:\n * get:\n * tags:\n * - Todos\n * responses:\n * 200:\n * description: Success\n * post:\n * tags:\n * - Todos\n * requestBody:\n * required: true\n * content:\n * application/json:\n * schema:\n * type: object\n * required: [title]\n * properties:\n * title:\n * type: string\n * description:\n * type: string\n * responses:\n * 201:\n * description: Created\n */\nrouter.get('/', getTodos);\nrouter.get('/:id', getTodoById);\nrouter.post('/', validate(createTodoSchema), createTodo);\nrouter.patch('/:id', validate(updateTodoSchema), updateTodo);\nrouter.delete('/:id', deleteTodo);\nexport { router as todoRouter };\n`); - - if (opts.auth === 'jwt') { - await writeFile(path.join(src, 'schemas', 'auth.schema.ts'), - `import { z } from 'zod';\nexport const loginSchema = z.object({ body: z.object({ email: z.string().email(), password: z.string().min(8) }) });\n`); - await writeFile(path.join(src, 'controllers', 'auth.controller.ts'), - `import type { Request, Response } from 'express';\nimport jwt from 'jsonwebtoken';\nimport { asyncHandler } from '../utils/asyncHandler.js';\nimport { ApiResponse } from '../utils/ApiResponse.js';\nimport { env } from '../config/env.js';\n\nexport const login = asyncHandler(async (req, res) => {\n // Demo logic: accept any valid email/password\n const token = jwt.sign({ email: req.body.email }, env.JWT_SECRET, { expiresIn: env.JWT_EXPIRES_IN as jwt.SignOptions['expiresIn'] });\n ${opts.jwtStorage === 'cookie' ? "res.cookie('token', token, { httpOnly: true, secure: env.NODE_ENV === 'production' });\n return ApiResponse.success(res, { token }, 'Logged in successfully');" : "return ApiResponse.success(res, { token }, 'Logged in successfully');"}\n});\n`); - await writeFile(path.join(src, 'routes', 'auth.routes.ts'), - `import { Router } from 'express';\nimport { login } from '../controllers/auth.controller.js';\nimport { validate } from '../middleware/validate.js';\nimport { loginSchema } from '../schemas/auth.schema.js';\nconst router = Router();\n/**\n * @openapi\n * /api/v1/auth/login:\n * post:\n * tags:\n * - Auth\n * requestBody:\n * required: true\n * content:\n * application/json:\n * schema:\n * type: object\n * required: [email, password]\n * properties:\n * email:\n * type: string\n * password:\n * type: string\n * responses:\n * 200:\n * description: Logged in successfully\n */\nrouter.post('/login', validate(loginSchema), login);\nexport { router as authRouter };\n`); + await tmpl.renderTemplateFile( + "structure/mvc/schemas/todo.schema.ts.eta", + path.join(src, "schemas", "todo.schema.ts"), + ); + await tmpl.renderTemplateFile( + "structure/mvc/services/todo.service.ts.eta", + path.join(src, "services", "todo.service.ts"), + ); + await tmpl.renderTemplateFile( + "structure/mvc/controllers/health.controller.ts.eta", + path.join(src, "controllers", "health.controller.ts"), + ); + await tmpl.renderTemplateFile( + "structure/mvc/controllers/todo.controller.ts.eta", + path.join(src, "controllers", "todo.controller.ts"), + ); + await tmpl.renderTemplateFile( + "structure/mvc/routes/index.ts.eta", + path.join(src, "routes", "index.ts"), + ); + await tmpl.renderTemplateFile( + "structure/mvc/routes/health.routes.ts.eta", + path.join(src, "routes", "health.routes.ts"), + ); + await tmpl.renderTemplateFile( + "structure/mvc/routes/todo.routes.ts.eta", + path.join(src, "routes", "todo.routes.ts"), + ); + + if (opts.auth === "jwt") { + await tmpl.renderTemplateFile( + "structure/mvc/schemas/auth.schema.ts.eta", + path.join(src, "schemas", "auth.schema.ts"), + ); + await tmpl.renderTemplateFile( + "structure/mvc/controllers/auth.controller.ts.eta", + path.join(src, "controllers", "auth.controller.ts"), + ); + await tmpl.renderTemplateFile( + "structure/mvc/routes/auth.routes.ts.eta", + path.join(src, "routes", "auth.routes.ts"), + ); } - - await writeFile(path.join(src, 'app.ts'), tplAppTs(opts)); - - await writeFile(path.join(src, 'server.ts'), tplServerTs(opts.logger)); } diff --git a/packages/create-express-forge/src/generator/templates.ts b/packages/create-express-forge/src/generator/templates.ts deleted file mode 100644 index 263fff1..0000000 --- a/packages/create-express-forge/src/generator/templates.ts +++ /dev/null @@ -1,339 +0,0 @@ -import type { CliOptions } from '../types.js'; - -// All template strings for files written into the user's generated project - -export function tplEnvConfig(opts: CliOptions): string { - const hasDb = opts.orm !== 'none' && opts.database !== 'none'; - const isJwt = opts.auth === 'jwt'; - const isSession = opts.auth === 'session'; - - return `import { z } from 'zod'; - -const envSchema = z.object({ - NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), - PORT: z.coerce.number().default(3000), - CORS_ORIGIN: z.string().default('*'), - RATE_LIMIT_WINDOW_MS: z.coerce.number().default(900_000), - RATE_LIMIT_MAX: z.coerce.number().default(100), -${hasDb ? ` DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL'),` : " // DATABASE_URL: z.string(),"} -${isJwt ? ` JWT_SECRET: z.string().min(1, 'JWT_SECRET is required'),\n JWT_EXPIRES_IN: z.string().default('1d'),` : ''}${isSession ? ` SESSION_SECRET: z.string().min(1, 'SESSION_SECRET is required'),` : ''} -${opts.cache === 'redis' ? ` REDIS_URL: z.string().url().default('redis://localhost:6379'),` : ''} -}); - -const parsed = envSchema.safeParse(process.env); - -if (!parsed.success) { - console.error('\\n❌ Invalid environment variables:'); - console.error(JSON.stringify(parsed.error.format(), null, 2)); - process.exit(1); -} - -export const env = parsed.data; -export type Env = typeof env; -`; -} - -export function tplApiError(): string { - return `export class ApiError extends Error { - public readonly statusCode: number; - public readonly errors: unknown[]; - public readonly isOperational: boolean; - - constructor(statusCode: number, message: string, errors: unknown[] = [], isOperational = true) { - super(message); - this.statusCode = statusCode; - this.errors = errors; - this.isOperational = isOperational; - Object.setPrototypeOf(this, ApiError.prototype); - Error.captureStackTrace(this, this.constructor); - } - - static badRequest(message = 'Bad Request', errors: unknown[] = []) { return new ApiError(400, message, errors); } - static unauthorized(message = 'Unauthorized') { return new ApiError(401, message); } - static forbidden(message = 'Forbidden') { return new ApiError(403, message); } - static notFound(message = 'Not Found') { return new ApiError(404, message); } - static conflict(message = 'Conflict') { return new ApiError(409, message); } - static internal(message = 'Internal Server Error') { return new ApiError(500, message, [], false); } -} -`; -} - -export function tplApiResponse(): string { - return `import type { Response } from 'express'; - -export class ApiResponse { - static success(res: Response, data: T, message = 'Success', statusCode = 200) { - return res.status(statusCode).json({ success: true, message, data }); - } - static created(res: Response, data: T, message = 'Created') { - return ApiResponse.success(res, data, message, 201); - } - static noContent(res: Response) { return res.status(204).send(); } - static paginated(res: Response, data: T[], pagination: { total: number; page: number; limit: number; pages: number }, message = 'Success') { - return res.status(200).json({ success: true, message, data, pagination }); - } -} -`; -} - -export function tplAsyncHandler(): string { - return `import type { Request, Response, NextFunction, RequestHandler } from 'express'; - -type AsyncFn = (req: Request, res: Response, next: NextFunction) => Promise; - -/** Wraps async route handlers — errors forwarded to global error handler */ -export const asyncHandler = (fn: AsyncFn): RequestHandler => - (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; -`; -} - -export function tplErrorHandler(): string { - return `import type { Request, Response, NextFunction } from 'express'; -import { ZodError } from 'zod'; -import { ApiError } from '../utils/ApiError.js'; -import { env } from '../config/env.js'; - -/** - * Global centralized error handler — must be the LAST middleware registered. - * Handles: ApiError, ZodError, and unknown errors. - */ -export const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction): void => { - const isDev = env.NODE_ENV === 'development'; - - if (err instanceof ApiError) { - res.status(err.statusCode).json({ - success: false, - message: err.message, - ...(err.errors.length > 0 && { errors: err.errors }), - ...(isDev && { stack: err.stack }), - }); - return; - } - - if (err instanceof ZodError) { - res.status(400).json({ - success: false, - message: 'Validation failed', - errors: err.errors.map((e) => ({ path: e.path.join('.'), message: e.message })), - }); - return; - } - - console.error('[UnhandledError]', err); - res.status(500).json({ - success: false, - message: 'Internal Server Error', - ...(isDev && { stack: err.stack }), - }); -}; -`; -} - -export function tplNotFound(): string { - return `import type { Request, Response, NextFunction } from 'express'; -import { ApiError } from '../utils/ApiError.js'; - -export const notFound = (req: Request, _res: Response, next: NextFunction): void => { - next(ApiError.notFound(\`Route \${req.method} \${req.url} not found\`)); -}; -`; -} - -export function tplRateLimiter(): string { - return `import rateLimit from 'express-rate-limit'; -import { env } from '../config/env.js'; - -export const rateLimiter = rateLimit({ - windowMs: env.RATE_LIMIT_WINDOW_MS, - max: env.RATE_LIMIT_MAX, - standardHeaders: true, - legacyHeaders: false, - message: { success: false, message: 'Too many requests, please try again later.' }, -}); -`; -} - -export function tplValidate(): string { - return `import type { Request, Response, NextFunction } from 'express'; -import { type AnyZodObject, ZodError } from 'zod'; -import { ApiError } from '../utils/ApiError.js'; - -/** Validates req.body / req.query / req.params against a Zod schema */ -export const validate = (schema: AnyZodObject) => - async (req: Request, _res: Response, next: NextFunction): Promise => { - try { - const validated = await schema.parseAsync({ body: req.body, query: req.query, params: req.params }); - req.body = validated.body; - req.query = validated.query; - req.params = validated.params; - next(); - } catch (err) { - if (err instanceof ZodError) next(ApiError.badRequest('Validation failed', err.errors)); - else next(err); - } - }; -`; -} - -export function tplExpressTypes(): string { - return `import 'express'; - -declare module 'express' { - interface Request { - user?: { id: string; email: string; role: string }; - } -} -`; -} - -export function tplAppTs(opts: CliOptions): string { - const isModular = opts.pattern === 'modular'; - const hasSwagger = opts.openapi; - const needsCookies = opts.jwtStorage === 'cookie' || opts.auth === 'session'; - - return `import express from 'express'; -import cors from 'cors'; -import helmet from 'helmet'; -import compression from 'compression'; -${needsCookies ? "import cookieParser from 'cookie-parser';" : ''} -${opts.auth === 'session' ? "import session from 'express-session';" : ''} -import { env } from './config/env.js'; -import { rateLimiter } from './middleware/rateLimiter.js'; -${opts.logger === 'pino' ? "import { httpLogger } from './middleware/httpLogger.js';" : ''} -import { notFound } from './middleware/notFound.js'; -import { errorHandler } from './middleware/errorHandler.js'; -${hasSwagger ? "import { setupSwagger } from './docs/swagger.js';" : ''} -${isModular ? `import { healthRouter } from './modules/health/health.routes.js'; -import { todosRouter } from './modules/todos/todos.routes.js'; -${opts.auth !== 'none' ? "import { authRouter } from './modules/auth/auth.routes.js';" : ''}` : "import { router } from './routes/index.js';"} - -const app = express(); - -app.use(helmet()); -app.use(cors({ origin: env.CORS_ORIGIN, credentials: true })); -${needsCookies ? 'app.use(cookieParser());' : ''} -${opts.auth === 'session' ? "app.use(session({ secret: env.SESSION_SECRET, resave: false, saveUninitialized: false }));" : ''} -app.use(express.json({ limit: '10mb' })); -app.use(express.urlencoded({ extended: true })); -app.use(compression()); -app.use(rateLimiter); -${opts.logger === 'pino' ? 'app.use(httpLogger);' : ''} - -${hasSwagger ? 'setupSwagger(app);' : ''} - -${ - isModular - ? `app.use('/api/health', healthRouter); -app.use('/api/v1/todos', todosRouter); -${opts.auth !== 'none' ? "app.use('/api/v1/auth', authRouter);" : ''}` - : "app.use('/api/v1', router);" -} - -app.use(notFound); -app.use(errorHandler); - -export { app }; -`; -} - -export function tplServerTs(loggerLib: string): string { - const hasLogger = loggerLib !== 'none'; - const logImport = hasLogger ? `import { logger } from './logger/index.js';\n` : ''; - const log = hasLogger ? 'logger.info' : 'console.log'; - return `import 'dotenv/config'; -${logImport}import { app } from './app.js'; -import { env } from './config/env.js'; - -const server = app.listen(env.PORT, () => { - ${log}(\`🚀 Server running on port \${env.PORT} in \${env.NODE_ENV} mode\`); -}); - -const shutdown = (signal: string) => { - ${log}(\`\${signal} received — shutting down gracefully\`); - server.close(() => process.exit(0)); -}; - -process.on('SIGTERM', () => shutdown('SIGTERM')); -process.on('SIGINT', () => shutdown('SIGINT')); -process.on('uncaughtException', (err) => { console.error('Uncaught:', err); process.exit(1); }); -process.on('unhandledRejection', (r) => { console.error('Unhandled:', r); process.exit(1); }); -`; -} - -export function tplTodosService(opts: CliOptions): string { - if (opts.orm === 'prisma') { - return `import { prisma } from '../../config/database.js'; -import { ApiError } from '../../utils/ApiError.js'; -import type { CreateTodoDto, UpdateTodoDto } from './todos.schema.js'; - -export const TodosService = { - findAll: async (userId: string) => { - return prisma.user.findUnique({ - where: { id: userId } - }).todos(); // Assuming a relation exists, or just filter: - // return prisma.todo.findMany({ where: { userId } }); - }, - - findById: async (id: string, userId: string) => { - const todo = await prisma.todo.findFirst({ where: { id, userId } }); - if (!todo) throw ApiError.notFound('Todo not found'); - return todo; - }, - - create: async (userId: string, data: CreateTodoDto) => { - return prisma.todo.create({ - data: { ...data, userId } - }); - }, - - update: async (id: string, userId: string, data: UpdateTodoDto) => { - const todo = await prisma.todo.findFirst({ where: { id, userId } }); - if (!todo) throw ApiError.notFound('Todo not found'); - return prisma.todo.update({ - where: { id }, - data - }); - }, - - remove: async (id: string, userId: string) => { - const todo = await prisma.todo.findFirst({ where: { id, userId } }); - if (!todo) throw ApiError.notFound('Todo not found'); - return prisma.todo.delete({ where: { id } }); - }, -}; -`; - } - - // Default in-memory store for 'none' or simple demo - return `import { ApiError } from '../../utils/ApiError.js'; - -export interface Todo { id: string; title: string; description?: string; completed: boolean; userId: string; } - -const store: Todo[] = []; - -export const TodosService = { - findAll: async (userId: string) => store.filter(t => t.userId === userId), - findById: async (id: string, userId: string) => { - const todo = store.find((t) => t.id === id && t.userId === userId); - if (!todo) throw ApiError.notFound('Todo not found'); - return todo; - }, - create: async (userId: string, data: any) => { - const todo = { id: Math.random().toString(36).substring(7), ...data, userId, completed: false }; - store.push(todo); - return todo; - }, - update: async (id: string, userId: string, data: any) => { - const todo = store.find((t) => t.id === id && t.userId === userId); - if (!todo) throw ApiError.notFound('Todo not found'); - Object.assign(todo, data); - return todo; - }, - remove: async (id: string, userId: string) => { - const idx = store.findIndex((t) => t.id === id && t.userId === userId); - if (idx === -1) throw ApiError.notFound('Todo not found'); - store.splice(idx, 1); - }, -}; -`; -} diff --git a/packages/create-express-forge/src/index.ts b/packages/create-express-forge/src/index.ts index 1b3e56f..0ccefe7 100644 --- a/packages/create-express-forge/src/index.ts +++ b/packages/create-express-forge/src/index.ts @@ -1,29 +1,36 @@ -import { Command } from 'commander'; -import { createRequire } from 'module'; +import { createRequire } from "node:module"; +import { Command } from "commander"; const require = createRequire(import.meta.url); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -const pkg = require('../package.json') as { version: string }; +const pkg = require("../package.json") as { version: string }; -import { runCLI } from './prompts.js'; +import { runCLI } from "./prompts.js"; const program = new Command(); program - .name('create-express-forge') - .description('⚡ Scaffold production-ready Express.js TypeScript backends in seconds') + .name("create-express-forge") + .description( + "⚡ Scaffold production-ready Express.js TypeScript backends in seconds", + ) .version(pkg.version) - .option('-y, --yes', 'Use default options for all prompts') - .option('--pattern ', 'Architecture pattern (modular, mvc)') - .option('--orm ', 'ORM to use (prisma, sequelize, none)') - .option('--db ', 'Database type (postgresql, mysql, sqlite, none)') - .option('--logger ', 'Logging library (winston, pino, none)') - .option('--test ', 'Testing framework (vitest, jest, none)') - .option('--docker ', 'Include Docker setup (true, false)') - .option('--install ', 'Auto-install dependencies (true, false)') - .argument('[project-name]', 'Name of the project') - .action(async (projectName?: string, options?: Record) => { - await runCLI(projectName, options); - }); + .option("-y, --yes", "Use default options for all prompts") + .option("--pattern ", "Architecture pattern (modular, mvc)") + .option("--orm ", "ORM to use (prisma, sequelize, none)") + .option("--db ", "Database type (postgresql, mysql, sqlite, none)") + .option("--logger ", "Logging library (winston, pino, none)") + .option("--test ", "Testing framework (vitest, jest, none)") + .option("--docker ", "Include Docker setup (true, false)") + .option("--install ", "Auto-install dependencies (true, false)") + .argument("[project-name]", "Name of the project") + .action( + async ( + projectName?: string, + options?: Record, + ) => { + await runCLI(projectName, options); + }, + ); program.parse(process.argv); diff --git a/packages/create-express-forge/src/prompts.ts b/packages/create-express-forge/src/prompts.ts index 231cea1..e14f649 100644 --- a/packages/create-express-forge/src/prompts.ts +++ b/packages/create-express-forge/src/prompts.ts @@ -1,25 +1,45 @@ -import { input, select, confirm } from '@inquirer/prompts'; -import path from 'path'; -import type { CliOptions, Pattern, ORM, Database, PackageManager, LoggerLib, TestingLib, AuthStrategy, JwtStorage, CacheLib } from './types.js'; -import { displayBanner } from './utils/display.js'; -import { generateProject } from './generator/index.js'; - -export async function runCLI(initialProjectName?: string, cmdOptions: Record = {}) { +import path from "node:path"; +import { confirm, input, select } from "@inquirer/prompts"; +import { generateProject } from "./generator/index.js"; +import type { + AuthStrategy, + CacheLib, + CliOptions, + Database, + JwtStorage, + LoggerLib, + ORM, + PackageManager, + Pattern, + TestingLib, +} from "./types.js"; +import { displayBanner } from "./utils/display.js"; + +export async function runCLI( + initialProjectName?: string, + cmdOptions: Record = {}, +) { displayBanner(); if (cmdOptions.yes) { - const projectName = initialProjectName ?? 'my-express-app'; + const projectName = initialProjectName ?? "my-express-app"; + + // Detect package manager (default to pnpm if we are in this workspace) + const packageManager = process.env.npm_config_user_agent?.includes("pnpm") + ? "pnpm" + : "npm"; + const options: CliOptions = { projectName, - pattern: 'modular', - orm: 'prisma', - database: 'postgresql', - packageManager: 'npm', - logger: 'winston', - testing: 'vitest', - auth: 'jwt', - jwtStorage: 'cookie', - cache: 'redis', + pattern: "modular", + orm: "prisma", + database: "postgresql", + packageManager, + logger: "winston", + testing: "vitest", + auth: "jwt", + jwtStorage: "cookie", + cache: "redis", importAlias: true, openapi: true, openapiUI: true, @@ -34,111 +54,175 @@ export async function runCLI(initialProjectName?: string, cmdOptions: Record /^[a-z0-9-_]+$/.test(v) ? true - : 'Use lowercase letters, numbers, hyphens, or underscores', + : "Use lowercase letters, numbers, hyphens, or underscores", })); - - const packageManager = await select({ - message: 'Package manager:', - choices: [ - { name: '📦 npm', value: 'npm' }, - { name: '🚀 pnpm', value: 'pnpm' }, - { name: '🧶 yarn', value: 'yarn' }, - { name: '🍞 bun', value: 'bun' }, - ], - }); - const pattern = cmdOptions.pattern ?? await select({ - message: 'Architecture pattern:', + const packageManager = await select({ + message: "Package manager:", choices: [ - { name: '📦 Modular — feature-based modules (recommended)', value: 'modular' }, - { name: '🏗️ MVC — Model / View / Controller', value: 'mvc' }, + { name: "📦 npm", value: "npm" }, + { name: "🚀 pnpm", value: "pnpm" }, + { name: "🧶 yarn", value: "yarn" }, + { name: "🍞 bun", value: "bun" }, ], }); - const orm = cmdOptions.orm ?? await select({ - message: 'ORM / Database layer:', - choices: [ - { name: '🔷 Prisma (type-safe, modern — recommended)', value: 'prisma' }, - { name: '🔶 Sequelize (battle-tested)', value: 'sequelize' }, - { name: '⬜ None (configure later)', value: 'none' }, - ], - }); + const pattern = + cmdOptions.pattern ?? + (await select({ + message: "Architecture pattern:", + choices: [ + { + name: "📦 Modular — feature-based modules (recommended)", + value: "modular", + }, + { name: "🏗️ MVC — Model / View / Controller", value: "mvc" }, + ], + })); + + const orm = + cmdOptions.orm ?? + (await select({ + message: "ORM / Database layer:", + choices: [ + { + name: "🔷 Prisma (type-safe, modern — recommended)", + value: "prisma", + }, + { name: "🔶 Sequelize (battle-tested)", value: "sequelize" }, + { name: "⬜ None (configure later)", value: "none" }, + ], + })); - const database: CliOptions['database'] = (cmdOptions.db as Database) ?? ( - orm !== 'none' - ? await select({ - message: 'Database:', + const database: CliOptions["database"] = + (cmdOptions.db as Database) ?? + (orm !== "none" + ? await select({ + message: "Database:", choices: [ - { name: '🐘 PostgreSQL', value: 'postgresql' }, - { name: '🐬 MySQL', value: 'mysql' }, - { name: '🪶 SQLite', value: 'sqlite' }, - { name: '⬜ None (configure later)', value: 'none' }, + { name: "🐘 PostgreSQL", value: "postgresql" }, + { name: "🐬 MySQL", value: "mysql" }, + { name: "🪶 SQLite", value: "sqlite" }, + { name: "⬜ None (configure later)", value: "none" }, ], }) - : 'none'); - - const logger = cmdOptions.logger ?? await select({ - message: 'Logger:', - choices: [ - { name: '🪵 Winston (feature-rich, transport-based)', value: 'winston' }, - { name: '⚡ Pino (ultra-fast, low-overhead)', value: 'pino' }, - { name: '⬜ None (console.log)', value: 'none' }, - ], - }); + : "none"); + + const logger = + cmdOptions.logger ?? + (await select({ + message: "Logger:", + choices: [ + { + name: "🪵 Winston (feature-rich, transport-based)", + value: "winston", + }, + { name: "⚡ Pino (ultra-fast, low-overhead)", value: "pino" }, + { name: "⬜ None (console.log)", value: "none" }, + ], + })); - const testing = cmdOptions.test ?? await select({ - message: 'Testing framework:', - choices: [ - { name: '⚡ Vitest (fast, ESM-native — recommended)', value: 'vitest' }, - { name: '🃏 Jest (widely-used)', value: 'jest' }, - { name: '⬜ None', value: 'none' }, - ], - }); + const testing = + cmdOptions.test ?? + (await select({ + message: "Testing framework:", + choices: [ + { + name: "⚡ Vitest (fast, ESM-native — recommended)", + value: "vitest", + }, + { name: "🃏 Jest (widely-used)", value: "jest" }, + { name: "⬜ None", value: "none" }, + ], + })); - const auth = cmdOptions.auth ?? await select({ - message: 'Authentication strategy:', - choices: [ - { name: '🔐 JWT (stateless, token-based — recommended)', value: 'jwt' }, - { name: '🍪 Session (stateful, cookie-based)', value: 'session' }, - { name: '⬜ None', value: 'none' }, - ], - }); + const auth = + cmdOptions.auth ?? + (await select({ + message: "Authentication strategy:", + choices: [ + { + name: "🔐 JWT (stateless, token-based — recommended)", + value: "jwt", + }, + { name: "🍪 Session (stateful, cookie-based)", value: "session" }, + { name: "⬜ None", value: "none" }, + ], + })); - const jwtStorage = cmdOptions.jwtStorage ?? ( - auth === 'jwt' - ? await select({ - message: 'JWT storage location:', + const jwtStorage = + cmdOptions.jwtStorage ?? + (auth === "jwt" + ? await select({ + message: "JWT storage location:", choices: [ - { name: '🍪 Cookie (more secure against XSS)', value: 'cookie' }, - { name: '📨 Header (Authorization: Bearer )', value: 'header' }, + { + name: "🍪 Cookie (more secure against XSS)", + value: "cookie", + }, + { + name: "📨 Header (Authorization: Bearer )", + value: "header", + }, ], }) : undefined); - const cache = cmdOptions.cache ?? await select({ - message: 'Caching layer:', - choices: [ - { name: '🔴 Redis (distributed, high-performance)', value: 'redis' }, - { name: '💾 Node-Cache (simple, in-memory)', value: 'node-cache' }, - { name: '⬜ None', value: 'none' }, - ], - }); - - const importAlias = cmdOptions.importAlias !== undefined ? cmdOptions.importAlias === 'true' : await confirm({ message: 'Configure import alias (@/)?', default: true }); + const cache = + cmdOptions.cache ?? + (await select({ + message: "Caching layer:", + choices: [ + { + name: "🔴 Redis (distributed, high-performance)", + value: "redis", + }, + { name: "💾 Node-Cache (simple, in-memory)", value: "node-cache" }, + { name: "⬜ None", value: "none" }, + ], + })); - const openapi = cmdOptions.openapi !== undefined ? cmdOptions.openapi === 'true' : await confirm({ message: 'Add OpenAPI (Swagger) documentation?', default: true }); + const importAlias = + cmdOptions.importAlias !== undefined + ? cmdOptions.importAlias === "true" + : await confirm({ + message: "Configure import alias (@/)?", + default: true, + }); + + const openapi = + cmdOptions.openapi !== undefined + ? cmdOptions.openapi === "true" + : await confirm({ + message: "Add OpenAPI (Swagger) documentation?", + default: true, + }); const openapiUI = openapi - ? (cmdOptions.openapiUI !== undefined ? cmdOptions.openapiUI === 'true' : await confirm({ message: 'Add Swagger UI for documentation?', default: true })) + ? cmdOptions.openapiUI !== undefined + ? cmdOptions.openapiUI === "true" + : await confirm({ + message: "Add Swagger UI for documentation?", + default: true, + }) : false; - const docker = cmdOptions.docker !== undefined ? cmdOptions.docker === 'true' : await confirm({ message: 'Add Docker + docker-compose?', default: true }); - const installDeps = cmdOptions.install !== undefined ? cmdOptions.install === 'true' : await confirm({ message: 'Install dependencies now?', default: true }); + const docker = + cmdOptions.docker !== undefined + ? cmdOptions.docker === "true" + : await confirm({ + message: "Add Docker + docker-compose?", + default: true, + }); + const installDeps = + cmdOptions.install !== undefined + ? cmdOptions.install === "true" + : await confirm({ message: "Install dependencies now?", default: true }); const options: CliOptions = { projectName, diff --git a/packages/create-express-forge/src/types.ts b/packages/create-express-forge/src/types.ts index a461356..d2994e6 100644 --- a/packages/create-express-forge/src/types.ts +++ b/packages/create-express-forge/src/types.ts @@ -1,12 +1,12 @@ -export type Pattern = 'modular' | 'mvc'; -export type ORM = 'prisma' | 'sequelize' | 'none'; -export type Database = 'postgresql' | 'mysql' | 'sqlite' | 'none'; -export type LoggerLib = 'winston' | 'pino' | 'none'; -export type TestingLib = 'vitest' | 'jest' | 'none'; -export type CacheLib = 'redis' | 'node-cache' | 'none'; -export type AuthStrategy = 'jwt' | 'session' | 'none'; -export type JwtStorage = 'cookie' | 'header'; -export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'; +export type Pattern = "modular" | "mvc"; +export type ORM = "prisma" | "sequelize" | "none"; +export type Database = "postgresql" | "mysql" | "sqlite" | "none"; +export type LoggerLib = "winston" | "pino" | "none"; +export type TestingLib = "vitest" | "jest" | "none"; +export type CacheLib = "redis" | "node-cache" | "none"; +export type AuthStrategy = "jwt" | "session" | "none"; +export type JwtStorage = "cookie" | "header"; +export type PackageManager = "npm" | "pnpm" | "yarn" | "bun"; export interface CliOptions { projectName: string; @@ -24,4 +24,5 @@ export interface CliOptions { openapiUI?: boolean; docker: boolean; installDeps: boolean; + skipPolish?: boolean; } diff --git a/packages/create-express-forge/src/utils/display.ts b/packages/create-express-forge/src/utils/display.ts index 0c9765c..bf9f981 100644 --- a/packages/create-express-forge/src/utils/display.ts +++ b/packages/create-express-forge/src/utils/display.ts @@ -1,50 +1,74 @@ -import chalk from 'chalk'; +import chalk from "chalk"; export function displayBanner(): void { console.log(); - console.log(chalk.bold.hex('#7C3AED')(' ╔══════════════════════════════════════╗')); console.log( - chalk.bold.hex('#7C3AED')(' ║') + - chalk.bold.white(' ⚡ create-express-forge ') + - chalk.bold.hex('#7C3AED')('║'), + chalk.bold.hex("#7C3AED")(" ╔══════════════════════════════════════╗"), ); console.log( - chalk.bold.hex('#7C3AED')(' ║') + - chalk.dim(' Production-ready backends. Fast. ') + - chalk.bold.hex('#7C3AED')('║'), + chalk.bold.hex("#7C3AED")(" ║") + + chalk.bold.white(" ⚡ create-express-forge ") + + chalk.bold.hex("#7C3AED")("║"), + ); + console.log( + chalk.bold.hex("#7C3AED")(" ║") + + chalk.dim(" Production-ready backends. Fast. ") + + chalk.bold.hex("#7C3AED")("║"), + ); + console.log( + chalk.bold.hex("#7C3AED")(" ╚══════════════════════════════════════╝"), ); - console.log(chalk.bold.hex('#7C3AED')(' ╚══════════════════════════════════════╝')); console.log(); } -export function displaySuccess(projectName: string, packageManager: string, installDeps: boolean): void { +export function displaySuccess( + projectName: string, + packageManager: string, + installDeps: boolean, +): void { const pm = packageManager; - const run = pm === 'npm' ? 'npm run' : pm; + const run = pm === "npm" ? "npm run" : pm; console.log(); - console.log(chalk.bold.green(' ✨ Success! Your project is ready at ') + chalk.cyan(`./${projectName}`)); - console.log(chalk.dim(' ────────────────────────────────────────────────────────────────')); + console.log( + chalk.bold.green(" ✨ Success! Your project is ready at ") + + chalk.cyan(`./${projectName}`), + ); + console.log( + chalk.dim( + " ────────────────────────────────────────────────────────────────", + ), + ); console.log(); - console.log(chalk.bold(' Next steps to get started:\n')); - + console.log(chalk.bold(" Next steps to get started:\n")); + let step = 1; console.log(chalk.white(` ${step++}. `) + chalk.cyan(`cd ${projectName}`)); - + if (!installDeps) { console.log(chalk.white(` ${step++}. `) + chalk.cyan(`${pm} install`)); } - - console.log(chalk.white(` ${step++}. `) + chalk.cyan('cp .env.example .env')); + + console.log( + chalk.white(` ${step++}. `) + chalk.cyan("cp .env.example .env"), + ); console.log(chalk.white(` ${step++}. `) + chalk.cyan(`${run} dev`)); - + console.log(); - console.log(chalk.bold(' Useful commands:\n')); - console.log(chalk.white(` • ${run} build `) + chalk.dim('— Compile the project')); - console.log(chalk.white(` • ${run} test `) + chalk.dim('— Run the test suite')); - console.log(chalk.white(` • ${run} lint `) + chalk.dim('— Run linting checks')); - + console.log(chalk.bold(" Useful commands:\n")); + console.log( + chalk.white(` • ${run} build `) + + chalk.dim("— Compile the project"), + ); + console.log( + chalk.white(` • ${run} test `) + chalk.dim("— Run the test suite"), + ); + console.log( + chalk.white(` • ${run} lint `) + chalk.dim("— Run linting checks"), + ); + console.log(); - console.log(chalk.bold.hex('#7C3AED')(' Happy building! 🚀\n')); + console.log(chalk.bold.hex("#7C3AED")(" Happy building! 🚀\n")); } export function displayError(message: string): void { diff --git a/packages/create-express-forge/src/utils/file.ts b/packages/create-express-forge/src/utils/file.ts index 1c33b71..43ecfa0 100644 --- a/packages/create-express-forge/src/utils/file.ts +++ b/packages/create-express-forge/src/utils/file.ts @@ -1,12 +1,15 @@ -import fs from 'fs-extra'; -import path from 'path'; +import path from "node:path"; +import fs from "fs-extra"; -export async function writeFile(filePath: string, content: string): Promise { +export async function writeFile( + filePath: string, + content: string, +): Promise { await fs.ensureDir(path.dirname(filePath)); - await fs.writeFile(filePath, content, 'utf-8'); + await fs.writeFile(filePath, content, "utf-8"); } export async function writeJson(filePath: string, data: object): Promise { await fs.ensureDir(path.dirname(filePath)); - await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8'); + await fs.writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf-8"); } diff --git a/packages/create-express-forge/src/utils/package-builder.ts b/packages/create-express-forge/src/utils/package-builder.ts index b41e6ca..afb1946 100644 --- a/packages/create-express-forge/src/utils/package-builder.ts +++ b/packages/create-express-forge/src/utils/package-builder.ts @@ -1,129 +1,134 @@ -import type { CliOptions } from '../types.js'; +import type { PackageJson } from "pkg-types"; +import type { CliOptions } from "../types.js"; -export function buildPackageJson(opts: CliOptions): object { - const { projectName, orm, database, logger, testing, cache, auth, openapi } = opts; +export function buildPackageJson(opts: CliOptions): PackageJson { + const { projectName, orm, database, logger, testing, cache, auth, openapi } = + opts; const deps: Record = { - express: '^4.19.2', - cors: '^2.8.5', - helmet: '^7.1.0', - compression: '^1.7.4', - dotenv: '^16.4.5', - 'express-rate-limit': '^7.3.1', - zod: '^3.23.8', + express: "^5.2.1", + cors: "^2.8.5", + helmet: "^8.0.0", + compression: "^1.7.5", + dotenv: "^16.4.5", + "express-rate-limit": "^7.4.0", + zod: "^3.24.1", }; const devDeps: Record = { - typescript: '^5.5.3', - tsx: '^4.15.0', - tsup: '^8.1.0', - '@types/node': '^20.14.0', - '@types/express': '^4.17.21', - '@types/cors': '^2.8.17', - '@types/compression': '^1.7.5', + typescript: "^5.6.2", + tsx: "^4.19.1", + tsup: "^8.3.0", + "@types/node": "^22.7.5", + "@types/express": "^5.0.0", + "@types/cors": "^2.8.17", + "@types/compression": "^1.7.5", + "@biomejs/biome": "^1.9.4", }; - if (orm === 'prisma') { - deps['@prisma/client'] = '^6.0.0'; - devDeps['prisma'] = '^6.0.0'; - } else if (orm === 'sequelize') { - deps['sequelize'] = '^6.37.3'; - deps['sequelize-typescript'] = '^2.1.6'; - deps['reflect-metadata'] = '^0.2.2'; - devDeps['sequelize-cli'] = '^6.6.2'; - if (database === 'postgresql') deps['pg'] = '^8.12.0'; - else if (database === 'mysql') deps['mysql2'] = '^3.10.1'; - else if (database === 'sqlite') deps['sqlite3'] = '^5.1.7'; + if (orm === "prisma") { + deps["@prisma/client"] = "^6.2.1"; + devDeps.prisma = "^6.2.1"; + } else if (orm === "sequelize") { + deps.sequelize = "^6.37.3"; + deps["sequelize-typescript"] = "^2.1.6"; + deps["reflect-metadata"] = "^0.2.2"; + devDeps["sequelize-cli"] = "^6.6.2"; + if (database === "postgresql") deps.pg = "^8.13.0"; + else if (database === "mysql") deps.mysql2 = "^3.11.2"; + else if (database === "sqlite") deps.sqlite3 = "^5.1.7"; } - if (logger === 'winston') { - deps['winston'] = '^3.13.0'; - deps['winston-daily-rotate-file'] = '^5.0.0'; - } else if (logger === 'pino') { - deps['pino'] = '^9.3.1'; - deps['pino-http'] = '^10.2.0'; - devDeps['pino-pretty'] = '^11.2.1'; + if (logger === "winston") { + deps.winston = "^3.19.0"; + deps["winston-daily-rotate-file"] = "^5.0.0"; + } else if (logger === "pino") { + deps.pino = "^10.3.1"; + deps["pino-http"] = "^10.3.0"; + devDeps["pino-pretty"] = "^11.2.2"; } - if (testing === 'vitest') { - devDeps['vitest'] = '^1.6.0'; - devDeps['@vitest/coverage-v8'] = '^1.6.0'; - devDeps['supertest'] = '^7.0.0'; - devDeps['@types/supertest'] = '^6.0.2'; - } else if (testing === 'jest') { - devDeps['jest'] = '^29.7.0'; - devDeps['ts-jest'] = '^29.2.2'; - devDeps['@types/jest'] = '^29.5.12'; - devDeps['supertest'] = '^7.0.0'; - devDeps['@types/supertest'] = '^6.0.2'; + if (testing === "vitest") { + devDeps.vitest = "^4.1.5"; + devDeps["@vitest/coverage-v8"] = "^4.1.5"; + devDeps.supertest = "^7.0.0"; + devDeps["@types/supertest"] = "^6.0.2"; + } else if (testing === "jest") { + devDeps.jest = "^29.7.0"; + devDeps["ts-jest"] = "^29.2.5"; + devDeps["@types/jest"] = "^29.5.13"; + devDeps.supertest = "^7.0.0"; + devDeps["@types/supertest"] = "^6.0.2"; } - if (cache === 'redis') { - deps['redis'] = '^4.6.14'; - } else if (cache === 'node-cache') { - deps['node-cache'] = '^5.1.2'; + if (cache === "redis") { + deps.redis = "^4.7.0"; + } else if (cache === "node-cache") { + deps["node-cache"] = "^5.1.2"; } - if (auth === 'jwt' || auth === 'session') { - deps['bcrypt'] = '^5.1.1'; - devDeps['@types/bcrypt'] = '^5.0.2'; + if (auth === "jwt" || auth === "session") { + deps.bcrypt = "^5.1.1"; + devDeps["@types/bcrypt"] = "^5.0.2"; } - if (auth === 'jwt') { - deps['jsonwebtoken'] = '^9.0.2'; - devDeps['@types/jsonwebtoken'] = '^9.0.6'; - if (opts.jwtStorage === 'cookie') { - deps['cookie-parser'] = '^1.4.6'; - devDeps['@types/cookie-parser'] = '^1.4.7'; + if (auth === "jwt") { + deps.jsonwebtoken = "^9.0.2"; + devDeps["@types/jsonwebtoken"] = "^9.0.7"; + if (opts.jwtStorage === "cookie") { + deps["cookie-parser"] = "^1.4.6"; + devDeps["@types/cookie-parser"] = "^1.4.7"; } - } else if (auth === 'session') { - deps['express-session'] = '^1.18.0'; - deps['cookie-parser'] = '^1.4.6'; - devDeps['@types/express-session'] = '^1.18.0'; - devDeps['@types/cookie-parser'] = '^1.4.7'; + } else if (auth === "session") { + deps["express-session"] = "^1.18.0"; + deps["cookie-parser"] = "^1.4.6"; + devDeps["@types/express-session"] = "^1.18.0"; + devDeps["@types/cookie-parser"] = "^1.4.7"; } if (openapi) { - deps['swagger-jsdoc'] = '^6.2.8'; - deps['swagger-ui-express'] = '^5.0.1'; - devDeps['@types/swagger-jsdoc'] = '^6.0.4'; - devDeps['@types/swagger-ui-express'] = '^4.1.6'; + deps["swagger-jsdoc"] = "^6.2.8"; + deps["swagger-ui-express"] = "^5.0.1"; + devDeps["@types/swagger-jsdoc"] = "^6.0.4"; + devDeps["@types/swagger-ui-express"] = "^4.1.6"; } const scripts: Record = { - dev: 'tsx watch src/server.ts', - build: 'tsup src/server.ts --format esm --dts', - start: 'node dist/server.js', - 'check-types': 'tsc --noEmit', - lint: 'tsc --noEmit', + dev: "tsx watch src/server.ts", + build: "tsup src/server.ts --format esm", + start: "node dist/server.js", + "check-types": "tsc --noEmit", + lint: "biome lint .", + format: "biome format --write .", }; - if (orm === 'prisma') { - scripts['db:generate'] = 'prisma generate'; - scripts['db:migrate'] = 'prisma migrate dev'; - scripts['db:push'] = 'prisma db push'; - scripts['db:studio'] = 'prisma studio'; - scripts['db:seed'] = 'tsx prisma/seed.ts'; + if (orm === "prisma") { + scripts["db:generate"] = "prisma generate"; + scripts["db:migrate"] = "prisma migrate dev"; + scripts["db:push"] = "prisma db push"; + scripts["db:studio"] = "prisma studio"; + scripts["db:seed"] = "tsx prisma/seed.ts"; + scripts.postinstall = "prisma generate"; } - if (testing === 'vitest') { - scripts['test'] = 'vitest run'; - scripts['test:watch'] = 'vitest'; - scripts['test:coverage'] = 'vitest run --coverage'; - } else if (testing === 'jest') { - scripts['test'] = 'jest'; - scripts['test:watch'] = 'jest --watch'; - scripts['test:coverage'] = 'jest --coverage'; + if (testing === "vitest") { + scripts.test = "vitest run"; + scripts["test:watch"] = "vitest"; + scripts["test:coverage"] = "vitest run --coverage"; + } else if (testing === "jest") { + scripts.test = "jest"; + scripts["test:watch"] = "jest --watch"; + scripts["test:coverage"] = "jest --coverage"; } return { name: projectName, - version: '0.1.0', - description: '', - type: 'module', + version: "0.1.0", + description: "", + type: "module", scripts, dependencies: deps, devDependencies: devDeps, - engines: { node: '>=18.0.0' }, + engines: { node: ">=18.0.0" }, }; } diff --git a/packages/create-express-forge/src/utils/template-manager.ts b/packages/create-express-forge/src/utils/template-manager.ts index 0d50df1..ab055b1 100644 --- a/packages/create-express-forge/src/utils/template-manager.ts +++ b/packages/create-express-forge/src/utils/template-manager.ts @@ -1,238 +1,61 @@ -import type { CliOptions } from '../types.js'; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { Eta } from "eta"; +import fs from "fs-extra"; +import type { CliOptions } from "../types.js"; -/** - * Advanced Template Manager to handle code generation with consistency. - * This avoids giant template strings and allows for more structured logic. - */ -export class TemplateManager { - constructor(private opts: CliOptions) {} - - renderTodoService(depth = 2): string { - const rel = '../'.repeat(depth); - const { orm } = this.opts; - - if (orm === 'prisma') { - return `import { prisma } from '${rel}config/database.js'; -import { ApiError } from '${rel}utils/ApiError.js'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); -export const TodosService = { - findAll: async (userId: string) => { - return prisma.todo.findMany({ where: { userId } }); - }, +// In development (src/utils/...), templates are at ../../templates +// In production (dist/...), templates are at ./templates (copied by tsup) +const devPath = path.resolve(__dirname, "../../templates"); +const prodPath = path.resolve(__dirname, "./templates"); - findById: async (id: string, userId: string) => { - const todo = await prisma.todo.findFirst({ where: { id, userId } }); - if (!todo) throw ApiError.notFound('Todo not found'); - return todo; - }, +const TEMPLATE_DIR = fs.existsSync(devPath) ? devPath : prodPath; - create: async (userId: string, data: { title: string; description?: string }) => { - return prisma.todo.create({ - data: { ...data, userId } - }); - }, +export class TemplateManager { + private eta: Eta; - update: async (id: string, userId: string, data: any) => { - const todo = await prisma.todo.findFirst({ where: { id, userId } }); - if (!todo) throw ApiError.notFound('Todo not found'); - return prisma.todo.update({ - where: { id }, - data + constructor(private opts: CliOptions) { + this.eta = new Eta({ + views: TEMPLATE_DIR, + cache: false, + autoEscape: false, }); - }, - - remove: async (id: string, userId: string) => { - const todo = await prisma.todo.findFirst({ where: { id, userId } }); - if (!todo) throw ApiError.notFound('Todo not found'); - return prisma.todo.delete({ where: { id } }); - }, -}; -`; - } - - if (orm === 'sequelize') { - return `import { Todo } from '${rel}models/Todo.model.js'; -import { ApiError } from '${rel}utils/ApiError.js'; - -export const TodosService = { - findAll: async (userId: string) => { - return Todo.findAll({ where: { userId } }); - }, - - findById: async (id: string, userId: string) => { - const todo = await Todo.findOne({ where: { id, userId } }); - if (!todo) throw ApiError.notFound('Todo not found'); - return todo; - }, - - create: async (userId: string, data: any) => { - return Todo.create({ ...data, userId }); - }, - - update: async (id: string, userId: string, data: any) => { - const todo = await Todo.findOne({ where: { id, userId } }); - if (!todo) throw ApiError.notFound('Todo not found'); - return todo.update(data); - }, - - remove: async (id: string, userId: string) => { - const todo = await Todo.findOne({ where: { id, userId } }); - if (!todo) throw ApiError.notFound('Todo not found'); - return todo.destroy(); - }, -}; -`; - } - - return `import { ApiError } from '${rel}utils/ApiError.js'; - -export interface Todo { id: string; title: string; description?: string; completed: boolean; userId: string; } - -const store: Todo[] = []; - -export const TodosService = { - findAll: async (userId: string) => store.filter(t => t.userId === userId), - findById: async (id: string, userId: string) => { - const todo = store.find((t) => t.id === id && t.userId === userId); - if (!todo) throw ApiError.notFound('Todo not found'); - return todo; - }, - create: async (userId: string, data: any) => { - const todo = { id: Math.random().toString(36).substring(7), ...data, userId, completed: false }; - store.push(todo); - return todo; - }, - update: async (id: string, userId: string, data: any) => { - const todo = store.find((t) => t.id === id && t.userId === userId); - if (!todo) throw ApiError.notFound('Todo not found'); - Object.assign(todo, data); - return todo; - }, - remove: async (id: string, userId: string) => { - const idx = store.findIndex((t) => t.id === id && t.userId === userId); - if (idx === -1) throw ApiError.notFound('Todo not found'); - store.splice(idx, 1); - }, -}; -`; } - renderAuthController(depth = 2): string { - const rel = '../'.repeat(depth); - const { orm, jwtStorage } = this.opts; - - return `import jwt from 'jsonwebtoken'; -import bcrypt from 'bcrypt'; -import { asyncHandler } from '${rel}utils/asyncHandler.js'; -import { ApiResponse } from '${rel}utils/ApiResponse.js'; -import { ApiError } from '${rel}utils/ApiError.js'; -import { env } from '${rel}config/env.js'; -${orm === 'prisma' ? `import { prisma } from '${rel}config/database.js';` : ''} -${orm === 'sequelize' ? `import { User } from '${rel}models/User.model.js';` : ''} - -export const register = asyncHandler(async (req, res) => { - const { email, password, name } = req.body; - - const hashedPassword = await bcrypt.hash(password, 10); - - ${ - orm === 'prisma' - ? `const existingUser = await prisma.user.findUnique({ where: { email } }); - if (existingUser) throw ApiError.conflict('User already exists'); - - const user = await prisma.user.create({ - data: { email, name, password: hashedPassword } - });` - : orm === 'sequelize' - ? `const existingUser = await User.findOne({ where: { email } }); - if (existingUser) throw ApiError.conflict('User already exists'); - - const user = await User.create({ email, name, password: hashedPassword });` - : `// Demo logic for 'none' or in-memory - const user = { id: Math.random().toString(36).substring(7), email, name };` - } - - return ApiResponse.created(res, { id: user.id, email: user.email }, 'User registered successfully'); -}); - -export const login = asyncHandler(async (req, res) => { - const { email, password } = req.body; - - ${ - orm === 'prisma' - ? `const user = await prisma.user.findUnique({ where: { email } }); - if (!user || !(await bcrypt.compare(password, user.password))) { - throw ApiError.unauthorized('Invalid email or password'); - }` - : orm === 'sequelize' - ? `const user = await User.findOne({ where: { email } }); - if (!user || !(await bcrypt.compare(password, user.password))) { - throw ApiError.unauthorized('Invalid email or password'); - }` - : `// Demo logic: accept any valid email/password - const user = { id: 'demo-user-id', email, role: 'USER' };` - } - - const token = jwt.sign( - { id: user.id, email: user.email, role: (user as any).role || 'USER' }, - env.JWT_SECRET, - { expiresIn: env.JWT_EXPIRES_IN as any } - ); - - ${ - jwtStorage === 'cookie' - ? `res.cookie('token', token, { - httpOnly: true, - secure: env.NODE_ENV === 'production', - sameSite: 'lax', - maxAge: 24 * 60 * 60 * 1000 // 1 day - });` - : '' - } - - return ApiResponse.success(res, { - user: { id: user.id, email: user.email }, - ${jwtStorage === 'header' ? 'token' : ''} - }, 'Logged in successfully'); -}); - -export const logout = asyncHandler(async (_req, res) => { - ${jwtStorage === 'cookie' ? "res.clearCookie('token');" : ''} - return ApiResponse.success(res, null, 'Logged out successfully'); -}); -`; - } - - renderAuthMiddleware(depth = 1): string { - const rel = '../'.repeat(depth); - const { jwtStorage } = this.opts; - - return `import jwt from 'jsonwebtoken'; -import { asyncHandler } from '${rel}utils/asyncHandler.js'; -import { ApiError } from '${rel}utils/ApiError.js'; -import { env } from '${rel}config/env.js'; - -export const auth = asyncHandler(async (req, _res, next) => { - let token: string | undefined; - - ${ - jwtStorage === 'cookie' - ? "token = req.cookies?.token;" - : "if (req.headers.authorization?.startsWith('Bearer ')) { token = req.headers.authorization.split(' ')[1]; }" - } - - if (!token) { - throw ApiError.unauthorized('Authentication required'); + /** + * Render an Eta template string or file to a destination. + * If sourcePath is provided, it reads from templates/. + */ + async renderTemplateFile( + templatePath: string, + destPath: string, + ): Promise { + const rendered = await this.eta.renderAsync(templatePath, this.opts); + await fs.ensureDir(path.dirname(destPath)); + await fs.writeFile(destPath, rendered, "utf-8"); + } + + /** + * Render a raw Eta template string directly. + */ + async renderString( + templateString: string, + additionalData: Record = {}, + ): Promise { + return this.eta.renderStringAsync(templateString, { + ...this.opts, + ...additionalData, + }); } - try { - const decoded = jwt.verify(token, env.JWT_SECRET) as any; - req.user = decoded; - next(); - } catch (err) { - throw ApiError.unauthorized('Invalid or expired token'); - } -}); -`; + /** + * Bulk copy an entire directory without templating (for static assets) + */ + async copyStaticDir(sourceDir: string, destDir: string): Promise { + const fullSource = path.join(TEMPLATE_DIR, sourceDir); + await fs.copy(fullSource, destDir, { overwrite: true }); } } diff --git a/packages/create-express-forge/templates/base/.env.example.eta b/packages/create-express-forge/templates/base/.env.example.eta new file mode 100644 index 0000000..952c877 --- /dev/null +++ b/packages/create-express-forge/templates/base/.env.example.eta @@ -0,0 +1,33 @@ +# Application +NODE_ENV=development +PORT=3000 + +# CORS +CORS_ORIGIN=http://localhost:3000 + +# Rate limiter +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX=100 + +<% if (it.orm !== 'none' && it.database !== 'none') { %> +# Database +DATABASE_URL="<% + if (it.database === 'mysql') { %>mysql://user:password@localhost:3306/mydb<% } + else if (it.database === 'sqlite') { %>file:./dev.db<% } + else { %>postgresql://user:password@localhost:5432/mydb<% } +%>" +<% } %> + +<% if (it.auth === 'jwt') { %> +# JWT Auth +JWT_SECRET=your_super_secret_jwt_key_change_me +JWT_EXPIRES_IN=1d +<% } else if (it.auth === 'session') { %> +# Session Auth +SESSION_SECRET=your_session_secret_key_change_me +<% } %> + +<% if (it.cache === 'redis') { %> +# Redis Cache +REDIS_URL="redis://localhost:6379" +<% } %> diff --git a/packages/create-express-forge/templates/base/.gitignore.eta b/packages/create-express-forge/templates/base/.gitignore.eta new file mode 100644 index 0000000..9738812 --- /dev/null +++ b/packages/create-express-forge/templates/base/.gitignore.eta @@ -0,0 +1,11 @@ +node_modules/ +dist/ +.env +*.log +logs/ +coverage/ +.DS_Store +<% if (it.orm === 'prisma') { %> +prisma/migrations/ +*.db +<% } %> diff --git a/packages/create-express-forge/templates/base/README.md.eta b/packages/create-express-forge/templates/base/README.md.eta new file mode 100644 index 0000000..504ca2b --- /dev/null +++ b/packages/create-express-forge/templates/base/README.md.eta @@ -0,0 +1,23 @@ +# <%= it.projectName %> + +> Scaffolded with [create-express-forge](https://github.com/CODE-Y02/create-express-forge) + +## Stack + +- TypeScript + Express.js +- Zod validation +<% if (it.orm === 'prisma') { %>- Prisma ORM + <%= it.database %><% } %> +<% if (it.orm === 'sequelize') { %>- Sequelize ORM + <%= it.database %><% } %> +<% if (it.logger !== 'none') { %>- <%= it.logger %> logger<% } %> +<% if (it.testing !== 'none') { %>- <%= it.testing %> tests<% } %> +<% if (it.docker) { %>- Docker + docker-compose<% } %> + +## Quick Start + +```bash +cp .env.example .env +<% if (it.orm === 'prisma') { %> +<%= it.packageManager === 'npm' ? 'npm run' : it.packageManager %> db:migrate +<% } %> +<%= it.packageManager %> run dev +``` diff --git a/packages/create-express-forge/templates/base/src/app.ts.eta b/packages/create-express-forge/templates/base/src/app.ts.eta new file mode 100644 index 0000000..d20b69d --- /dev/null +++ b/packages/create-express-forge/templates/base/src/app.ts.eta @@ -0,0 +1,66 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import compression from 'compression'; +<% if (it.jwtStorage === 'cookie' || it.auth === 'session') { %> +import cookieParser from 'cookie-parser'; +<% } %> +<% if (it.auth === 'session') { %> +import session from 'express-session'; +<% } %> +import { env } from './config/env.js'; +import { rateLimiter } from './middleware/rateLimiter.js'; +<% if (it.logger === 'pino') { %> +import { httpLogger } from './middleware/httpLogger.js'; +<% } %> +import { notFound } from './middleware/notFound.js'; +import { errorHandler } from './middleware/errorHandler.js'; +<% if (it.openapi) { %> +import { setupSwagger } from './docs/swagger.js'; +<% } %> +<% if (it.pattern === 'modular') { %> +import { healthRouter } from './modules/health/health.routes.js'; +import { todosRouter } from './modules/todos/todos.routes.js'; +<% if (it.auth !== 'none') { %> +import { authRouter } from './modules/auth/auth.routes.js'; +<% } %> +<% } else { %> +import { router } from './routes/index.js'; +<% } %> + +const app = express(); + +app.use(helmet()); +app.use(cors({ origin: env.CORS_ORIGIN, credentials: true })); +<% if (it.jwtStorage === 'cookie' || it.auth === 'session') { %> +app.use(cookieParser()); +<% } %> +<% if (it.auth === 'session') { %> +app.use(session({ secret: env.SESSION_SECRET, resave: false, saveUninitialized: false })); +<% } %> +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true })); +app.use(compression()); +app.use(rateLimiter); +<% if (it.logger === 'pino') { %> +app.use(httpLogger); +<% } %> + +<% if (it.openapi) { %> +setupSwagger(app); +<% } %> + +<% if (it.pattern === 'modular') { %> +app.use('/api/health', healthRouter); +app.use('/api/v1/todos', todosRouter); +<% if (it.auth !== 'none') { %> +app.use('/api/v1/auth', authRouter); +<% } %> +<% } else { %> +app.use('/api/v1', router); +<% } %> + +app.use(notFound); +app.use(errorHandler); + +export { app }; diff --git a/packages/create-express-forge/templates/base/src/config/env.ts.eta b/packages/create-express-forge/templates/base/src/config/env.ts.eta new file mode 100644 index 0000000..66d0c06 --- /dev/null +++ b/packages/create-express-forge/templates/base/src/config/env.ts.eta @@ -0,0 +1,35 @@ +import { z } from 'zod'; + +const envSchema = z.object({ + NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), + PORT: z.coerce.number().default(3000), + CORS_ORIGIN: z.string().default('*'), + RATE_LIMIT_WINDOW_MS: z.coerce.number().default(900_000), + RATE_LIMIT_MAX: z.coerce.number().default(100), +<% if (it.orm !== 'none' && it.database !== 'none') { %> + DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL'), +<% } else { %> + // DATABASE_URL: z.string(), +<% } %> +<% if (it.auth === 'jwt') { %> + JWT_SECRET: z.string().min(1, 'JWT_SECRET is required'), + JWT_EXPIRES_IN: z.string().default('1d'), +<% } %> +<% if (it.auth === 'session') { %> + SESSION_SECRET: z.string().min(1, 'SESSION_SECRET is required'), +<% } %> +<% if (it.cache === 'redis') { %> + REDIS_URL: z.string().url().default('redis://localhost:6379'), +<% } %> +}); + +const parsed = envSchema.safeParse(process.env); + +if (!parsed.success) { + console.error('\n❌ Invalid environment variables:'); + console.error(JSON.stringify(parsed.error.format(), null, 2)); + process.exit(1); +} + +export const env = parsed.data; +export type Env = typeof env; diff --git a/packages/create-express-forge/templates/base/src/middleware/errorHandler.ts.eta b/packages/create-express-forge/templates/base/src/middleware/errorHandler.ts.eta new file mode 100644 index 0000000..fb287ff --- /dev/null +++ b/packages/create-express-forge/templates/base/src/middleware/errorHandler.ts.eta @@ -0,0 +1,38 @@ +import type { Request, Response, NextFunction } from 'express'; +import { ZodError } from 'zod'; +import { ApiError } from '../utils/ApiError.js'; +import { env } from '../config/env.js'; + +/** + * Global centralized error handler — must be the LAST middleware registered. + * Handles: ApiError, ZodError, and unknown errors. + */ +export const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction): void => { + const isDev = env.NODE_ENV === 'development'; + + if (err instanceof ApiError) { + res.status(err.statusCode).json({ + success: false, + message: err.message, + ...(err.errors.length > 0 && { errors: err.errors }), + ...(isDev && { stack: err.stack }), + }); + return; + } + + if (err instanceof ZodError) { + res.status(400).json({ + success: false, + message: 'Validation failed', + errors: err.errors.map((e) => ({ path: e.path.join('.'), message: e.message })), + }); + return; + } + + console.error('[UnhandledError]', err); + res.status(500).json({ + success: false, + message: 'Internal Server Error', + ...(isDev && { stack: err.stack }), + }); +}; diff --git a/packages/create-express-forge/templates/base/src/middleware/notFound.ts.eta b/packages/create-express-forge/templates/base/src/middleware/notFound.ts.eta new file mode 100644 index 0000000..a0f2269 --- /dev/null +++ b/packages/create-express-forge/templates/base/src/middleware/notFound.ts.eta @@ -0,0 +1,6 @@ +import type { Request, Response, NextFunction } from 'express'; +import { ApiError } from '../utils/ApiError.js'; + +export const notFound = (req: Request, _res: Response, next: NextFunction): void => { + next(ApiError.notFound(`Route ${req.method} ${req.url} not found`)); +}; diff --git a/packages/create-express-forge/templates/base/src/middleware/rateLimiter.ts.eta b/packages/create-express-forge/templates/base/src/middleware/rateLimiter.ts.eta new file mode 100644 index 0000000..0fc703e --- /dev/null +++ b/packages/create-express-forge/templates/base/src/middleware/rateLimiter.ts.eta @@ -0,0 +1,10 @@ +import rateLimit from 'express-rate-limit'; +import { env } from '../config/env.js'; + +export const rateLimiter = rateLimit({ + windowMs: env.RATE_LIMIT_WINDOW_MS, + max: env.RATE_LIMIT_MAX, + standardHeaders: true, + legacyHeaders: false, + message: { success: false, message: 'Too many requests, please try again later.' }, +}); diff --git a/packages/create-express-forge/templates/base/src/middleware/validate.ts.eta b/packages/create-express-forge/templates/base/src/middleware/validate.ts.eta new file mode 100644 index 0000000..f6210ab --- /dev/null +++ b/packages/create-express-forge/templates/base/src/middleware/validate.ts.eta @@ -0,0 +1,18 @@ +import type { Request, Response, NextFunction } from 'express'; +import { type AnyZodObject, ZodError } from 'zod'; +import { ApiError } from '../utils/ApiError.js'; + +/** Validates req.body / req.query / req.params against a Zod schema */ +export const validate = (schema: AnyZodObject) => + async (req: Request, _res: Response, next: NextFunction): Promise => { + try { + const validated = await schema.parseAsync({ body: req.body, query: req.query, params: req.params }); + req.body = validated.body; + req.query = validated.query; + req.params = validated.params; + next(); + } catch (err) { + if (err instanceof ZodError) next(ApiError.badRequest('Validation failed', err.errors)); + else next(err); + } + }; diff --git a/packages/create-express-forge/templates/base/src/server.ts.eta b/packages/create-express-forge/templates/base/src/server.ts.eta new file mode 100644 index 0000000..3676d10 --- /dev/null +++ b/packages/create-express-forge/templates/base/src/server.ts.eta @@ -0,0 +1,58 @@ +import 'dotenv/config'; +<% if (it.logger !== 'none') { %> +import { logger } from './logger/index.js'; +<% } %> +import { app } from './app.js'; +import { env } from './config/env.js'; + +<% if (it.orm === 'prisma') { %> +import { prisma } from './config/database.js'; +try { + await prisma.$connect(); + <% if (it.logger !== 'none') { %>logger.info('✅ Database connected');<% } %> +} catch (err) { + <% if (it.logger !== 'none') { %>logger.error('❌ Database connection failed', err);<% } %> + process.exit(1); +} +<% } %> +<% if (it.orm === 'sequelize') { %> +import { connectDB } from './config/database.js'; +await connectDB(); +<% } %> + +const server = app.listen(env.PORT, () => { + <% if (it.logger !== 'none') { %> + logger.info(`🚀 Server running on port ${env.PORT} in ${env.NODE_ENV} mode`); + <% } else { %> + console.log(`🚀 Server running on port ${env.PORT} in ${env.NODE_ENV} mode`); + <% } %> +}); + +const shutdown = async (signal: string) => { + <% if (it.logger !== 'none') { %> + logger.info(`${signal} received — shutting down gracefully`); + <% } else { %> + console.log(`${signal} received — shutting down gracefully`); + <% } %> + + server.close(async () => { + <% if (it.orm === 'prisma') { %> + await prisma.$disconnect(); + <% } %> + process.exit(0); + }); + + // Force shutdown after 10s + setTimeout(() => process.exit(1), 10000); +}; + +process.on('SIGTERM', () => shutdown('SIGTERM')); +process.on('SIGINT', () => shutdown('SIGINT')); +process.on('uncaughtException', (err) => { + <% if (it.logger !== 'none') { %>logger.error('Uncaught Exception', err);<% } else { %>console.error('Uncaught Exception', err);<% } %> + process.exit(1); +}); +process.on('unhandledRejection', (reason) => { + <% if (it.logger !== 'none') { %>logger.error('Unhandled Rejection', reason);<% } else { %>console.error('Unhandled Rejection', reason);<% } %> + process.exit(1); +}); diff --git a/packages/create-express-forge/templates/base/src/types/express.d.ts.eta b/packages/create-express-forge/templates/base/src/types/express.d.ts.eta new file mode 100644 index 0000000..f79c3d9 --- /dev/null +++ b/packages/create-express-forge/templates/base/src/types/express.d.ts.eta @@ -0,0 +1,7 @@ +import 'express'; + +declare module 'express' { + interface Request { + user?: { id: string; email: string; role: string }; + } +} diff --git a/packages/create-express-forge/templates/base/src/utils/ApiError.ts.eta b/packages/create-express-forge/templates/base/src/utils/ApiError.ts.eta new file mode 100644 index 0000000..8942286 --- /dev/null +++ b/packages/create-express-forge/templates/base/src/utils/ApiError.ts.eta @@ -0,0 +1,21 @@ +export class ApiError extends Error { + public readonly statusCode: number; + public readonly errors: unknown[]; + public readonly isOperational: boolean; + + constructor(statusCode: number, message: string, errors: unknown[] = [], isOperational = true) { + super(message); + this.statusCode = statusCode; + this.errors = errors; + this.isOperational = isOperational; + Object.setPrototypeOf(this, ApiError.prototype); + Error.captureStackTrace(this, this.constructor); + } + + static badRequest(message = 'Bad Request', errors: unknown[] = []) { return new ApiError(400, message, errors); } + static unauthorized(message = 'Unauthorized') { return new ApiError(401, message); } + static forbidden(message = 'Forbidden') { return new ApiError(403, message); } + static notFound(message = 'Not Found') { return new ApiError(404, message); } + static conflict(message = 'Conflict') { return new ApiError(409, message); } + static internal(message = 'Internal Server Error') { return new ApiError(500, message, [], false); } +} diff --git a/packages/create-express-forge/templates/base/src/utils/ApiResponse.ts.eta b/packages/create-express-forge/templates/base/src/utils/ApiResponse.ts.eta new file mode 100644 index 0000000..437e027 --- /dev/null +++ b/packages/create-express-forge/templates/base/src/utils/ApiResponse.ts.eta @@ -0,0 +1,14 @@ +import type { Response } from 'express'; + +export class ApiResponse { + static success(res: Response, data: T, message = 'Success', statusCode = 200) { + return res.status(statusCode).json({ success: true, message, data }); + } + static created(res: Response, data: T, message = 'Created') { + return ApiResponse.success(res, data, message, 201); + } + static noContent(res: Response) { return res.status(204).send(); } + static paginated(res: Response, data: T[], pagination: { total: number; page: number; limit: number; pages: number }, message = 'Success') { + return res.status(200).json({ success: true, message, data, pagination }); + } +} diff --git a/packages/create-express-forge/templates/base/tsconfig.json.eta b/packages/create-express-forge/templates/base/tsconfig.json.eta new file mode 100644 index 0000000..9169df9 --- /dev/null +++ b/packages/create-express-forge/templates/base/tsconfig.json.eta @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": false, + "sourceMap": true<% if (it.importAlias) { %>, + "baseUrl": ".", + "paths": { "@/*": ["src/*"] }<% } %><% if (it.orm === 'sequelize') { %>, + "experimentalDecorators": true, + "emitDecoratorMetadata": true<% } %> + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/create-express-forge/templates/features/auth/jwt-auth.middleware.ts.eta b/packages/create-express-forge/templates/features/auth/jwt-auth.middleware.ts.eta new file mode 100644 index 0000000..5786854 --- /dev/null +++ b/packages/create-express-forge/templates/features/auth/jwt-auth.middleware.ts.eta @@ -0,0 +1,29 @@ +import type { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; +import { ApiError } from '../utils/ApiError.js'; +import { env } from '../config/env.js'; + +export const auth = (req: Request, _res: Response, next: NextFunction) => { + let token: string | undefined; + +<% if (it.jwtStorage === 'cookie') { %> + token = req.cookies?.token; +<% } else { %> + const authHeader = req.headers.authorization; + if (authHeader?.startsWith('Bearer ')) { + token = authHeader.split(' ')[1]; + } +<% } %> + + if (!token) { + return next(ApiError.unauthorized('No token provided')); + } + + try { + const decoded = jwt.verify(token, env.JWT_SECRET); + req.user = decoded as any; + next(); + } catch (err) { + next(ApiError.unauthorized('Invalid or expired token')); + } +}; diff --git a/packages/create-express-forge/templates/features/auth/session-auth.middleware.ts.eta b/packages/create-express-forge/templates/features/auth/session-auth.middleware.ts.eta new file mode 100644 index 0000000..bc73f73 --- /dev/null +++ b/packages/create-express-forge/templates/features/auth/session-auth.middleware.ts.eta @@ -0,0 +1,9 @@ +import type { Request, Response, NextFunction } from 'express'; +import { ApiError } from '../utils/ApiError.js'; + +export const auth = (req: Request, _res: Response, next: NextFunction) => { + if (!req.session || !(req.session as any).user) { + return next(ApiError.unauthorized('Session expired or invalid')); + } + next(); +}; diff --git a/packages/create-express-forge/templates/features/cache/node-cache.ts.eta b/packages/create-express-forge/templates/features/cache/node-cache.ts.eta new file mode 100644 index 0000000..4c3628d --- /dev/null +++ b/packages/create-express-forge/templates/features/cache/node-cache.ts.eta @@ -0,0 +1,3 @@ +import NodeCache from 'node-cache'; + +export const cache = new NodeCache({ stdTTL: 600, checkperiod: 120 }); diff --git a/packages/create-express-forge/templates/features/cache/redis.ts.eta b/packages/create-express-forge/templates/features/cache/redis.ts.eta new file mode 100644 index 0000000..6895f6c --- /dev/null +++ b/packages/create-express-forge/templates/features/cache/redis.ts.eta @@ -0,0 +1,12 @@ +import { createClient } from 'redis'; +import { env } from '../config/env.js'; +<% if (it.logger !== 'none') { %>import { logger } from '../logger/index.js';<% } %> + +export const redisClient = createClient({ url: env.REDIS_URL }); + +redisClient.on('error', (err) => <% if (it.logger !== 'none') { %>logger.error('Redis Client Error', err)<% } else { %>console.error('Redis Client Error', err)<% } %>); +redisClient.on('connect', () => <% if (it.logger !== 'none') { %>logger.info('✅ Redis connected')<% } else { %>console.log('✅ Redis connected')<% } %>); + +export async function connectRedis() { + await redisClient.connect(); +} diff --git a/packages/create-express-forge/templates/features/docker/Dockerfile.eta b/packages/create-express-forge/templates/features/docker/Dockerfile.eta new file mode 100644 index 0000000..1678cc3 --- /dev/null +++ b/packages/create-express-forge/templates/features/docker/Dockerfile.eta @@ -0,0 +1,43 @@ +# Stage 1: Build +FROM node:20-alpine AS builder +WORKDIR /app + +# Copy dependency files +COPY package*.json ./ +<% if (it.orm === 'prisma') { %>COPY prisma ./prisma/ <% } %> + +# Install all dependencies (including devDependencies) +RUN <%= it.packageManager %> install + +# Copy source and build +COPY . . +RUN <%= it.packageManager %> run build + +<% if (it.orm === 'prisma') { %> +# Generate Prisma Client +RUN npx prisma generate +<% } %> + +# Prune devDependencies to keep the image small +# Note: This keeps the generated Prisma client in node_modules +RUN npm prune --production + +# Stage 2: Run +FROM node:20-alpine AS runner +WORKDIR /app + +# Set production environment +ENV NODE_ENV=production + +# Copy built assets and pruned node_modules +COPY --from=builder /app/package*.json ./ +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/dist ./dist +<% if (it.orm === 'prisma') { %>COPY --from=builder /app/prisma ./prisma<% } %> + +EXPOSE 3000 + +# Using a non-root user for security +USER node + +CMD ["node", "dist/server.js"] diff --git a/packages/create-express-forge/templates/features/docker/docker-compose.yml.eta b/packages/create-express-forge/templates/features/docker/docker-compose.yml.eta new file mode 100644 index 0000000..21adf5a --- /dev/null +++ b/packages/create-express-forge/templates/features/docker/docker-compose.yml.eta @@ -0,0 +1,56 @@ +version: '3.8' +services: + api: + build: . + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - PORT=3000 +<% if (it.database === 'postgresql') { %> + - DATABASE_URL=postgresql://user:password@db:5432/mydb +<% } else if (it.database === 'mysql') { %> + - DATABASE_URL=mysql://user:password@db:3306/mydb +<% } %> +<% if (it.cache === 'redis') { %> + - REDIS_URL=redis://redis:6379 +<% } %> + depends_on: +<% if (it.database !== 'sqlite' && it.database !== 'none') { %> - db<% } %> +<% if (it.cache === 'redis') { %> - redis<% } %> + +<% if (it.database === 'postgresql') { %> + db: + image: postgres:15-alpine + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: mydb + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data +<% } else if (it.database === 'mysql') { %> + db: + image: mysql:8.0 + environment: + MYSQL_USER: user + MYSQL_PASSWORD: password + MYSQL_DATABASE: mydb + MYSQL_ROOT_PASSWORD: root_password + ports: + - "3306:3306" + volumes: + - mysqldata:/var/lib/mysql +<% } %> + +<% if (it.cache === 'redis') { %> + redis: + image: redis:7-alpine + ports: + - "6379:6379" +<% } %> + +volumes: +<% if (it.database === 'postgresql') { %> pgdata:<% } %> +<% if (it.database === 'mysql') { %> mysqldata:<% } %> diff --git a/packages/create-express-forge/templates/features/logger/pino.ts.eta b/packages/create-express-forge/templates/features/logger/pino.ts.eta new file mode 100644 index 0000000..a867be3 --- /dev/null +++ b/packages/create-express-forge/templates/features/logger/pino.ts.eta @@ -0,0 +1,19 @@ +import pino from 'pino'; +import pinoHttp from 'pino-http'; + +export const logger = pino({ + level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', + transport: process.env.NODE_ENV === 'production' ? undefined : { + target: 'pino-pretty', + options: { colorize: true, translateTime: 'SYS:standard' } + } +}); + +export const httpLogger = pinoHttp({ + logger, + customLogLevel: (req, res, err) => { + if (res.statusCode >= 500 || err) return 'error'; + if (res.statusCode >= 400) return 'warn'; + return 'info'; + } +}); diff --git a/packages/create-express-forge/templates/features/logger/winston.ts.eta b/packages/create-express-forge/templates/features/logger/winston.ts.eta new file mode 100644 index 0000000..b10a567 --- /dev/null +++ b/packages/create-express-forge/templates/features/logger/winston.ts.eta @@ -0,0 +1,30 @@ +import winston from 'winston'; +import 'winston-daily-rotate-file'; + +const { combine, timestamp, printf, colorize, errors } = winston.format; + +const logFormat = printf(({ level, message, timestamp, stack }) => { + return `${timestamp} ${level}: ${stack || message}`; +}); + +export const logger = winston.createLogger({ + level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', + format: combine( + errors({ stack: true }), + timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + logFormat + ), + transports: [ + new winston.transports.Console({ + format: combine(colorize(), logFormat) + }), + new winston.transports.DailyRotateFile({ + dirname: 'logs', + filename: 'application-%DATE%.log', + datePattern: 'YYYY-MM-DD', + maxFiles: '14d', + maxSize: '20m', + zippedArchive: true, + }) + ] +}); diff --git a/packages/create-express-forge/templates/features/openapi/swagger.ts.eta b/packages/create-express-forge/templates/features/openapi/swagger.ts.eta new file mode 100644 index 0000000..442620a --- /dev/null +++ b/packages/create-express-forge/templates/features/openapi/swagger.ts.eta @@ -0,0 +1,34 @@ +import swaggerJsdoc from 'swagger-jsdoc'; +import swaggerUi from 'swagger-ui-express'; +import type { Express } from 'express'; + +const options: swaggerJsdoc.Options = { + definition: { + openapi: '3.0.0', + info: { + title: '<%= it.projectName %> API', + version: '0.1.0', + description: 'API documentation for <%= it.projectName %>', + }, + servers: [{ url: '/api/v1' }], +<% if (it.auth === 'jwt') { %> + components: { + securitySchemes: { + bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' } + } + }, + security: [{ bearerAuth: [] }] +<% } %> + }, + apis: ['./src/routes/*.ts', './src/modules/**/*.routes.ts', './src/controllers/*.ts'], +}; + +const swaggerSpec = swaggerJsdoc(options); + +export function setupSwagger(app: Express) { + app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + app.get('/api-docs.json', (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.send(swaggerSpec); + }); +} diff --git a/packages/create-express-forge/templates/features/prisma/database.ts.eta b/packages/create-express-forge/templates/features/prisma/database.ts.eta new file mode 100644 index 0000000..897167c --- /dev/null +++ b/packages/create-express-forge/templates/features/prisma/database.ts.eta @@ -0,0 +1,5 @@ +import { PrismaClient } from '@prisma/client'; + +export const prisma = new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'warn', 'error'] : ['error'], +}); diff --git a/packages/create-express-forge/templates/features/prisma/schema.prisma.eta b/packages/create-express-forge/templates/features/prisma/schema.prisma.eta new file mode 100644 index 0000000..693d9b1 --- /dev/null +++ b/packages/create-express-forge/templates/features/prisma/schema.prisma.eta @@ -0,0 +1,47 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { +<% if (it.database === 'sqlite') { %> + provider = "sqlite" +<% } else if (it.database === 'mysql') { %> + provider = "mysql" +<% } else { %> + provider = "postgresql" +<% } %> + url = env("DATABASE_URL") +} + +<% if (it.auth !== 'none') { %> +model User { + id String @id @default(uuid()) + name String + email String @unique + password String + role Role @default(USER) + todos Todo[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("users") +} + +model Todo { + id String @id @default(cuid()) + title String + description String? + completed Boolean @default(false) + userId String + user User @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("todos") +} + +enum Role { + USER + ADMIN +} +<% } %> diff --git a/packages/create-express-forge/templates/features/prisma/seed.ts.eta b/packages/create-express-forge/templates/features/prisma/seed.ts.eta new file mode 100644 index 0000000..0341b81 --- /dev/null +++ b/packages/create-express-forge/templates/features/prisma/seed.ts.eta @@ -0,0 +1,36 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('🌱 Seeding database...'); + +<% if (it.auth !== 'none') { %> + const admin = await prisma.user.upsert({ + where: { email: 'admin@example.com' }, + update: {}, + create: { + email: 'admin@example.com', + name: 'Admin User', + password: 'password123', // In real app, hash this! + role: 'ADMIN', + }, + }); + + console.log({ admin }); +<% } else { %> + // Add seed data here + console.log('Seeding models...'); +<% } %> + + console.log('✅ Seeding completed.'); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/packages/create-express-forge/templates/features/sequelize/.sequelizerc.eta b/packages/create-express-forge/templates/features/sequelize/.sequelizerc.eta new file mode 100644 index 0000000..5ae0181 --- /dev/null +++ b/packages/create-express-forge/templates/features/sequelize/.sequelizerc.eta @@ -0,0 +1,7 @@ +const path = require('path'); +module.exports = { + config: path.resolve('src', 'config', 'sequelize.cjs'), + 'models-path': path.resolve('src', 'models'), + 'seeders-path': path.resolve('db', 'seeders'), + 'migrations-path': path.resolve('db', 'migrations'), +}; diff --git a/packages/create-express-forge/templates/features/sequelize/User.ts.eta b/packages/create-express-forge/templates/features/sequelize/User.ts.eta new file mode 100644 index 0000000..1bca274 --- /dev/null +++ b/packages/create-express-forge/templates/features/sequelize/User.ts.eta @@ -0,0 +1,11 @@ +import { Table, Column, Model, DataType, CreatedAt, UpdatedAt, PrimaryKey, Default, Unique } from 'sequelize-typescript'; + +@Table({ tableName: 'users', timestamps: true }) +export class User extends Model { + @PrimaryKey @Default(DataType.UUIDV4) @Column(DataType.UUID) declare id: string; + @Column({ type: DataType.STRING, allowNull: false }) declare name: string; + @Unique @Column({ type: DataType.STRING, allowNull: false }) declare email: string; + @Column({ type: DataType.STRING, allowNull: false }) declare password: string; + @CreatedAt declare createdAt: Date; + @UpdatedAt declare updatedAt: Date; +} diff --git a/packages/create-express-forge/templates/features/sequelize/database.ts.eta b/packages/create-express-forge/templates/features/sequelize/database.ts.eta new file mode 100644 index 0000000..1f5fc47 --- /dev/null +++ b/packages/create-express-forge/templates/features/sequelize/database.ts.eta @@ -0,0 +1,17 @@ +<% const dialect = it.database === 'mysql' ? 'mysql' : it.database === 'sqlite' ? 'sqlite' : 'postgres'; %> +import { Sequelize } from 'sequelize-typescript'; +import { env } from './env.js'; +<% if (it.auth !== 'none') { %> +import { User } from '../models/User.js'; +<% } %> + +export const sequelize = new Sequelize(env.DATABASE_URL, { + dialect: '<%= dialect %>' as const, + logging: env.NODE_ENV === 'development' ? console.log : false, + models: [<% if (it.auth !== 'none') { %>'User'<% } %>] +}); + +export async function connectDB() { + await sequelize.authenticate(); + console.log('✅ Database connected'); +} diff --git a/packages/create-express-forge/templates/features/sequelize/sequelize.cjs.eta b/packages/create-express-forge/templates/features/sequelize/sequelize.cjs.eta new file mode 100644 index 0000000..766746f --- /dev/null +++ b/packages/create-express-forge/templates/features/sequelize/sequelize.cjs.eta @@ -0,0 +1,6 @@ +<% const dialect = it.database === 'mysql' ? 'mysql' : it.database === 'sqlite' ? 'sqlite' : 'postgres'; %> +module.exports = { + development: { use_env_variable: 'DATABASE_URL', dialect: '<%= dialect %>', logging: console.log }, + test: { use_env_variable: 'DATABASE_URL', dialect: '<%= dialect %>', logging: false }, + production: { use_env_variable: 'DATABASE_URL', dialect: '<%= dialect %>', logging: false }, +}; diff --git a/packages/create-express-forge/templates/features/testing/jest.config.js.eta b/packages/create-express-forge/templates/features/testing/jest.config.js.eta new file mode 100644 index 0000000..28caa9d --- /dev/null +++ b/packages/create-express-forge/templates/features/testing/jest.config.js.eta @@ -0,0 +1,7 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + moduleNameMapper: { '^@/(.*)$': '/src/$1' }, + collectCoverageFrom: ['src/**/*.{ts,js}', '!src/**/*.d.ts'], +}; diff --git a/packages/create-express-forge/templates/features/testing/smoke.test.ts.eta b/packages/create-express-forge/templates/features/testing/smoke.test.ts.eta new file mode 100644 index 0000000..7ee02d7 --- /dev/null +++ b/packages/create-express-forge/templates/features/testing/smoke.test.ts.eta @@ -0,0 +1,9 @@ +import request from 'supertest'; +import { app } from '../src/app.js'; + +describe('Smoke Test', () => { + it('should return 404 for unknown route', async () => { + const res = await request(app).get('/api/unknown-route-123'); + expect(res.status).toBe(404); + }); +}); diff --git a/packages/create-express-forge/templates/features/testing/vitest.config.ts.eta b/packages/create-express-forge/templates/features/testing/vitest.config.ts.eta new file mode 100644 index 0000000..7c33155 --- /dev/null +++ b/packages/create-express-forge/templates/features/testing/vitest.config.ts.eta @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + coverage: { provider: 'v8', reporter: ['text', 'lcov'] } + } +}); diff --git a/packages/create-express-forge/templates/structure/modular/modules/auth/auth.controller.ts.eta b/packages/create-express-forge/templates/structure/modular/modules/auth/auth.controller.ts.eta new file mode 100644 index 0000000..5b02504 --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/auth/auth.controller.ts.eta @@ -0,0 +1,16 @@ +import type { Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; +import { ApiResponse } from '../../utils/ApiResponse.js'; +import { env } from '../../config/env.js'; + +export const login = async (req: Request, res: Response) => { + // Demo logic: accept any valid email/password + const token = jwt.sign({ email: req.body.email }, env.JWT_SECRET, { expiresIn: env.JWT_EXPIRES_IN as any }); + +<% if (it.jwtStorage === 'cookie') { %> + res.cookie('token', token, { httpOnly: true, secure: env.NODE_ENV === 'production' }); + return ApiResponse.success(res, { token }, 'Logged in successfully'); +<% } else { %> + return ApiResponse.success(res, { token }, 'Logged in successfully'); +<% } %> +}; diff --git a/packages/create-express-forge/templates/structure/modular/modules/auth/auth.routes.ts.eta b/packages/create-express-forge/templates/structure/modular/modules/auth/auth.routes.ts.eta new file mode 100644 index 0000000..b298dda --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/auth/auth.routes.ts.eta @@ -0,0 +1,32 @@ +import { Router } from 'express'; +import { login } from './auth.controller.js'; +import { validate } from '../../middleware/validate.js'; +import { loginSchema } from './auth.schema.js'; + +const router = Router(); + +/** + * @openapi + * /api/v1/auth/login: + * post: + * tags: + * - Auth + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [email, password] + * properties: + * email: + * type: string + * password: + * type: string + * responses: + * 200: + * description: Logged in successfully + */ +router.post('/login', validate(loginSchema), login); + +export { router as authRouter }; diff --git a/packages/create-express-forge/templates/structure/modular/modules/auth/auth.schema.ts.eta b/packages/create-express-forge/templates/structure/modular/modules/auth/auth.schema.ts.eta new file mode 100644 index 0000000..2a7b752 --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/auth/auth.schema.ts.eta @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const loginSchema = z.object({ + body: z.object({ + email: z.string().email(), + password: z.string().min(8) + }) +}); diff --git a/packages/create-express-forge/templates/structure/modular/modules/health/health.controller.ts.eta b/packages/create-express-forge/templates/structure/modular/modules/health/health.controller.ts.eta new file mode 100644 index 0000000..bb9396d --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/health/health.controller.ts.eta @@ -0,0 +1,5 @@ +import type { Request, Response } from 'express'; +import { ApiResponse } from '../../utils/ApiResponse.js'; + +export const getHealth = (_req: Request, res: Response) => + ApiResponse.success(res, { status: 'ok', timestamp: new Date().toISOString() }, 'Service healthy'); diff --git a/packages/create-express-forge/templates/structure/modular/modules/health/health.routes.ts.eta b/packages/create-express-forge/templates/structure/modular/modules/health/health.routes.ts.eta new file mode 100644 index 0000000..ca895e2 --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/health/health.routes.ts.eta @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import { getHealth } from './health.controller.js'; + +const router = Router(); + +/** + * @openapi + * /api/health: + * get: + * tags: + * - Health + * description: Responds if the app is up and running + * responses: + * 200: + * description: App is up and running + */ +router.get('/', getHealth); + +export { router as healthRouter }; diff --git a/packages/create-express-forge/templates/structure/modular/modules/todos/todos.controller.ts.eta b/packages/create-express-forge/templates/structure/modular/modules/todos/todos.controller.ts.eta new file mode 100644 index 0000000..a5d54db --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/todos/todos.controller.ts.eta @@ -0,0 +1,37 @@ +import type { Request, Response } from 'express'; +import { ApiResponse } from '../../utils/ApiResponse.js'; +import { TodosService } from './todos.service.js'; +import type { CreateTodoDto, UpdateTodoDto } from './todos.schema.js'; + +export const getTodos = async (req: Request, res: Response) => { + const todos = await TodosService.findAll(<% if (it.auth !== 'none') { %>req.user?.id || 'guest'<% } %>); + return ApiResponse.success(res, todos, 'Todos fetched'); +}; + +export const getTodoById = async (req: Request, res: Response) => { + const todo = await TodosService.findById(req.params.id as string<% if (it.auth !== 'none') { %>, req.user?.id || 'guest'<% } %>); + return ApiResponse.success(res, todo); +}; + +export const createTodo = async (req: Request, res: Response) => { + const todo = await TodosService.create({ + ...(req.body as CreateTodoDto), + completed: false<% if (it.auth !== 'none') { %>, + userId: req.user?.id || 'guest'<% } %> + }); + return ApiResponse.created(res, todo, 'Todo created'); +}; + +export const updateTodo = async (req: Request, res: Response) => { + const todo = await TodosService.update( + req.params.id as string, + <% if (it.auth !== 'none') { %>req.user?.id || 'guest', <% } %> + req.body as UpdateTodoDto + ); + return ApiResponse.success(res, todo, 'Todo updated'); +}; + +export const deleteTodo = async (req: Request, res: Response) => { + await TodosService.remove(req.params.id as string<% if (it.auth !== 'none') { %>, req.user?.id || 'guest'<% } %>); + return ApiResponse.noContent(res); +}; diff --git a/packages/create-express-forge/templates/structure/modular/modules/todos/todos.routes.ts.eta b/packages/create-express-forge/templates/structure/modular/modules/todos/todos.routes.ts.eta new file mode 100644 index 0000000..ea030c1 --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/todos/todos.routes.ts.eta @@ -0,0 +1,45 @@ +import { Router } from 'express'; +import { getTodos, getTodoById, createTodo, updateTodo, deleteTodo } from './todos.controller.js'; +import { validate } from '../../middleware/validate.js'; +import { createTodoSchema, updateTodoSchema } from './todos.schema.js'; +<% if (it.auth !== 'none') { %>import { auth } from '../../middleware/auth.middleware.js';<% } %> + +const router = Router(); + +<% if (it.auth !== 'none') { %>router.use(auth);<% } %> + +/** + * @openapi + * /api/v1/todos: + * get: + * tags: + * - Todos + * responses: + * 200: + * description: Success + * post: + * tags: + * - Todos + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [title] + * properties: + * title: + * type: string + * description: + * type: string + * responses: + * 201: + * description: Created + */ +router.get('/', getTodos); +router.get('/:id', getTodoById); +router.post('/', validate(createTodoSchema), createTodo); +router.patch('/:id', validate(updateTodoSchema), updateTodo); +router.delete('/:id', deleteTodo); + +export { router as todosRouter }; diff --git a/packages/create-express-forge/templates/structure/modular/modules/todos/todos.schema.ts.eta b/packages/create-express-forge/templates/structure/modular/modules/todos/todos.schema.ts.eta new file mode 100644 index 0000000..c861cdb --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/todos/todos.schema.ts.eta @@ -0,0 +1,20 @@ +import { z } from 'zod'; + +export const createTodoSchema = z.object({ + body: z.object({ + title: z.string().min(1).max(100), + description: z.string().max(500).optional(), + }), +}); + +export const updateTodoSchema = z.object({ + params: z.object({ id: z.string() }), + body: z.object({ + title: z.string().min(1).max(100).optional(), + description: z.string().max(500).optional(), + completed: z.boolean().optional(), + }), +}); + +export type CreateTodoDto = z.infer['body']; +export type UpdateTodoDto = z.infer['body']; diff --git a/packages/create-express-forge/templates/structure/modular/modules/todos/todos.service.ts.eta b/packages/create-express-forge/templates/structure/modular/modules/todos/todos.service.ts.eta new file mode 100644 index 0000000..5a8ecc1 --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/todos/todos.service.ts.eta @@ -0,0 +1,41 @@ +import { ApiError } from '../../utils/ApiError.js'; + +export interface Todo { + id: string; + title: string; + description?: string; + completed: boolean; + <% if (it.auth !== 'none') { %>userId: string;<% } %> +} + +const store: Todo[] = []; + +export const TodosService = { + findAll: async (<% if (it.auth !== 'none') { %>userId: string<% } %>) => + store<% if (it.auth !== 'none') { %>.filter(t => t.userId === userId)<% } %>, + + findById: async (id: string<% if (it.auth !== 'none') { %>, userId: string<% } %>) => { + const todo = store.find((t) => t.id === id<% if (it.auth !== 'none') { %> && t.userId === userId<% } %>); + if (!todo) throw ApiError.notFound('Todo not found'); + return todo; + }, + + create: async (data: Omit) => { + const todo = { id: Math.random().toString(36).substring(7), ...data }; + store.push(todo); + return todo; + }, + + update: async (id: string, <% if (it.auth !== 'none') { %>userId: string, <% } %>data: Partial) => { + const todo = store.find((t) => t.id === id<% if (it.auth !== 'none') { %> && t.userId === userId<% } %>); + if (!todo) throw ApiError.notFound('Todo not found'); + Object.assign(todo, data); + return todo; + }, + + remove: async (id: string<% if (it.auth !== 'none') { %>, userId: string<% } %>) => { + const idx = store.findIndex((t) => t.id === id<% if (it.auth !== 'none') { %> && t.userId === userId<% } %>); + if (idx === -1) throw ApiError.notFound('Todo not found'); + store.splice(idx, 1); + }, +}; diff --git a/packages/create-express-forge/templates/structure/mvc/controllers/auth.controller.ts.eta b/packages/create-express-forge/templates/structure/mvc/controllers/auth.controller.ts.eta new file mode 100644 index 0000000..233e5ab --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/controllers/auth.controller.ts.eta @@ -0,0 +1,16 @@ +import type { Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; +import { ApiResponse } from '../utils/ApiResponse.js'; +import { env } from '../config/env.js'; + +export const login = async (req: Request, res: Response) => { + // Demo logic: accept any valid email/password + const token = jwt.sign({ email: req.body.email }, env.JWT_SECRET, { expiresIn: env.JWT_EXPIRES_IN as any }); + +<% if (it.jwtStorage === 'cookie') { %> + res.cookie('token', token, { httpOnly: true, secure: env.NODE_ENV === 'production' }); + return ApiResponse.success(res, { token }, 'Logged in successfully'); +<% } else { %> + return ApiResponse.success(res, { token }, 'Logged in successfully'); +<% } %> +}; diff --git a/packages/create-express-forge/templates/structure/mvc/controllers/health.controller.ts.eta b/packages/create-express-forge/templates/structure/mvc/controllers/health.controller.ts.eta new file mode 100644 index 0000000..07e4d6e --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/controllers/health.controller.ts.eta @@ -0,0 +1,5 @@ +import type { Request, Response } from 'express'; +import { ApiResponse } from '../utils/ApiResponse.js'; + +export const getHealth = (_req: Request, res: Response) => + ApiResponse.success(res, { status: 'ok', timestamp: new Date().toISOString() }, 'Service healthy'); diff --git a/packages/create-express-forge/templates/structure/mvc/controllers/todo.controller.ts.eta b/packages/create-express-forge/templates/structure/mvc/controllers/todo.controller.ts.eta new file mode 100644 index 0000000..18851d7 --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/controllers/todo.controller.ts.eta @@ -0,0 +1,29 @@ +import type { Request, Response } from 'express'; +import { ApiResponse } from '../utils/ApiResponse.js'; +import { TodoService } from '../services/todo.service.js'; +import type { CreateTodoDto, UpdateTodoDto } from '../schemas/todo.schema.js'; + +export const getTodos = async (req: Request, res: Response) => + ApiResponse.success(res, await TodoService.findAll(<% if (it.auth !== 'none') { %>req.user?.id || 'guest'<% } %>), 'Todos fetched'); + +export const getTodoById = async (req: Request, res: Response) => + ApiResponse.success(res, await TodoService.findById(req.params.id as string<% if (it.auth !== 'none') { %>, req.user?.id || 'guest'<% } %>)); + +export const createTodo = async (req: Request, res: Response) => + ApiResponse.created(res, await TodoService.create({ + ...(req.body as CreateTodoDto), + completed: false<% if (it.auth !== 'none') { %>, + userId: req.user?.id || 'guest'<% } %> + }), 'Todo created'); + +export const updateTodo = async (req: Request, res: Response) => + ApiResponse.success(res, await TodoService.update( + req.params.id as string, + <% if (it.auth !== 'none') { %>req.user?.id || 'guest', <% } %> + req.body as UpdateTodoDto + ), 'Todo updated'); + +export const deleteTodo = async (req: Request, res: Response) => { + await TodoService.remove(req.params.id as string<% if (it.auth !== 'none') { %>, req.user?.id || 'guest'<% } %>); + return ApiResponse.noContent(res); +}; diff --git a/packages/create-express-forge/templates/structure/mvc/routes/auth.routes.ts.eta b/packages/create-express-forge/templates/structure/mvc/routes/auth.routes.ts.eta new file mode 100644 index 0000000..e9c101f --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/routes/auth.routes.ts.eta @@ -0,0 +1,32 @@ +import { Router } from 'express'; +import { login } from '../controllers/auth.controller.js'; +import { validate } from '../middleware/validate.js'; +import { loginSchema } from '../schemas/auth.schema.js'; + +const router = Router(); + +/** + * @openapi + * /api/v1/auth/login: + * post: + * tags: + * - Auth + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [email, password] + * properties: + * email: + * type: string + * password: + * type: string + * responses: + * 200: + * description: Logged in successfully + */ +router.post('/login', validate(loginSchema), login); + +export { router as authRouter }; diff --git a/packages/create-express-forge/templates/structure/mvc/routes/health.routes.ts.eta b/packages/create-express-forge/templates/structure/mvc/routes/health.routes.ts.eta new file mode 100644 index 0000000..9332f45 --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/routes/health.routes.ts.eta @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import { getHealth } from '../controllers/health.controller.js'; + +const router = Router(); + +/** + * @openapi + * /api/health: + * get: + * tags: + * - Health + * description: Responds if the app is up and running + * responses: + * 200: + * description: App is up and running + */ +router.get('/', getHealth); + +export { router as healthRouter }; diff --git a/packages/create-express-forge/templates/structure/mvc/routes/index.ts.eta b/packages/create-express-forge/templates/structure/mvc/routes/index.ts.eta new file mode 100644 index 0000000..927c317 --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/routes/index.ts.eta @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { healthRouter } from './health.routes.js'; +import { todoRouter } from './todo.routes.js'; +<% if (it.auth === 'jwt') { %>import { authRouter } from './auth.routes.js';<% } %> + +const router = Router(); + +router.use('/health', healthRouter); +router.use('/todos', todoRouter); +<% if (it.auth === 'jwt') { %>router.use('/auth', authRouter);<% } %> + +export { router }; diff --git a/packages/create-express-forge/templates/structure/mvc/routes/todo.routes.ts.eta b/packages/create-express-forge/templates/structure/mvc/routes/todo.routes.ts.eta new file mode 100644 index 0000000..de4cea9 --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/routes/todo.routes.ts.eta @@ -0,0 +1,45 @@ +import { Router } from 'express'; +import { getTodos, getTodoById, createTodo, updateTodo, deleteTodo } from '../controllers/todo.controller.js'; +import { validate } from '../middleware/validate.js'; +import { createTodoSchema, updateTodoSchema } from '../schemas/todo.schema.ts'; +<% if (it.auth !== 'none') { %>import { auth } from '../middleware/auth.middleware.js';<% } %> + +const router = Router(); + +<% if (it.auth !== 'none') { %>router.use(auth);<% } %> + +/** + * @openapi + * /api/v1/todos: + * get: + * tags: + * - Todos + * responses: + * 200: + * description: Success + * post: + * tags: + * - Todos + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [title] + * properties: + * title: + * type: string + * description: + * type: string + * responses: + * 201: + * description: Created + */ +router.get('/', getTodos); +router.get('/:id', getTodoById); +router.post('/', validate(createTodoSchema), createTodo); +router.patch('/:id', validate(updateTodoSchema), updateTodo); +router.delete('/:id', deleteTodo); + +export { router as todoRouter }; diff --git a/packages/create-express-forge/templates/structure/mvc/schemas/auth.schema.ts.eta b/packages/create-express-forge/templates/structure/mvc/schemas/auth.schema.ts.eta new file mode 100644 index 0000000..2a7b752 --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/schemas/auth.schema.ts.eta @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const loginSchema = z.object({ + body: z.object({ + email: z.string().email(), + password: z.string().min(8) + }) +}); diff --git a/packages/create-express-forge/templates/structure/mvc/schemas/todo.schema.ts.eta b/packages/create-express-forge/templates/structure/mvc/schemas/todo.schema.ts.eta new file mode 100644 index 0000000..ce29481 --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/schemas/todo.schema.ts.eta @@ -0,0 +1,20 @@ +import { z } from 'zod'; + +export const createTodoSchema = z.object({ + body: z.object({ + title: z.string().min(1).max(100), + description: z.string().max(500).optional() + }) +}); + +export const updateTodoSchema = z.object({ + params: z.object({ id: z.string() }), + body: z.object({ + title: z.string().min(1).max(100).optional(), + description: z.string().max(500).optional(), + completed: z.boolean().optional() + }) +}); + +export type CreateTodoDto = z.infer['body']; +export type UpdateTodoDto = z.infer['body']; diff --git a/packages/create-express-forge/templates/structure/mvc/services/todo.service.ts.eta b/packages/create-express-forge/templates/structure/mvc/services/todo.service.ts.eta new file mode 100644 index 0000000..ae6bca8 --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/services/todo.service.ts.eta @@ -0,0 +1,41 @@ +import { ApiError } from '../utils/ApiError.js'; + +export interface Todo { + id: string; + title: string; + description?: string; + completed: boolean; + <% if (it.auth !== 'none') { %>userId: string;<% } %> +} + +const store: Todo[] = []; + +export const TodoService = { + findAll: async (<% if (it.auth !== 'none') { %>userId: string<% } %>) => + store<% if (it.auth !== 'none') { %>.filter(t => t.userId === userId)<% } %>, + + findById: async (id: string<% if (it.auth !== 'none') { %>, userId: string<% } %>) => { + const t = store.find((t) => t.id === id<% if (it.auth !== 'none') { %> && t.userId === userId<% } %>); + if (!t) throw ApiError.notFound('Todo not found'); + return t; + }, + + create: async (data: Omit) => { + const t = { id: Math.random().toString(36).substring(7), ...data }; + store.push(t); + return t; + }, + + update: async (id: string, <% if (it.auth !== 'none') { %>userId: string, <% } %>data: Partial) => { + const t = store.find((t) => t.id === id<% if (it.auth !== 'none') { %> && t.userId === userId<% } %>); + if (!t) throw ApiError.notFound('Todo not found'); + Object.assign(t, data); + return t; + }, + + remove: async (id: string<% if (it.auth !== 'none') { %>, userId: string<% } %>) => { + const i = store.findIndex((t) => t.id === id<% if (it.auth !== 'none') { %> && t.userId === userId<% } %>); + if (i === -1) throw ApiError.notFound('Todo not found'); + store.splice(i, 1); + }, +}; diff --git a/packages/create-express-forge/tests/generator.test.ts b/packages/create-express-forge/tests/generator.test.ts deleted file mode 100644 index 0c29e2b..0000000 --- a/packages/create-express-forge/tests/generator.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import path from 'path'; -import fs from 'fs-extra'; -import os from 'os'; -import { generateProject } from '../src/generator/index.js'; -import type { CliOptions } from '../src/types.js'; - -async function makeTempDir(): Promise { - return fs.mkdtemp(path.join(os.tmpdir(), 'cef-test-')); -} - -const baseOpts: CliOptions = { - projectName: 'test-app', - pattern: 'modular', - orm: 'none', - database: 'none', - packageManager: 'npm', - logger: 'none', - testing: 'none', - cache: 'none', - auth: 'none', - importAlias: false, - openapi: false, - openapiUI: false, - docker: false, - installDeps: false, -}; - -describe('generateProject – modular, no extras', () => { - let tmpDir: string; - - beforeEach(async () => { tmpDir = await makeTempDir(); }); - afterEach(async () => { await fs.remove(tmpDir); }); - - it('creates package.json', async () => { - await generateProject(baseOpts, tmpDir); - expect(await fs.pathExists(path.join(tmpDir, 'package.json'))).toBe(true); - }); - - it('creates src/app.ts', async () => { - await generateProject(baseOpts, tmpDir); - expect(await fs.pathExists(path.join(tmpDir, 'src', 'app.ts'))).toBe(true); - }); - - it('creates src/server.ts', async () => { - await generateProject(baseOpts, tmpDir); - expect(await fs.pathExists(path.join(tmpDir, 'src', 'server.ts'))).toBe(true); - }); - - it('creates global error handler', async () => { - await generateProject(baseOpts, tmpDir); - const content = await fs.readFile(path.join(tmpDir, 'src', 'middleware', 'errorHandler.ts'), 'utf-8'); - expect(content).toContain('ApiError'); - expect(content).toContain('ZodError'); - }); - - it('creates .env.example', async () => { - await generateProject(baseOpts, tmpDir); - expect(await fs.pathExists(path.join(tmpDir, '.env.example'))).toBe(true); - }); -}); - -describe('generateProject – MVC pattern', () => { - let tmpDir: string; - beforeEach(async () => { tmpDir = await makeTempDir(); }); - afterEach(async () => { await fs.remove(tmpDir); }); - - it('creates routes/index.ts', async () => { - await generateProject({ ...baseOpts, pattern: 'mvc' }, tmpDir); - expect(await fs.pathExists(path.join(tmpDir, 'src', 'routes', 'index.ts'))).toBe(true); - }); - - it('creates controllers/', async () => { - await generateProject({ ...baseOpts, pattern: 'mvc' }, tmpDir); - expect(await fs.pathExists(path.join(tmpDir, 'src', 'controllers'))).toBe(true); - }); -}); - -describe('generateProject – Docker', () => { - let tmpDir: string; - beforeEach(async () => { tmpDir = await makeTempDir(); }); - afterEach(async () => { await fs.remove(tmpDir); }); - - it('creates Dockerfile and docker-compose.yml', async () => { - await generateProject({ ...baseOpts, docker: true }, tmpDir); - expect(await fs.pathExists(path.join(tmpDir, 'Dockerfile'))).toBe(true); - expect(await fs.pathExists(path.join(tmpDir, 'docker-compose.yml'))).toBe(true); - }); -}); diff --git a/packages/create-express-forge/tests/main.test.ts b/packages/create-express-forge/tests/main.test.ts new file mode 100644 index 0000000..7c60b21 --- /dev/null +++ b/packages/create-express-forge/tests/main.test.ts @@ -0,0 +1,99 @@ +import os from "node:os"; +import path from "node:path"; +import fs from "fs-extra"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { generateBaseFiles } from "../src/generator/base.js"; +import { generateDocker } from "../src/generator/features/docker.js"; +import { generateModularStructure } from "../src/generator/structure/modular.js"; +import { generateMvcStructure } from "../src/generator/structure/mvc.js"; +import type { CliOptions } from "../src/types.js"; +import { buildPackageJson } from "../src/utils/package-builder.js"; +import { TemplateManager } from "../src/utils/template-manager.js"; + +vi.setConfig({ testTimeout: 10000 }); + +async function makeTempDir(prefix: string): Promise { + return fs.mkdtemp(path.join(os.tmpdir(), `cef-test-${prefix}-`)); +} + +const baseOpts: CliOptions = { + projectName: "test-app", + pattern: "modular", + orm: "none", + database: "none", + packageManager: "npm", + logger: "none", + testing: "none", + cache: "none", + auth: "none", + importAlias: false, + openapi: false, + openapiUI: false, + docker: false, + installDeps: false, +}; + +describe("Codebase Logic: Package Builder", () => { + it("should build a valid package.json object", () => { + const pkg = buildPackageJson(baseOpts); + expect(pkg.name).toBe("test-app"); + expect(pkg.dependencies).toBeDefined(); + expect(pkg.devDependencies).toHaveProperty("@biomejs/biome"); + }); +}); + +describe("Codebase Logic: Template Rendering", () => { + let tmpDir: string; + let tmpl: TemplateManager; + + beforeAll(async () => { + tmpDir = await makeTempDir("logic"); + tmpl = new TemplateManager(baseOpts); + }); + + afterAll(async () => { + if (tmpDir) await fs.remove(tmpDir); + }); + + it("generates base configuration files", async () => { + await generateBaseFiles(baseOpts, tmpDir, tmpl); + expect(await fs.pathExists(path.join(tmpDir, "tsconfig.json"))).toBe(true); + expect(await fs.pathExists(path.join(tmpDir, "src", "app.ts"))).toBe(true); + expect( + await fs.pathExists( + path.join(tmpDir, "src", "middleware", "errorHandler.ts"), + ), + ).toBe(true); + }); + + it("generates modular architecture structure", async () => { + const modularDir = path.join(tmpDir, "modular-test"); + await fs.ensureDir(modularDir); + await generateModularStructure(baseOpts, modularDir, tmpl); + expect( + await fs.pathExists(path.join(modularDir, "src", "modules", "todos")), + ).toBe(true); + }); + + it("generates MVC architecture structure", async () => { + const mvcDir = path.join(tmpDir, "mvc-test"); + await fs.ensureDir(mvcDir); + await generateMvcStructure({ ...baseOpts, pattern: "mvc" }, mvcDir, tmpl); + expect(await fs.pathExists(path.join(mvcDir, "src", "controllers"))).toBe( + true, + ); + expect( + await fs.pathExists(path.join(mvcDir, "src", "routes", "index.ts")), + ).toBe(true); + }); + + it("generates Docker configuration", async () => { + const dockerDir = path.join(tmpDir, "docker-test"); + await fs.ensureDir(dockerDir); + await generateDocker(baseOpts, dockerDir, tmpl); + expect(await fs.pathExists(path.join(dockerDir, "Dockerfile"))).toBe(true); + expect( + await fs.pathExists(path.join(dockerDir, "docker-compose.yml")), + ).toBe(true); + }); +}); diff --git a/packages/create-express-forge/tests/smoke.integration.test.ts b/packages/create-express-forge/tests/smoke.integration.test.ts index db8077a..f6307a0 100644 --- a/packages/create-express-forge/tests/smoke.integration.test.ts +++ b/packages/create-express-forge/tests/smoke.integration.test.ts @@ -1,66 +1,80 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { execa } from 'execa'; -import path from 'path'; -import fs from 'fs-extra'; -import os from 'os'; +import os from "node:os"; +import path from "node:path"; +import { execa } from "execa"; +import fs from "fs-extra"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; -describe('CLI Integration (Smoke Test)', () => { +describe("CLI Integration (Smoke Test)", () => { let testDir: string; - const projectName = 'smoke-test-app'; + const projectName = "smoke-test-app"; afterAll(async () => { - if (testDir && await fs.pathExists(testDir)) { + if (testDir && (await fs.pathExists(testDir))) { await fs.remove(testDir); } }); beforeAll(async () => { - // 1. Create a clean temp directory for testing - const baseTempDir = path.join(os.tmpdir(), 'cef-smoke-'); + // 1. Create a clean temp directory with unique suffix for testing + const uniqueId = Math.random().toString(36).substring(2, 8); + const baseTempDir = path.join(os.tmpdir(), `cef-smoke-${uniqueId}-`); testDir = await fs.mkdtemp(baseTempDir); - + // 2. Build the CLI to ensure dist/ exists - await execa('npm', ['run', 'build'], { cwd: process.cwd() }); - }, 60000); + await execa("npm", ["run", "build"], { cwd: process.cwd() }); + }, 120000); - it('should scaffold a project with all defaults using --yes flag', async () => { + it("should scaffold a project with all defaults using --yes flag", async () => { const targetPath = path.join(testDir, projectName); // Run the CLI - await execa('node', [ - path.resolve(process.cwd(), 'dist/index.js'), - projectName, - '--yes' - ], { - cwd: testDir, - env: { ...process.env, NODE_ENV: 'test' } - }); + await execa( + "node", + [path.resolve(process.cwd(), "dist/index.js"), projectName, "--yes"], + { + cwd: testDir, + env: { ...process.env, NODE_ENV: "test" }, + }, + ); // Verify critical files - expect(await fs.pathExists(path.join(targetPath, 'package.json'))).toBe(true); - expect(await fs.pathExists(path.join(targetPath, 'src/app.ts'))).toBe(true); - expect(await fs.pathExists(path.join(targetPath, 'src/config/env.ts'))).toBe(true); - expect(await fs.pathExists(path.join(targetPath, 'src/docs/swagger.ts'))).toBe(true); - expect(await fs.pathExists(path.join(targetPath, '.env.example'))).toBe(true); + expect(await fs.pathExists(path.join(targetPath, "package.json"))).toBe( + true, + ); + expect(await fs.pathExists(path.join(targetPath, "src/app.ts"))).toBe(true); + expect( + await fs.pathExists(path.join(targetPath, "src/config/env.ts")), + ).toBe(true); + expect( + await fs.pathExists(path.join(targetPath, "src/docs/swagger.ts")), + ).toBe(true); + expect(await fs.pathExists(path.join(targetPath, ".env.example"))).toBe( + true, + ); // Verify the TODO app boilerplate exists - const todoServicePath = path.join(targetPath, 'src/modules/todos/todos.service.ts'); + const todoServicePath = path.join( + targetPath, + "src/modules/todos/todos.service.ts", + ); expect(await fs.pathExists(todoServicePath)).toBe(true); - const serviceContent = await fs.readFile(todoServicePath, 'utf-8'); - expect(serviceContent).toContain('TodosService'); + const serviceContent = await fs.readFile(todoServicePath, "utf-8"); + expect(serviceContent).toContain("TodosService"); }, 180000); // 3 minutes for scaffolding + npm install - it('should build the generated project successfully', async () => { + it("should build the generated project successfully", async () => { const targetPath = path.join(testDir, projectName); // 1. Run type checking first (more thorough than build) - console.log('Running type check in generated project...'); - const typeCheck = await execa('npm', ['run', 'check-types'], { cwd: targetPath }); + console.log("Running type check in generated project..."); + const typeCheck = await execa("npm", ["run", "check-types"], { + cwd: targetPath, + }); expect(typeCheck.exitCode).toBe(0); // 2. Run the actual build - console.log('Running build in generated project...'); - const build = await execa('npm', ['run', 'build'], { cwd: targetPath }); + console.log("Running build in generated project..."); + const build = await execa("npm", ["run", "build"], { cwd: targetPath }); expect(build.exitCode).toBe(0); }, 240000); // 4 minutes for build + typecheck }); diff --git a/packages/create-express-forge/tsup.config.ts b/packages/create-express-forge/tsup.config.ts index 006ac48..c1517cf 100644 --- a/packages/create-express-forge/tsup.config.ts +++ b/packages/create-express-forge/tsup.config.ts @@ -1,11 +1,17 @@ -import { defineConfig } from 'tsup'; +import path from "node:path"; +import fs from "fs-extra"; +import { defineConfig } from "tsup"; export default defineConfig({ - entry: ['src/index.ts'], - format: ['esm'], + entry: ["src/index.ts"], + format: ["esm"], dts: true, clean: true, - banner: { js: '#!/usr/bin/env node' }, - target: 'es2022', + banner: { js: "#!/usr/bin/env node" }, + target: "es2022", splitting: false, + onSuccess: async () => { + await fs.copy(path.resolve("templates"), path.resolve("dist/templates")); + console.log("✅ Copied templates to dist/"); + }, }); diff --git a/packages/create-express-forge/vitest.config.ts b/packages/create-express-forge/vitest.config.ts index d3e16d0..dbcb9d6 100644 --- a/packages/create-express-forge/vitest.config.ts +++ b/packages/create-express-forge/vitest.config.ts @@ -1,12 +1,12 @@ -import { defineConfig } from 'vitest/config'; +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globals: true, - environment: 'node', + environment: "node", coverage: { - provider: 'v8', - reporter: ['text', 'html', 'lcov'], + provider: "v8", + reporter: ["text", "html", "lcov"], }, }, }); diff --git a/packages/eslint-config/index.js b/packages/eslint-config/index.js deleted file mode 100644 index fe438b5..0000000 --- a/packages/eslint-config/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/** @type {import("eslint").Linter.Config} */ -module.exports = { - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint"], - extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], - rules: { - "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], - "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/consistent-type-imports": "warn", - }, - env: { node: true, es2022: true }, -}; diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json deleted file mode 100644 index 1d5efd1..0000000 --- a/packages/eslint-config/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@repo/eslint-config", - "version": "0.0.0", - "private": true, - "license": "MIT", - "main": "index.js", - "dependencies": { - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0" - } -} diff --git a/packages/lint-config/biome.base.json b/packages/lint-config/biome.base.json new file mode 100644 index 0000000..79dbe5d --- /dev/null +++ b/packages/lint-config/biome.base.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.13/schema.json", + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "files": { + "includes": [ + "**", + "!**/dist", + "!**/node_modules", + "!**/pnpm-lock.yaml", + "!docs/.vitepress/cache", + "!docs/.vitepress/dist" + ] + } +} diff --git a/packages/lint-config/package.json b/packages/lint-config/package.json new file mode 100644 index 0000000..c3d8db3 --- /dev/null +++ b/packages/lint-config/package.json @@ -0,0 +1,8 @@ +{ + "name": "@repo/lint-config", + "version": "0.0.0", + "private": true, + "files": [ + "biome.base.json" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b9bf6d..f5bf5c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,18 +8,15 @@ importers: .: devDependencies: + '@biomejs/biome': + specifier: ^2.4.13 + version: 2.4.13 '@changesets/cli': specifier: ^2.27.7 version: 2.31.0(@types/node@20.19.39) - eslint: - specifier: ^8.57.0 - version: 8.57.1 husky: specifier: ^9.1.7 version: 9.1.7 - prettier: - specifier: ^3.3.3 - version: 3.8.3 turbo: specifier: ^2.0.9 version: 2.9.6 @@ -47,6 +44,9 @@ importers: commander: specifier: ^12.1.0 version: 12.1.0 + eta: + specifier: ^3.5.0 + version: 3.5.0 execa: specifier: ^9.3.0 version: 9.6.1 @@ -56,10 +56,13 @@ importers: ora: specifier: ^8.1.1 version: 8.2.0 + pkg-types: + specifier: ^1.2.0 + version: 1.3.1 devDependencies: - '@repo/eslint-config': + '@repo/lint-config': specifier: workspace:* - version: link:../eslint-config + version: link:../lint-config '@repo/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -69,9 +72,6 @@ importers: '@types/node': specifier: ^20.14.0 version: 20.19.39 - eslint: - specifier: ^8.57.0 - version: 8.57.1 tsup: specifier: ^8.1.0 version: 8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.5.3) @@ -85,14 +85,7 @@ importers: specifier: ^1.6.0 version: 1.6.1(@types/node@20.19.39) - packages/eslint-config: - dependencies: - '@typescript-eslint/eslint-plugin': - specifier: ^7.0.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1)(typescript@5.5.3) - '@typescript-eslint/parser': - specifier: ^7.0.0 - version: 7.18.0(eslint@8.57.1)(typescript@5.5.3) + packages/lint-config: {} packages/typescript-config: {} @@ -195,6 +188,59 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@biomejs/biome@2.4.13': + resolution: {integrity: sha512-gLXOwkOBBg0tr7bDsqlkIh4uFeKuMjxvqsrb1Tukww1iDmHcfr4Uu8MoQxp0Rcte+69+osRNWXwHsu/zxT6XqA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.13': + resolution: {integrity: sha512-2KImO1jhNFBa2oWConyr0x6flxbQpGKv6902uGXpYM62Xyem8U80j441SyUJ8KyngsmKbQjeIv1q2CQfDkNnYg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.13': + resolution: {integrity: sha512-BKrJklbaFN4p1Ts4kPBczo+PkbsHQg57kmJ+vON9u2t6uN5okYHaSr7h/MutPCWQgg2lglaWoSmm+zhYW+oOkg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.13': + resolution: {integrity: sha512-U5MsuBQW25dXaYtqWWSPM3P96H6Y+fHuja3TQpMNnylocHW0tEbtFTDlUj6oM+YJLntvEkQy4grBvQNUD4+RCg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.4.13': + resolution: {integrity: sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.4.13': + resolution: {integrity: sha512-Z601MienRgTBDza/+u2CH3RSrWoXo9rtr8NK6A4KJzqGgfxx+H3VlyLgTJ4sRo40T3pIsqpTmiOQEvYzQvBRvQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.4.13': + resolution: {integrity: sha512-Az3ZZedYRBo9EQzNnD9SxFcR1G5QsGo6VEc2hIyVPZ1rdKwee/7E9oeBBZFpE8Z44ekxsDQBqbiWGW5ShOhUSQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.4.13': + resolution: {integrity: sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.13': + resolution: {integrity: sha512-tTcMkXyBrmHi9BfrD2VNHs/5rYIUKETqsBlYOvSAABwBkJhSDVb5e7wPukftsQbO3WzQkXe6kaztC6WtUOXSoQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@changesets/apply-release-plan@7.1.1': resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} @@ -567,37 +613,6 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead - '@iconify-json/simple-icons@1.2.79': resolution: {integrity: sha512-aNyO7Fd1qej9oQfIyohYFRv0lhQLaZ+6UkK1c1qwax0MDPUOZOdq65MlU500kow97pD/W+b2u1And3e25eE24Q==} @@ -998,64 +1013,6 @@ packages: '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} - '@typescript-eslint/eslint-plugin@7.18.0': - resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/parser@7.18.0': - resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/scope-manager@7.18.0': - resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} - engines: {node: ^18.18.0 || >=20.0.0} - - '@typescript-eslint/type-utils@7.18.0': - resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/types@7.18.0': - resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} - engines: {node: ^18.18.0 || >=20.0.0} - - '@typescript-eslint/typescript-estree@7.18.0': - resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/utils@7.18.0': - resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - - '@typescript-eslint/visitor-keys@7.18.0': - resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} - engines: {node: ^18.18.0 || >=20.0.0} - '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -1169,11 +1126,6 @@ packages: '@vueuse/shared@12.8.2': resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-walk@8.3.5: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} @@ -1183,9 +1135,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - ajv@6.14.0: - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - algoliasearch@5.50.2: resolution: {integrity: sha512-Tfp26yoNWurUjfgK4GOrVJQhSNXu9tJtHfFFNosgT2YClG+vPyUjX/gbC8rG39qLncnZg8Fj34iarQWpMkqefw==} engines: {node: '>= 14.0.0'} @@ -1226,9 +1175,6 @@ packages: assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -1236,12 +1182,6 @@ packages: birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} - brace-expansion@1.1.14: - resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} - - brace-expansion@2.1.0: - resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1256,10 +1196,6 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1267,10 +1203,6 @@ packages: resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} engines: {node: '>=4'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -1321,9 +1253,6 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -1355,9 +1284,6 @@ packages: resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} engines: {node: '>=6'} - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1377,10 +1303,6 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} @@ -1408,54 +1330,20 @@ packages: engines: {node: '>=18'} hasBin: true - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true - - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} + eta@3.5.0: + resolution: {integrity: sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==} + engines: {node: '>=6.0.0'} execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} @@ -1468,19 +1356,10 @@ packages: extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1497,10 +1376,6 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1509,20 +1384,9 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} - - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - focus-trap@7.8.0: resolution: {integrity: sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==} @@ -1538,9 +1402,6 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1568,18 +1429,6 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -1587,13 +1436,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} @@ -1631,21 +1473,6 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1666,10 +1493,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -1720,28 +1543,12 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -1761,13 +1568,6 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -1821,13 +1621,6 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - - minimatch@9.0.9: - resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} - engines: {node: '>=16 || 14 >=14.17'} - minisearch@7.2.0: resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} @@ -1856,9 +1649,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1871,9 +1661,6 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} @@ -1885,10 +1672,6 @@ packages: oniguruma-to-es@3.1.1: resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - ora@8.2.0: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} @@ -1904,10 +1687,6 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - p-limit@5.0.0: resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} engines: {node: '>=18'} @@ -1916,10 +1695,6 @@ packages: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - p-map@2.1.0: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} @@ -1931,10 +1706,6 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} @@ -1943,10 +1714,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2018,20 +1785,11 @@ packages: preact@10.29.1: resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==} - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true - prettier@3.8.3: - resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} - engines: {node: '>=14'} - hasBin: true - pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2043,10 +1801,6 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -2073,10 +1827,6 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -2095,11 +1845,6 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - rollup@4.60.2: resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2203,10 +1948,6 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - strip-literal@2.1.1: resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} @@ -2219,10 +1960,6 @@ packages: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} @@ -2230,9 +1967,6 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -2269,12 +2003,6 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' - ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -2306,18 +2034,10 @@ packages: resolution: {integrity: sha512-+v2QJey7ZUeUiuigkU+uFfklvNUyPI2VO2vBpMYJA+a1hKFLFiKtUYlRHdb3P9CrAvMzi0upbjI4WT+zKtqkBg==} hasBin: true - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - type-detect@4.1.0: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - typescript@5.5.3: resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} engines: {node: '>=14.17'} @@ -2356,9 +2076,6 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -2456,21 +2173,10 @@ packages: engines: {node: '>=8'} hasBin: true - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - yocto-queue@1.2.2: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} @@ -2615,6 +2321,41 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@biomejs/biome@2.4.13': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.13 + '@biomejs/cli-darwin-x64': 2.4.13 + '@biomejs/cli-linux-arm64': 2.4.13 + '@biomejs/cli-linux-arm64-musl': 2.4.13 + '@biomejs/cli-linux-x64': 2.4.13 + '@biomejs/cli-linux-x64-musl': 2.4.13 + '@biomejs/cli-win32-arm64': 2.4.13 + '@biomejs/cli-win32-x64': 2.4.13 + + '@biomejs/cli-darwin-arm64@2.4.13': + optional: true + + '@biomejs/cli-darwin-x64@2.4.13': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.13': + optional: true + + '@biomejs/cli-linux-arm64@2.4.13': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.13': + optional: true + + '@biomejs/cli-linux-x64@2.4.13': + optional: true + + '@biomejs/cli-win32-arm64@2.4.13': + optional: true + + '@biomejs/cli-win32-x64@2.4.13': + optional: true + '@changesets/apply-release-plan@7.1.1': dependencies: '@changesets/config': 3.1.4 @@ -2929,41 +2670,6 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': - dependencies: - eslint: 8.57.1 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/eslintrc@2.1.4': - dependencies: - ajv: 6.14.0 - debug: 4.4.3 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@8.57.1': {} - - '@humanwhocodes/config-array@0.13.0': - dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.3 - minimatch: 3.1.5 - transitivePeerDependencies: - - supports-color - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/object-schema@2.0.3': {} - '@iconify-json/simple-icons@1.2.79': dependencies: '@iconify/types': 2.0.0 @@ -3318,87 +3024,6 @@ snapshots: '@types/web-bluetooth@0.0.21': {} - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1)(typescript@5.5.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.5.3) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.5.3) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.5.3) - '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - ts-api-utils: 1.4.3(typescript@5.5.3) - optionalDependencies: - typescript: 5.5.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.5.3)': - dependencies: - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.3) - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.3 - eslint: 8.57.1 - optionalDependencies: - typescript: 5.5.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@7.18.0': - dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 - - '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.5.3)': - dependencies: - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.3) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.5.3) - debug: 4.4.3 - eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@5.5.3) - optionalDependencies: - typescript: 5.5.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@7.18.0': {} - - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.5.3)': - dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.3 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.9 - semver: 7.7.4 - ts-api-utils: 1.4.3(typescript@5.5.3) - optionalDependencies: - typescript: 5.5.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.5.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.3) - eslint: 8.57.1 - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/visitor-keys@7.18.0': - dependencies: - '@typescript-eslint/types': 7.18.0 - eslint-visitor-keys: 3.4.3 - '@ungap/structured-clone@1.3.0': {} '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@20.19.39))(vue@3.5.33(typescript@5.5.3))': @@ -3534,23 +3159,12 @@ snapshots: transitivePeerDependencies: - typescript - acorn-jsx@5.3.2(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - acorn-walk@8.3.5: dependencies: acorn: 8.16.0 acorn@8.16.0: {} - ajv@6.14.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - algoliasearch@5.50.2: dependencies: '@algolia/abtesting': 1.16.2 @@ -3592,23 +3206,12 @@ snapshots: assertion-error@1.1.0: {} - balanced-match@1.0.2: {} - better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 birpc@2.9.0: {} - brace-expansion@1.1.14: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.1.0: - dependencies: - balanced-match: 1.0.2 - braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -3620,8 +3223,6 @@ snapshots: cac@6.7.14: {} - callsites@3.1.0: {} - ccount@2.0.1: {} chai@4.5.0: @@ -3634,11 +3235,6 @@ snapshots: pathval: 1.1.1 type-detect: 4.1.0 - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chalk@5.6.2: {} character-entities-html4@2.1.0: {} @@ -3675,8 +3271,6 @@ snapshots: commander@4.1.1: {} - concat-map@0.0.1: {} - confbox@0.1.8: {} consola@3.4.2: {} @@ -3701,8 +3295,6 @@ snapshots: dependencies: type-detect: 4.1.0 - deep-is@0.1.4: {} - dequal@2.0.3: {} detect-indent@6.1.0: {} @@ -3717,10 +3309,6 @@ snapshots: dependencies: path-type: 4.0.0 - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - emoji-regex-xs@1.0.0: {} emoji-regex@10.6.0: {} @@ -3789,83 +3377,15 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 - escape-string-regexp@4.0.0: {} - - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint@8.57.1: - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@eslint-community/regexpp': 4.12.2 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.1 - '@humanwhocodes/config-array': 0.13.0 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.0 - ajv: 6.14.0 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.1 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.5 - natural-compare: 1.4.0 - optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - - espree@9.6.1: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 3.4.3 - esprima@4.0.1: {} - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - estree-walker@2.0.2: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 - esutils@2.0.3: {} + eta@3.5.0: {} execa@8.0.1: dependencies: @@ -3896,8 +3416,6 @@ snapshots: extendable-error@0.1.7: {} - fast-deep-equal@3.1.3: {} - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3906,10 +3424,6 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -3922,10 +3436,6 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.2.0 - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -3935,25 +3445,12 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 mlly: 1.8.2 rollup: 4.60.2 - flat-cache@3.2.0: - dependencies: - flatted: 3.4.2 - keyv: 4.5.4 - rimraf: 3.0.2 - - flatted@3.4.2: {} - focus-trap@7.8.0: dependencies: tabbable: 6.4.0 @@ -3976,8 +3473,6 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true @@ -4000,23 +3495,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.5 - once: 1.4.0 - path-is-absolute: 1.0.1 - - globals@13.24.0: - dependencies: - type-fest: 0.20.2 - globby@11.1.0: dependencies: array-union: 2.1.0 @@ -4028,10 +3506,6 @@ snapshots: graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - - has-flag@4.0.0: {} - hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 @@ -4068,20 +3542,6 @@ snapshots: ignore@5.3.2: {} - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - imurmurhash@0.1.4: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -4094,8 +3554,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-plain-obj@4.1.0: {} is-stream@3.0.0: {} @@ -4129,12 +3587,6 @@ snapshots: dependencies: argparse: 2.0.1 - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -4145,15 +3597,6 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -4169,12 +3612,6 @@ snapshots: dependencies: p-locate: 4.1.0 - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.merge@4.6.2: {} - lodash.startcase@4.4.0: {} log-symbols@6.0.0: @@ -4234,14 +3671,6 @@ snapshots: mimic-function@5.0.1: {} - minimatch@3.1.5: - dependencies: - brace-expansion: 1.1.14 - - minimatch@9.0.9: - dependencies: - brace-expansion: 2.1.0 - minisearch@7.2.0: {} mitt@3.0.1: {} @@ -4267,8 +3696,6 @@ snapshots: nanoid@3.3.11: {} - natural-compare@1.4.0: {} - npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -4280,10 +3707,6 @@ snapshots: object-assign@4.1.1: {} - once@1.4.0: - dependencies: - wrappy: 1.0.2 - onetime@6.0.0: dependencies: mimic-fn: 4.0.0 @@ -4298,15 +3721,6 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - ora@8.2.0: dependencies: chalk: 5.6.2 @@ -4329,10 +3743,6 @@ snapshots: dependencies: p-try: 2.2.0 - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - p-limit@5.0.0: dependencies: yocto-queue: 1.2.2 @@ -4341,10 +3751,6 @@ snapshots: dependencies: p-limit: 2.3.0 - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - p-map@2.1.0: {} p-try@2.2.0: {} @@ -4353,16 +3759,10 @@ snapshots: dependencies: quansync: 0.2.11 - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - parse-ms@4.0.0: {} path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} - path-key@3.1.1: {} path-key@4.0.0: {} @@ -4408,12 +3808,8 @@ snapshots: preact@10.29.1: {} - prelude-ls@1.2.1: {} - prettier@2.8.8: {} - prettier@3.8.3: {} - pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 @@ -4426,8 +3822,6 @@ snapshots: property-information@7.1.0: {} - punycode@2.3.1: {} - quansync@0.2.11: {} queue-microtask@1.2.3: {} @@ -4453,8 +3847,6 @@ snapshots: dependencies: regex-utilities: 2.3.0 - resolve-from@4.0.0: {} - resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -4468,10 +3860,6 @@ snapshots: rfdc@1.4.1: {} - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - rollup@4.60.2: dependencies: '@types/estree': 1.0.8 @@ -4588,8 +3976,6 @@ snapshots: strip-final-newline@4.0.0: {} - strip-json-comments@3.1.1: {} - strip-literal@2.1.1: dependencies: js-tokens: 9.0.1 @@ -4608,16 +3994,10 @@ snapshots: dependencies: copy-anything: 4.0.5 - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - tabbable@6.4.0: {} term-size@2.2.1: {} - text-table@0.2.0: {} - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -4647,10 +4027,6 @@ snapshots: trim-lines@3.0.1: {} - ts-api-utils@1.4.3(typescript@5.5.3): - dependencies: - typescript: 5.5.3 - ts-interface-checker@0.1.13: {} tsup@8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.5.3): @@ -4697,14 +4073,8 @@ snapshots: '@turbo/windows-64': 2.9.6 '@turbo/windows-arm64': 2.9.6 - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - type-detect@4.1.0: {} - type-fest@0.20.2: {} - typescript@5.5.3: {} ufo@1.6.3: {} @@ -4740,10 +4110,6 @@ snapshots: universalify@2.0.1: {} - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -4883,18 +4249,12 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - word-wrap@1.2.5: {} - wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - wrappy@1.0.2: {} - - yocto-queue@0.1.0: {} - yocto-queue@1.2.2: {} yoctocolors-cjs@2.1.3: {} diff --git a/turbo.json b/turbo.json index febf947..3d5d5d7 100644 --- a/turbo.json +++ b/turbo.json @@ -18,7 +18,8 @@ "dependsOn": ["^lint"] }, "test": { - "dependsOn": ["build"] + "dependsOn": ["build"], + "cache": false }, "dev": { "cache": false,