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/CONTRIBUTING.md b/CONTRIBUTING.md index 4549af8..3f59478 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,11 +35,11 @@ pnpm run test ## Adding a New Generator Feature 1. Add your option to `packages/create-express-forge/src/types.ts` -2. Add the prompt in `src/prompts.ts` -3. Create a generator in `src/generator/features/your-feature.ts` -4. Wire it into `src/generator/index.ts` -5. Update `src/utils/package-builder.ts` if it adds dependencies -6. Add a test in `tests/` +2. Add the prompt in `packages/create-express-forge/src/prompts.ts` +3. Add templates in `packages/create-express-forge/templates/` +4. Wire logic into `packages/create-express-forge/src/generator/index.ts` +5. Update `packages/create-express-forge/src/utils/package-builder.ts` if it adds dependencies +6. Add a test in `packages/create-express-forge/tests/` ## Releasing diff --git a/README.md b/README.md index 37f3318..c81ebe5 100644 --- a/README.md +++ b/README.md @@ -10,28 +10,27 @@ ```bash npx create-express-forge my-api -# or -npm create express-forge my-api -# or (short alias) -npx cef my-api +# or scaffold in current directory +npx create-express-forge . ``` ## What You Get Interactive prompts let you choose: -| Option | Choices | -|--------|---------| -| **Architecture** | Modular (feature-based) · MVC | -| **ORM** | Prisma · Sequelize · None | -| **Database** | PostgreSQL · MySQL · SQLite · None | -| **Logger** | Winston · Pino · None | -| **Testing** | Vitest · Jest · None | -| **Docker** | Dockerfile + docker-compose | +| Option | Choices | +| ---------------- | ---------------------------------- | +| **Architecture** | Modular (feature-based) · MVC | +| **ORM** | Prisma · Sequelize · None | +| **Database** | PostgreSQL · MySQL · SQLite · None | +| **Logger** | Winston · Pino · None | +| **Testing** | Vitest · Jest · None | +| **Docker** | Dockerfile + docker-compose | ## Generated Project Includes -- ✅ **TypeScript** + `tsx` hot-reload dev server +- ✅ **TypeScript** + `tsx` hot-reload dev server + **Path Aliases (`@/`)** +- ✅ **Biome** — 20x faster linting and formatting (replaces ESLint/Prettier) - ✅ **Zod** env validation on startup — fails fast on bad config - ✅ **Global centralized error handler** — `ApiError`, `ZodError`, unknown errors all handled - ✅ **Request validation middleware** via `validate(schema)` @@ -42,6 +41,7 @@ Interactive prompts let you choose: - ✅ **Graceful shutdown** (SIGTERM / SIGINT) - ✅ **Multi-stage Dockerfile** with healthcheck - ✅ **docker-compose** with correct DB service +- ✅ **Automated OpenAPI (Swagger)** — Zero-JSDoc documentation via Zod schemas ## Repository Structure @@ -49,23 +49,78 @@ Interactive prompts let you choose: create-express-forge/ ├── packages/ │ ├── create-express-forge/ ← The published CLI +│ ├── mcp/ ← @create-express-forge/mcp Server │ ├── typescript-config/ ← Shared internal TS config -│ └── eslint-config/ ← Shared internal ESLint config +│ └── lint-config/ ← Shared internal Biome/Lint config ├── examples/ │ └── modular-postgres-prisma/ ← Pre-generated example └── .github/workflows/ ← CI + Release ``` +## 🤖 AI & MCP Integration + +Create Express Forge is designed to be AI-friendly. We provide a built-in **MCP (Model Context Protocol)** server that lets you chat with your AI assistant about the project, fetch documentation, and generate scaffolding commands. + +### Using the MCP Server + +You can run the MCP server directly via `npx` (recommended) or by building the source. + +**Option 1: Using `npx` (Recommended)** +Add this to your Claude Desktop config or other MCP client: + +```json +{ + "mcpServers": { + "create-express-forge": { + "command": "npx", + "args": ["-y", "@create-express-forge/mcp"] + } + } +} +``` + +**Option 2: From Source** +1. **Build the project**: `pnpm build` +2. **Add to your MCP Client**: +```json +{ + "mcpServers": { + "create-express-forge": { + "command": "node", + "args": ["/absolute/path/to/express-cli/packages/mcp/dist/index.js"] + } + } +} +``` + +### LLM Documentation + +We also provide machine-readable documentation files for LLMs: + +- **`llms.txt`**: [Project summary](https://code-y02.github.io/express-cli/llms.txt) +- **`llms-full.txt`**: [Full documentation context](https://code-y02.github.io/express-cli/llms-full.txt) +- **`ai.json`**: [Capability manifest and CLI flags](https://code-y02.github.io/express-cli/ai.json) + +## Legacy Support (v3.x) + +Documentation for the legacy v3.x (LTS) version is available at [https://code-y02.github.io/express-cli/v3/](https://code-y02.github.io/express-cli/v3/). + +To scaffold a project using v3.x, run: + +```bash +npx create-express-forge@3.3.2 [project-name] +``` + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). ## Versioning & Branches -| Branch | npm tag | Description | -|--------|---------|-------------| -| `main` | `latest` | Stable releases | -| `next` | `next` | Pre-releases / beta | +| Branch | npm tag | Description | +| ------ | -------- | ------------------- | +| `main` | `latest` | Stable releases | +| `next` | `next` | Pre-releases / beta | ## License 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..1bfc759 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -5,17 +5,75 @@ const rawBase = process.env.VITEPRESS_BASE || "/express-cli/"; const base = rawBase.endsWith("/") ? rawBase : `${rawBase}/`; export default defineConfig({ - title: "Express Forge", + title: "Create Express Forge", description: "⚡ Production-ready Express backends in seconds", base: base, + cleanUrls: true, + lastUpdated: true, + sitemap: { + hostname: "https://code-y02.github.io/express-cli", + }, 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: "create-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:image", content: `${base}og-image.png` }], + ["meta", { name: "twitter:card", content: "summary_large_image" }], + ["meta", { name: "twitter:site", content: "@code_y02" }], + ["meta", { name: "twitter:title", content: "Create Express Forge" }], + [ + "meta", + { + name: "twitter:description", + content: "⚡ Production-ready Express backends in seconds", + }, + ], + ["meta", { name: "twitter:image", content: `${base}og-image.png` }], + [ + "script", + { type: "application/ld+json" }, + JSON.stringify({ + "@context": "https://schema.org", + "@type": "SoftwareApplication", + "name": "Create Express Forge", + "operatingSystem": "Node.js", + "applicationCategory": "DeveloperApplication", + "description": "The ultimate CLI for scaffolding production-ready Express.js TypeScript backends.", + "offers": { + "@type": "Offer", + "price": "0", + "priceCurrency": "USD", + }, + "author": { + "@type": "Person", + "name": "Yatharth Lakhate", + }, + }), + ], ], themeConfig: { @@ -29,45 +87,84 @@ 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" }, - ], - - sidebar: [ - { - text: "Introduction", - items: [ - { text: "What is Express Forge?", link: "/" }, - { text: "Getting Started", link: "/guide/getting-started" }, - ], - }, - { - text: "Core Concepts", - items: [ - { text: "Architecture Patterns", link: "/guide/architecture" }, - { text: "Project Structure", link: "/guide/structure" }, - { text: "Core Features", link: "/guide/features" }, - { text: "Authentication", link: "/guide/auth" }, - { text: "Caching", link: "/guide/caching" }, - { text: "API Documentation", link: "/guide/openapi" }, - ], - }, { - text: "Advanced", + text: "Versions", items: [ - { text: "Deployment", link: "/guide/deployment" }, - { text: "Testing Strategy", link: "/guide/testing" }, - { text: "Troubleshooting", link: "/guide/troubleshooting" }, + { text: "v4.x (Latest)", link: "/" }, + { text: "v3.x (LTS)", link: "/v3/" }, ], }, { - text: "Reference", - items: [ - { text: "CLI Options", link: "/reference/cli-options" }, - { text: "Configuration", link: "/reference/config" }, - ], + text: "⭐ Star on GitHub", + link: "https://github.com/CODE-Y02/express-cli", }, ], + sidebar: { + "/": [ + { + text: "Introduction", + items: [ + { text: "What is create-express-forge?", link: "/" }, + { text: "Getting Started", link: "/guide/getting-started" }, + ], + }, + { + text: "Core Concepts", + items: [ + { text: "Architecture Patterns", link: "/guide/architecture" }, + { text: "Project Structure", link: "/guide/structure" }, + { text: "Core Features", link: "/guide/features" }, + { text: "Authentication", link: "/guide/auth" }, + { text: "Caching", link: "/guide/caching" }, + { text: "API Documentation", link: "/guide/openapi" }, + ], + }, + { + text: "Advanced", + items: [ + { text: "Deployment", link: "/guide/deployment" }, + { text: "Testing Strategy", link: "/guide/testing" }, + { text: "Troubleshooting", link: "/guide/troubleshooting" }, + ], + }, + { + text: "Reference", + items: [ + { text: "CLI Options", link: "/reference/cli-options" }, + { text: "Configuration", link: "/reference/config" }, + ], + }, + ], + "/v3/": [ + { + text: "v3 Docs", + items: [ + { text: "Introduction", link: "/v3/" }, + { text: "Getting Started", link: "/v3/guide/getting-started" }, + ], + }, + { + text: "Core Concepts", + items: [ + { text: "Architecture Patterns", link: "/v3/guide/architecture" }, + { text: "Project Structure", link: "/v3/guide/structure" }, + { text: "Core Features", link: "/v3/guide/features" }, + { text: "Authentication", link: "/v3/guide/auth" }, + { text: "Caching", link: "/v3/guide/caching" }, + { text: "API Documentation", link: "/v3/guide/openapi" }, + ], + }, + { + text: "Reference", + items: [ + { text: "CLI Options", link: "/v3/reference/cli-options" }, + { text: "Configuration", link: "/v3/reference/config" }, + ], + }, + ], + }, + socialLinks: [ { icon: "github", link: "https://github.com/CODE-Y02/express-cli" }, ], diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index beaa8ab..e6ecd12 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,108 @@ /* 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; + padding-bottom: 0.1em; + line-height: 1.2; } /* 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); + line-height: 1.3; + padding-bottom: 0.1em; } .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 +157,7 @@ h1, h2, h3, h4, h5, h6 { } .VPSidebarItem.level-0 > .item > .text { - font-weight: 700 !important; + font-weight: 700; } /* Table Responsiveness */ @@ -152,22 +171,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; } } @@ -269,6 +288,11 @@ h1, h2, h3, h4, h5, h6 { background: var(--vp-home-hero-name-background); -webkit-background-clip: text; -webkit-text-fill-color: transparent; + padding: 0.2em 0; + line-height: 1.4; + display: table; + margin-left: auto; + margin-right: auto; } .VPHome .vp-doc ul { 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/docs/guide/architecture.md b/docs/guide/architecture.md index 355cdc3..ecb8298 100644 --- a/docs/guide/architecture.md +++ b/docs/guide/architecture.md @@ -1,6 +1,6 @@ # Architecture Patterns -Express Forge supports two main architecture patterns to fit your project's needs. Choosing the right one is crucial for long-term maintainability. +Create Express Forge supports two main architecture patterns to fit your project's needs. Choosing the right one is crucial for long-term maintainability. ## 📦 Modular Architecture (Recommended) @@ -43,7 +43,22 @@ src/ services/ # Business logic ``` +## 🏷️ Path Aliases (@/) + +Regardless of the architecture you choose, Create Express Forge pre-configures **Path Aliases**. This means you can use absolute imports from the `src` directory instead of messy relative paths. + +**Instead of this:** +```typescript +import { User } from '../../../models/user.js'; +``` + +**You do this:** +```typescript +import { User } from '@/models/user.js'; +``` + +This ensures that moving files around won't break your imports. + ## Which one should I use? If you are building a production API that you expect to grow over time, **Modular Architecture** is almost always the better choice. It prevents the "Fat Controller" and "Fat Model" syndromes by keeping related logic close together. - diff --git a/docs/guide/auth.md b/docs/guide/auth.md index 379a829..3dc9bc8 100644 --- a/docs/guide/auth.md +++ b/docs/guide/auth.md @@ -1,6 +1,6 @@ # Authentication -Express Forge provides two battle-tested authentication strategies: **JWT** (JSON Web Tokens) and **Sessions**. +Create Express Forge provides two battle-tested authentication strategies: **JWT** (JSON Web Tokens) and **Sessions**. ## Strategies @@ -43,4 +43,4 @@ export const getProfile = (req: Request, res: Response) => { ## Security Best Practices 1. **Secret Management**: Never commit your `JWT_SECRET` or `SESSION_SECRET` to version control. Use `.env` files. 2. **HTTPS**: Always serve your API over HTTPS in production to protect tokens and session cookies. -3. **HTTP-Only Cookies**: If using sessions, Express Forge pre-configures cookies to be `httpOnly` to prevent XSS attacks. +3. **HTTP-Only Cookies**: If using sessions, Create Express Forge pre-configures cookies to be `httpOnly` to prevent XSS attacks. diff --git a/docs/guide/caching.md b/docs/guide/caching.md index 098518e..4a0467b 100644 --- a/docs/guide/caching.md +++ b/docs/guide/caching.md @@ -1,6 +1,6 @@ # Caching -Express Forge provides a flexible caching layer to boost your API performance and reduce database load. You can choose between **Redis** (distributed) or **Node-Cache** (in-memory) during the scaffolding process. +Create Express Forge provides a flexible caching layer to boost your API performance and reduce database load. You can choose between **Redis** (distributed) or **Node-Cache** (in-memory) during the scaffolding process. ## Supported Drivers @@ -47,4 +47,4 @@ await cache.del('user:123'); ## Best Practices 1. **Cache Invalidation**: Always delete or update the cache when the underlying data in the database changes. 2. **Serialization**: Since Redis only stores strings, ensure you `JSON.stringify()` your objects before setting and `JSON.parse()` when getting. -3. **Fail-Safe**: Express Forge handles Redis connection errors gracefully via the logger, preventing your entire app from crashing if the cache is down. +3. **Pro Fail-Fast**: In production, Create Express Forge strictly validates Redis connectivity on startup. If Redis is down, the app fails early to prevent inconsistent states. In development, it provides a clear warning and continues. diff --git a/docs/guide/deployment.md b/docs/guide/deployment.md index af9d0fc..32bef43 100644 --- a/docs/guide/deployment.md +++ b/docs/guide/deployment.md @@ -1,6 +1,6 @@ # Deployment -Express Forge provides production-ready configurations to help you ship your API with confidence. +Create Express Forge provides production-ready configurations to help you ship your API with confidence. ## 🐳 Docker Deployment (Recommended) diff --git a/docs/guide/features.md b/docs/guide/features.md index e6eaad6..1f9307f 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -1,9 +1,14 @@ +--- +title: Core Features | Create Express Forge +description: Explore the production-ready features of Create Express Forge, including JWT Auth, Prisma integration, and automated OpenAPI documentation. +--- + # Core Features -Express Forge comes packed with everything you need to build robust APIs. +Create Express Forge comes packed with everything you need to build robust APIs. ## 🛡️ TypeScript First -Type safety is at the core of Express Forge. Every scaffolded project includes: +Type safety is at the core of Create Express Forge. Every scaffolded project includes: - Strict TypeScript configuration. - Path aliases (e.g., `@/config/env`). - Type-safe environment variables via **Zod**. @@ -22,7 +27,7 @@ Choose your favorite ORM and get started instantly: - **Migrations**: Pre-configured scripts to handle database schema changes. ## 🧪 Testing Suite -Don't ship broken code. Express Forge sets up a complete testing environment: +Don't ship broken code. Create Express Forge sets up a complete testing environment: - **Vitest/Jest**: Choose your favorite test runner. - **Supertest**: For high-level API integration tests. - **Example Tests**: Every scaffolded project includes example unit and integration tests. @@ -41,23 +46,29 @@ Stay secure by default with pre-configured industry standards: ## 📝 Logging & Monitoring - **Pino/Winston**: High-performance, structured logging. Pino is used by default for its extreme speed and JSON output, which is perfect for log aggregators like ELK or Datadog. -- **Health Checks**: A standard `/health` endpoint is included, providing uptime, memory usage, and database connectivity status. +- **Pro Fail-Fast**: In production, the app strictly validates database and Redis connections on startup. If a dependency is down, the app fails early to prevent inconsistent states. In development, it provides clear warnings. +- **Health Checks**: A standard `/api/v1/health` endpoint is included, providing uptime, memory usage, and database connectivity status. ## 📜 OpenAPI Documentation Never let your documentation get out of sync: -- **Swagger UI**: Integrated UI to explore and test your API endpoints directly from the browser. -- **Auto-generated Spec**: The CLI generates a `docs.json` endpoint that is always up-to-date with your code's JSDoc annotations. -- **Security Schemas**: Pre-configured security definitions for your chosen auth strategy (Cookie or Bearer). +- **Swagger UI / Scalar**: Beautiful, integrated UI to explore and test your API endpoints directly from the browser. +- **Zero-JSDoc Spec**: Documentation is generated directly from your **Zod schemas** and a centralized registry. No more clunky JSDoc comments in your controllers! +- **Type-Safe Documentation**: Your runtime validation and your API documentation are always 100% in sync. + +## ⚡ Modern Tooling +- **Biome**: Replaces ESLint and Prettier for 20x faster linting and formatting. +- **Import Aliases**: Pre-configured `@/` paths for clean, absolute imports. +- **ESM Native**: Built from the ground up for modern Node.js and ECMAScript Modules. ## 🧱 Graceful Shutdown -Every Express Forge project handles `SIGTERM` and `SIGINT` signals correctly. This ensures that: +Every Create Express Forge project handles `SIGTERM` and `SIGINT` signals correctly. This ensures that: 1. No new requests are accepted. 2. Existing requests are finished. 3. Database connections are closed cleanly. 4. The process exits without data corruption. ## 🛠️ Error Handling & Responses -Express Forge enforces a consistent communication pattern between your API and clients. +Create Express Forge enforces a consistent communication pattern between your API and clients. ### Centralized Error Handling A global error middleware is the "safety net" for your application. It catches all errors and transforms them into structured JSON responses, handling `Zod` validation errors and custom `ApiError` instances automatically. @@ -80,3 +91,17 @@ Ensure your frontend team always knows what to expect. Every success response fo ### Async Error Wrapper The provided `asyncHandler` utility eliminates the need for `try-catch` blocks in your controllers, automatically forwarding any promise rejections to the global error handler. + +## 🆚 Comparison: v4 vs v3 + +| Feature | v3.x (Legacy) | v4.x (Latest) | +| :--- | :--- | :--- | +| **OpenAPI Docs** | JSDoc-based (`swagger-jsdoc`) | **Zero-JSDoc** (via Zod schemas) | +| **Linting & Formatting** | ESLint + Prettier | **Biome** (20x faster) | +| **Path Aliases** | Not supported by default | **Native Support** (`@/` aliases) | +| **CLI Flexibility** | Only new directories | **Scaffold in `.`** supported | +| **Scaffolding Speed** | Standard | **Ultra-Fast** (Refactored logic) | +| **Reliability** | Standard startup | **Pro Fail-Fast** (DB/Redis checks) | +| **Imports** | Relative only (`../../`) | **Automated Alias resolution** | +| **Architecture** | Basic Modular/MVC | **Hardened Structures** | +| **Deployment** | Basic Dockerfiles | **Optimized Multi-stage builds** | diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 93fca69..551aa3d 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -1,6 +1,11 @@ +--- +title: Getting Started | Create Express Forge +description: Learn how to scaffold a new Express.js project in seconds using Create Express Forge. +--- + # Getting Started -Express Forge is designed to get you up and running with a production-grade Express backend in seconds. +Create Express Forge is designed to get you up and running with a production-grade Express backend in seconds. ## Quick Start @@ -18,6 +23,11 @@ If you want to skip the prompts and use the recommended defaults, use the `--yes npx create-express-forge@latest my-api --yes ``` +> [!TIP] +> You can scaffold a project in your **current directory** by using a period (`.`): +> `npx create-express-forge@latest .` + + ## Step-by-Step Guide ### 1. Initialize Project diff --git a/docs/guide/openapi.md b/docs/guide/openapi.md index 4ce3631..1714b2c 100644 --- a/docs/guide/openapi.md +++ b/docs/guide/openapi.md @@ -1,48 +1,69 @@ -# API Documentation (OpenAPI) - -Express Forge integrates **Swagger UI** to provide interactive, live documentation for your API. This allows your frontend team or external partners to test endpoints directly from the browser. +--- +title: API Documentation | Create Express Forge +description: Learn how to generate automated, type-safe OpenAPI (Swagger) documentation using Zod schemas with Create Express Forge. +--- +# API Documentation (OpenAPI) + +Create Express Forge uses `@asteasolutions/zod-to-openapi` to provide interactive, type-safe documentation for your API. This ensures that your runtime validation and your API documentation are always in sync. + ## Getting Started - + If you enabled OpenAPI during scaffolding, your documentation is available at: - -- **Swagger UI**: `http://localhost:3000/docs` (Interactive) -- **OpenAPI Spec**: `http://localhost:3000/docs.json` (Raw JSON) - + +- **Interactive UI**: `http://localhost:3000/api-docs` +- **OpenAPI Spec**: `http://localhost:3000/api-docs.json` (Raw JSON) + > [!TIP] -> The `/docs.json` endpoint is always available for external tools (like Postman), even if you chose to disable the Swagger UI during scaffolding. - -## Configuration - -The documentation configuration is located in `src/docs/swagger.ts`. It uses `swagger-jsdoc` to parse JSDoc comments in your route files. - -## Documenting Endpoints - -To add an endpoint to the Swagger UI, simply add a `@openapi` or `@swagger` block above your route definition. - -### Example - +> On server startup, the CLI automatically logs the documentation URL for easy access. + +## Zero-JSDoc Documentation + +Unlike traditional Express apps that rely on clunky JSDoc comments (`@openapi`), Create Express Forge uses your **Zod schemas** to generate the OpenAPI specification. This makes your documentation: +1. **DRY (Don't Repeat Yourself)**: Define your schema once, use it for validation and documentation. +2. **Type-Safe**: Any changes to your Zod schemas are automatically reflected in the Swagger UI. +3. **Clean**: Your controller files stay free of giant comment blocks. + +## How it Works + +### 1. The Registry +A centralized registry is located at `src/docs/registry.ts`. This registry collects all your path and component definitions. + +### 2. Documenting a Path +Paths are registered directly in your `schema.ts` files. This keeps the documentation close to the validation logic. + ```typescript -/** - * @openapi - * /users: - * get: - * summary: Retrieve a list of users - * description: Returns a list of users from the database. - * responses: - * 200: - * description: A list of users. - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/User' - */ -router.get('/', userController.getAllUsers); +import { z } from 'zod'; +import { registry } from '@/docs/registry.js'; + +// Define your schema +export const UserSchema = registry.register('User', z.object({ + id: z.string().uuid(), + name: z.string(), +})); + +// Register the path +registry.registerPath({ + method: 'get', + path: '/users', + summary: 'Get all users', + responses: { + 200: { + description: 'List of users', + content: { + 'application/json': { + schema: z.array(UserSchema), + }, + }, + }, + }, +}); ``` - + +### 3. Setting Up Swagger +The `setupSwagger` function in `src/docs/swagger.ts` generates the final OpenAPI document from the registry and attaches the Swagger UI to your Express app. + ## Benefits - **Live Testing**: Use the "Try it out" button to make real requests to your development server. -- **Auto-Sync**: Your documentation lives next to your code, making it easier to keep it updated. -- **Standardized**: Uses the OpenAPI 3.0 specification, compatible with many other tools (Postman, Insomnia, etc.). +- **Auto-Sync**: Your documentation is built from your code, ensuring it never gets stale. +- **Standardized**: Uses the OpenAPI 3.0 specification, compatible with Postman, Insomnia, and code generators. diff --git a/docs/guide/structure.md b/docs/guide/structure.md index cfa0e14..d5643fb 100644 --- a/docs/guide/structure.md +++ b/docs/guide/structure.md @@ -1,6 +1,6 @@ # Project Structure -Express Forge scaffolds a clean, professional directory structure. Depending on your chosen architecture, the structure will vary slightly. +Create Express Forge scaffolds a clean, professional directory structure. Depending on your chosen architecture, the structure will vary slightly. ## 📦 Modular Architecture @@ -15,14 +15,13 @@ This structure is organized by **features**. Each module is self-contained. │ │ ├── users.service.ts │ │ ├── users.routes.ts │ │ └── users.schema.ts -│ ├── shared/ # Code shared across modules -│ │ ├── middleware/ -│ │ ├── utils/ -│ │ └── constants/ +│ ├── middleware/ # Global middleware +│ ├── utils/ # Shared utilities (ApiResponse, etc.) +│ ├── docs/ # OpenAPI registry and swagger setup │ ├── config/ # App configuration │ └── app.ts # App initialization ├── prisma/ # Database schema (if Prisma chosen) -├── tests/ # Integration and unit tests +├── src/__tests__/ # Integration and unit tests ├── .env # Environment variables └── package.json ``` diff --git a/docs/guide/testing.md b/docs/guide/testing.md index 773ee11..cecedbe 100644 --- a/docs/guide/testing.md +++ b/docs/guide/testing.md @@ -1,6 +1,6 @@ # Testing Strategy -Express Forge encourages a test-driven approach to development. We provide a pre-configured testing environment using either **Vitest** (recommended for speed) or **Jest**. +Create Express Forge encourages a test-driven approach to development. We provide a pre-configured testing environment using either **Vitest** (recommended for speed) or **Jest**. ## 🧪 Types of Tests @@ -52,9 +52,9 @@ describe('GET /health', () => { ## 🛠 Contributing: CLI Smoke Testing -If you are contributing to the `create-express-forge` CLI itself, you should run the automated smoke test to verify your changes. This test scaffolds a project, runs a full TypeScript type-check, and builds the resulting app. +If you are contributing to the `Create Express Forge` CLI itself, you should run the automated smoke test to verify your changes. This test scaffolds a project, runs a full TypeScript type-check, and builds the resulting app. ```bash -cd packages/create-express-forge +cd packages/Create Express Forge pnpm run test:smoke ``` diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md index fab7dce..def622c 100644 --- a/docs/guide/troubleshooting.md +++ b/docs/guide/troubleshooting.md @@ -1,6 +1,6 @@ # Troubleshooting -Common issues and how to resolve them when using Express Forge. +Common issues and how to resolve them when using Create Express Forge. ## 💾 Database Issues @@ -29,7 +29,7 @@ Common issues and how to resolve them when using Express Forge. ### Environment variables are missing **Issue**: Zod validation error on startup. -**Solution**: Express Forge validates your `.env` on startup. Ensure all required variables listed in `src/config/index.ts` are present in your `.env` file. +**Solution**: Create Express Forge validates your `.env` on startup. Ensure all required variables listed in `src/config/index.ts` are present in your `.env` file. ### Modules not found (Path Aliases) **Issue**: TypeScript can't find modules starting with `@`. @@ -39,7 +39,7 @@ Common issues and how to resolve them when using Express Forge. ### Tests hanging **Issue**: Tests don't exit after completion. -**Solution**: Ensure you are closing your database connections and server in an `afterAll` hook. Express Forge handles this by default in the generated boilerplate. +**Solution**: Ensure you are closing your database connections and server in an `afterAll` hook. Create Express Forge handles this by default in the generated boilerplate. --- diff --git a/docs/index.md b/docs/index.md index 5a7c27d..1f17b65 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,22 @@ --- layout: home +title: Create Express Forge | Modern Express.js CLI +description: The ultimate CLI for scaffolding production-ready, enterprise-grade Express.js backends with TypeScript, Auth, ORM, and OpenAPI. hero: - name: Express Forge + name: Create Express Forge text: Build Better Express APIs, Faster. tagline: The definitive CLI for scaffolding production-ready, enterprise-grade Express.js backends. image: src: /logo.svg - alt: Express Forge Logo + alt: Create Express Forge Logo actions: - theme: brand text: Get Started link: /guide/getting-started + - theme: alt + text: 🆚 v4 vs v3 + link: /guide/features#comparison-v4-vs-v3 - theme: alt text: ⭐ Star on GitHub link: https://github.com/CODE-Y02/express-cli @@ -34,7 +39,7 @@ features: details: Pre-configured JWT or Session-based authentication with best-practice security. - icon: 📜 title: OpenAPI Docs - details: Automatically generated Swagger documentation from your Zod schemas and routes. + details: Zero-JSDoc documentation generated directly from your Zod schemas and registry. - icon: 🔴 title: Redis Caching details: Built-in support for Redis and Node-Cache to boost your API performance. @@ -55,10 +60,10 @@ features: ### 🛠 Built With -TypeScript · Express.js · Prisma · Docker · Vitest · Zod · Pino +TypeScript · Express.js · Prisma · Docker · Vitest · Zod · Biome · Pino --- ### ❤️ Support the Project -If Express Forge has saved you time, please consider giving us a star on [GitHub](https://github.com/CODE-Y02/express-cli)! It helps us reach more developers and continue improving the project. +If Create Express Forge has saved you time, please consider giving us a star on [GitHub](https://github.com/CODE-Y02/express-cli)! It helps us reach more developers and continue improving the project. diff --git a/docs/package.json b/docs/package.json index 040abff..558a762 100644 --- a/docs/package.json +++ b/docs/package.json @@ -5,11 +5,14 @@ "type": "module", "scripts": { "dev": "vitepress dev", - "build": "vitepress build", + "build": "tsx scripts/generate-llms-docs.ts && vitepress build", "preview": "vitepress preview", "lint": "echo 'no lint'" }, "devDependencies": { + "@types/fs-extra": "^11.0.4", + "fs-extra": "^11.2.0", + "tsx": "^4.15.0", "vitepress": "^1.3.1", "vue": "^3.4.31" } diff --git a/docs/public/ai.json b/docs/public/ai.json new file mode 100644 index 0000000..d35b7b8 --- /dev/null +++ b/docs/public/ai.json @@ -0,0 +1,48 @@ +{ + "name": "Create Express Forge", + "description": "Production-ready Express.js TypeScript backend generator", + "latestVersion": "4.1.2", + "ltsVersion": "3.3.2", + "repository": "https://github.com/CODE-Y02/express-cli", + "homepage": "https://code-y02.github.io/express-cli", + "versions": { + "v4": { + "status": "latest", + "fullDocs": "https://code-y02.github.io/express-cli/llms-full.txt", + "slugs": [ + "index", + "guide/architecture", + "guide/auth", + "guide/caching", + "guide/deployment", + "guide/features", + "guide/getting-started", + "guide/openapi", + "guide/structure", + "guide/testing", + "guide/troubleshooting", + "reference/cli-options", + "reference/config" + ] + }, + "v3": { + "status": "lts", + "fullDocs": "https://code-y02.github.io/express-cli/v3/llms-full.txt", + "slugs": [ + "index", + "guide/architecture", + "guide/auth", + "guide/caching", + "guide/deployment", + "guide/features", + "guide/getting-started", + "guide/openapi", + "guide/structure", + "guide/testing", + "guide/troubleshooting", + "reference/cli-options", + "reference/config" + ] + } + } +} diff --git a/docs/public/llms-full.txt b/docs/public/llms-full.txt new file mode 100644 index 0000000..6bd9c73 --- /dev/null +++ b/docs/public/llms-full.txt @@ -0,0 +1,714 @@ +# Create Express Forge v4.x (Latest) - Full Documentation + + + +--- FILE: index.md --- + +### 🚀 Key Benefits + +- **Zero Configuration**: Sensible defaults that work out of the box. +- **Enterprise Patterns**: Modular architecture that grows with your team. +- **Developer Experience**: Auto-reloading, linting, and formatting pre-configured. +- **Security First**: Best practices for CORS, rate limiting, and environment management. + +### 🛠 Built With + +TypeScript · Express.js · Prisma · Docker · Vitest · Zod · Biome · Pino + +--- + +### ❤️ Support the Project + +If Create Express Forge has saved you time, please consider giving us a star on [GitHub](https://github.com/CODE-Y02/express-cli)! It helps us reach more developers and continue improving the project. + + +--- FILE: guide/architecture.md --- + +# Architecture Patterns + +Create Express Forge supports two main architecture patterns to fit your project's needs. Choosing the right one is crucial for long-term maintainability. + +## 📦 Modular Architecture (Recommended) + +This is the default and recommended pattern for medium to large applications. It organizes code by **features** (modules) rather than technical roles (controllers, models). + +### Why choose Modular? +- **Scalability**: Each module is self-contained, making it easier to manage as the app grows. +- **Isolation**: Changes to one feature are less likely to break another. +- **Team-Friendly**: Different developers can work on different modules without merge conflicts. + +### Structure: +```text +src/ + modules/ + users/ + users.controller.ts # Request handling + users.service.ts # Business logic + users.routes.ts # Route definitions + users.schema.ts # Validation schemas + products/ + ... +``` + +--- + +## 🏛 MVC Architecture + +A classic Model-View-Controller pattern. Best for smaller projects or those who prefer a traditional separation of concerns based on technical layers. + +### Why choose MVC? +- **Familiarity**: Most developers are familiar with this pattern from frameworks like Rails or Django. +- **Simplicity**: For very small apps, it might be quicker to navigate. + +### Structure: +```text +src/ + controllers/ # All controllers in one place + models/ # Database models + routes/ # All route definitions + services/ # Business logic +``` + +## 🏷️ Path Aliases (@/) + +Regardless of the architecture you choose, Create Express Forge pre-configures **Path Aliases**. This means you can use absolute imports from the `src` directory instead of messy relative paths. + +**Instead of this:** +```typescript +import { User } from '../../../models/user.js'; +``` + +**You do this:** +```typescript +import { User } from '@/models/user.js'; +``` + +This ensures that moving files around won't break your imports. + +## Which one should I use? + +If you are building a production API that you expect to grow over time, **Modular Architecture** is almost always the better choice. It prevents the "Fat Controller" and "Fat Model" syndromes by keeping related logic close together. + + +--- FILE: guide/auth.md --- + +# Authentication + +Create Express Forge provides two battle-tested authentication strategies: **JWT** (JSON Web Tokens) and **Sessions**. + +## Strategies + +### 🔐 JWT (Stateless) +The modern standard for web APIs. Highly scalable and perfect for mobile apps and SPAs. +- **Middleware**: `src/middleware/auth.ts` +- **Storage Options**: + - **🍪 Cookie**: More secure against XSS. Uses `httpOnly` and `secure` flags. + - **📨 Header**: Standard `Authorization: Bearer `. Recommended for mobile apps. +- **Config**: Set `JWT_SECRET` and `JWT_EXPIRES_IN` in your `.env`. + +### 🍪 Session (Stateful) +Traditional cookie-based authentication. Excellent for server-side rendered apps or when you need built-in session management. +- **Middleware**: `src/middleware/auth.ts` +- **Config**: Set `SESSION_SECRET` in your `.env`. + +## Using the Auth Middleware + +To protect a route, simply add the `auth` middleware to your route definition. + +```typescript +import { auth } from '../middleware/auth.js'; +import { todosController } from '../modules/todos/todos.controller.js'; + +// Protected route +router.get('/', auth, todosController.getTodos); +``` + +## Accessing the User + +Once a user is authenticated, their information is attached to the `req.user` object (for JWT) or `req.session.user` (for Sessions). + +```typescript +export const getProfile = (req: Request, res: Response) => { + const user = req.user; // For JWT + return ApiResponse.success(res, user); +}; +``` + +## Security Best Practices +1. **Secret Management**: Never commit your `JWT_SECRET` or `SESSION_SECRET` to version control. Use `.env` files. +2. **HTTPS**: Always serve your API over HTTPS in production to protect tokens and session cookies. +3. **HTTP-Only Cookies**: If using sessions, Create Express Forge pre-configures cookies to be `httpOnly` to prevent XSS attacks. + + +--- FILE: guide/caching.md --- + +# Caching + +Create Express Forge provides a flexible caching layer to boost your API performance and reduce database load. You can choose between **Redis** (distributed) or **Node-Cache** (in-memory) during the scaffolding process. + +## Supported Drivers + +### 🔴 Redis +Recommended for production environments and distributed systems where multiple server instances need to share a cache. +- **Requirement**: A running Redis instance. +- **Config**: Set `REDIS_URL` in your `.env` file. + +### 💾 Node-Cache +An in-memory caching solution that requires zero external dependencies. Perfect for simple applications or single-server setups. +- **Requirement**: None. +- **Config**: Automatic. + +## Usage + +The caching logic is encapsulated in `src/cache/index.ts`. It provides a unified interface regardless of the driver you chose. + +### Getting a Value +```typescript +import { cache } from '../cache/index.js'; + +const user = await cache.get('user:123'); +if (user) { + return JSON.parse(user); +} +``` + +### Setting a Value +You can optionally set a Time-To-Live (TTL) in seconds. +```typescript +import { cache } from '../cache/index.js'; + +// Cache for 1 hour (3600 seconds) +await cache.set('user:123', JSON.stringify(userData), 3600); +``` + +### Deleting a Value +```typescript +import { cache } from '../cache/index.js'; + +await cache.del('user:123'); +``` + +## Best Practices +1. **Cache Invalidation**: Always delete or update the cache when the underlying data in the database changes. +2. **Serialization**: Since Redis only stores strings, ensure you `JSON.stringify()` your objects before setting and `JSON.parse()` when getting. +3. **Pro Fail-Fast**: In production, Create Express Forge strictly validates Redis connectivity on startup. If Redis is down, the app fails early to prevent inconsistent states. In development, it provides a clear warning and continues. + + +--- FILE: guide/deployment.md --- + +# Deployment + +Create Express Forge provides production-ready configurations to help you ship your API with confidence. + +## 🐳 Docker Deployment (Recommended) + +The easiest way to deploy is using the provided multi-stage `Dockerfile`. + +### Build Image +```bash +docker build -t my-express-api . +``` + +### Run Container +```bash +docker run -p 3000:3000 --env-file .env my-express-api +``` + +### Why Multi-stage? +Our Dockerfile uses multi-stage builds to: +1. **Reduce Image Size**: The final image only contains the compiled JavaScript and production dependencies. +2. **Security**: Source code and build tools are not included in the final production image. + +## ☁️ Cloud Platforms + +### Railway / Render / Fly.io +Most modern PaaS platforms will automatically detect the `Dockerfile` or the `start` script in `package.json`. + +1. Connect your GitHub repository. +2. Configure your environment variables (copy from `.env`). +3. Set the build command to `npm run build` (if not using Docker). + > **Note for Prisma users**: The generated `package.json` includes a `postinstall: "prisma generate"` script, which ensures your Prisma client is generated automatically before the build step on most PaaS platforms. +4. Set the start command to `npm start`. + +## 🛡️ Production Checklist + +Before going live, ensure: +- [ ] **Environment Variables**: `NODE_ENV` is set to `production`. +- [ ] **Database**: Migrations have been run on the production database. +- [ ] **Logging**: Log level is set appropriately (e.g., `info` or `error`). +- [ ] **Security**: CORS is restricted to your frontend domain. +- [ ] **Rate Limiting**: Configured for your production traffic. + + +--- FILE: guide/features.md --- + +# Core Features + +Create Express Forge comes packed with everything you need to build robust APIs. + +## 🛡️ TypeScript First +Type safety is at the core of Create Express Forge. Every scaffolded project includes: +- Strict TypeScript configuration. +- Path aliases (e.g., `@/config/env`). +- Type-safe environment variables via **Zod**. +- Automated scaffolding with your choice of **npm**, **pnpm**, **yarn**, or **bun**. + +## 🔐 Flexible Authentication +Scaffold a complete authentication system with a single choice: +- **JWT Authentication**: Choose between **HttpOnly Cookies** (recommended for web) or **Bearer Headers** (recommended for mobile/API clients). +- **Session Auth**: Battle-tested session management for stateful applications. +- **Protected Routes**: Every boilerplate includes a protected resource showing you exactly how to use the auth middleware. + +## 💾 Database Integration +Choose your favorite ORM and get started instantly: +- **Prisma**: Modern ORM with auto-generated client and type-safe queries. +- **Sequelize**: The most popular traditional ORM for Node.js. +- **Migrations**: Pre-configured scripts to handle database schema changes. + +## 🧪 Testing Suite +Don't ship broken code. Create Express Forge sets up a complete testing environment: +- **Vitest/Jest**: Choose your favorite test runner. +- **Supertest**: For high-level API integration tests. +- **Example Tests**: Every scaffolded project includes example unit and integration tests. + +## 🐳 Docker Support +Ship to production with confidence: +- **Multi-stage Build**: Optimized Dockerfiles for smaller production images. +- **Docker Compose**: Includes a `docker-compose.yml` with a database setup for local development. + +## 🔐 Security Best Practices +Stay secure by default with pre-configured industry standards: +- **Helmet**: Automatically sets security-related HTTP headers to protect against common vulnerabilities. +- **CORS**: Flexible Cross-Origin Resource Sharing configuration. +- **Rate Limiting**: Integrated `express-rate-limit` to prevent brute-force attacks and DDoS. +- **Dotenv & Zod**: Every environment variable is validated on startup. If a variable is missing or malformed, the app fails fast with a clear error message. + +## 📝 Logging & Monitoring +- **Pino/Winston**: High-performance, structured logging. Pino is used by default for its extreme speed and JSON output, which is perfect for log aggregators like ELK or Datadog. +- **Pro Fail-Fast**: In production, the app strictly validates database and Redis connections on startup. If a dependency is down, the app fails early to prevent inconsistent states. In development, it provides clear warnings. +- **Health Checks**: A standard `/api/v1/health` endpoint is included, providing uptime, memory usage, and database connectivity status. + +## 📜 OpenAPI Documentation +Never let your documentation get out of sync: +- **Swagger UI / Scalar**: Beautiful, integrated UI to explore and test your API endpoints directly from the browser. +- **Zero-JSDoc Spec**: Documentation is generated directly from your **Zod schemas** and a centralized registry. No more clunky JSDoc comments in your controllers! +- **Type-Safe Documentation**: Your runtime validation and your API documentation are always 100% in sync. + +## ⚡ Modern Tooling +- **Biome**: Replaces ESLint and Prettier for 20x faster linting and formatting. +- **Import Aliases**: Pre-configured `@/` paths for clean, absolute imports. +- **ESM Native**: Built from the ground up for modern Node.js and ECMAScript Modules. + +## 🧱 Graceful Shutdown +Every Create Express Forge project handles `SIGTERM` and `SIGINT` signals correctly. This ensures that: +1. No new requests are accepted. +2. Existing requests are finished. +3. Database connections are closed cleanly. +4. The process exits without data corruption. + +## 🛠️ Error Handling & Responses +Create Express Forge enforces a consistent communication pattern between your API and clients. + +### Centralized Error Handling +A global error middleware is the "safety net" for your application. It catches all errors and transforms them into structured JSON responses, handling `Zod` validation errors and custom `ApiError` instances automatically. + +### Custom `ApiError` Class +Stop throwing generic strings. Use the built-in `ApiError` class to provide context, status codes, and operational flags: +- `ApiError.notFound('User not found')` +- `ApiError.unauthorized()` +- `ApiError.badRequest('Invalid input', validationErrors)` + +### Standardized `ApiResponse` +Ensure your frontend team always knows what to expect. Every success response follows a predictable schema: +```json +{ + "success": true, + "message": "Operation successful", + "data": { ... } +} +``` + +### Async Error Wrapper +The provided `asyncHandler` utility eliminates the need for `try-catch` blocks in your controllers, automatically forwarding any promise rejections to the global error handler. + +## 🆚 Comparison: v4 vs v3 + +| Feature | v3.x (Legacy) | v4.x (Latest) | +| :--- | :--- | :--- | +| **OpenAPI Docs** | JSDoc-based (`swagger-jsdoc`) | **Zero-JSDoc** (via Zod schemas) | +| **Linting & Formatting** | ESLint + Prettier | **Biome** (20x faster) | +| **Path Aliases** | Not supported by default | **Native Support** (`@/` aliases) | +| **CLI Flexibility** | Only new directories | **Scaffold in `.`** supported | +| **Scaffolding Speed** | Standard | **Ultra-Fast** (Refactored logic) | +| **Reliability** | Standard startup | **Pro Fail-Fast** (DB/Redis checks) | +| **Imports** | Relative only (`../../`) | **Automated Alias resolution** | +| **Architecture** | Basic Modular/MVC | **Hardened Structures** | +| **Deployment** | Basic Dockerfiles | **Optimized Multi-stage builds** | + + +--- FILE: guide/getting-started.md --- + +# Getting Started + +Create Express Forge is designed to get you up and running with a production-grade Express backend in seconds. + +## Quick Start + +The fastest way to create a new project is using `npx`. You don't even need to install the CLI globally! + +```bash +npx create-express-forge@latest [project-name] +``` + +### Instant Scaffolding + +If you want to skip the prompts and use the recommended defaults, use the `--yes` flag: + +```bash +npx create-express-forge@latest my-api --yes +``` + +> [!TIP] +> You can scaffold a project in your **current directory** by using a period (`.`): +> `npx create-express-forge@latest .` + + +## Step-by-Step Guide + +### 1. Initialize Project +Run the command and follow the interactive prompts. You'll be asked to: +- Give your project a **name**. +- Select your preferred **package manager** (npm, pnpm, yarn). +- Choose an **architecture** (Modular or MVC). +- Select an **ORM** (Prisma, Sequelize, or none). +- Choose a **Testing Framework** (Vitest or Jest). + +### 2. Enter Directory +```bash +cd my-awesome-api +``` + +### 3. Start Development +```bash +npm run dev +``` + +Your API will now be running at `http://localhost:3000` with hot-reloading enabled. + +## Next Steps + +- Explore the [Architecture Patterns](./architecture) to understand how your code is organized. +- Check out the [Core Features](./features) to see what's included out of the box. +- Configure your environment variables in the `.env` file. + + +--- FILE: guide/openapi.md --- + +# API Documentation (OpenAPI) + +Create Express Forge uses `@asteasolutions/zod-to-openapi` to provide interactive, type-safe documentation for your API. This ensures that your runtime validation and your API documentation are always in sync. + +## Getting Started + +If you enabled OpenAPI during scaffolding, your documentation is available at: + +- **Interactive UI**: `http://localhost:3000/api-docs` +- **OpenAPI Spec**: `http://localhost:3000/api-docs.json` (Raw JSON) + +> [!TIP] +> On server startup, the CLI automatically logs the documentation URL for easy access. + +## Zero-JSDoc Documentation + +Unlike traditional Express apps that rely on clunky JSDoc comments (`@openapi`), Create Express Forge uses your **Zod schemas** to generate the OpenAPI specification. This makes your documentation: +1. **DRY (Don't Repeat Yourself)**: Define your schema once, use it for validation and documentation. +2. **Type-Safe**: Any changes to your Zod schemas are automatically reflected in the Swagger UI. +3. **Clean**: Your controller files stay free of giant comment blocks. + +## How it Works + +### 1. The Registry +A centralized registry is located at `src/docs/registry.ts`. This registry collects all your path and component definitions. + +### 2. Documenting a Path +Paths are registered directly in your `schema.ts` files. This keeps the documentation close to the validation logic. + +```typescript +import { z } from 'zod'; +import { registry } from '@/docs/registry.js'; + +// Define your schema +export const UserSchema = registry.register('User', z.object({ + id: z.string().uuid(), + name: z.string(), +})); + +// Register the path +registry.registerPath({ + method: 'get', + path: '/users', + summary: 'Get all users', + responses: { + 200: { + description: 'List of users', + content: { + 'application/json': { + schema: z.array(UserSchema), + }, + }, + }, + }, +}); +``` + +### 3. Setting Up Swagger +The `setupSwagger` function in `src/docs/swagger.ts` generates the final OpenAPI document from the registry and attaches the Swagger UI to your Express app. + +## Benefits +- **Live Testing**: Use the "Try it out" button to make real requests to your development server. +- **Auto-Sync**: Your documentation is built from your code, ensuring it never gets stale. +- **Standardized**: Uses the OpenAPI 3.0 specification, compatible with Postman, Insomnia, and code generators. + + +--- FILE: guide/structure.md --- + +# Project Structure + +Create Express Forge scaffolds a clean, professional directory structure. Depending on your chosen architecture, the structure will vary slightly. + +## 📦 Modular Architecture + +This structure is organized by **features**. Each module is self-contained. + +```text +. +├── src/ +│ ├── modules/ # Feature modules +│ │ └── users/ # Example module +│ │ ├── users.controller.ts +│ │ ├── users.service.ts +│ │ ├── users.routes.ts +│ │ └── users.schema.ts +│ ├── middleware/ # Global middleware +│ ├── utils/ # Shared utilities (ApiResponse, etc.) +│ ├── docs/ # OpenAPI registry and swagger setup +│ ├── config/ # App configuration +│ └── app.ts # App initialization +├── prisma/ # Database schema (if Prisma chosen) +├── src/__tests__/ # Integration and unit tests +├── .env # Environment variables +└── package.json +``` + +## 🏛 MVC Architecture + +A traditional technical-layer separation. + +```text +. +├── src/ +│ ├── controllers/ # Route handlers +│ ├── models/ # Data models +│ ├── services/ # Business logic +│ ├── routes/ # Route definitions +│ ├── middleware/ # Global middleware +│ ├── config/ +│ └── app.ts +├── ... +``` + +## Key Files + +- **`src/app.ts`**: The entry point where Express is initialized, middleware is registered, and routes are attached. +- **`src/config/`**: Contains configuration for the database, logger, and environment variables. +- **`prisma/schema.prisma`**: (Optional) Defines your database models and relationships. +- **`docker-compose.yml`**: Defines the local development environment (e.g., PostgreSQL/MySQL containers). + + +--- FILE: guide/testing.md --- + +# Testing Strategy + +Create Express Forge encourages a test-driven approach to development. We provide a pre-configured testing environment using either **Vitest** (recommended for speed) or **Jest**. + +## 🧪 Types of Tests + +### Unit Tests +Focused on testing individual functions or services in isolation. +- **Location**: `src/modules/**/__tests__/*.unit.spec.ts` +- **Focus**: Business logic, utility functions, validation. + +### Integration Tests +Testing the full API flow, including database interactions. +- **Location**: `tests/*.int.spec.ts` +- **Focus**: HTTP status codes, API responses, database persistence. + +## 🚀 Running Tests + +```bash +# Run all tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Generate coverage report +npm run test:coverage +``` + +## 🛠 Testing Tools + +- **Supertest**: Used for making HTTP requests to your app without starting a real server. +- **Prisma Mocking**: (If using Prisma) We provide patterns for mocking the Prisma client for unit tests. +- **Test Database**: Integration tests automatically use a separate test database defined in `.env.test`. + +## Example Integration Test + +```typescript +import request from 'supertest'; +import { app } from '../src/app'; + +describe('GET /health', () => { + it('should return 200 OK', async () => { + const response = await request(app).get('/health'); + expect(response.status).toBe(200); + expect(response.body.status).toBe('ok'); + }); +}); +``` + +--- + +## 🛠 Contributing: CLI Smoke Testing + +If you are contributing to the `Create Express Forge` CLI itself, you should run the automated smoke test to verify your changes. This test scaffolds a project, runs a full TypeScript type-check, and builds the resulting app. + +```bash +cd packages/Create Express Forge +pnpm run test:smoke +``` + + +--- FILE: guide/troubleshooting.md --- + +# Troubleshooting + +Common issues and how to resolve them when using Create Express Forge. + +## 💾 Database Issues + +### Prisma migration failed +**Issue**: `prisma migrate dev` fails with a connection error. +**Solution**: +1. Ensure your database container is running: `docker compose up -d`. +2. Check your `DATABASE_URL` in the `.env` file. If running locally (not in Docker), use `localhost` instead of the service name. +3. Ensure the database user has sufficient permissions. + +### Sequelize connection error +**Issue**: `Unable to connect to the database`. +**Solution**: Check the `dialect` and `port` in your `.env` file. Ensure the database service is reachable from your host machine. + +## 🐳 Docker Issues + +### Permission Denied +**Issue**: Error when running Docker commands. +**Solution**: Run the commands with `sudo` or add your user to the `docker` group. + +### Port already in use +**Issue**: `Bind for 0.0.0.0:3000 failed: port is already allocated`. +**Solution**: Another process is using port 3000. You can change the port in your `.env` file or kill the existing process. + +## 🚀 Runtime Issues + +### Environment variables are missing +**Issue**: Zod validation error on startup. +**Solution**: Create Express Forge validates your `.env` on startup. Ensure all required variables listed in `src/config/index.ts` are present in your `.env` file. + +### Modules not found (Path Aliases) +**Issue**: TypeScript can't find modules starting with `@`. +**Solution**: This is usually handled by `tsconfig-paths`. Ensure you are starting the app with `npm run dev`. If you've added new modules, you might need to restart the dev server. + +## 🧪 Testing Issues + +### Tests hanging +**Issue**: Tests don't exit after completion. +**Solution**: Ensure you are closing your database connections and server in an `afterAll` hook. Create Express Forge handles this by default in the generated boilerplate. + +--- + +Still having trouble? [Open an issue on GitHub](https://github.com/CODE-Y02/express-cli/issues) + + +--- FILE: reference/cli-options.md --- + +# CLI Reference + +The `Create Express Forge` command can be used with various flags to bypass the interactive prompts and speed up your workflow. + +## Usage + +```bash +npx create-express-forge [project-name] [options] +``` + +## Options + +| Flag | Description | Values | +|------|-------------|--------| +| `--help` | Show help information | - | +| `--version` | Show current version | - | +| `--pattern` | Architecture pattern | `modular`, `mvc` | +| `--orm` | ORM to use | `prisma`, `sequelize`, `none` | +| `--db` | Database type | `postgres`, `mysql`, `sqlite` | +| `--logger` | Logging library | `winston`, `pino` | +| `--test` | Testing framework | `vitest`, `jest` | +| `--docker` | Include Docker setup | `true`, `false` | +| `--install` | Auto-install dependencies | `true`, `false` | + +## Example + +Scaffold a modular project with Prisma and Vitest: + +```bash +npx create-express-forge my-api --pattern modular --orm prisma --test vitest --install true +``` + + +--- FILE: reference/config.md --- + +# Configuration Reference + +Every project created with Create Express Forge uses a centralized configuration system powered by environment variables. + +## 🌍 Environment Variables + +Create a `.env` file in the root of your project. + +| Variable | Description | Default | Required | +|----------|-------------|---------|----------| +| `PORT` | The port the server listens on | `3000` | No | +| `NODE_ENV` | `development`, `production`, `test` | `development` | No | +| `DATABASE_URL` | Connection string for your DB | - | Yes* | +| `LOG_LEVEL` | `fatal`, `error`, `warn`, `info`, `debug`, `trace` | `debug` | No | +| `CORS_ORIGIN` | Allowed origins (comma separated) | `*` | No | +| `RATE_LIMIT_MAX` | Max requests per window | `100` | No | +| `RATE_LIMIT_WINDOW` | Window size in minutes | `15` | No | + +*\*Required if an ORM is selected.* + +## ⚙️ App Configuration + +Configuration is managed in `src/config/index.ts`. This file: +1. Validates environment variables using **Zod**. +2. Exports a typed configuration object. +3. Provides default values for optional variables. + +## 💾 Database Config + +If you chose Prisma, your configuration is primarily in `prisma/schema.prisma`. + +For Sequelize, configuration is found in `src/config/database.ts`, which handles the connection pooling and dialect-specific settings. + +## 📝 Logger Config + +Logging configuration is found in `src/config/logger.ts`. You can toggle between `pretty-print` (for development) and `JSON` (for production) logging here. diff --git a/docs/public/llms.txt b/docs/public/llms.txt new file mode 100644 index 0000000..caee4c2 --- /dev/null +++ b/docs/public/llms.txt @@ -0,0 +1,16 @@ +# Create Express Forge +The ultimate CLI for scaffolding production-ready Express.js TypeScript backends. + +## Versions +- v4.x (Latest): Modern, Biome, Zod-OpenAPI, Path Aliases. +- v3.x (LTS): Stable, ESLint/Prettier, swagger-jsdoc. + +## Quick Start (v4) +npx create-express-forge@latest my-api + +## Quick Start (v3) +npx create-express-forge@3.3.2 my-api + +## Documentation +- v4 Full Docs: https://code-y02.github.io/express-cli/llms-full.txt +- v3 Full Docs: https://code-y02.github.io/express-cli/v3/llms-full.txt \ No newline at end of file diff --git a/docs/public/og-image.png b/docs/public/og-image.png new file mode 100644 index 0000000..c374851 Binary files /dev/null and b/docs/public/og-image.png differ diff --git a/docs/public/v3/llms-full.txt b/docs/public/v3/llms-full.txt new file mode 100644 index 0000000..4058f11 --- /dev/null +++ b/docs/public/v3/llms-full.txt @@ -0,0 +1,662 @@ +# Create Express Forge v3.x (LTS) - Full Documentation + + + +--- FILE: index.md --- + +> [!WARNING] +> You are viewing the documentation for **Create Express Forge v3.x (LTS)**. +> For the latest version (v4.x), please switch using the version dropdown above. + +### 🚀 Key Benefits + +- **Zero Configuration**: Sensible defaults that work out of the box. +- **Enterprise Patterns**: Modular architecture that grows with your team. +- **Developer Experience**: Auto-reloading, linting, and formatting pre-configured. +- **Security First**: Best practices for CORS, rate limiting, and environment management. + +### 🛠 Built With + +TypeScript · Express.js · Prisma · Docker · Vitest · Zod · Pino + +--- + +### ❤️ Support the Project + +If Create Express Forge has saved you time, please consider giving us a star on [GitHub](https://github.com/CODE-Y02/express-cli)! It helps us reach more developers and continue improving the project. + + +--- FILE: guide/architecture.md --- + +# Architecture Patterns + +Create Express Forge supports two main architecture patterns to fit your project's needs. Choosing the right one is crucial for long-term maintainability. + +## 📦 Modular Architecture (Recommended) + +This is the default and recommended pattern for medium to large applications. It organizes code by **features** (modules) rather than technical roles (controllers, models). + +### Why choose Modular? +- **Scalability**: Each module is self-contained, making it easier to manage as the app grows. +- **Isolation**: Changes to one feature are less likely to break another. +- **Team-Friendly**: Different developers can work on different modules without merge conflicts. + +### Structure: +```text +src/ + modules/ + users/ + users.controller.ts # Request handling + users.service.ts # Business logic + users.routes.ts # Route definitions + users.schema.ts # Validation schemas + products/ + ... +``` + +--- + +## 🏛 MVC Architecture + +A classic Model-View-Controller pattern. Best for smaller projects or those who prefer a traditional separation of concerns based on technical layers. + +### Why choose MVC? +- **Familiarity**: Most developers are familiar with this pattern from frameworks like Rails or Django. +- **Simplicity**: For very small apps, it might be quicker to navigate. + +### Structure: +```text +src/ + controllers/ # All controllers in one place + models/ # Database models + routes/ # All route definitions + services/ # Business logic +``` + +## Which one should I use? + +If you are building a production API that you expect to grow over time, **Modular Architecture** is almost always the better choice. It prevents the "Fat Controller" and "Fat Model" syndromes by keeping related logic close together. + + +--- FILE: guide/auth.md --- + +# Authentication + +Create Express Forge provides two battle-tested authentication strategies: **JWT** (JSON Web Tokens) and **Sessions**. + +## Strategies + +### 🔐 JWT (Stateless) +The modern standard for web APIs. Highly scalable and perfect for mobile apps and SPAs. +- **Middleware**: `src/middleware/auth.ts` +- **Storage Options**: + - **🍪 Cookie**: More secure against XSS. Uses `httpOnly` and `secure` flags. + - **📨 Header**: Standard `Authorization: Bearer `. Recommended for mobile apps. +- **Config**: Set `JWT_SECRET` and `JWT_EXPIRES_IN` in your `.env`. + +### 🍪 Session (Stateful) +Traditional cookie-based authentication. Excellent for server-side rendered apps or when you need built-in session management. +- **Middleware**: `src/middleware/auth.ts` +- **Config**: Set `SESSION_SECRET` in your `.env`. + +## Using the Auth Middleware + +To protect a route, simply add the `auth` middleware to your route definition. + +```typescript +import { auth } from '../middleware/auth.js'; +import { todosController } from '../modules/todos/todos.controller.js'; + +// Protected route +router.get('/', auth, todosController.getTodos); +``` + +## Accessing the User + +Once a user is authenticated, their information is attached to the `req.user` object (for JWT) or `req.session.user` (for Sessions). + +```typescript +export const getProfile = (req: Request, res: Response) => { + const user = req.user; // For JWT + return ApiResponse.success(res, user); +}; +``` + +## Security Best Practices +1. **Secret Management**: Never commit your `JWT_SECRET` or `SESSION_SECRET` to version control. Use `.env` files. +2. **HTTPS**: Always serve your API over HTTPS in production to protect tokens and session cookies. +3. **HTTP-Only Cookies**: If using sessions, Create Express Forge pre-configures cookies to be `httpOnly` to prevent XSS attacks. + + +--- FILE: guide/caching.md --- + +# Caching + +Create Express Forge provides a flexible caching layer to boost your API performance and reduce database load. You can choose between **Redis** (distributed) or **Node-Cache** (in-memory) during the scaffolding process. + +## Supported Drivers + +### 🔴 Redis +Recommended for production environments and distributed systems where multiple server instances need to share a cache. +- **Requirement**: A running Redis instance. +- **Config**: Set `REDIS_URL` in your `.env` file. + +### 💾 Node-Cache +An in-memory caching solution that requires zero external dependencies. Perfect for simple applications or single-server setups. +- **Requirement**: None. +- **Config**: Automatic. + +## Usage + +The caching logic is encapsulated in `src/cache/index.ts`. It provides a unified interface regardless of the driver you chose. + +### Getting a Value +```typescript +import { cache } from '../cache/index.js'; + +const user = await cache.get('user:123'); +if (user) { + return JSON.parse(user); +} +``` + +### Setting a Value +You can optionally set a Time-To-Live (TTL) in seconds. +```typescript +import { cache } from '../cache/index.js'; + +// Cache for 1 hour (3600 seconds) +await cache.set('user:123', JSON.stringify(userData), 3600); +``` + +### Deleting a Value +```typescript +import { cache } from '../cache/index.js'; + +await cache.del('user:123'); +``` + +## Best Practices +1. **Cache Invalidation**: Always delete or update the cache when the underlying data in the database changes. +2. **Serialization**: Since Redis only stores strings, ensure you `JSON.stringify()` your objects before setting and `JSON.parse()` when getting. +3. **Fail-Safe**: Create Express Forge handles Redis connection errors gracefully via the logger, preventing your entire app from crashing if the cache is down. + + +--- FILE: guide/deployment.md --- + +# Deployment + +Create Express Forge provides production-ready configurations to help you ship your API with confidence. + +## 🐳 Docker Deployment (Recommended) + +The easiest way to deploy is using the provided multi-stage `Dockerfile`. + +### Build Image +```bash +docker build -t my-express-api . +``` + +### Run Container +```bash +docker run -p 3000:3000 --env-file .env my-express-api +``` + +### Why Multi-stage? +Our Dockerfile uses multi-stage builds to: +1. **Reduce Image Size**: The final image only contains the compiled JavaScript and production dependencies. +2. **Security**: Source code and build tools are not included in the final production image. + +## ☁️ Cloud Platforms + +### Railway / Render / Fly.io +Most modern PaaS platforms will automatically detect the `Dockerfile` or the `start` script in `package.json`. + +1. Connect your GitHub repository. +2. Configure your environment variables (copy from `.env`). +3. Set the build command to `npm run build` (if not using Docker). + > **Note for Prisma users**: The generated `package.json` includes a `postinstall: "prisma generate"` script, which ensures your Prisma client is generated automatically before the build step on most PaaS platforms. +4. Set the start command to `npm start`. + +## 🛡️ Production Checklist + +Before going live, ensure: +- [ ] **Environment Variables**: `NODE_ENV` is set to `production`. +- [ ] **Database**: Migrations have been run on the production database. +- [ ] **Logging**: Log level is set appropriately (e.g., `info` or `error`). +- [ ] **Security**: CORS is restricted to your frontend domain. +- [ ] **Rate Limiting**: Configured for your production traffic. + + +--- FILE: guide/features.md --- + +# Core Features + +Create Express Forge comes packed with everything you need to build robust APIs. + +## 🛡️ TypeScript First +Type safety is at the core of Create Express Forge. Every scaffolded project includes: +- Strict TypeScript configuration. +- Path aliases (e.g., `@/config/env`). +- Type-safe environment variables via **Zod**. +- Automated scaffolding with your choice of **npm**, **pnpm**, **yarn**, or **bun**. + +## 🔐 Flexible Authentication +Scaffold a complete authentication system with a single choice: +- **JWT Authentication**: Choose between **HttpOnly Cookies** (recommended for web) or **Bearer Headers** (recommended for mobile/API clients). +- **Session Auth**: Battle-tested session management for stateful applications. +- **Protected Routes**: Every boilerplate includes a protected resource showing you exactly how to use the auth middleware. + +## 💾 Database Integration +Choose your favorite ORM and get started instantly: +- **Prisma**: Modern ORM with auto-generated client and type-safe queries. +- **Sequelize**: The most popular traditional ORM for Node.js. +- **Migrations**: Pre-configured scripts to handle database schema changes. + +## 🧪 Testing Suite +Don't ship broken code. Create Express Forge sets up a complete testing environment: +- **Vitest/Jest**: Choose your favorite test runner. +- **Supertest**: For high-level API integration tests. +- **Example Tests**: Every scaffolded project includes example unit and integration tests. + +## 🐳 Docker Support +Ship to production with confidence: +- **Multi-stage Build**: Optimized Dockerfiles for smaller production images. +- **Docker Compose**: Includes a `docker-compose.yml` with a database setup for local development. + +## 🔐 Security Best Practices +Stay secure by default with pre-configured industry standards: +- **Helmet**: Automatically sets security-related HTTP headers to protect against common vulnerabilities. +- **CORS**: Flexible Cross-Origin Resource Sharing configuration. +- **Rate Limiting**: Integrated `express-rate-limit` to prevent brute-force attacks and DDoS. +- **Dotenv & Zod**: Every environment variable is validated on startup. If a variable is missing or malformed, the app fails fast with a clear error message. + +## 📝 Logging & Monitoring +- **Pino/Winston**: High-performance, structured logging. Pino is used by default for its extreme speed and JSON output, which is perfect for log aggregators like ELK or Datadog. +- **Health Checks**: A standard `/health` endpoint is included, providing uptime, memory usage, and database connectivity status. + +## 📜 OpenAPI Documentation +Never let your documentation get out of sync: +- **Swagger UI**: Integrated UI to explore and test your API endpoints directly from the browser. +- **Auto-generated Spec**: The CLI generates a `docs.json` endpoint that is always up-to-date with your code's JSDoc annotations. +- **Security Schemas**: Pre-configured security definitions for your chosen auth strategy (Cookie or Bearer). + +## 🧱 Graceful Shutdown +Every Create Express Forge project handles `SIGTERM` and `SIGINT` signals correctly. This ensures that: +1. No new requests are accepted. +2. Existing requests are finished. +3. Database connections are closed cleanly. +4. The process exits without data corruption. + +## 🛠️ Error Handling & Responses +Create Express Forge enforces a consistent communication pattern between your API and clients. + +### Centralized Error Handling +A global error middleware is the "safety net" for your application. It catches all errors and transforms them into structured JSON responses, handling `Zod` validation errors and custom `ApiError` instances automatically. + +### Custom `ApiError` Class +Stop throwing generic strings. Use the built-in `ApiError` class to provide context, status codes, and operational flags: +- `ApiError.notFound('User not found')` +- `ApiError.unauthorized()` +- `ApiError.badRequest('Invalid input', validationErrors)` + +### Standardized `ApiResponse` +Ensure your frontend team always knows what to expect. Every success response follows a predictable schema: +```json +{ + "success": true, + "message": "Operation successful", + "data": { ... } +} +``` + +### Async Error Wrapper +The provided `asyncHandler` utility eliminates the need for `try-catch` blocks in your controllers, automatically forwarding any promise rejections to the global error handler. + + +--- FILE: guide/getting-started.md --- + +# Getting Started + +Create Express Forge is designed to get you up and running with a production-grade Express backend in seconds. + +## Quick Start + +The fastest way to create a new project is using `npx`. You don't even need to install the CLI globally! + +```bash +npx create-express-forge@3.3.2 [project-name] +``` + +### Instant Scaffolding + +If you want to skip the prompts and use the recommended defaults, use the `--yes` flag: + +```bash +npx create-express-forge@3.3.2 my-api --yes +``` + +## Step-by-Step Guide + +### 1. Initialize Project +Run the command and follow the interactive prompts. You'll be asked to: +- Give your project a **name**. +- Select your preferred **package manager** (npm, pnpm, yarn). +- Choose an **architecture** (Modular or MVC). +- Select an **ORM** (Prisma, Sequelize, or none). +- Choose a **Testing Framework** (Vitest or Jest). + +### 2. Enter Directory +```bash +cd my-awesome-api +``` + +### 3. Start Development +```bash +npm run dev +``` + +Your API will now be running at `http://localhost:3000` with hot-reloading enabled. + +## Next Steps + +- Explore the [Architecture Patterns](./architecture) to understand how your code is organized. +- Check out the [Core Features](./features) to see what's included out of the box. +- Configure your environment variables in the `.env` file. + + +--- FILE: guide/openapi.md --- + +# API Documentation (OpenAPI) + +Create Express Forge integrates **Swagger UI** to provide interactive, live documentation for your API. This allows your frontend team or external partners to test endpoints directly from the browser. + +## Getting Started + +If you enabled OpenAPI during scaffolding, your documentation is available at: + +- **Swagger UI**: `http://localhost:3000/docs` (Interactive) +- **OpenAPI Spec**: `http://localhost:3000/docs.json` (Raw JSON) + +> [!TIP] +> The `/docs.json` endpoint is always available for external tools (like Postman), even if you chose to disable the Swagger UI during scaffolding. + +## Configuration + +The documentation configuration is located in `src/docs/swagger.ts`. It uses `swagger-jsdoc` to parse JSDoc comments in your route files. + +## Documenting Endpoints + +To add an endpoint to the Swagger UI, simply add a `@openapi` or `@swagger` block above your route definition. + +### Example + +```typescript +/** + * @openapi + * /users: + * get: + * summary: Retrieve a list of users + * description: Returns a list of users from the database. + * responses: + * 200: + * description: A list of users. + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/User' + */ +router.get('/', userController.getAllUsers); +``` + +## Benefits +- **Live Testing**: Use the "Try it out" button to make real requests to your development server. +- **Auto-Sync**: Your documentation lives next to your code, making it easier to keep it updated. +- **Standardized**: Uses the OpenAPI 3.0 specification, compatible with many other tools (Postman, Insomnia, etc.). + + +--- FILE: guide/structure.md --- + +# Project Structure + +Create Express Forge scaffolds a clean, professional directory structure. Depending on your chosen architecture, the structure will vary slightly. + +## 📦 Modular Architecture + +This structure is organized by **features**. Each module is self-contained. + +```text +. +├── src/ +│ ├── modules/ # Feature modules +│ │ └── users/ # Example module +│ │ ├── users.controller.ts +│ │ ├── users.service.ts +│ │ ├── users.routes.ts +│ │ └── users.schema.ts +│ ├── shared/ # Code shared across modules +│ │ ├── middleware/ +│ │ ├── utils/ +│ │ └── constants/ +│ ├── config/ # App configuration +│ └── app.ts # App initialization +├── prisma/ # Database schema (if Prisma chosen) +├── tests/ # Integration and unit tests +├── .env # Environment variables +└── package.json +``` + +## 🏛 MVC Architecture + +A traditional technical-layer separation. + +```text +. +├── src/ +│ ├── controllers/ # Route handlers +│ ├── models/ # Data models +│ ├── services/ # Business logic +│ ├── routes/ # Route definitions +│ ├── middleware/ # Global middleware +│ ├── config/ +│ └── app.ts +├── ... +``` + +## Key Files + +- **`src/app.ts`**: The entry point where Express is initialized, middleware is registered, and routes are attached. +- **`src/config/`**: Contains configuration for the database, logger, and environment variables. +- **`prisma/schema.prisma`**: (Optional) Defines your database models and relationships. +- **`docker-compose.yml`**: Defines the local development environment (e.g., PostgreSQL/MySQL containers). + + +--- FILE: guide/testing.md --- + +# Testing Strategy + +Create Express Forge encourages a test-driven approach to development. We provide a pre-configured testing environment using either **Vitest** (recommended for speed) or **Jest**. + +## 🧪 Types of Tests + +### Unit Tests +Focused on testing individual functions or services in isolation. +- **Location**: `src/modules/**/__tests__/*.unit.spec.ts` +- **Focus**: Business logic, utility functions, validation. + +### Integration Tests +Testing the full API flow, including database interactions. +- **Location**: `tests/*.int.spec.ts` +- **Focus**: HTTP status codes, API responses, database persistence. + +## 🚀 Running Tests + +```bash +# Run all tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Generate coverage report +npm run test:coverage +``` + +## 🛠 Testing Tools + +- **Supertest**: Used for making HTTP requests to your app without starting a real server. +- **Prisma Mocking**: (If using Prisma) We provide patterns for mocking the Prisma client for unit tests. +- **Test Database**: Integration tests automatically use a separate test database defined in `.env.test`. + +## Example Integration Test + +```typescript +import request from 'supertest'; +import { app } from '../src/app'; + +describe('GET /health', () => { + it('should return 200 OK', async () => { + const response = await request(app).get('/health'); + expect(response.status).toBe(200); + expect(response.body.status).toBe('ok'); + }); +}); +``` + +--- + +## 🛠 Contributing: CLI Smoke Testing + +If you are contributing to the `Create Express Forge` CLI itself, you should run the automated smoke test to verify your changes. This test scaffolds a project, runs a full TypeScript type-check, and builds the resulting app. + +```bash +cd packages/Create Express Forge +pnpm run test:smoke +``` + + +--- FILE: guide/troubleshooting.md --- + +# Troubleshooting + +Common issues and how to resolve them when using Create Express Forge. + +## 💾 Database Issues + +### Prisma migration failed +**Issue**: `prisma migrate dev` fails with a connection error. +**Solution**: +1. Ensure your database container is running: `docker compose up -d`. +2. Check your `DATABASE_URL` in the `.env` file. If running locally (not in Docker), use `localhost` instead of the service name. +3. Ensure the database user has sufficient permissions. + +### Sequelize connection error +**Issue**: `Unable to connect to the database`. +**Solution**: Check the `dialect` and `port` in your `.env` file. Ensure the database service is reachable from your host machine. + +## 🐳 Docker Issues + +### Permission Denied +**Issue**: Error when running Docker commands. +**Solution**: Run the commands with `sudo` or add your user to the `docker` group. + +### Port already in use +**Issue**: `Bind for 0.0.0.0:3000 failed: port is already allocated`. +**Solution**: Another process is using port 3000. You can change the port in your `.env` file or kill the existing process. + +## 🚀 Runtime Issues + +### Environment variables are missing +**Issue**: Zod validation error on startup. +**Solution**: Create Express Forge validates your `.env` on startup. Ensure all required variables listed in `src/config/index.ts` are present in your `.env` file. + +### Modules not found (Path Aliases) +**Issue**: TypeScript can't find modules starting with `@`. +**Solution**: This is usually handled by `tsconfig-paths`. Ensure you are starting the app with `npm run dev`. If you've added new modules, you might need to restart the dev server. + +## 🧪 Testing Issues + +### Tests hanging +**Issue**: Tests don't exit after completion. +**Solution**: Ensure you are closing your database connections and server in an `afterAll` hook. Create Express Forge handles this by default in the generated boilerplate. + +--- + +Still having trouble? [Open an issue on GitHub](https://github.com/CODE-Y02/express-cli/issues) + + +--- FILE: reference/cli-options.md --- + +# CLI Reference + +The `Create Express Forge` command can be used with various flags to bypass the interactive prompts and speed up your workflow. + +## Usage + +```bash +npx create-express-forge [project-name] [options] +``` + +## Options + +| Flag | Description | Values | +|------|-------------|--------| +| `--help` | Show help information | - | +| `--version` | Show current version | - | +| `--pattern` | Architecture pattern | `modular`, `mvc` | +| `--orm` | ORM to use | `prisma`, `sequelize`, `none` | +| `--db` | Database type | `postgres`, `mysql`, `sqlite` | +| `--logger` | Logging library | `winston`, `pino` | +| `--test` | Testing framework | `vitest`, `jest` | +| `--docker` | Include Docker setup | `true`, `false` | +| `--install` | Auto-install dependencies | `true`, `false` | + +## Example + +Scaffold a modular project with Prisma and Vitest: + +```bash +npx create-express-forge my-api --pattern modular --orm prisma --test vitest --install true +``` + + +--- FILE: reference/config.md --- + +# Configuration Reference + +Every project created with Create Express Forge uses a centralized configuration system powered by environment variables. + +## 🌍 Environment Variables + +Create a `.env` file in the root of your project. + +| Variable | Description | Default | Required | +|----------|-------------|---------|----------| +| `PORT` | The port the server listens on | `3000` | No | +| `NODE_ENV` | `development`, `production`, `test` | `development` | No | +| `DATABASE_URL` | Connection string for your DB | - | Yes* | +| `LOG_LEVEL` | `fatal`, `error`, `warn`, `info`, `debug`, `trace` | `debug` | No | +| `CORS_ORIGIN` | Allowed origins (comma separated) | `*` | No | +| `RATE_LIMIT_MAX` | Max requests per window | `100` | No | +| `RATE_LIMIT_WINDOW` | Window size in minutes | `15` | No | + +*\*Required if an ORM is selected.* + +## ⚙️ App Configuration + +Configuration is managed in `src/config/index.ts`. This file: +1. Validates environment variables using **Zod**. +2. Exports a typed configuration object. +3. Provides default values for optional variables. + +## 💾 Database Config + +If you chose Prisma, your configuration is primarily in `prisma/schema.prisma`. + +For Sequelize, configuration is found in `src/config/database.ts`, which handles the connection pooling and dialect-specific settings. + +## 📝 Logger Config + +Logging configuration is found in `src/config/logger.ts`. You can toggle between `pretty-print` (for development) and `JSON` (for production) logging here. diff --git a/docs/reference/cli-options.md b/docs/reference/cli-options.md index c07d95c..e4d9408 100644 --- a/docs/reference/cli-options.md +++ b/docs/reference/cli-options.md @@ -1,6 +1,6 @@ # CLI Reference -The `create-express-forge` command can be used with various flags to bypass the interactive prompts and speed up your workflow. +The `Create Express Forge` command can be used with various flags to bypass the interactive prompts and speed up your workflow. ## Usage diff --git a/docs/reference/config.md b/docs/reference/config.md index 83efca7..afdaaa3 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -1,6 +1,6 @@ # Configuration Reference -Every project created with Express Forge uses a centralized configuration system powered by environment variables. +Every project created with Create Express Forge uses a centralized configuration system powered by environment variables. ## 🌍 Environment Variables diff --git a/docs/scripts/generate-llms-docs.ts b/docs/scripts/generate-llms-docs.ts new file mode 100644 index 0000000..dd47d84 --- /dev/null +++ b/docs/scripts/generate-llms-docs.ts @@ -0,0 +1,97 @@ +import fs from 'fs-extra'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DOCS_DIR = path.join(__dirname, '..'); +const PUBLIC_DIR = path.join(DOCS_DIR, 'public'); + +async function generate() { + console.log('🚀 Generating LLM documentation files...'); + + // --- Helper to generate for a specific directory --- + async function generateForVersion(versionDir: string, publicSubDir: string, versionName: string) { + const baseDir = path.join(DOCS_DIR, versionDir); + const targetPublicDir = path.join(PUBLIC_DIR, publicSubDir); + await fs.ensureDir(targetPublicDir); + + const guideFiles = (await fs.readdir(path.join(baseDir, 'guide'))) + .filter(f => f.endsWith('.md')) + .map(f => `guide/${f}`); + + const referenceFiles = (await fs.readdir(path.join(baseDir, 'reference'))) + .filter(f => f.endsWith('.md')) + .map(f => `reference/${f}`); + + const allFiles = ['index.md', ...guideFiles, ...referenceFiles]; + + // 1. Generate llms-full.txt + let fullDocs = `# Create Express Forge ${versionName} - Full Documentation\n\n`; + for (const file of allFiles) { + const filePath = path.join(baseDir, file); + if (!(await fs.pathExists(filePath))) continue; + const content = await fs.readFile(filePath, 'utf-8'); + const cleanContent = content.replace(/^---[\s\S]*?---/, '').trim(); + fullDocs += `\n\n--- FILE: ${file} ---\n\n${cleanContent}\n`; + } + await fs.writeFile(path.join(targetPublicDir, 'llms-full.txt'), fullDocs); + return allFiles.map(f => f.replace('.md', '')); + } + + // Generate Latest (v4) + const v4Slugs = await generateForVersion('.', '.', 'v4.x (Latest)'); + + // Generate v3 (if exists) + let v3Slugs: string[] = []; + if (await fs.pathExists(path.join(DOCS_DIR, 'v3'))) { + v3Slugs = await generateForVersion('v3', 'v3', 'v3.x (LTS)'); + } + + // 2. Generate llms.txt (Main Summary) + const summary = ` +# Create Express Forge +The ultimate CLI for scaffolding production-ready Express.js TypeScript backends. + +## Versions +- v4.x (Latest): Modern, Biome, Zod-OpenAPI, Path Aliases. +- v3.x (LTS): Stable, ESLint/Prettier, swagger-jsdoc. + +## Quick Start (v4) +npx create-express-forge@latest my-api + +## Quick Start (v3) +npx create-express-forge@3.3.2 my-api + +## Documentation +- v4 Full Docs: https://code-y02.github.io/express-cli/llms-full.txt +- v3 Full Docs: https://code-y02.github.io/express-cli/v3/llms-full.txt +`.trim(); + await fs.writeFile(path.join(PUBLIC_DIR, 'llms.txt'), summary); + + // 3. Generate ai.json (Manifest) + const manifest = { + name: "Create Express Forge", + description: "Production-ready Express.js TypeScript backend generator", + latestVersion: "4.1.2", + ltsVersion: "3.3.2", + repository: "https://github.com/CODE-Y02/express-cli", + homepage: "https://code-y02.github.io/express-cli", + versions: { + v4: { + status: "latest", + fullDocs: "https://code-y02.github.io/express-cli/llms-full.txt", + slugs: v4Slugs + }, + v3: { + status: "lts", + fullDocs: "https://code-y02.github.io/express-cli/v3/llms-full.txt", + slugs: v3Slugs + } + } + }; + await fs.writeJson(path.join(PUBLIC_DIR, 'ai.json'), manifest, { spaces: 2 }); + + console.log('✅ LLM documentation files generated in docs/public/'); +} + +generate().catch(console.error); diff --git a/docs/v3/guide/architecture.md b/docs/v3/guide/architecture.md new file mode 100644 index 0000000..84deeb9 --- /dev/null +++ b/docs/v3/guide/architecture.md @@ -0,0 +1,49 @@ +# Architecture Patterns + +Create Express Forge supports two main architecture patterns to fit your project's needs. Choosing the right one is crucial for long-term maintainability. + +## 📦 Modular Architecture (Recommended) + +This is the default and recommended pattern for medium to large applications. It organizes code by **features** (modules) rather than technical roles (controllers, models). + +### Why choose Modular? +- **Scalability**: Each module is self-contained, making it easier to manage as the app grows. +- **Isolation**: Changes to one feature are less likely to break another. +- **Team-Friendly**: Different developers can work on different modules without merge conflicts. + +### Structure: +```text +src/ + modules/ + users/ + users.controller.ts # Request handling + users.service.ts # Business logic + users.routes.ts # Route definitions + users.schema.ts # Validation schemas + products/ + ... +``` + +--- + +## 🏛 MVC Architecture + +A classic Model-View-Controller pattern. Best for smaller projects or those who prefer a traditional separation of concerns based on technical layers. + +### Why choose MVC? +- **Familiarity**: Most developers are familiar with this pattern from frameworks like Rails or Django. +- **Simplicity**: For very small apps, it might be quicker to navigate. + +### Structure: +```text +src/ + controllers/ # All controllers in one place + models/ # Database models + routes/ # All route definitions + services/ # Business logic +``` + +## Which one should I use? + +If you are building a production API that you expect to grow over time, **Modular Architecture** is almost always the better choice. It prevents the "Fat Controller" and "Fat Model" syndromes by keeping related logic close together. + diff --git a/docs/v3/guide/auth.md b/docs/v3/guide/auth.md new file mode 100644 index 0000000..3dc9bc8 --- /dev/null +++ b/docs/v3/guide/auth.md @@ -0,0 +1,46 @@ +# Authentication + +Create Express Forge provides two battle-tested authentication strategies: **JWT** (JSON Web Tokens) and **Sessions**. + +## Strategies + +### 🔐 JWT (Stateless) +The modern standard for web APIs. Highly scalable and perfect for mobile apps and SPAs. +- **Middleware**: `src/middleware/auth.ts` +- **Storage Options**: + - **🍪 Cookie**: More secure against XSS. Uses `httpOnly` and `secure` flags. + - **📨 Header**: Standard `Authorization: Bearer `. Recommended for mobile apps. +- **Config**: Set `JWT_SECRET` and `JWT_EXPIRES_IN` in your `.env`. + +### 🍪 Session (Stateful) +Traditional cookie-based authentication. Excellent for server-side rendered apps or when you need built-in session management. +- **Middleware**: `src/middleware/auth.ts` +- **Config**: Set `SESSION_SECRET` in your `.env`. + +## Using the Auth Middleware + +To protect a route, simply add the `auth` middleware to your route definition. + +```typescript +import { auth } from '../middleware/auth.js'; +import { todosController } from '../modules/todos/todos.controller.js'; + +// Protected route +router.get('/', auth, todosController.getTodos); +``` + +## Accessing the User + +Once a user is authenticated, their information is attached to the `req.user` object (for JWT) or `req.session.user` (for Sessions). + +```typescript +export const getProfile = (req: Request, res: Response) => { + const user = req.user; // For JWT + return ApiResponse.success(res, user); +}; +``` + +## Security Best Practices +1. **Secret Management**: Never commit your `JWT_SECRET` or `SESSION_SECRET` to version control. Use `.env` files. +2. **HTTPS**: Always serve your API over HTTPS in production to protect tokens and session cookies. +3. **HTTP-Only Cookies**: If using sessions, Create Express Forge pre-configures cookies to be `httpOnly` to prevent XSS attacks. diff --git a/docs/v3/guide/caching.md b/docs/v3/guide/caching.md new file mode 100644 index 0000000..a4c2525 --- /dev/null +++ b/docs/v3/guide/caching.md @@ -0,0 +1,50 @@ +# Caching + +Create Express Forge provides a flexible caching layer to boost your API performance and reduce database load. You can choose between **Redis** (distributed) or **Node-Cache** (in-memory) during the scaffolding process. + +## Supported Drivers + +### 🔴 Redis +Recommended for production environments and distributed systems where multiple server instances need to share a cache. +- **Requirement**: A running Redis instance. +- **Config**: Set `REDIS_URL` in your `.env` file. + +### 💾 Node-Cache +An in-memory caching solution that requires zero external dependencies. Perfect for simple applications or single-server setups. +- **Requirement**: None. +- **Config**: Automatic. + +## Usage + +The caching logic is encapsulated in `src/cache/index.ts`. It provides a unified interface regardless of the driver you chose. + +### Getting a Value +```typescript +import { cache } from '../cache/index.js'; + +const user = await cache.get('user:123'); +if (user) { + return JSON.parse(user); +} +``` + +### Setting a Value +You can optionally set a Time-To-Live (TTL) in seconds. +```typescript +import { cache } from '../cache/index.js'; + +// Cache for 1 hour (3600 seconds) +await cache.set('user:123', JSON.stringify(userData), 3600); +``` + +### Deleting a Value +```typescript +import { cache } from '../cache/index.js'; + +await cache.del('user:123'); +``` + +## Best Practices +1. **Cache Invalidation**: Always delete or update the cache when the underlying data in the database changes. +2. **Serialization**: Since Redis only stores strings, ensure you `JSON.stringify()` your objects before setting and `JSON.parse()` when getting. +3. **Fail-Safe**: Create Express Forge handles Redis connection errors gracefully via the logger, preventing your entire app from crashing if the cache is down. diff --git a/docs/v3/guide/deployment.md b/docs/v3/guide/deployment.md new file mode 100644 index 0000000..32bef43 --- /dev/null +++ b/docs/v3/guide/deployment.md @@ -0,0 +1,42 @@ +# Deployment + +Create Express Forge provides production-ready configurations to help you ship your API with confidence. + +## 🐳 Docker Deployment (Recommended) + +The easiest way to deploy is using the provided multi-stage `Dockerfile`. + +### Build Image +```bash +docker build -t my-express-api . +``` + +### Run Container +```bash +docker run -p 3000:3000 --env-file .env my-express-api +``` + +### Why Multi-stage? +Our Dockerfile uses multi-stage builds to: +1. **Reduce Image Size**: The final image only contains the compiled JavaScript and production dependencies. +2. **Security**: Source code and build tools are not included in the final production image. + +## ☁️ Cloud Platforms + +### Railway / Render / Fly.io +Most modern PaaS platforms will automatically detect the `Dockerfile` or the `start` script in `package.json`. + +1. Connect your GitHub repository. +2. Configure your environment variables (copy from `.env`). +3. Set the build command to `npm run build` (if not using Docker). + > **Note for Prisma users**: The generated `package.json` includes a `postinstall: "prisma generate"` script, which ensures your Prisma client is generated automatically before the build step on most PaaS platforms. +4. Set the start command to `npm start`. + +## 🛡️ Production Checklist + +Before going live, ensure: +- [ ] **Environment Variables**: `NODE_ENV` is set to `production`. +- [ ] **Database**: Migrations have been run on the production database. +- [ ] **Logging**: Log level is set appropriately (e.g., `info` or `error`). +- [ ] **Security**: CORS is restricted to your frontend domain. +- [ ] **Rate Limiting**: Configured for your production traffic. diff --git a/docs/v3/guide/features.md b/docs/v3/guide/features.md new file mode 100644 index 0000000..9f30de0 --- /dev/null +++ b/docs/v3/guide/features.md @@ -0,0 +1,82 @@ +# Core Features + +Create Express Forge comes packed with everything you need to build robust APIs. + +## 🛡️ TypeScript First +Type safety is at the core of Create Express Forge. Every scaffolded project includes: +- Strict TypeScript configuration. +- Path aliases (e.g., `@/config/env`). +- Type-safe environment variables via **Zod**. +- Automated scaffolding with your choice of **npm**, **pnpm**, **yarn**, or **bun**. + +## 🔐 Flexible Authentication +Scaffold a complete authentication system with a single choice: +- **JWT Authentication**: Choose between **HttpOnly Cookies** (recommended for web) or **Bearer Headers** (recommended for mobile/API clients). +- **Session Auth**: Battle-tested session management for stateful applications. +- **Protected Routes**: Every boilerplate includes a protected resource showing you exactly how to use the auth middleware. + +## 💾 Database Integration +Choose your favorite ORM and get started instantly: +- **Prisma**: Modern ORM with auto-generated client and type-safe queries. +- **Sequelize**: The most popular traditional ORM for Node.js. +- **Migrations**: Pre-configured scripts to handle database schema changes. + +## 🧪 Testing Suite +Don't ship broken code. Create Express Forge sets up a complete testing environment: +- **Vitest/Jest**: Choose your favorite test runner. +- **Supertest**: For high-level API integration tests. +- **Example Tests**: Every scaffolded project includes example unit and integration tests. + +## 🐳 Docker Support +Ship to production with confidence: +- **Multi-stage Build**: Optimized Dockerfiles for smaller production images. +- **Docker Compose**: Includes a `docker-compose.yml` with a database setup for local development. + +## 🔐 Security Best Practices +Stay secure by default with pre-configured industry standards: +- **Helmet**: Automatically sets security-related HTTP headers to protect against common vulnerabilities. +- **CORS**: Flexible Cross-Origin Resource Sharing configuration. +- **Rate Limiting**: Integrated `express-rate-limit` to prevent brute-force attacks and DDoS. +- **Dotenv & Zod**: Every environment variable is validated on startup. If a variable is missing or malformed, the app fails fast with a clear error message. + +## 📝 Logging & Monitoring +- **Pino/Winston**: High-performance, structured logging. Pino is used by default for its extreme speed and JSON output, which is perfect for log aggregators like ELK or Datadog. +- **Health Checks**: A standard `/health` endpoint is included, providing uptime, memory usage, and database connectivity status. + +## 📜 OpenAPI Documentation +Never let your documentation get out of sync: +- **Swagger UI**: Integrated UI to explore and test your API endpoints directly from the browser. +- **Auto-generated Spec**: The CLI generates a `docs.json` endpoint that is always up-to-date with your code's JSDoc annotations. +- **Security Schemas**: Pre-configured security definitions for your chosen auth strategy (Cookie or Bearer). + +## 🧱 Graceful Shutdown +Every Create Express Forge project handles `SIGTERM` and `SIGINT` signals correctly. This ensures that: +1. No new requests are accepted. +2. Existing requests are finished. +3. Database connections are closed cleanly. +4. The process exits without data corruption. + +## 🛠️ Error Handling & Responses +Create Express Forge enforces a consistent communication pattern between your API and clients. + +### Centralized Error Handling +A global error middleware is the "safety net" for your application. It catches all errors and transforms them into structured JSON responses, handling `Zod` validation errors and custom `ApiError` instances automatically. + +### Custom `ApiError` Class +Stop throwing generic strings. Use the built-in `ApiError` class to provide context, status codes, and operational flags: +- `ApiError.notFound('User not found')` +- `ApiError.unauthorized()` +- `ApiError.badRequest('Invalid input', validationErrors)` + +### Standardized `ApiResponse` +Ensure your frontend team always knows what to expect. Every success response follows a predictable schema: +```json +{ + "success": true, + "message": "Operation successful", + "data": { ... } +} +``` + +### Async Error Wrapper +The provided `asyncHandler` utility eliminates the need for `try-catch` blocks in your controllers, automatically forwarding any promise rejections to the global error handler. diff --git a/docs/v3/guide/getting-started.md b/docs/v3/guide/getting-started.md new file mode 100644 index 0000000..a5f0504 --- /dev/null +++ b/docs/v3/guide/getting-started.md @@ -0,0 +1,47 @@ +# Getting Started + +Create Express Forge is designed to get you up and running with a production-grade Express backend in seconds. + +## Quick Start + +The fastest way to create a new project is using `npx`. You don't even need to install the CLI globally! + +```bash +npx create-express-forge@3.3.2 [project-name] +``` + +### Instant Scaffolding + +If you want to skip the prompts and use the recommended defaults, use the `--yes` flag: + +```bash +npx create-express-forge@3.3.2 my-api --yes +``` + +## Step-by-Step Guide + +### 1. Initialize Project +Run the command and follow the interactive prompts. You'll be asked to: +- Give your project a **name**. +- Select your preferred **package manager** (npm, pnpm, yarn). +- Choose an **architecture** (Modular or MVC). +- Select an **ORM** (Prisma, Sequelize, or none). +- Choose a **Testing Framework** (Vitest or Jest). + +### 2. Enter Directory +```bash +cd my-awesome-api +``` + +### 3. Start Development +```bash +npm run dev +``` + +Your API will now be running at `http://localhost:3000` with hot-reloading enabled. + +## Next Steps + +- Explore the [Architecture Patterns](./architecture) to understand how your code is organized. +- Check out the [Core Features](./features) to see what's included out of the box. +- Configure your environment variables in the `.env` file. diff --git a/docs/v3/guide/openapi.md b/docs/v3/guide/openapi.md new file mode 100644 index 0000000..99f4fbf --- /dev/null +++ b/docs/v3/guide/openapi.md @@ -0,0 +1,48 @@ +# API Documentation (OpenAPI) + +Create Express Forge integrates **Swagger UI** to provide interactive, live documentation for your API. This allows your frontend team or external partners to test endpoints directly from the browser. + +## Getting Started + +If you enabled OpenAPI during scaffolding, your documentation is available at: + +- **Swagger UI**: `http://localhost:3000/docs` (Interactive) +- **OpenAPI Spec**: `http://localhost:3000/docs.json` (Raw JSON) + +> [!TIP] +> The `/docs.json` endpoint is always available for external tools (like Postman), even if you chose to disable the Swagger UI during scaffolding. + +## Configuration + +The documentation configuration is located in `src/docs/swagger.ts`. It uses `swagger-jsdoc` to parse JSDoc comments in your route files. + +## Documenting Endpoints + +To add an endpoint to the Swagger UI, simply add a `@openapi` or `@swagger` block above your route definition. + +### Example + +```typescript +/** + * @openapi + * /users: + * get: + * summary: Retrieve a list of users + * description: Returns a list of users from the database. + * responses: + * 200: + * description: A list of users. + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/User' + */ +router.get('/', userController.getAllUsers); +``` + +## Benefits +- **Live Testing**: Use the "Try it out" button to make real requests to your development server. +- **Auto-Sync**: Your documentation lives next to your code, making it easier to keep it updated. +- **Standardized**: Uses the OpenAPI 3.0 specification, compatible with many other tools (Postman, Insomnia, etc.). diff --git a/docs/v3/guide/structure.md b/docs/v3/guide/structure.md new file mode 100644 index 0000000..b2c9a92 --- /dev/null +++ b/docs/v3/guide/structure.md @@ -0,0 +1,52 @@ +# Project Structure + +Create Express Forge scaffolds a clean, professional directory structure. Depending on your chosen architecture, the structure will vary slightly. + +## 📦 Modular Architecture + +This structure is organized by **features**. Each module is self-contained. + +```text +. +├── src/ +│ ├── modules/ # Feature modules +│ │ └── users/ # Example module +│ │ ├── users.controller.ts +│ │ ├── users.service.ts +│ │ ├── users.routes.ts +│ │ └── users.schema.ts +│ ├── shared/ # Code shared across modules +│ │ ├── middleware/ +│ │ ├── utils/ +│ │ └── constants/ +│ ├── config/ # App configuration +│ └── app.ts # App initialization +├── prisma/ # Database schema (if Prisma chosen) +├── tests/ # Integration and unit tests +├── .env # Environment variables +└── package.json +``` + +## 🏛 MVC Architecture + +A traditional technical-layer separation. + +```text +. +├── src/ +│ ├── controllers/ # Route handlers +│ ├── models/ # Data models +│ ├── services/ # Business logic +│ ├── routes/ # Route definitions +│ ├── middleware/ # Global middleware +│ ├── config/ +│ └── app.ts +├── ... +``` + +## Key Files + +- **`src/app.ts`**: The entry point where Express is initialized, middleware is registered, and routes are attached. +- **`src/config/`**: Contains configuration for the database, logger, and environment variables. +- **`prisma/schema.prisma`**: (Optional) Defines your database models and relationships. +- **`docker-compose.yml`**: Defines the local development environment (e.g., PostgreSQL/MySQL containers). diff --git a/docs/v3/guide/testing.md b/docs/v3/guide/testing.md new file mode 100644 index 0000000..cecedbe --- /dev/null +++ b/docs/v3/guide/testing.md @@ -0,0 +1,60 @@ +# Testing Strategy + +Create Express Forge encourages a test-driven approach to development. We provide a pre-configured testing environment using either **Vitest** (recommended for speed) or **Jest**. + +## 🧪 Types of Tests + +### Unit Tests +Focused on testing individual functions or services in isolation. +- **Location**: `src/modules/**/__tests__/*.unit.spec.ts` +- **Focus**: Business logic, utility functions, validation. + +### Integration Tests +Testing the full API flow, including database interactions. +- **Location**: `tests/*.int.spec.ts` +- **Focus**: HTTP status codes, API responses, database persistence. + +## 🚀 Running Tests + +```bash +# Run all tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Generate coverage report +npm run test:coverage +``` + +## 🛠 Testing Tools + +- **Supertest**: Used for making HTTP requests to your app without starting a real server. +- **Prisma Mocking**: (If using Prisma) We provide patterns for mocking the Prisma client for unit tests. +- **Test Database**: Integration tests automatically use a separate test database defined in `.env.test`. + +## Example Integration Test + +```typescript +import request from 'supertest'; +import { app } from '../src/app'; + +describe('GET /health', () => { + it('should return 200 OK', async () => { + const response = await request(app).get('/health'); + expect(response.status).toBe(200); + expect(response.body.status).toBe('ok'); + }); +}); +``` + +--- + +## 🛠 Contributing: CLI Smoke Testing + +If you are contributing to the `Create Express Forge` CLI itself, you should run the automated smoke test to verify your changes. This test scaffolds a project, runs a full TypeScript type-check, and builds the resulting app. + +```bash +cd packages/Create Express Forge +pnpm run test:smoke +``` diff --git a/docs/v3/guide/troubleshooting.md b/docs/v3/guide/troubleshooting.md new file mode 100644 index 0000000..def622c --- /dev/null +++ b/docs/v3/guide/troubleshooting.md @@ -0,0 +1,46 @@ +# Troubleshooting + +Common issues and how to resolve them when using Create Express Forge. + +## 💾 Database Issues + +### Prisma migration failed +**Issue**: `prisma migrate dev` fails with a connection error. +**Solution**: +1. Ensure your database container is running: `docker compose up -d`. +2. Check your `DATABASE_URL` in the `.env` file. If running locally (not in Docker), use `localhost` instead of the service name. +3. Ensure the database user has sufficient permissions. + +### Sequelize connection error +**Issue**: `Unable to connect to the database`. +**Solution**: Check the `dialect` and `port` in your `.env` file. Ensure the database service is reachable from your host machine. + +## 🐳 Docker Issues + +### Permission Denied +**Issue**: Error when running Docker commands. +**Solution**: Run the commands with `sudo` or add your user to the `docker` group. + +### Port already in use +**Issue**: `Bind for 0.0.0.0:3000 failed: port is already allocated`. +**Solution**: Another process is using port 3000. You can change the port in your `.env` file or kill the existing process. + +## 🚀 Runtime Issues + +### Environment variables are missing +**Issue**: Zod validation error on startup. +**Solution**: Create Express Forge validates your `.env` on startup. Ensure all required variables listed in `src/config/index.ts` are present in your `.env` file. + +### Modules not found (Path Aliases) +**Issue**: TypeScript can't find modules starting with `@`. +**Solution**: This is usually handled by `tsconfig-paths`. Ensure you are starting the app with `npm run dev`. If you've added new modules, you might need to restart the dev server. + +## 🧪 Testing Issues + +### Tests hanging +**Issue**: Tests don't exit after completion. +**Solution**: Ensure you are closing your database connections and server in an `afterAll` hook. Create Express Forge handles this by default in the generated boilerplate. + +--- + +Still having trouble? [Open an issue on GitHub](https://github.com/CODE-Y02/express-cli/issues) diff --git a/docs/v3/index.md b/docs/v3/index.md new file mode 100644 index 0000000..d6fc466 --- /dev/null +++ b/docs/v3/index.md @@ -0,0 +1,69 @@ +--- +title: Create Express Forge v3.x | Modern Express.js CLI +layout: home + +hero: + name: Create Express Forge + text: Build Better Express APIs, Faster. + tagline: The definitive CLI for scaffolding production-ready, enterprise-grade Express.js backends. + image: + src: /logo.svg + alt: Create Express Forge Logo + actions: + - theme: brand + text: Get Started + link: /v3/guide/getting-started + - theme: alt + text: ⭐ Star on GitHub + link: https://github.com/CODE-Y02/express-cli + +features: + - icon: ⚡ + title: Instant Scaffolding + details: Go from zero to a fully structured API in under 30 seconds with our interactive CLI. + - icon: 🛡️ + title: TypeScript Native + details: Deeply integrated TypeScript support with strict typing and modern ESM configuration. + - icon: 📦 + title: Modern Architecture + details: Choose between Feature-driven Modular or traditional MVC patterns to suit your scale. + - icon: 💾 + title: Type-safe ORM + details: First-class support for Prisma and Sequelize with pre-configured migrations. + - icon: 🔐 + title: Auth Scaffolding + details: Pre-configured JWT or Session-based authentication with best-practice security. + - icon: 📜 + title: OpenAPI Docs + details: Automatically generated Swagger documentation from your Zod schemas and routes. + - icon: 🔴 + title: Redis Caching + details: Built-in support for Redis and Node-Cache to boost your API performance. + - icon: 🧪 + title: Quality Assured + details: Pre-configured testing suites with Vitest or Jest, including example integration tests. + - icon: 🐳 + title: Docker Ready + details: Multi-stage Dockerfiles and docker-compose setups included for seamless deployment. +--- + +> [!WARNING] +> You are viewing the documentation for **Create Express Forge v3.x (LTS)**. +> For the latest version (v4.x), please switch using the version dropdown above. + +### 🚀 Key Benefits + +- **Zero Configuration**: Sensible defaults that work out of the box. +- **Enterprise Patterns**: Modular architecture that grows with your team. +- **Developer Experience**: Auto-reloading, linting, and formatting pre-configured. +- **Security First**: Best practices for CORS, rate limiting, and environment management. + +### 🛠 Built With + +TypeScript · Express.js · Prisma · Docker · Vitest · Zod · Pino + +--- + +### ❤️ Support the Project + +If Create Express Forge has saved you time, please consider giving us a star on [GitHub](https://github.com/CODE-Y02/express-cli)! It helps us reach more developers and continue improving the project. diff --git a/docs/v3/reference/cli-options.md b/docs/v3/reference/cli-options.md new file mode 100644 index 0000000..e4d9408 --- /dev/null +++ b/docs/v3/reference/cli-options.md @@ -0,0 +1,32 @@ +# CLI Reference + +The `Create Express Forge` command can be used with various flags to bypass the interactive prompts and speed up your workflow. + +## Usage + +```bash +npx create-express-forge [project-name] [options] +``` + +## Options + +| Flag | Description | Values | +|------|-------------|--------| +| `--help` | Show help information | - | +| `--version` | Show current version | - | +| `--pattern` | Architecture pattern | `modular`, `mvc` | +| `--orm` | ORM to use | `prisma`, `sequelize`, `none` | +| `--db` | Database type | `postgres`, `mysql`, `sqlite` | +| `--logger` | Logging library | `winston`, `pino` | +| `--test` | Testing framework | `vitest`, `jest` | +| `--docker` | Include Docker setup | `true`, `false` | +| `--install` | Auto-install dependencies | `true`, `false` | + +## Example + +Scaffold a modular project with Prisma and Vitest: + +```bash +npx create-express-forge my-api --pattern modular --orm prisma --test vitest --install true +``` + diff --git a/docs/v3/reference/config.md b/docs/v3/reference/config.md new file mode 100644 index 0000000..afdaaa3 --- /dev/null +++ b/docs/v3/reference/config.md @@ -0,0 +1,36 @@ +# Configuration Reference + +Every project created with Create Express Forge uses a centralized configuration system powered by environment variables. + +## 🌍 Environment Variables + +Create a `.env` file in the root of your project. + +| Variable | Description | Default | Required | +|----------|-------------|---------|----------| +| `PORT` | The port the server listens on | `3000` | No | +| `NODE_ENV` | `development`, `production`, `test` | `development` | No | +| `DATABASE_URL` | Connection string for your DB | - | Yes* | +| `LOG_LEVEL` | `fatal`, `error`, `warn`, `info`, `debug`, `trace` | `debug` | No | +| `CORS_ORIGIN` | Allowed origins (comma separated) | `*` | No | +| `RATE_LIMIT_MAX` | Max requests per window | `100` | No | +| `RATE_LIMIT_WINDOW` | Window size in minutes | `15` | No | + +*\*Required if an ORM is selected.* + +## ⚙️ App Configuration + +Configuration is managed in `src/config/index.ts`. This file: +1. Validates environment variables using **Zod**. +2. Exports a typed configuration object. +3. Provides default values for optional variables. + +## 💾 Database Config + +If you chose Prisma, your configuration is primarily in `prisma/schema.prisma`. + +For Sequelize, configuration is found in `src/config/database.ts`, which handles the connection pooling and dialect-specific settings. + +## 📝 Logger Config + +Logging configuration is found in `src/config/logger.ts`. You can toggle between `pretty-print` (for development) and `JSON` (for production) logging here. diff --git a/package.json b/package.json index 31d0e6e..eb53e41 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", @@ -14,13 +14,14 @@ "docs:dev": "cd docs && pnpm run dev", "docs:build": "cd docs && pnpm run build", "docs:preview": "cd docs && pnpm run preview", + "mcp:dev": "pnpm --filter @create-express-forge/mcp dev", + "mcp:start": "pnpm --filter @create-express-forge/mcp start", "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/CHANGELOG.md b/packages/create-express-forge/CHANGELOG.md index 3f1fde5..7a5be4e 100644 --- a/packages/create-express-forge/CHANGELOG.md +++ b/packages/create-express-forge/CHANGELOG.md @@ -1,5 +1,77 @@ # Changelog +## 4.1.4 + +### Patch Changes + +- e7113ba: - Added support for scaffolding in the current directory (`.`) + - Implemented automated import alias resolution (`@/`) for scaffolded projects + - Fixed relative import bug in smoke test template + - Synchronized documentation with latest v4 features (Zod-to-OpenAPI, Biome, Pro Fail-Fast) + - Added a **High-Fidelity Personalized Welcome** message using a large gradient-styled font (powered by `figlet` and `gradient-string`) to greet users by name + - Added documentation versioning support with v3 LTS dropdown + - Enhanced documentation SEO, OpenGraph metadata, and social sharing tags + - Standardized product naming to "Create Express Forge" across docs and CLI + - Added a dedicated **MCP Server** (`@create-express-forge/mcp`) for AI-assisted scaffolding and documentation access + - Automated generation of LLM-friendly documentation files (`llms.txt`, `llms-full.txt`, `ai.json`) + - Highlighted **Ultra-Fast scaffolding** in the v3 vs v4 comparison (powered by internal refactoring) + - Updated root and package READMEs with new features and Biome integration + - Removed deprecated `cef` alias from documentation + - Fixed file paths in CONTRIBUTING.md + - Updated v3 documentation and root README to target the correct stable legacy version (`3.3.2`) + +## 4.1.3 + +### Patch Changes + +- 28c3e1b: - Added support for scaffolding in the current directory (`.`) + - Implemented automated import alias resolution (`@/`) for scaffolded projects + - Fixed relative import bug in smoke test template + - Synchronized documentation with latest v4 features (Zod-to-OpenAPI, Biome, Pro Fail-Fast) + - Added a **High-Fidelity Personalized Welcome** message using a large gradient-styled font (powered by `figlet` and `gradient-string`) to greet users by name + - Added documentation versioning support with v3 LTS dropdown + - Enhanced documentation SEO, OpenGraph metadata, and social sharing tags + - Standardized product naming to "Create Express Forge" across docs and CLI + - Added a dedicated **MCP Server** (`@create-express-forge/mcp`) for AI-assisted scaffolding and documentation access + - Automated generation of LLM-friendly documentation files (`llms.txt`, `llms-full.txt`, `ai.json`) + - Highlighted **Ultra-Fast scaffolding** in the v3 vs v4 comparison (powered by internal refactoring) + - Updated root and package READMEs with new features and Biome integration + - Removed deprecated `cef` alias from documentation + - Fixed file paths in CONTRIBUTING.md + - Updated v3 documentation and root README to target the correct stable legacy version (`3.3.2`) + +## 4.1.2 + +### Patch Changes + +- 4ffc258: - **Zod-to-OpenAPI Integration**: Implemented automated, DRY documentation using `@asteasolutions/zod-to-openapi`. Removed all clunky `@openapi` JSDoc comments from route files in favor of pure TypeScript path registration. + - **Unified API Paths**: Standardized all routes and health checks under `/api/v1/` prefix. + - **Improved DX**: Added automatic logging of the API Documentation URL on server startup. + - **Biome 2.4 Integration**: Migrated the monorepo and generated templates to Biome for 20x faster linting and formatting. Templates now pin `@biomejs/biome@2.4.13` for consistent DX across environments. + - **"Pro" Fail-Fast Logic**: Implemented strict connection checks for DB/Redis in production with warnings in development. + - **Template Hardening**: Fixed multiple linting errors in generated templates (auth controllers, ApiResponse, and middleware). + - **Docker Fix**: Resolved `depends_on` formatting and removed obsolete version tags in `docker-compose.yml`. + - **Smoke Test Hardening**: Added mandatory linting checks to the generator test suite. + +## 4.1.1 + +### Patch Changes + +- 3752d64: fix build issues + +## 4.1.0 + +### Minor Changes + +- 3f3aa68: fix linting issues with package + +## 4.0.0 + +### Major Changes + +- d617dbf: ### 🚀 Core CLI & Codebase Modernization +- d617dbf: ### 🚀 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`). + ## 3.3.2 ### Patch Changes @@ -32,17 +104,20 @@ ### Major Changes - e1e588f: ## Added + - Support for multiple package managers (npm, pnpm, yarn, bun). - JWT authentication with HttpOnly Cookie and Header options. - Swagger UI integration for interactive API documentation. ## Fixed + - Minor fixes and improvements in the CLI scaffolding process. - Support for multiple package managers (npm, pnpm, yarn, bun). - JWT authentication with HttpOnly Cookie and Header options. - Swagger UI integration for interactive API documentation. ## Fixed + - Minor fixes and improvements in the CLI scaffolding process. ## 2.0.0 diff --git a/packages/create-express-forge/README.md b/packages/create-express-forge/README.md index 7a1619b..cc9e8d5 100644 --- a/packages/create-express-forge/README.md +++ b/packages/create-express-forge/README.md @@ -10,8 +10,8 @@ --- -## 🎯 Why use Express Forge? -Searching for an **Express TypeScript starter**? Most boilerplates are either too bloated or too empty. Express Forge gives you a modular, configurable, and type-safe foundation using the tools you already love. +## 🎯 Why use create-express-forge? +Searching for an **Express TypeScript starter**? Most boilerplates are either too bloated or too empty. create-express-forge gives you a modular, configurable, and type-safe foundation using the tools you already love. ## 🚀 Quick Start Create your new project instantly: @@ -19,17 +19,23 @@ Create your new project instantly: npx create-express-forge@latest my-api ``` +Or scaffold in your **current directory**: +```bash +npx create-express-forge@latest . +``` + --- ## 🔥 Key Features - **🏗️ Smart Architectures**: Choose between **Modular (feature-based)** or **MVC** patterns. -- **🛡️ Type-Safe by Default**: Fully configured **TypeScript** with **Zod** for environment and request validation. +- **🛡️ Type-Safe by Default**: Fully configured **TypeScript** with **Path Aliases (`@/`)** and **Zod** for validation. +- **⚡ Modern Tooling**: **Biome** is used for 20x faster linting and formatting (replaces ESLint/Prettier). - **🔐 Professional Auth**: Ready-to-use **JWT Authentication** with **HttpOnly Cookies** or **Bearer Headers**. - **🗄️ ORM Flexibility**: Support for **Prisma**, **Sequelize**, or raw drivers. -- **📜 Automated Documentation**: Built-in **OpenAPI / Swagger** support with a live UI. +- **📜 Automated Documentation**: **Zero-JSDoc** OpenAPI support — documentation generated directly from Zod schemas. - **🐳 DevOps Ready**: One-click **Docker** and **docker-compose** setup. - **🪵 Observability**: High-performance logging with **Pino** or **Winston**. -- **⚡ Performance**: Optimized for fast startup and low overhead. +- **🚀 Performance**: Optimized for fast startup, low overhead, and developer velocity. --- diff --git a/packages/create-express-forge/package.json b/packages/create-express-forge/package.json index b7e40dd..945106b 100644 --- a/packages/create-express-forge/package.json +++ b/packages/create-express-forge/package.json @@ -1,6 +1,6 @@ { "name": "create-express-forge", - "version": "3.3.2", + "version": "4.1.4", "description": "⚡ Scaffold production-ready Express.js TypeScript backends in seconds", "license": "MIT", "type": "module", @@ -37,6 +37,7 @@ }, "files": [ "dist", + "templates", "README.md", "CHANGELOG.md" ], @@ -47,23 +48,30 @@ "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" }, "dependencies": { "@inquirer/prompts": "^7.1.0", "chalk": "^5.3.0", + "chalk-animation": "^2.0.3", "commander": "^12.1.0", + "eta": "^3.5.0", "execa": "^9.3.0", + "figlet": "^1.11.0", "fs-extra": "^11.2.0", - "ora": "^8.1.1" + "gradient-string": "^3.0.0", + "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/chalk-animation": "^1.6.3", + "@types/figlet": "^1.7.0", "@types/fs-extra": "^11.0.4", + "@types/gradient-string": "^1.1.6", "@types/node": "^20.14.0", "tsup": "^8.1.0", "tsx": "^4.15.0", diff --git a/packages/create-express-forge/src/generator/base.ts b/packages/create-express-forge/src/generator/base.ts index da9e787..e7669dd 100644 --- a/packages/create-express-forge/src/generator/base.ts +++ b/packages/create-express-forge/src/generator/base.ts @@ -1,69 +1,87 @@ -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"); + + // Root files + await tmpl.renderTemplateFile( + "base/tsconfig.json.eta", + path.join(dir, "tsconfig.json"), + ); + await tmpl.renderTemplateFile( + "base/biome.json.eta", + path.join(dir, "biome.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"), + ); -export async function generateBaseFiles(opts: CliOptions, dir: string): Promise { - const { orm, database, logger, testing, docker, projectName } = opts; - const hasDb = orm !== 'none' && database !== 'none'; + // 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 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'], - }); + // Config + await tmpl.renderTemplateFile( + "base/src/config/env.ts.eta", + path.join(src, "config", "env.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' : ''}`, + // 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"), ); - 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')); + // 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..5556185 100644 --- a/packages/create-express-forge/src/generator/features/openapi.ts +++ b/packages/create-express-forge/src/generator/features/openapi.ts @@ -1,63 +1,21 @@ -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"), + ); + await tmpl.renderTemplateFile( + "features/openapi/registry.ts.eta", + path.join(docsDir, "registry.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..8ddef02 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@2.4.13", "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..a712977 100644 --- a/packages/create-express-forge/src/generator/structure/modular.ts +++ b/packages/create-express-forge/src/generator/structure/modular.ts @@ -1,96 +1,65 @@ -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'; +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): Promise { - const src = path.join(dir, 'src'); - const tm = new TemplateManager(opts); +export async function generateModularStructure( + opts: CliOptions, + dir: string, + tmpl: TemplateManager, +): Promise { + const src = path.join(dir, "src"); - 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)); + 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.schema.ts.eta", + path.join(src, "modules", "health", "health.schema.ts"), + ); + 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 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"), + ); - 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`); + 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..32d98c3 100644 --- a/packages/create-express-forge/src/generator/structure/mvc.ts +++ b/packages/create-express-forge/src/generator/structure/mvc.ts @@ -1,61 +1,66 @@ -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'; +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): Promise { - const src = path.join(dir, 'src'); - const tm = new TemplateManager(opts); +export async function generateMvcStructure( + opts: CliOptions, + dir: string, + tmpl: TemplateManager, +): Promise { + const src = path.join(dir, "src"); - 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)); + 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 tmpl.renderTemplateFile( + "structure/mvc/schemas/health.schema.ts.eta", + path.join(src, "schemas", "health.schema.ts"), + ); + 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"), + ); - 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`); + 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..bef87c6 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 = {}) { - displayBanner(); +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 = {}, +) { + await 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, @@ -27,6 +47,9 @@ export async function runCLI(initialProjectName?: string, cmdOptions: Record - /^[a-z0-9-_]+$/.test(v) + /^[a-z0-9-_.]+$/.test(v) ? true - : 'Use lowercase letters, numbers, hyphens, or underscores', + : "Use lowercase letters, numbers, hyphens, underscores, or a period", })); - - 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, @@ -159,5 +246,8 @@ export async function runCLI(initialProjectName?: string, cmdOptions: Record { + const username = os.userInfo().username; + const capitalizedUser = + username.charAt(0).toUpperCase() + username.slice(1).toLowerCase(); + + const welcomeText = figlet.textSync(`WELCOME, ${capitalizedUser}!`, { + font: "Slant", + horizontalLayout: "default", + }); + + console.log(); + const rainbow = chalkAnimation.rainbow(welcomeText); + + // Let the rainbow shine for a bit + await new Promise((resolve) => setTimeout(resolve, 1500)); + rainbow.stop(); + + console.log(); + console.log( + chalk.bold.hex("#7C3AED")( + " ────────────────────────────────────────────────────────────────", + ), + ); 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..c992055 100644 --- a/packages/create-express-forge/src/utils/package-builder.ts +++ b/packages/create-express-forge/src/utils/package-builder.ts @@ -1,129 +1,135 @@ -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"; + deps["@asteasolutions/zod-to-openapi"] = "^7.3.0"; + 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..892eeee 100644 --- a/packages/create-express-forge/src/utils/template-manager.ts +++ b/packages/create-express-forge/src/utils/template-manager.ts @@ -1,238 +1,81 @@ -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; + /** + * 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 { + let rendered = await this.eta.renderAsync(templatePath, this.opts); + + if (this.opts.importAlias && destPath.includes(`${path.sep}src${path.sep}`)) { + const srcIndex = destPath.lastIndexOf(`${path.sep}src${path.sep}`); + const srcDir = destPath.substring(0, srcIndex + 4); // path/to/src + + const importRegex = /(from\s+|import\(\s*)(['"])(\.\/|\.\.\/)([^'"]+)\2/g; + + rendered = rendered.replace(importRegex, (match, prefix, quote, dotPrefix, relPath) => { + const currentDir = path.dirname(destPath); + const absoluteImportPath = path.resolve(currentDir, dotPrefix + relPath); + + if (absoluteImportPath.startsWith(srcDir)) { + let aliasPath = path.relative(srcDir, absoluteImportPath); + aliasPath = aliasPath.split(path.sep).join('/'); // Ensure forward slashes + return `${prefix}${quote}@/${aliasPath}${quote}`; + } + return match; + }); + } - ${ - jwtStorage === 'cookie' - ? "token = req.cookies?.token;" - : "if (req.headers.authorization?.startsWith('Bearer ')) { token = req.headers.authorization.split(' ')[1]; }" + await fs.ensureDir(path.dirname(destPath)); + await fs.writeFile(destPath, rendered, "utf-8"); } - if (!token) { - throw ApiError.unauthorized('Authentication required'); + /** + * 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/biome.json.eta b/packages/create-express-forge/templates/base/biome.json.eta new file mode 100644 index 0000000..3b680af --- /dev/null +++ b/packages/create-express-forge/templates/base/biome.json.eta @@ -0,0 +1,30 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.13/schema.json", + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noNonNullAssertion": "warn" + }, + "correctness": { + "noUnusedVariables": "warn" + } + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80 + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "trailingCommas": "all" + } + }, + "files": { + "ignore": ["dist/**", "node_modules/**"] + } +} 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..1ecba59 --- /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/v1/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..58533a4 --- /dev/null +++ b/packages/create-express-forge/templates/base/src/server.ts.eta @@ -0,0 +1,76 @@ +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);<% } %> + if (env.NODE_ENV === 'production') { + process.exit(1); + } + <% if (it.logger !== 'none') { %>logger.warn('⚠️ Continuing in development mode without database');<% } %> +} +<% } %> + +<% if (it.cache === 'redis') { %> +import { connectRedis } from './cache/index.js'; +try { + await connectRedis(); +} catch (err) { + <% if (it.logger !== 'none') { %>logger.error('❌ Redis connection failed', err);<% } %> + if (env.NODE_ENV === 'production') { + process.exit(1); + } + <% if (it.logger !== 'none') { %>logger.warn('⚠️ Continuing in development mode without Redis');<% } %> +} +<% } %> + +const server = app.listen(env.PORT, () => { + <% if (it.logger !== 'none') { %> + logger.info(`🚀 Server running on port ${env.PORT} in ${env.NODE_ENV} mode`); + <% if (it.openapi) { %> + logger.info(`📖 API Documentation available at http://localhost:${env.PORT}/api-docs`); + <% } %> + <% } else { %> + console.log(`🚀 Server running on port ${env.PORT} in ${env.NODE_ENV} mode`); + <% if (it.openapi) { %> + console.log(`📖 API Documentation available at http://localhost:${env.PORT}/api-docs`); + <% } %> + <% } %> +}); + +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..3bfe5c6 --- /dev/null +++ b/packages/create-express-forge/templates/base/src/utils/ApiResponse.ts.eta @@ -0,0 +1,24 @@ +import type { Response } from 'express'; + +export interface Pagination { + total: number; + page: number; + limit: number; + pages: number; +} + +export function sendSuccess(res: Response, data: T, message = 'Success', statusCode = 200) { + return res.status(statusCode).json({ success: true, message, data }); +} + +export function sendCreated(res: Response, data: T, message = 'Created') { + return sendSuccess(res, data, message, 201); +} + +export function sendNoContent(res: Response) { + return res.status(204).send(); +} + +export function sendPaginated(res: Response, data: T[], pagination: Pagination, 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..3c91815 --- /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 { id: string; email: string; role: string }; + 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..a725e24 --- /dev/null +++ b/packages/create-express-forge/templates/features/docker/docker-compose.yml.eta @@ -0,0 +1,66 @@ +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 +<% } %> +<% if ((it.database !== 'sqlite' && it.database !== 'none') || it.cache === 'redis') { %> + 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" +<% } %> + +<% if (it.database !== 'sqlite' && it.database !== 'none') { %> +volumes: +<% if (it.database === 'postgresql') { %> + pgdata: +<% } else 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/registry.ts.eta b/packages/create-express-forge/templates/features/openapi/registry.ts.eta new file mode 100644 index 0000000..7f369e9 --- /dev/null +++ b/packages/create-express-forge/templates/features/openapi/registry.ts.eta @@ -0,0 +1,3 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; + +export const registry = new OpenAPIRegistry(); 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..0fa6d06 --- /dev/null +++ b/packages/create-express-forge/templates/features/openapi/swagger.ts.eta @@ -0,0 +1,35 @@ +import { OpenApiGeneratorV3 } from '@asteasolutions/zod-to-openapi'; +import swaggerUi from 'swagger-ui-express'; +import type { Express } from 'express'; +import { registry } from './registry.js'; + +export function setupSwagger(app: Express) { +<% if (it.auth === 'jwt') { %> + registry.registerComponent('securitySchemes', 'bearerAuth', { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }); +<% } %> + + const generator = new OpenApiGeneratorV3(registry.definitions); + + const swaggerSpec = generator.generateDocument({ + 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') { %> + security: [{ bearerAuth: [] }] +<% } %> + }); + + 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..f167313 --- /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 '../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..ba9e582 --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/auth/auth.controller.ts.eta @@ -0,0 +1,20 @@ +import type { Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; +import { sendSuccess } 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 jwt.SignOptions['expiresIn'] } + ); + +<% if (it.jwtStorage === 'cookie') { %> + res.cookie('token', token, { httpOnly: true, secure: env.NODE_ENV === 'production' }); + return sendSuccess(res, { token }, 'Logged in successfully'); +<% } else { %> + return sendSuccess(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..578ae40 --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/auth/auth.routes.ts.eta @@ -0,0 +1,10 @@ +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(); + +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..05bb54c --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/auth/auth.schema.ts.eta @@ -0,0 +1,47 @@ +import { z } from 'zod'; +<% if (it.openapi) { %> +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import { registry } from '../../docs/registry.js'; + +extendZodWithOpenApi(z); + +export const AuthSchema = registry.register('Auth', z.object({ + token: z.string(), +})); + +registry.registerPath({ + method: 'post', + path: '/auth/login', + tags: ['Auth'], + summary: 'Login to get a JWT token', + request: { + body: { + content: { + 'application/json': { + schema: z.object({ + email: z.string().email(), + password: z.string().min(8), + }), + }, + }, + }, + }, + responses: { + 200: { + description: 'Login successful', + content: { + 'application/json': { + schema: AuthSchema, + }, + }, + }, + }, +}); +<% } %> + +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..468def9 --- /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 { sendSuccess } from '../../utils/ApiResponse.js'; + +export const getHealth = (_req: Request, res: Response) => + sendSuccess(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..a1fba6c --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/health/health.routes.ts.eta @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { getHealth } from './health.controller.js'; +import './health.schema.js'; + +const router = Router(); + +router.get('/', getHealth); + +export { router as healthRouter }; diff --git a/packages/create-express-forge/templates/structure/modular/modules/health/health.schema.ts.eta b/packages/create-express-forge/templates/structure/modular/modules/health/health.schema.ts.eta new file mode 100644 index 0000000..9c40dff --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/health/health.schema.ts.eta @@ -0,0 +1,35 @@ +import { z } from 'zod'; +<% if (it.openapi) { %> +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import { registry } from '../../docs/registry.js'; + +extendZodWithOpenApi(z); + +registry.registerPath({ + method: 'get', + path: '/health', + tags: ['Health'], + summary: 'Health check endpoint', + description: 'Responds if the app is up and running', + responses: { + 200: { + description: 'App is up and running', + content: { + 'application/json': { + schema: z.object({ + success: z.boolean(), + message: z.string(), + timestamp: z.string(), + }), + }, + }, + }, + }, +}); +<% } %> + +export const healthResponseSchema = z.object({ + success: z.boolean(), + message: z.string(), + timestamp: z.string(), +}); 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..0710a16 --- /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 { sendSuccess, sendCreated, sendNoContent } 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 sendSuccess(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 sendSuccess(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 sendCreated(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 sendSuccess(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 sendNoContent(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..7ef58b3 --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/todos/todos.routes.ts.eta @@ -0,0 +1,17 @@ +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);<% } %> + +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..679cf3c --- /dev/null +++ b/packages/create-express-forge/templates/structure/modular/modules/todos/todos.schema.ts.eta @@ -0,0 +1,81 @@ +import { z } from 'zod'; +<% if (it.openapi) { %> +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import { registry } from '../../docs/registry.js'; + +extendZodWithOpenApi(z); + +export const TodoSchema = registry.register('Todo', z.object({ + id: z.string().uuid(), + title: z.string().min(1).max(100), + description: z.string().max(500).optional(), + completed: z.boolean(), + createdAt: z.date(), + updatedAt: z.date(), +})); + +registry.registerPath({ + method: 'get', + path: '/todos', + tags: ['Todos'], + summary: 'Get all todos', + responses: { + 200: { + description: 'List of todos', + content: { + 'application/json': { + schema: z.array(TodoSchema), + }, + }, + }, + }, +}); + +registry.registerPath({ + method: 'post', + path: '/todos', + tags: ['Todos'], + summary: 'Create a new todo', + request: { + body: { + content: { + 'application/json': { + schema: z.object({ + title: z.string().min(1).max(100), + description: z.string().max(500).optional(), + }), + }, + }, + }, + }, + responses: { + 201: { + description: 'Todo created', + content: { + 'application/json': { + schema: TodoSchema, + }, + }, + }, + }, +}); +<% } %> + +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..a60716a --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/controllers/auth.controller.ts.eta @@ -0,0 +1,20 @@ +import type { Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; +import { sendSuccess } 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 jwt.SignOptions['expiresIn'] } + ); + +<% if (it.jwtStorage === 'cookie') { %> + res.cookie('token', token, { httpOnly: true, secure: env.NODE_ENV === 'production' }); + return sendSuccess(res, { token }, 'Logged in successfully'); +<% } else { %> + return sendSuccess(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..c1bdf16 --- /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 { sendSuccess } from '../utils/ApiResponse.js'; + +export const getHealth = (_req: Request, res: Response) => + sendSuccess(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..5d6c4a2 --- /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 { sendSuccess, sendCreated, sendNoContent } 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) => + sendSuccess(res, await TodoService.findAll(<% if (it.auth !== 'none') { %>req.user?.id || 'guest'<% } %>), 'Todos fetched'); + +export const getTodoById = async (req: Request, res: Response) => + sendSuccess(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) => + sendCreated(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) => + sendSuccess(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 sendNoContent(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..cd4c3a4 --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/routes/auth.routes.ts.eta @@ -0,0 +1,10 @@ +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(); + +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..e27288d --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/routes/health.routes.ts.eta @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { getHealth } from '../controllers/health.controller.js'; +import '../schemas/health.schema.js'; + +const router = Router(); + +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..fef2e58 --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/routes/todo.routes.ts.eta @@ -0,0 +1,17 @@ +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.js'; +<% if (it.auth !== 'none') { %>import { auth } from '../middleware/auth.middleware.js';<% } %> + +const router = Router(); + +<% if (it.auth !== 'none') { %>router.use(auth);<% } %> + +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..0a68a8e --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/schemas/auth.schema.ts.eta @@ -0,0 +1,47 @@ +import { z } from 'zod'; +<% if (it.openapi) { %> +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import { registry } from '../docs/registry.js'; + +extendZodWithOpenApi(z); + +export const AuthSchema = registry.register('Auth', z.object({ + token: z.string(), +})); + +registry.registerPath({ + method: 'post', + path: '/auth/login', + tags: ['Auth'], + summary: 'Login to get a JWT token', + request: { + body: { + content: { + 'application/json': { + schema: z.object({ + email: z.string().email(), + password: z.string().min(8), + }), + }, + }, + }, + }, + responses: { + 200: { + description: 'Login successful', + content: { + 'application/json': { + schema: AuthSchema, + }, + }, + }, + }, +}); +<% } %> + +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/health.schema.ts.eta b/packages/create-express-forge/templates/structure/mvc/schemas/health.schema.ts.eta new file mode 100644 index 0000000..444ebca --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/schemas/health.schema.ts.eta @@ -0,0 +1,35 @@ +import { z } from 'zod'; +<% if (it.openapi) { %> +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import { registry } from '../docs/registry.js'; + +extendZodWithOpenApi(z); + +registry.registerPath({ + method: 'get', + path: '/health', + tags: ['Health'], + summary: 'Health check endpoint', + description: 'Responds if the app is up and running', + responses: { + 200: { + description: 'App is up and running', + content: { + 'application/json': { + schema: z.object({ + success: z.boolean(), + message: z.string(), + timestamp: z.string(), + }), + }, + }, + }, + }, +}); +<% } %> + +export const healthResponseSchema = z.object({ + success: z.boolean(), + message: z.string(), + timestamp: z.string(), +}); 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..f73af1e --- /dev/null +++ b/packages/create-express-forge/templates/structure/mvc/schemas/todo.schema.ts.eta @@ -0,0 +1,81 @@ +import { z } from 'zod'; +<% if (it.openapi) { %> +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import { registry } from '../docs/registry.js'; + +extendZodWithOpenApi(z); + +export const TodoSchema = registry.register('Todo', z.object({ + id: z.string().uuid(), + title: z.string().min(1).max(100), + description: z.string().max(500).optional(), + completed: z.boolean(), + createdAt: z.date(), + updatedAt: z.date(), +})); + +registry.registerPath({ + method: 'get', + path: '/todos', + tags: ['Todos'], + summary: 'Get all todos', + responses: { + 200: { + description: 'List of todos', + content: { + 'application/json': { + schema: z.array(TodoSchema), + }, + }, + }, + }, +}); + +registry.registerPath({ + method: 'post', + path: '/todos', + tags: ['Todos'], + summary: 'Create a new todo', + request: { + body: { + content: { + 'application/json': { + schema: z.object({ + title: z.string().min(1).max(100), + description: z.string().max(500).optional(), + }), + }, + }, + }, + }, + responses: { + 201: { + description: 'Todo created', + content: { + 'application/json': { + schema: TodoSchema, + }, + }, + }, + }, +}); +<% } %> + +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..fea6d1d 100644 --- a/packages/create-express-forge/tests/smoke.integration.test.ts +++ b/packages/create-express-forge/tests/smoke.integration.test.ts @@ -1,66 +1,88 @@ -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 + + it("should pass linting checks", async () => { + const targetPath = path.join(testDir, projectName); + + console.log("Running lint check in generated project..."); + const lint = await execa("npm", ["run", "lint"], { cwd: targetPath }); + expect(lint.exitCode).toBe(0); + }, 60000); }); 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/CHANGELOG.md b/packages/lint-config/CHANGELOG.md new file mode 100644 index 0000000..7990100 --- /dev/null +++ b/packages/lint-config/CHANGELOG.md @@ -0,0 +1,8 @@ +# @repo/lint-config + +## 0.1.0 + +### Minor Changes + +- d617dbf: ### 🚀 Core CLI & Codebase Modernization +- d617dbf: Internal refactor 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..111aa64 --- /dev/null +++ b/packages/lint-config/package.json @@ -0,0 +1,8 @@ +{ + "name": "@repo/lint-config", + "version": "0.1.0", + "private": true, + "files": [ + "biome.base.json" + ] +} diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md new file mode 100644 index 0000000..f5553bf --- /dev/null +++ b/packages/mcp/CHANGELOG.md @@ -0,0 +1,21 @@ +# @create-express-forge/mcp + +## 1.0.1 + +### Patch Changes + +- e7113ba: - Added support for scaffolding in the current directory (`.`) + - Implemented automated import alias resolution (`@/`) for scaffolded projects + - Fixed relative import bug in smoke test template + - Synchronized documentation with latest v4 features (Zod-to-OpenAPI, Biome, Pro Fail-Fast) + - Added a **High-Fidelity Personalized Welcome** message using a large gradient-styled font (powered by `figlet` and `gradient-string`) to greet users by name + - Added documentation versioning support with v3 LTS dropdown + - Enhanced documentation SEO, OpenGraph metadata, and social sharing tags + - Standardized product naming to "Create Express Forge" across docs and CLI + - Added a dedicated **MCP Server** (`@create-express-forge/mcp`) for AI-assisted scaffolding and documentation access + - Automated generation of LLM-friendly documentation files (`llms.txt`, `llms-full.txt`, `ai.json`) + - Highlighted **Ultra-Fast scaffolding** in the v3 vs v4 comparison (powered by internal refactoring) + - Updated root and package READMEs with new features and Biome integration + - Removed deprecated `cef` alias from documentation + - Fixed file paths in CONTRIBUTING.md + - Updated v3 documentation and root README to target the correct stable legacy version (`3.3.2`) diff --git a/packages/mcp/package.json b/packages/mcp/package.json new file mode 100644 index 0000000..2c88e16 --- /dev/null +++ b/packages/mcp/package.json @@ -0,0 +1,27 @@ +{ + "name": "@create-express-forge/mcp", + "version": "1.0.1", + "description": "MCP Server for Create Express Forge documentation and tools", + "type": "module", + "bin": { + "create-express-forge-mcp": "./dist/index.js" + }, + "scripts": { + "dev": "tsx src/index.ts", + "build": "tsup src/index.ts --format esm --clean", + "start": "node dist/index.js", + "check-types": "tsc --noEmit", + "lint": "biome lint ." + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@repo/typescript-config": "workspace:*", + "@types/node": "^20.14.0", + "tsup": "^8.1.0", + "tsx": "^4.15.0", + "typescript": "5.5.3" + } +} diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts new file mode 100644 index 0000000..147caf2 --- /dev/null +++ b/packages/mcp/src/index.ts @@ -0,0 +1,168 @@ +#!/usr/bin/env node +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; + +const DOCS_BASE = "https://code-y02.github.io/express-cli"; + +const server = new McpServer({ + name: "@create-express-forge/mcp", + version: "1.0.0", +}); + +// ─── Resources ───────────────────────────────────────────────────────────── + +// Discovery Manifest (JSON index) +server.registerResource( + "manifest", + `${DOCS_BASE}/ai.json`, + { + description: "Machine-readable index of capabilities, CLI flags, common patterns, and documentation slugs.", + }, + async (uri) => { + try { + const res = await fetch(uri.href); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const json = await res.text(); + return { contents: [{ uri: uri.href, mimeType: "application/json", text: json }] }; + } catch (_e) { + // Fallback to llms.txt if ai.json is missing + const fallbackUrl = `${DOCS_BASE}/llms.txt`; + const res = await fetch(fallbackUrl); + if (!res.ok) throw new Error(`Manifest and Fallback failed: ${res.status}`); + const text = await res.text(); + return { contents: [{ uri: fallbackUrl, mimeType: "text/plain", text }] }; + } + }, +); + +// Full documentation (primary resource) +server.registerResource( + "docs-full", + `${DOCS_BASE}/llms-full.txt`, + { + description: "Full Create Express Forge API documentation, architecture guides, and CLI reference.", + }, + async (uri) => { + try { + const res = await fetch(uri.href); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const text = await res.text(); + return { contents: [{ uri: uri.href, mimeType: "text/plain", text }] }; + } catch (_e) { + const fallbackUrl = `${DOCS_BASE}/llms.txt`; + const res = await fetch(fallbackUrl); + if (!res.ok) throw new Error(`Full Docs and Fallback failed: ${res.status}`); + const text = await res.text(); + return { contents: [{ uri: fallbackUrl, mimeType: "text/plain", text }] }; + } + }, +); + +// ─── Tools ────────────────────────────────────────────────────────────────── + +/** + * get-docs — fetch the full documentation on demand. + */ +server.registerTool( + "get-docs", + { + description: "Fetch the full documentation for a specific version of Create Express Forge.", + inputSchema: z.object({ + version: z.enum(["v4", "v3"]).default("v4").describe("Documentation version (v4 = latest, v3 = lts)"), + }), + }, + async ({ version }) => { + const subPath = version === "v3" ? "/v3" : ""; + const res = await fetch(`${DOCS_BASE}${subPath}/llms-full.txt`); + if (!res.ok) { + return { + content: [{ type: "text", text: `Failed to fetch ${version} documentation (HTTP ${res.status}).` }], + isError: true, + }; + } + const text = await res.text(); + return { + content: [{ type: "text", text: `# Create Express Forge ${version} Documentation\nSource: ${DOCS_BASE}${subPath}/llms-full.txt\n\n${text}` }], + }; + } +); + +/** + * get-page — fetch a specific documentation page by slug and version. + */ +server.registerTool( + "get-page", + { + description: "Fetch a specific Create Express Forge documentation page for a specific version.", + inputSchema: z.object({ + slug: z.string().describe("Page slug, e.g. 'guide/getting-started' or 'reference/cli-options'"), + version: z.enum(["v4", "v3"]).default("v4").describe("Documentation version"), + }), + }, + async ({ slug, version }) => { + const cleanSlug = slug.replace(/^\/+|\/+$/g, ""); + const subPath = version === "v3" ? "/v3" : ""; + const url = `${DOCS_BASE}${subPath}/${cleanSlug}.md`; + const res = await fetch(url); + if (!res.ok) { + return { + content: [{ type: "text", text: `Page not found for version ${version}: ${url}. Check the 'manifest' resource for available slugs.` }], + isError: true, + }; + } + const text = await res.text(); + return { + content: [{ type: "text", text: `# Documentation (${version}): ${cleanSlug}\nSource: ${url}\n\n${text}` }], + }; + } +); + +/** + * get-scaffold-command — helper to generate the correct npx command based on requirements. + */ +server.registerTool( + "get-scaffold-command", + { + description: "Generate the correct npx create-express-forge command with appropriate flags based on project requirements.", + inputSchema: z.object({ + version: z.enum(["v4", "v3"]).default("v4").describe("Version of the CLI to use (v4 = latest, v3 = 3.3.2)"), + projectName: z.string().default("my-api").describe("Name of the project"), + pattern: z.enum(["modular", "mvc"]).optional().describe("Architecture pattern"), + orm: z.enum(["prisma", "sequelize", "none"]).optional().describe("ORM to use"), + db: z.enum(["postgresql", "mysql", "sqlite", "none"]).optional().describe("Database type"), + install: z.boolean().optional().describe("Whether to auto-install dependencies"), + }), + }, + async ({ version, projectName, pattern, orm, db, install }) => { + const versionTag = version === "v3" ? "3.3.2" : "latest"; + let command = `npx create-express-forge@${versionTag} ${projectName}`; + if (pattern) command += ` --pattern ${pattern}`; + if (orm) command += ` --orm ${orm}`; + if (db) command += ` --db ${db}`; + if (install !== undefined) command += ` --install ${install}`; + + return { + content: [ + { + type: "text", + text: `Use the following command to scaffold your project:\n\n\`\`\`bash\n${command}\n\`\`\`` + } + ], + }; + } +); + +// ─── Start Server ─────────────────────────────────────────────────────────── + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Create Express Forge MCP server running on stdio"); +} + +main().catch((error) => { + console.error("Fatal error in main():", error); + process.exit(1); +}); + diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json new file mode 100644 index 0000000..e3459f4 --- /dev/null +++ b/packages/mcp/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/typescript-config/CHANGELOG.md b/packages/typescript-config/CHANGELOG.md new file mode 100644 index 0000000..e5347f5 --- /dev/null +++ b/packages/typescript-config/CHANGELOG.md @@ -0,0 +1,8 @@ +# @repo/typescript-config + +## 0.1.0 + +### Minor Changes + +- d617dbf: ### 🚀 Core CLI & Codebase Modernization +- d617dbf: Internal refactor diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index 3c3fc92..b01697d 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@repo/typescript-config", - "version": "0.0.0", + "version": "0.1.0", "private": true, "license": "MIT" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b9bf6d..a689b60 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 @@ -29,6 +26,15 @@ importers: docs: devDependencies: + '@types/fs-extra': + specifier: ^11.0.4 + version: 11.0.4 + fs-extra: + specifier: ^11.2.0 + version: 11.3.4 + tsx: + specifier: ^4.15.0 + version: 4.21.0 vitepress: specifier: ^1.3.1 version: 1.6.4(@algolia/client-search@5.50.2)(@types/node@20.19.39)(postcss@8.5.10)(search-insights@2.17.3)(typescript@5.5.3) @@ -44,34 +50,55 @@ importers: chalk: specifier: ^5.3.0 version: 5.6.2 + chalk-animation: + specifier: ^2.0.3 + version: 2.0.3 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 + figlet: + specifier: ^1.11.0 + version: 1.11.0 fs-extra: specifier: ^11.2.0 version: 11.3.4 + gradient-string: + specifier: ^3.0.0 + version: 3.0.0 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 + '@types/chalk-animation': + specifier: ^1.6.3 + version: 1.6.3 + '@types/figlet': + specifier: ^1.7.0 + version: 1.7.0 '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 + '@types/gradient-string': + specifier: ^1.1.6 + version: 1.1.6 '@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 +112,32 @@ importers: specifier: ^1.6.0 version: 1.6.1(@types/node@20.19.39) - packages/eslint-config: + packages/lint-config: {} + + packages/mcp: 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) + '@modelcontextprotocol/sdk': + specifier: ^1.0.1 + version: 1.29.0(zod@3.25.76) + zod: + specifier: ^3.23.8 + version: 3.25.76 + devDependencies: + '@repo/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/node': + specifier: ^20.14.0 + version: 20.19.39 + tsup: + specifier: ^8.1.0 + version: 8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.5.3) + tsx: + specifier: ^4.15.0 + version: 4.21.0 + typescript: + specifier: 5.5.3 + version: 5.5.3 packages/typescript-config: {} @@ -174,6 +219,10 @@ packages: resolution: {integrity: sha512-Mu9BFtgzGqDUy5Bcs2nMyoILIFSN13GKQaklKAFIsd0K3/9CpNyfeBc+/+Qs6mFZLlxG9qzullO7h+bjcTBuGQ==} engines: {node: '>= 14.0.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -195,6 +244,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,36 +669,11 @@ 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} + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} 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 + hono: ^4 '@iconify-json/simple-icons@1.2.79': resolution: {integrity: sha512-aNyO7Fd1qej9oQfIyohYFRv0lhQLaZ+6UkK1c1qwax0MDPUOZOdq65MlU500kow97pD/W+b2u1And3e25eE24Q==} @@ -761,6 +838,16 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -962,12 +1049,21 @@ packages: cpu: [arm64] os: [win32] + '@types/chalk-animation@1.6.3': + resolution: {integrity: sha512-UdaYgzPvgOAJY+5cn3y1rQK5EpJDJOBivF5ftFxe5lNruU1/zTPkmh11iVvRd8tyzVwx92QQLaAWi1fpBAj7aw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/figlet@1.7.0': + resolution: {integrity: sha512-KwrT7p/8Eo3Op/HBSIwGXOsTZKYiM9NpWRBJ5sVjWP/SmlS+oxxRvJht/FNAtliJvja44N3ul1yATgohnVBV0Q==} + '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + '@types/gradient-string@1.1.6': + resolution: {integrity: sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -986,76 +1082,27 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/minimist@1.2.5': + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} '@types/node@20.19.39': resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/tinycolor2@1.4.6': + resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} '@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,10 +1216,9 @@ 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 + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} acorn-walk@8.3.5: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} @@ -1183,8 +1229,16 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - ajv@6.14.0: - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} algoliasearch@5.50.2: resolution: {integrity: sha512-Tfp26yoNWurUjfgK4GOrVJQhSNXu9tJtHfFFNosgT2YClG+vPyUjX/gbC8rG39qLncnZg8Fj34iarQWpMkqefw==} @@ -1223,12 +1277,13 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + 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,11 +1291,9 @@ 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==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -1252,13 +1305,29 @@ packages: peerDependencies: esbuild: '>=0.18' + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + camelcase-keys@7.0.2: + resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} + engines: {node: '>=12'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1267,6 +1336,11 @@ packages: resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} engines: {node: '>=4'} + chalk-animation@2.0.3: + resolution: {integrity: sha512-Q/GJj6eqcI3HUSz72hUIZtjTlzIHXet8GOteO79Fzx8FMlSywCNP8tpX8uiOOdeMSXFXTXCajZ/Y2FvcU3MOLA==} + engines: {node: '>=12'} + hasBin: true + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1317,13 +1391,14 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@4.1.1: 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==} @@ -1331,10 +1406,30 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1351,12 +1446,25 @@ packages: supports-color: optional: true + decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decamelize@5.0.1: + resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} + engines: {node: '>=10'} + deep-eql@4.1.4: 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==} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} @@ -1377,9 +1485,12 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} @@ -1390,6 +1501,10 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -1398,6 +1513,21 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -1408,54 +1538,35 @@ 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} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 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'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} @@ -1465,6 +1576,16 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} + express-rate-limit@8.4.1: + resolution: {integrity: sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -1475,11 +1596,8 @@ packages: 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==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1493,18 +1611,23 @@ packages: picomatch: optional: true + figlet@1.11.0: + resolution: {integrity: sha512-EEx3OS/l2bFqcUNN2NM9FPJp8vAMrgbCxsbl2hbcJNNxOEwVe3mEzrhan7TbJQViZa8mMqhihlbCaqD+LyYKTQ==} + engines: {node: '>= 17.0.0'} + hasBin: true + figures@6.1.0: 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'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -1516,16 +1639,17 @@ packages: 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==} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-extra@11.3.4: resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} engines: {node: '>=14.14'} @@ -1538,14 +1662,14 @@ 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} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-east-asian-width@1.5.0: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} @@ -1553,6 +1677,14 @@ packages: get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -1568,44 +1700,65 @@ 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'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + 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==} + gradient-string@2.0.2: + resolution: {integrity: sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw==} + engines: {node: '>=10'} + + gradient-string@3.0.0: + resolution: {integrity: sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg==} + engines: {node: '>=14'} + + hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hono@4.12.15: + resolution: {integrity: sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==} + engines: {node: '>=16.9.0'} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -1631,21 +1784,28 @@ 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. + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1666,14 +1826,17 @@ 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@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1705,10 +1868,16 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} @@ -1720,14 +1889,14 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -1735,12 +1904,9 @@ packages: 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'} + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} @@ -1765,9 +1931,6 @@ packages: 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==} @@ -1778,15 +1941,43 @@ packages: loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + meow@10.1.5: + resolution: {integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -1813,6 +2004,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} @@ -1821,12 +2020,9 @@ 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'} + minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} minisearch@7.2.0: resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} @@ -1856,8 +2052,13 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} @@ -1871,6 +2072,14 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1885,10 +2094,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'} @@ -1931,22 +2136,22 @@ 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-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-exists@4.0.0: 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'} @@ -1955,6 +2160,9 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -1990,6 +2198,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -2018,20 +2230,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,9 +2246,13 @@ 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'} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -2053,9 +2260,29 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + read-pkg-up@8.0.0: + resolution: {integrity: sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==} + engines: {node: '>=12'} + + read-pkg@6.0.0: + resolution: {integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==} + engines: {node: '>=12'} + read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -2064,6 +2291,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + redent@4.0.0: + resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} + engines: {node: '>=12'} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -2073,9 +2304,9 @@ 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'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} @@ -2095,16 +2326,15 @@ 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'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2119,6 +2349,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2130,6 +2371,22 @@ packages: shiki@2.5.0: resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2155,6 +2412,18 @@ packages: spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + speakingurl@14.0.1: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} @@ -2165,6 +2434,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -2203,9 +2476,9 @@ 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-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} strip-literal@2.1.1: resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} @@ -2230,9 +2503,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'} @@ -2243,6 +2513,9 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -2250,6 +2523,9 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinygradient@1.1.5: + resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} + tinypool@0.8.4: resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} engines: {node: '>=14.0.0'} @@ -2262,6 +2538,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -2269,11 +2549,9 @@ 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' + trim-newlines@4.1.1: + resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==} + engines: {node: '>=12'} ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -2306,18 +2584,18 @@ 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==} + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} engines: {node: '>=10'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typescript@5.5.3: resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} engines: {node: '>=14.17'} @@ -2356,8 +2634,16 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -2456,10 +2742,6 @@ 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'} @@ -2467,6 +2749,13 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2483,6 +2772,14 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -2600,6 +2897,12 @@ snapshots: dependencies: '@algolia/client-common': 5.50.2 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -2615,6 +2918,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,40 +3267,9 @@ 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': + '@hono/node-server@1.19.14(hono@4.12.15)': 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': {} + hono: 4.12.15 '@iconify-json/simple-icons@1.2.79': dependencies: @@ -3129,6 +3436,28 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.15) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.8 + express: 5.2.1 + express-rate-limit: 8.4.1(express@5.2.1) + hono: 4.12.15 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3280,13 +3609,21 @@ snapshots: '@turbo/windows-arm64@2.9.6': optional: true + '@types/chalk-animation@1.6.3': {} + '@types/estree@1.0.8': {} + '@types/figlet@1.7.0': {} + '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 '@types/node': 20.19.39 + '@types/gradient-string@1.1.6': + dependencies: + '@types/tinycolor2': 1.4.6 + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -3308,96 +3645,21 @@ snapshots: '@types/mdurl@2.0.0': {} + '@types/minimist@1.2.5': {} + '@types/node@12.20.55': {} '@types/node@20.19.39': dependencies: undici-types: 6.21.0 - '@types/unist@3.0.3': {} - - '@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 + '@types/normalize-package-data@2.4.4': {} - '@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 + '@types/tinycolor2@1.4.6': {} - '@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 + '@types/unist@3.0.3': {} - '@typescript-eslint/visitor-keys@7.18.0': - dependencies: - '@typescript-eslint/types': 7.18.0 - eslint-visitor-keys: 3.4.3 + '@types/web-bluetooth@0.0.21': {} '@ungap/structured-clone@1.3.0': {} @@ -3534,9 +3796,10 @@ snapshots: transitivePeerDependencies: - typescript - acorn-jsx@5.3.2(acorn@8.16.0): + accepts@2.0.0: dependencies: - acorn: 8.16.0 + mime-types: 3.0.2 + negotiator: 1.0.0 acorn-walk@8.3.5: dependencies: @@ -3544,12 +3807,16 @@ snapshots: acorn@8.16.0: {} - ajv@6.14.0: + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.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 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 algoliasearch@5.50.2: dependencies: @@ -3590,9 +3857,9 @@ snapshots: array-union@2.1.0: {} - assertion-error@1.1.0: {} + arrify@1.0.1: {} - balanced-match@1.0.2: {} + assertion-error@1.1.0: {} better-path-resolve@1.0.0: dependencies: @@ -3600,14 +3867,19 @@ snapshots: 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: + body-parser@2.2.2: dependencies: - balanced-match: 1.0.2 + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color braces@3.0.3: dependencies: @@ -3618,9 +3890,28 @@ snapshots: esbuild: 0.27.7 load-tsconfig: 0.2.5 + bytes@3.1.2: {} + cac@6.7.14: {} - callsites@3.1.0: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + camelcase-keys@7.0.2: + dependencies: + camelcase: 6.3.0 + map-obj: 4.3.0 + quick-lru: 5.1.1 + type-fest: 1.4.0 + + camelcase@6.3.0: {} ccount@2.0.1: {} @@ -3634,6 +3925,12 @@ snapshots: pathval: 1.1.1 type-detect: 4.1.0 + chalk-animation@2.0.3: + dependencies: + chalk: 4.1.2 + gradient-string: 2.0.2 + meow: 10.1.5 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -3673,18 +3970,31 @@ snapshots: commander@12.1.0: {} - commander@4.1.1: {} + commander@14.0.3: {} - concat-map@0.0.1: {} + commander@4.1.1: {} confbox@0.1.8: {} consola@3.4.2: {} + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + copy-anything@4.0.5: dependencies: is-what: 5.5.0 + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3697,11 +4007,20 @@ snapshots: dependencies: ms: 2.1.3 + decamelize-keys@1.1.1: + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + + decamelize@1.2.0: {} + + decamelize@5.0.1: {} + deep-eql@4.1.4: dependencies: type-detect: 4.1.0 - deep-is@0.1.4: {} + depd@2.0.0: {} dequal@2.0.3: {} @@ -3717,9 +4036,13 @@ snapshots: dependencies: path-type: 4.0.0 - doctrine@3.0.0: + dunder-proto@1.0.1: dependencies: - esutils: 2.0.3 + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} emoji-regex-xs@1.0.0: {} @@ -3727,6 +4050,8 @@ snapshots: emoji-regex@8.0.0: {} + encodeurl@2.0.0: {} + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -3734,6 +4059,18 @@ snapshots: entities@7.0.1: {} + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -3789,83 +4126,25 @@ 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 + escape-html@1.0.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: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.8: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.8 execa@8.0.1: dependencies: @@ -3894,6 +4173,44 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + express-rate-limit@8.4.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extendable-error@0.1.7: {} fast-deep-equal@3.1.3: {} @@ -3906,9 +4223,7 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} fastq@1.20.1: dependencies: @@ -3918,18 +4233,29 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - figures@6.1.0: + figlet@1.11.0: dependencies: - is-unicode-supported: 2.1.0 + commander: 14.0.3 - file-entry-cache@6.0.1: + figures@6.1.0: dependencies: - flat-cache: 3.2.0 + is-unicode-supported: 2.1.0 fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -3946,18 +4272,14 @@ snapshots: 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 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fs-extra@11.3.4: dependencies: graceful-fs: 4.2.11 @@ -3976,15 +4298,33 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + get-east-asian-width@1.5.0: {} get-func-name@2.0.2: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@8.0.1: {} get-stream@9.0.1: @@ -4000,23 +4340,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 @@ -4026,12 +4349,30 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} - graphemer@1.4.0: {} + gradient-string@2.0.2: + dependencies: + chalk: 4.1.2 + tinygradient: 1.1.5 + + gradient-string@3.0.0: + dependencies: + chalk: 5.6.2 + tinygradient: 1.1.5 + + hard-rejection@2.1.0: {} has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 @@ -4050,10 +4391,24 @@ snapshots: dependencies: '@types/hast': 3.0.4 + hono@4.12.15: {} + hookable@5.5.3: {} + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + html-void-elements@3.0.0: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + human-id@4.1.3: {} human-signals@5.0.0: {} @@ -4068,19 +4423,19 @@ snapshots: ignore@5.3.2: {} - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 + indent-string@5.0.0: {} - imurmurhash@0.1.4: {} + inherits@2.0.4: {} - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 + ip-address@10.1.0: {} - inherits@2.0.4: {} + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.3 is-extglob@2.1.1: {} @@ -4094,10 +4449,12 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} + is-plain-obj@1.1.0: {} is-plain-obj@4.1.0: {} + is-promise@4.0.0: {} + is-stream@3.0.0: {} is-stream@4.0.1: {} @@ -4116,8 +4473,12 @@ snapshots: isexe@2.0.0: {} + jose@6.2.3: {} + joycon@3.1.1: {} + js-tokens@4.0.0: {} + js-tokens@9.0.1: {} js-yaml@3.14.2: @@ -4129,11 +4490,11 @@ snapshots: dependencies: argparse: 2.0.1 - json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} - json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} - json-stable-stringify-without-jsonify@1.0.1: {} + json-schema-typed@8.0.2: {} jsonfile@4.0.0: optionalDependencies: @@ -4145,14 +4506,7 @@ 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 + kind-of@6.0.3: {} lilconfig@3.1.3: {} @@ -4173,8 +4527,6 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.merge@4.6.2: {} - lodash.startcase@4.4.0: {} log-symbols@6.0.0: @@ -4186,12 +4538,22 @@ snapshots: dependencies: get-func-name: 2.0.2 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + map-obj@1.0.1: {} + + map-obj@4.3.0: {} + mark.js@8.11.1: {} + math-intrinsics@1.1.0: {} + mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.4 @@ -4204,6 +4566,25 @@ snapshots: unist-util-visit: 5.1.0 vfile: 6.0.3 + media-typer@1.1.0: {} + + meow@10.1.5: + dependencies: + '@types/minimist': 1.2.5 + camelcase-keys: 7.0.2 + decamelize: 5.0.1 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 8.0.0 + redent: 4.0.0 + trim-newlines: 4.1.1 + type-fest: 1.4.0 + yargs-parser: 20.2.9 + + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -4230,17 +4611,21 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mimic-fn@4.0.0: {} mimic-function@5.0.1: {} - minimatch@3.1.5: - dependencies: - brace-expansion: 1.1.14 - - minimatch@9.0.9: + minimist-options@4.1.0: dependencies: - brace-expansion: 2.1.0 + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 minisearch@7.2.0: {} @@ -4267,7 +4652,14 @@ snapshots: nanoid@3.3.11: {} - natural-compare@1.4.0: {} + negotiator@1.0.0: {} + + normalize-package-data@3.0.3: + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.16.1 + semver: 7.7.4 + validate-npm-package-license: 3.0.4 npm-run-path@5.3.0: dependencies: @@ -4280,6 +4672,12 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -4298,15 +4696,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 @@ -4353,20 +4742,25 @@ snapshots: dependencies: quansync: 0.2.11 - parent-module@1.0.1: + parse-json@5.2.0: dependencies: - callsites: 3.1.0 + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 parse-ms@4.0.0: {} - path-exists@4.0.0: {} + parseurl@1.3.3: {} - path-is-absolute@1.0.1: {} + path-exists@4.0.0: {} path-key@3.1.1: {} path-key@4.0.0: {} + path-to-regexp@8.4.2: {} + path-type@4.0.0: {} pathe@1.1.2: {} @@ -4387,6 +4781,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -4408,12 +4804,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,14 +4818,45 @@ snapshots: property-information@7.1.0: {} - punycode@2.3.1: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.15.1: + dependencies: + side-channel: 1.1.0 quansync@0.2.11: {} queue-microtask@1.2.3: {} + quick-lru@5.1.1: {} + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + react-is@18.3.1: {} + read-pkg-up@8.0.0: + dependencies: + find-up: 5.0.0 + read-pkg: 6.0.0 + type-fest: 1.4.0 + + read-pkg@6.0.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 3.0.3 + parse-json: 5.2.0 + type-fest: 1.4.0 + read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -4443,6 +4866,11 @@ snapshots: readdirp@4.1.2: {} + redent@4.0.0: + dependencies: + indent-string: 5.0.0 + strip-indent: 4.1.1 + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -4453,7 +4881,7 @@ snapshots: dependencies: regex-utilities: 2.3.0 - resolve-from@4.0.0: {} + require-from-string@2.0.2: {} resolve-from@5.0.0: {} @@ -4468,10 +4896,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 @@ -4503,6 +4927,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.2 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -4513,6 +4947,33 @@ snapshots: semver@7.7.4: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4530,6 +4991,34 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -4547,12 +5036,28 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.23 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + speakingurl@14.0.1: {} sprintf-js@1.0.3: {} stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} stdin-discarder@0.2.2: {} @@ -4588,7 +5093,7 @@ snapshots: strip-final-newline@4.0.0: {} - strip-json-comments@3.1.1: {} + strip-indent@4.1.1: {} strip-literal@2.1.1: dependencies: @@ -4616,8 +5121,6 @@ snapshots: term-size@2.2.1: {} - text-table@0.2.0: {} - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -4628,6 +5131,8 @@ snapshots: tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@0.3.2: {} tinyglobby@0.2.16: @@ -4635,6 +5140,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinygradient@1.1.5: + dependencies: + '@types/tinycolor2': 1.4.6 + tinycolor2: 1.6.0 + tinypool@0.8.4: {} tinyspy@2.2.1: {} @@ -4643,13 +5153,13 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + tree-kill@1.2.2: {} trim-lines@3.0.1: {} - ts-api-utils@1.4.3(typescript@5.5.3): - dependencies: - typescript: 5.5.3 + trim-newlines@4.1.1: {} ts-interface-checker@0.1.13: {} @@ -4697,13 +5207,15 @@ 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: {} + type-fest@1.4.0: {} + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 typescript@5.5.3: {} @@ -4740,9 +5252,14 @@ snapshots: universalify@2.0.1: {} - uri-js@4.4.1: + unpipe@1.0.0: {} + + validate-npm-package-license@3.0.4: dependencies: - punycode: 2.3.1 + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + vary@1.1.2: {} vfile-message@4.0.3: dependencies: @@ -4883,8 +5400,6 @@ 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 @@ -4893,6 +5408,10 @@ snapshots: wrappy@1.0.2: {} + yallist@4.0.0: {} + + yargs-parser@20.2.9: {} + yocto-queue@0.1.0: {} yocto-queue@1.2.2: {} @@ -4901,4 +5420,10 @@ snapshots: yoctocolors@2.1.2: {} + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} + zwitch@2.0.4: {} 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,