diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b7f24a57..9f7db664 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -32,7 +32,13 @@ "Bash(tsc --noEmit)", "WebSearch", "WebFetch(domain:elysiajs.com)", - "Bash(tree:*)" + "Bash(tree:*)", + "Bash(git checkout:*)", + "Bash(npx tailwindcss init:*)", + "Bash(pkill:*)", + "Bash(taskkill:*)", + "Bash(npx eslint:*)", + "Bash(export NODE_ENV=production)" ], "deny": [] } diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index ae3a973b..f3240fda 100644 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -1,5 +1,4 @@ import { useState, useEffect } from 'react' -import './App.css' import { api, apiCall, getErrorMessage } from './lib/eden-api' import type { User } from '@/shared/types' @@ -18,7 +17,7 @@ function App() { useEffect(() => { checkApiStatus() loadUsers() - }, []) + }, []) // eslint-disable-line react-hooks/exhaustive-deps const checkApiStatus = async () => { try { @@ -47,7 +46,7 @@ function App() { try { setSubmitting(true) - const result = await apiCall(api.users.post({ name: name.trim(), email: email.trim() })) as any + const result = await apiCall(api.users.post({ name: name.trim(), email: email.trim() })) as { success: boolean; user: User } if (result?.success && result?.user) { setUsers(prev => [...prev, result.user]) @@ -87,74 +86,140 @@ function App() { } const renderOverview = () => ( -
- Framework full-stack TypeScript moderno com hot reload coordenado! 🚀 -
-Backend rápido e type-safe com Bun runtime
-Frontend moderno com hot-reload ultrarrápido
-API type-safe com inferência automática de tipos
-Deploy fácil com configurações otimizadas
-Vitest + Testing Library configurados
-Instalação e builds extremamente rápidos
++ Framework full-stack TypeScript moderno com hot reload coordenado e Tailwind CSS 4! 🚀 +
+{feature.description}
+Teste a API em tempo real com hot reload coordenado 🚀
- - {/* Stats */} -+ Teste a API em tempo real com hot reload coordenado e Eden Treaty 🚀 +
+Adicione o primeiro usuário usando o formulário acima
-Carregando usuários...
+Adicione o primeiro usuário usando o formulário acima
+{user.email}
+ +Documentação interativa gerada automaticamente com Swagger
++ Documentação interativa gerada automaticamente com Swagger UI +
+Interface completa para testar todos os endpoints da API
- - 🚀 Abrir em Nova Aba - + {/* Quick Links */} +Interface completa para testar todos os endpoints da API
+ + 🚀 Abrir em Nova Aba + +Especificação OpenAPI em formato JSON para integração
- - 📋 Ver JSON - +Especificação OpenAPI em formato JSON para integração
+ + 📋 Ver JSON + +{`import { treaty } from '@elysiajs/eden'
+ {/* Eden Treaty Guide */}
+
+
+ 🔧 Como usar Eden Treaty
+
+
+
+ Configuração do Cliente:
+
+{`import { treaty } from '@elysiajs/eden'
import type { App } from './server'
const client = treaty('http://localhost:3000')
-export const api = client.api`}
+export const api = client.api`}
+
-
- Exemplos de Uso:
- {`// Listar usuários
+
+ Exemplos de Uso:
+
+{`// Listar usuários
const users = await api.users.get()
// Criar usuário
@@ -337,12 +451,14 @@ const newUser = await api.users.post({
await api.users["1"].delete()
// Health check
-const health = await api.health.get()`}
+const health = await api.health.get()`}
+
-
- Com tratamento de erros:
- {`try {
+
+ Com tratamento de erros:
+
+{`try {
const result = await apiCall(api.users.post({
name: "Maria Silva",
email: "maria@example.com"
@@ -353,41 +469,49 @@ const health = await api.health.get()`}
}
} catch (error) {
console.error('Erro:', getErrorMessage(error))
-}`}
+}`}
+
-
- ✨ Funcionalidades
-
-
- 🔒
-
- Type Safety
- Tipos TypeScript inferidos automaticamente
-
-
-
- ⚡
-
- Auto-complete
- IntelliSense completo no editor
-
-
-
- 🔄
-
- Sincronização
- Mudanças no backend refletem automaticamente no frontend
-
-
-
- 🐛
-
- Debugging
- Erros de tipo detectados em tempo de compilação
-
+ {/* Features */}
+
+
+ ✨ Funcionalidades
+
+
+
+ {[
+ {
+ icon: "🔒",
+ title: "Type Safety",
+ description: "Tipos TypeScript inferidos automaticamente"
+ },
+ {
+ icon: "⚡",
+ title: "Auto-complete",
+ description: "IntelliSense completo no editor"
+ },
+ {
+ icon: "🔄",
+ title: "Sincronização",
+ description: "Mudanças no backend refletem automaticamente no frontend"
+ },
+ {
+ icon: "🐛",
+ title: "Debugging",
+ description: "Erros de tipo detectados em tempo de compilação"
+ }
+ ].map((feature, index) => (
+
+ {feature.icon}
+
+ {feature.title}
+ {feature.description}
+
+
+ ))}
@@ -395,44 +519,84 @@ const health = await api.health.get()`}
)
return (
-
+
{/* Header */}
-
-
-
-
- 🔥 FluxStack v1.4.0
+
+
+
+ {/* Logo and Navigation */}
+
+
+
+ 🔥 FluxStack
+
+
+ v1.4.0
+
+
+
+ {/* Navigation Tabs */}
+
+
+
+ {/* Status Badge */}
+
+
+ API {apiStatus === 'online' ? 'Online' : 'Offline'}
-
-
-
- API {apiStatus === 'online' ? 'Online' : 'Offline'}
+
+ {/* Mobile Navigation */}
+
+
{/* Main Content */}
-
+
{activeTab === 'overview' && renderOverview()}
{activeTab === 'demo' && renderDemo()}
{activeTab === 'api-docs' && renderApiDocs()}
@@ -441,10 +605,19 @@ const health = await api.health.get()`}
{/* Toast Notification */}
{message && (
setMessage(null)}
>
- {message.text}
+
+
+ {message.type === 'success' ? '✅' : '❌'}
+
+ {message.text}
+
)}
diff --git a/app/client/src/index.css b/app/client/src/index.css
index e69de29b..51cbfa2f 100644
--- a/app/client/src/index.css
+++ b/app/client/src/index.css
@@ -0,0 +1,51 @@
+@import "tailwindcss";
+
+/* Custom CSS variables for dynamic colors */
+:root {
+ --gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ --gradient-secondary: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ --gradient-accent: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+}
+
+/* Base styles with improved typography */
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ line-height: 1.6;
+}
+
+/* Smooth scrolling */
+html {
+ scroll-behavior: smooth;
+}
+
+/* Custom scrollbar for webkit browsers */
+::-webkit-scrollbar {
+ width: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: #f1f5f9;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #cbd5e1;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #94a3b8;
+}
+
+/* Focus styles for accessibility */
+*:focus {
+ outline: 2px solid #3b82f6;
+ outline-offset: 2px;
+}
\ No newline at end of file
diff --git a/app/server/index.ts b/app/server/index.ts
index 087fb022..1103422a 100644
--- a/app/server/index.ts
+++ b/app/server/index.ts
@@ -1,5 +1,6 @@
// User application entry point
-import { FluxStackFramework, loggerPlugin, vitePlugin, swaggerPlugin } from "@/core/server"
+import { FluxStackFramework, loggerPlugin, vitePlugin, swaggerPlugin, staticPlugin } from "@/core/server"
+import { isDevelopment } from "@/core/utils/helpers"
import { apiRoutes } from "./routes"
// Criar aplicação com framework
@@ -35,9 +36,14 @@ const app = new FluxStackFramework({
// Usar plugins de infraestrutura primeiro (mas NÃO o Swagger ainda)
-app
- .use(loggerPlugin)
- .use(vitePlugin)
+app.use(loggerPlugin)
+
+// Usar plugins condicionalmente baseado no ambiente
+if (isDevelopment()) {
+ app.use(vitePlugin)
+} else {
+ app.use(staticPlugin)
+}
// Registrar rotas da aplicação PRIMEIRO
@@ -48,45 +54,8 @@ app.use(swaggerPlugin)
-// Configurar proxy/static files
-const framework = app.getApp()
-const context = app.getContext()
-
-if (context.isDevelopment) {
- // Import the proxy function from the Vite plugin
- const { proxyToVite } = await import("@/core/plugins/built-in/vite")
-
- // Proxy para Vite em desenvolvimento com detecção automática de porta
- framework.get("*", async ({ request }) => {
- const url = new URL(request.url)
-
- if (url.pathname.startsWith("/api")) {
- return new Response("Not Found", { status: 404 })
- }
-
- // Use the intelligent proxy function that auto-detects the port
- const vitePort = context.config.client?.port || 5173
- return await proxyToVite(request, "localhost", vitePort, 5000)
- })
-} else {
- // Servir arquivos estáticos em produção
- const { join } = await import("path")
-
- framework.get("*", ({ request }) => {
- const url = new URL(request.url)
- const clientDistPath = join(process.cwd(), "app/client/dist")
- const filePath = join(clientDistPath, url.pathname)
-
- if (!url.pathname.includes(".")) {
- return Bun.file(join(clientDistPath, "index.html"))
- }
-
- return Bun.file(filePath)
- })
-}
-
// Iniciar servidor
app.listen()
// Exportar tipo da aplicação para Eden Treaty (método correto)
-export type App = typeof framework
\ No newline at end of file
+export type App = typeof app
\ No newline at end of file
diff --git a/bun.lock b/bun.lock
index 97c77916..51c3db36 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,15 +6,18 @@
"dependencies": {
"@elysiajs/eden": "^1.3.2",
"@elysiajs/swagger": "^1.3.1",
+ "@types/http-proxy-middleware": "^1.0.0",
"@vitejs/plugin-react": "^4.6.0",
"chokidar": "^4.0.3",
"elysia": "^1.4.6",
+ "http-proxy-middleware": "^3.0.5",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"vite": "^7.0.4",
},
"devDependencies": {
"@eslint/js": "^9.30.1",
+ "@tailwindcss/vite": "^4.1.13",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
@@ -29,6 +32,7 @@
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"jsdom": "^26.1.0",
+ "tailwindcss": "^4.1.13",
"typescript": "^5.8.3",
"typescript-eslint": "^8.35.1",
"vitest": "^3.2.4",
@@ -180,6 +184,8 @@
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
+ "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
+
"@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
@@ -254,6 +260,36 @@
"@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="],
+ "@tailwindcss/node": ["@tailwindcss/node@4.1.13", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.18", "source-map-js": "^1.2.1", "tailwindcss": "4.1.13" } }, "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.13", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.13", "@tailwindcss/oxide-darwin-arm64": "4.1.13", "@tailwindcss/oxide-darwin-x64": "4.1.13", "@tailwindcss/oxide-freebsd-x64": "4.1.13", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", "@tailwindcss/oxide-linux-x64-musl": "4.1.13", "@tailwindcss/oxide-wasm32-wasi": "4.1.13", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" } }, "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.13", "", { "os": "android", "cpu": "arm64" }, "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.13", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13", "", { "os": "linux", "cpu": "arm" }, "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.13", "", { "dependencies": { "@emnapi/core": "^1.4.5", "@emnapi/runtime": "^1.4.5", "@emnapi/wasi-threads": "^1.0.4", "@napi-rs/wasm-runtime": "^0.2.12", "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.13", "", { "os": "win32", "cpu": "x64" }, "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw=="],
+
+ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.13", "", { "dependencies": { "@tailwindcss/node": "4.1.13", "@tailwindcss/oxide": "4.1.13", "tailwindcss": "4.1.13" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ=="],
+
"@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
"@testing-library/jest-dom": ["@testing-library/jest-dom@6.8.0", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ=="],
@@ -284,6 +320,10 @@
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+ "@types/http-proxy": ["@types/http-proxy@1.17.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w=="],
+
+ "@types/http-proxy-middleware": ["@types/http-proxy-middleware@1.0.0", "", { "dependencies": { "http-proxy-middleware": "*" } }, "sha512-/s8lFX6rT43hSPqjjD8KNuu0SkPKY7uIdR6u9DCxVqCRhAvfKxGbVOixJsAT2mdpSnCyrGFAGoB39KFh6tmRxw=="],
+
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
@@ -380,6 +420,8 @@
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
+ "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
+
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -414,6 +456,8 @@
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+ "detect-libc": ["detect-libc@2.1.0", "", {}, "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg=="],
+
"dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
@@ -424,6 +468,8 @@
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+ "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
+
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
@@ -456,6 +502,8 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+ "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
+
"exact-mirror": ["exact-mirror@0.2.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA=="],
"expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="],
@@ -488,6 +536,8 @@
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
+ "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
+
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -502,6 +552,8 @@
"globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
@@ -512,8 +564,12 @@
"html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
+ "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="],
+
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
+ "http-proxy-middleware": ["http-proxy-middleware@3.0.5", "", { "dependencies": { "@types/http-proxy": "^1.17.15", "debug": "^4.3.6", "http-proxy": "^1.18.1", "is-glob": "^4.0.3", "is-plain-object": "^5.0.0", "micromatch": "^4.0.8" } }, "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg=="],
+
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
@@ -536,6 +592,8 @@
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
+ "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="],
+
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
@@ -550,6 +608,8 @@
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
+ "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
+
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
@@ -570,6 +630,28 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+ "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
+
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
@@ -596,6 +678,10 @@
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
+ "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
+
+ "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
+
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -660,6 +746,8 @@
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+ "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="],
+
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
@@ -718,6 +806,12 @@
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
+ "tailwindcss": ["tailwindcss@4.1.13", "", {}, "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w=="],
+
+ "tapable": ["tapable@2.2.3", "", {}, "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg=="],
+
+ "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
+
"test-exclude": ["test-exclude@7.0.1", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^9.0.4" } }, "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
@@ -802,7 +896,7 @@
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
- "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+ "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
@@ -836,6 +930,18 @@
"@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="],
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
"@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
"@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
@@ -858,6 +964,8 @@
"test-exclude/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
+ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
diff --git a/core/cli/command-registry.ts b/core/cli/command-registry.ts
new file mode 100644
index 00000000..0b095755
--- /dev/null
+++ b/core/cli/command-registry.ts
@@ -0,0 +1,334 @@
+import type { CliCommand, CliContext, CliArgument, CliOption } from "../plugins/types"
+import { getConfigSync } from "../config"
+import { logger } from "../utils/logger"
+import { createTimer, formatBytes, isProduction, isDevelopment } from "../utils/helpers"
+
+export class CliCommandRegistry {
+ private commands = new Map()
+ private aliases = new Map()
+ private context: CliContext
+
+ constructor() {
+ const config = getConfigSync()
+
+ this.context = {
+ config,
+ logger: {
+ debug: (message: string, meta?: any) => logger.debug(message, meta),
+ info: (message: string, meta?: any) => logger.info(message, meta),
+ warn: (message: string, meta?: any) => logger.warn(message, meta),
+ error: (message: string, meta?: any) => logger.error(message, meta),
+ child: (context: any) => (logger as any).child(context),
+ time: (label: string) => (logger as any).time(label),
+ timeEnd: (label: string) => (logger as any).timeEnd(label),
+ request: (method: string, path: string, status?: number, duration?: number) =>
+ logger.request(method, path, status, duration)
+ },
+ utils: {
+ createTimer,
+ formatBytes,
+ isProduction,
+ isDevelopment,
+ getEnvironment: () => process.env.NODE_ENV || 'development',
+ createHash: (data: string) => {
+ const crypto = require('crypto')
+ return crypto.createHash('sha256').update(data).digest('hex')
+ },
+ deepMerge: (target: any, source: any) => {
+ const result = { ...target }
+ for (const key in source) {
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
+ result[key] = this.context.utils.deepMerge(result[key] || {}, source[key])
+ } else {
+ result[key] = source[key]
+ }
+ }
+ return result
+ },
+ validateSchema: (_data: any, _schema: any) => {
+ try {
+ return { valid: true, errors: [] }
+ } catch (error) {
+ return { valid: false, errors: [error instanceof Error ? error.message : 'Validation failed'] }
+ }
+ }
+ },
+ workingDir: process.cwd(),
+ packageInfo: {
+ name: 'fluxstack',
+ version: '1.0.0'
+ }
+ }
+ }
+
+ register(command: CliCommand): void {
+ // Register main command
+ this.commands.set(command.name, command)
+
+ // Register aliases
+ if (command.aliases) {
+ for (const alias of command.aliases) {
+ this.aliases.set(alias, command.name)
+ }
+ }
+ }
+
+ get(name: string): CliCommand | undefined {
+ // Check direct command
+ const command = this.commands.get(name)
+ if (command) return command
+
+ // Check alias
+ const aliasTarget = this.aliases.get(name)
+ if (aliasTarget) {
+ return this.commands.get(aliasTarget)
+ }
+
+ return undefined
+ }
+
+ has(name: string): boolean {
+ return this.commands.has(name) || this.aliases.has(name)
+ }
+
+ getAll(): CliCommand[] {
+ return Array.from(this.commands.values())
+ }
+
+ getAllByCategory(): Map {
+ const categories = new Map()
+
+ for (const command of this.commands.values()) {
+ if (command.hidden) continue
+
+ const category = command.category || 'General'
+ if (!categories.has(category)) {
+ categories.set(category, [])
+ }
+ categories.get(category)!.push(command)
+ }
+
+ return categories
+ }
+
+ async execute(commandName: string, args: string[]): Promise {
+ const command = this.get(commandName)
+
+ if (!command) {
+ console.error(`❌ Unknown command: ${commandName}`)
+ this.showHelp()
+ return 1
+ }
+
+ try {
+ // Parse arguments and options
+ const { parsedArgs, parsedOptions } = this.parseArgs(command, args)
+
+ // Validate required arguments
+ if (command.arguments) {
+ for (let i = 0; i < command.arguments.length; i++) {
+ const arg = command.arguments[i]
+ if (arg.required && !parsedArgs[i]) {
+ console.error(`❌ Missing required argument: ${arg.name}`)
+ this.showCommandHelp(command)
+ return 1
+ }
+ }
+ }
+
+ // Validate required options
+ if (command.options) {
+ for (const option of command.options) {
+ if (option.required && !(option.name in parsedOptions)) {
+ console.error(`❌ Missing required option: --${option.name}`)
+ this.showCommandHelp(command)
+ return 1
+ }
+ }
+ }
+
+ // Execute command
+ await command.handler(parsedArgs, parsedOptions, this.context)
+ return 0
+
+ } catch (error) {
+ console.error(`❌ Command failed:`, error instanceof Error ? error.message : String(error))
+ return 1
+ }
+ }
+
+ private parseArgs(command: CliCommand, args: string[]): { parsedArgs: any[], parsedOptions: any } {
+ const parsedArgs: any[] = []
+ const parsedOptions: any = {}
+
+ let i = 0
+ while (i < args.length) {
+ const arg = args[i]
+
+ // Handle options (--name or -n)
+ if (arg.startsWith('--')) {
+ const optionName = arg.slice(2)
+ const option = command.options?.find(o => o.name === optionName)
+
+ if (option) {
+ if (option.type === 'boolean') {
+ parsedOptions[optionName] = true
+ } else if (option.type === 'array') {
+ parsedOptions[optionName] = parsedOptions[optionName] || []
+ if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
+ parsedOptions[optionName].push(args[++i])
+ }
+ } else {
+ if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
+ parsedOptions[optionName] = this.convertType(args[++i], option.type)
+ }
+ }
+ }
+ }
+ // Handle short options (-n)
+ else if (arg.startsWith('-') && arg.length === 2) {
+ const shortName = arg.slice(1)
+ const option = command.options?.find(o => o.short === shortName)
+
+ if (option) {
+ if (option.type === 'boolean') {
+ parsedOptions[option.name] = true
+ } else {
+ if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
+ parsedOptions[option.name] = this.convertType(args[++i], option.type)
+ }
+ }
+ }
+ }
+ // Handle positional arguments
+ else {
+ const argIndex = parsedArgs.length
+ const argDef = command.arguments?.[argIndex]
+
+ if (argDef) {
+ parsedArgs.push(this.convertType(arg, argDef.type))
+ } else {
+ parsedArgs.push(arg)
+ }
+ }
+
+ i++
+ }
+
+ // Apply defaults
+ if (command.arguments) {
+ for (let i = 0; i < command.arguments.length; i++) {
+ if (parsedArgs[i] === undefined && command.arguments[i].default !== undefined) {
+ parsedArgs[i] = command.arguments[i].default
+ }
+ }
+ }
+
+ if (command.options) {
+ for (const option of command.options) {
+ if (!(option.name in parsedOptions) && option.default !== undefined) {
+ parsedOptions[option.name] = option.default
+ }
+ }
+ }
+
+ return { parsedArgs, parsedOptions }
+ }
+
+ private convertType(value: string, type?: 'string' | 'number' | 'boolean' | 'array'): any {
+ if (!type || type === 'string') return value
+ if (type === 'number') return Number(value)
+ if (type === 'boolean') return value.toLowerCase() === 'true'
+ if (type === 'array') return [value] // Convert single value to array
+ return value
+ }
+
+ showHelp(): void {
+ console.log(`
+⚡ FluxStack Framework CLI
+
+Usage:
+ flux [options] [arguments]
+ fluxstack [options] [arguments]
+
+Built-in Commands:`)
+
+ const categories = this.getAllByCategory()
+
+ for (const [category, commands] of categories) {
+ console.log(`\n${category}:`)
+ for (const command of commands) {
+ const aliases = command.aliases?.length ? ` (${command.aliases.join(', ')})` : ''
+ console.log(` ${command.name}${aliases.padEnd(20)} ${command.description}`)
+ }
+ }
+
+ console.log(`
+Examples:
+ flux dev # Start development server
+ flux build --production # Build for production
+ flux create my-app # Create new project
+ flux help # Show help for specific command
+
+Use "flux help " for more information about a specific command.`)
+ }
+
+ showCommandHelp(command: CliCommand): void {
+ console.log(`\n${command.description}`)
+
+ if (command.usage) {
+ console.log(`\nUsage:\n ${command.usage}`)
+ } else {
+ let usage = `flux ${command.name}`
+
+ if (command.arguments) {
+ for (const arg of command.arguments) {
+ if (arg.required) {
+ usage += ` <${arg.name}>`
+ } else {
+ usage += ` [${arg.name}]`
+ }
+ }
+ }
+
+ if (command.options?.length) {
+ usage += ` [options]`
+ }
+
+ console.log(`\nUsage:\n ${usage}`)
+ }
+
+ if (command.arguments?.length) {
+ console.log(`\nArguments:`)
+ for (const arg of command.arguments) {
+ const required = arg.required ? ' (required)' : ''
+ const defaultValue = arg.default !== undefined ? ` (default: ${arg.default})` : ''
+ console.log(` ${arg.name.padEnd(15)} ${arg.description}${required}${defaultValue}`)
+ }
+ }
+
+ if (command.options?.length) {
+ console.log(`\nOptions:`)
+ for (const option of command.options) {
+ const short = option.short ? `-${option.short}, ` : ' '
+ const required = option.required ? ' (required)' : ''
+ const defaultValue = option.default !== undefined ? ` (default: ${option.default})` : ''
+ console.log(` ${short}--${option.name.padEnd(15)} ${option.description}${required}${defaultValue}`)
+ }
+ }
+
+ if (command.examples?.length) {
+ console.log(`\nExamples:`)
+ for (const example of command.examples) {
+ console.log(` ${example}`)
+ }
+ }
+
+ if (command.aliases?.length) {
+ console.log(`\nAliases: ${command.aliases.join(', ')}`)
+ }
+ }
+}
+
+// Global registry instance
+export const cliRegistry = new CliCommandRegistry()
\ No newline at end of file
diff --git a/core/cli/index.ts b/core/cli/index.ts
index f56000c9..d73ede3a 100644
--- a/core/cli/index.ts
+++ b/core/cli/index.ts
@@ -3,10 +3,220 @@
import { FluxStackBuilder } from "../build"
import { ProjectCreator } from "../templates/create-project"
import { getConfigSync } from "../config"
+import { cliRegistry } from "./command-registry"
+import { pluginDiscovery } from "./plugin-discovery"
const command = process.argv[2]
+const args = process.argv.slice(3)
-switch (command) {
+// Register built-in commands
+async function registerBuiltInCommands() {
+ // Help command
+ cliRegistry.register({
+ name: 'help',
+ description: 'Show help information',
+ category: 'General',
+ aliases: ['h', '--help', '-h'],
+ arguments: [
+ {
+ name: 'command',
+ description: 'Command to show help for',
+ required: false
+ }
+ ],
+ handler: async (args, options, context) => {
+ if (args[0]) {
+ const targetCommand = cliRegistry.get(args[0])
+ if (targetCommand) {
+ cliRegistry.showCommandHelp(targetCommand)
+ } else {
+ console.error(`❌ Unknown command: ${args[0]}`)
+ cliRegistry.showHelp()
+ }
+ } else {
+ cliRegistry.showHelp()
+ }
+ }
+ })
+
+ // Dev command
+ cliRegistry.register({
+ name: 'dev',
+ description: 'Start full-stack development server',
+ category: 'Development',
+ usage: 'flux dev [options]',
+ examples: [
+ 'flux dev # Start development server',
+ 'flux dev --port 4000 # Start on custom port'
+ ],
+ options: [
+ {
+ name: 'port',
+ short: 'p',
+ description: 'Port for backend server',
+ type: 'number',
+ default: 3000
+ },
+ {
+ name: 'frontend-port',
+ description: 'Port for frontend server',
+ type: 'number',
+ default: 5173
+ }
+ ],
+ handler: async (args, options, context) => {
+ console.log("⚡ FluxStack Full-Stack Development")
+ console.log(`🌐 Frontend: http://localhost:${options['frontend-port']}`)
+ console.log(`🚀 Backend: http://localhost:${options.port}`)
+ console.log("🔄 Hot Reload Coordenado: Backend + Vite automático")
+ console.log("📦 Starting services...")
+ console.log()
+
+ const { spawn } = await import("child_process")
+ const devProcess = spawn("concurrently", [
+ "--prefix", "{name}",
+ "--names", "BACKEND,VITE",
+ "--prefix-colors", "blue,green",
+ "--kill-others-on-fail",
+ "\"bun --watch app/server/index.ts\"",
+ "\"vite --config vite.config.ts\""
+ ], {
+ stdio: "inherit",
+ cwd: process.cwd(),
+ shell: true
+ })
+
+ process.on('SIGINT', () => {
+ devProcess.kill('SIGINT')
+ process.exit(0)
+ })
+
+ devProcess.on('close', (code) => {
+ process.exit(code || 0)
+ })
+ }
+ })
+
+ // Build command
+ cliRegistry.register({
+ name: 'build',
+ description: 'Build the application for production',
+ category: 'Build',
+ usage: 'flux build [options]',
+ examples: [
+ 'flux build # Build both frontend and backend',
+ 'flux build --frontend-only # Build only frontend',
+ 'flux build --backend-only # Build only backend'
+ ],
+ options: [
+ {
+ name: 'frontend-only',
+ description: 'Build only frontend',
+ type: 'boolean'
+ },
+ {
+ name: 'backend-only',
+ description: 'Build only backend',
+ type: 'boolean'
+ },
+ {
+ name: 'production',
+ description: 'Build for production (minified)',
+ type: 'boolean',
+ default: true
+ }
+ ],
+ handler: async (args, options, context) => {
+ const config = getConfigSync()
+ const builder = new FluxStackBuilder(config)
+
+ if (options['frontend-only']) {
+ await builder.buildClient()
+ } else if (options['backend-only']) {
+ await builder.buildServer()
+ } else {
+ await builder.build()
+ }
+ }
+ })
+
+ // Create command
+ cliRegistry.register({
+ name: 'create',
+ description: 'Create a new FluxStack project',
+ category: 'Project',
+ usage: 'flux create [template]',
+ examples: [
+ 'flux create my-app # Create basic project',
+ 'flux create my-app full # Create full-featured project'
+ ],
+ arguments: [
+ {
+ name: 'project-name',
+ description: 'Name of the project to create',
+ required: true,
+ type: 'string'
+ },
+ {
+ name: 'template',
+ description: 'Project template to use',
+ required: false,
+ type: 'string',
+ default: 'basic',
+ choices: ['basic', 'full']
+ }
+ ],
+ handler: async (args, options, context) => {
+ const [projectName, template] = args
+
+ if (!/^[a-zA-Z0-9-_]+$/.test(projectName)) {
+ console.error("❌ Project name can only contain letters, numbers, hyphens, and underscores")
+ return
+ }
+
+ try {
+ const creator = new ProjectCreator({
+ name: projectName,
+ template: template as 'basic' | 'full' || 'basic'
+ })
+
+ await creator.create()
+ } catch (error) {
+ console.error("❌ Failed to create project:", error instanceof Error ? error.message : String(error))
+ throw error
+ }
+ }
+ })
+}
+
+// Main CLI logic
+async function main() {
+ // Register built-in commands
+ await registerBuiltInCommands()
+
+ // Discover and register plugin commands
+ await pluginDiscovery.discoverAndRegisterCommands()
+
+ // Handle special cases first
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
+ await cliRegistry.execute('help', args)
+ return
+ }
+
+ // Check if it's a registered command (built-in or plugin)
+ if (cliRegistry.has(command)) {
+ const exitCode = await cliRegistry.execute(command, args)
+ process.exit(exitCode)
+ return
+ }
+
+ // Fallback to legacy command handling for backward compatibility
+ await handleLegacyCommands()
+}
+
+// Legacy command handling for backward compatibility
+async function handleLegacyCommands() {
+ switch (command) {
case "dev":
console.log("⚡ FluxStack Full-Stack Development")
console.log("🌐 Frontend: http://localhost:5173")
@@ -167,4 +377,11 @@ Environment Variables:
BACKEND_PORT=3001 # Backend port
API_URL=http://localhost:3001 # API URL for frontend
`)
-}
\ No newline at end of file
+ }
+}
+
+// Run main CLI
+main().catch(error => {
+ console.error('❌ CLI Error:', error instanceof Error ? error.message : String(error))
+ process.exit(1)
+})
\ No newline at end of file
diff --git a/core/cli/plugin-discovery.ts b/core/cli/plugin-discovery.ts
new file mode 100644
index 00000000..87a0c851
--- /dev/null
+++ b/core/cli/plugin-discovery.ts
@@ -0,0 +1,200 @@
+import { existsSync } from 'fs'
+import { join } from 'path'
+import type { Plugin } from '../plugins/types'
+import { cliRegistry } from './command-registry'
+import { logger } from '../utils/logger'
+
+export class CliPluginDiscovery {
+ private loadedPlugins = new Set()
+
+ async discoverAndRegisterCommands(): Promise {
+ // 1. Load built-in plugins with CLI commands
+ await this.loadBuiltInPlugins()
+
+ // 2. Load external plugins from node_modules
+ await this.loadExternalPlugins()
+
+ // 3. Load local plugins from project
+ await this.loadLocalPlugins()
+ }
+
+ private async loadBuiltInPlugins(): Promise {
+ const builtInPluginsDir = join(__dirname, '../server/plugins')
+
+ if (!existsSync(builtInPluginsDir)) {
+ return
+ }
+
+ try {
+ // For now, we'll manually list built-in plugins that might have CLI commands
+ const potentialPlugins = [
+ 'logger',
+ 'vite',
+ 'swagger',
+ 'static',
+ 'database'
+ ]
+
+ for (const pluginName of potentialPlugins) {
+ try {
+ const pluginPath = join(builtInPluginsDir, `${pluginName}.ts`)
+ if (existsSync(pluginPath)) {
+ const pluginModule = await import(pluginPath)
+ const plugin = pluginModule[`${pluginName}Plugin`] as Plugin
+
+ if (plugin && plugin.commands) {
+ this.registerPluginCommands(plugin)
+ }
+ }
+ } catch (error) {
+ logger.debug(`Failed to load built-in plugin ${pluginName}:`, error)
+ }
+ }
+ } catch (error) {
+ logger.debug('Failed to scan built-in plugins:', error)
+ }
+ }
+
+ private async loadExternalPlugins(): Promise {
+ const nodeModulesDir = join(process.cwd(), 'node_modules')
+
+ if (!existsSync(nodeModulesDir)) {
+ return
+ }
+
+ try {
+ const fs = await import('fs')
+ const entries = fs.readdirSync(nodeModulesDir, { withFileTypes: true })
+
+ for (const entry of entries) {
+ if (!entry.isDirectory()) continue
+
+ // Check for FluxStack plugins (convention: fluxstack-plugin-*)
+ if (entry.name.startsWith('fluxstack-plugin-')) {
+ await this.loadExternalPlugin(entry.name)
+ }
+
+ // Check for scoped packages (@fluxstack/plugin-*)
+ if (entry.name.startsWith('@fluxstack')) {
+ const scopedDir = join(nodeModulesDir, entry.name)
+ if (existsSync(scopedDir)) {
+ const scopedEntries = fs.readdirSync(scopedDir, { withFileTypes: true })
+ for (const scopedEntry of scopedEntries) {
+ if (scopedEntry.isDirectory() && scopedEntry.name.startsWith('plugin-')) {
+ await this.loadExternalPlugin(`${entry.name}/${scopedEntry.name}`)
+ }
+ }
+ }
+ }
+ }
+ } catch (error) {
+ logger.debug('Failed to scan external plugins:', error)
+ }
+ }
+
+ private async loadExternalPlugin(packageName: string): Promise {
+ try {
+ const packagePath = join(process.cwd(), 'node_modules', packageName)
+ const packageJsonPath = join(packagePath, 'package.json')
+
+ if (!existsSync(packageJsonPath)) {
+ return
+ }
+
+ const packageJson = JSON.parse(await import('fs').then(fs =>
+ fs.readFileSync(packageJsonPath, 'utf-8')
+ ))
+
+ // Check if this is a FluxStack plugin
+ if (packageJson.fluxstack?.plugin) {
+ const entryPoint = packageJson.main || 'index.js'
+ const pluginPath = join(packagePath, entryPoint)
+
+ if (existsSync(pluginPath)) {
+ const pluginModule = await import(pluginPath)
+ const plugin = pluginModule.default || pluginModule[packageJson.fluxstack.plugin] as Plugin
+
+ if (plugin && plugin.commands) {
+ this.registerPluginCommands(plugin)
+ }
+ }
+ }
+ } catch (error) {
+ logger.debug(`Failed to load external plugin ${packageName}:`, error)
+ }
+ }
+
+ private async loadLocalPlugins(): Promise {
+ const localPluginsDir = join(process.cwd(), 'plugins')
+
+ if (!existsSync(localPluginsDir)) {
+ return
+ }
+
+ try {
+ const fs = await import('fs')
+ const entries = fs.readdirSync(localPluginsDir, { withFileTypes: true })
+
+ for (const entry of entries) {
+ if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
+ const pluginPath = join(localPluginsDir, entry.name)
+
+ try {
+ const pluginModule = await import(pluginPath)
+ const plugin = pluginModule.default || Object.values(pluginModule).find(
+ (exp: any) => exp && typeof exp === 'object' && exp.name && exp.commands
+ ) as Plugin
+
+ if (plugin && plugin.commands) {
+ this.registerPluginCommands(plugin)
+ }
+ } catch (error) {
+ logger.debug(`Failed to load local plugin ${entry.name}:`, error)
+ }
+ }
+ }
+ } catch (error) {
+ logger.debug('Failed to scan local plugins:', error)
+ }
+ }
+
+ private registerPluginCommands(plugin: Plugin): void {
+ if (!plugin.commands || this.loadedPlugins.has(plugin.name)) {
+ return
+ }
+
+ try {
+ for (const command of plugin.commands) {
+ // Prefix command with plugin name to avoid conflicts
+ const prefixedCommand = {
+ ...command,
+ name: `${plugin.name}:${command.name}`,
+ category: command.category || `Plugin: ${plugin.name}`,
+ aliases: command.aliases?.map(alias => `${plugin.name}:${alias}`)
+ }
+
+ cliRegistry.register(prefixedCommand)
+
+ // Also register without prefix if no conflict exists
+ if (!cliRegistry.has(command.name)) {
+ cliRegistry.register({
+ ...command,
+ category: command.category || `Plugin: ${plugin.name}`
+ })
+ }
+ }
+
+ this.loadedPlugins.add(plugin.name)
+ logger.debug(`Registered ${plugin.commands.length} CLI commands from plugin: ${plugin.name}`)
+
+ } catch (error) {
+ logger.error(`Failed to register CLI commands for plugin ${plugin.name}:`, error)
+ }
+ }
+
+ getLoadedPlugins(): string[] {
+ return Array.from(this.loadedPlugins)
+ }
+}
+
+export const pluginDiscovery = new CliPluginDiscovery()
\ No newline at end of file
diff --git a/core/framework/__tests__/server.test.ts b/core/framework/__tests__/server.test.ts
index 55c8f9fc..b58bef32 100644
--- a/core/framework/__tests__/server.test.ts
+++ b/core/framework/__tests__/server.test.ts
@@ -52,8 +52,9 @@ vi.mock('../../utils/errors/handlers', () => ({
vi.mock('elysia', () => ({
Elysia: vi.fn(() => ({
onRequest: vi.fn().mockReturnThis(),
- options: vi.fn().mockReturnThis(),
+ onAfterHandle: vi.fn().mockReturnThis(),
onError: vi.fn().mockReturnThis(),
+ options: vi.fn().mockReturnThis(),
use: vi.fn().mockReturnThis(),
listen: vi.fn((_port, callback) => {
if (callback) callback()
diff --git a/core/framework/server.ts b/core/framework/server.ts
index aa8a680f..dc00f548 100644
--- a/core/framework/server.ts
+++ b/core/framework/server.ts
@@ -84,6 +84,7 @@ export class FluxStackFramework {
}
this.setupCors()
+ this.setupHooks()
this.setupErrorHandling()
logger.framework('FluxStack framework initialized', {
@@ -110,19 +111,263 @@ export class FluxStackFramework {
})
}
+ private setupHooks() {
+ // Setup onRequest hook and onBeforeRoute hook
+ this.app.onRequest(async ({ request, set }) => {
+ const startTime = Date.now()
+ const url = new URL(request.url)
+
+ const requestContext = {
+ request,
+ path: url.pathname,
+ method: request.method,
+ headers: (() => {
+ const headers: Record = {}
+ request.headers.forEach((value: string, key: string) => {
+ headers[key] = value
+ })
+ return headers
+ })(),
+ query: Object.fromEntries(url.searchParams.entries()),
+ params: {},
+ startTime,
+ handled: false,
+ response: undefined
+ }
+
+ // Execute onRequest hooks for all plugins first (logging, auth, etc.)
+ await this.executePluginHooks('onRequest', requestContext)
+
+ // Execute onBeforeRoute hooks - allow plugins to handle requests before routing
+ const handledResponse = await this.executePluginBeforeRouteHooks(requestContext)
+
+ // If a plugin handled the request, return the response
+ if (handledResponse) {
+ return handledResponse
+ }
+ })
+
+ // Setup onResponse hook
+ this.app.onAfterHandle(async ({ request, response, set }) => {
+ const startTime = Date.now()
+ const url = new URL(request.url)
+
+ const responseContext = {
+ request,
+ path: url.pathname,
+ method: request.method,
+ headers: (() => {
+ const headers: Record = {}
+ request.headers.forEach((value: string, key: string) => {
+ headers[key] = value
+ })
+ return headers
+ })(),
+ query: Object.fromEntries(url.searchParams.entries()),
+ params: {},
+ response,
+ statusCode: (response as any)?.status || set.status || 200,
+ duration: Date.now() - startTime,
+ startTime
+ }
+
+ // Execute onResponse hooks for all plugins
+ await this.executePluginHooks('onResponse', responseContext)
+ })
+ }
+
private setupErrorHandling() {
const errorHandler = createErrorHandler({
logger: this.pluginContext.logger,
isDevelopment: this.context.isDevelopment
})
- this.app.onError(({ error, request, path }) => {
+ this.app.onError(async ({ error, request, path, set }) => {
+ const startTime = Date.now()
+ const url = new URL(request.url)
+
+ const errorContext = {
+ request,
+ path: url.pathname,
+ method: request.method,
+ headers: (() => {
+ const headers: Record = {}
+ request.headers.forEach((value: string, key: string) => {
+ headers[key] = value
+ })
+ return headers
+ })(),
+ query: Object.fromEntries(url.searchParams.entries()),
+ params: {},
+ error: error instanceof Error ? error : new Error(String(error)),
+ duration: Date.now() - startTime,
+ handled: false,
+ startTime
+ }
+
+ // Execute onError hooks for all plugins - allow them to handle the error
+ const handledResponse = await this.executePluginErrorHooks(errorContext)
+
+ // If a plugin handled the error, return the response
+ if (handledResponse) {
+ return handledResponse
+ }
+
+ // Check if it's a NOT_FOUND error and we're in development with Vite plugin
+ if (error.constructor.name === 'NotFoundError' && this.context.isDevelopment) {
+ // Skip API routes and swagger - these should remain 404
+ if (!url.pathname.startsWith("/api") && !url.pathname.startsWith("/swagger")) {
+ // Try to proxy to Vite
+ const vitePort = this.context.config.client?.port || 5173
+
+ try {
+ const viteUrl = `http://localhost:${vitePort}${url.pathname}${url.search}`
+
+ // Create headers object from request
+ const headers: Record = {}
+ request.headers.forEach((value: string, key: string) => {
+ headers[key] = value
+ })
+
+ // Forward request to Vite
+ const response = await fetch(viteUrl, {
+ method: request.method,
+ headers
+ })
+
+ // Return a proper Response object with all headers and status
+ const body = await response.arrayBuffer()
+
+ return new Response(body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers: response.headers
+ })
+
+ } catch (viteError) {
+ // If Vite fails, fall back to normal error handling
+ console.warn(`Vite proxy error: ${viteError}`)
+ }
+ }
+ }
+
// Convert Elysia error to standard Error if needed
const standardError = error instanceof Error ? error : new Error(String(error))
return errorHandler(standardError, request, path)
})
}
+ private async executePluginHooks(hookName: string, context: any): Promise {
+ const loadOrder = this.pluginRegistry.getLoadOrder()
+
+ for (const pluginName of loadOrder) {
+ const plugin = this.pluginRegistry.get(pluginName)
+ if (!plugin) continue
+
+ const hookFn = (plugin as any)[hookName]
+ if (typeof hookFn === 'function') {
+ try {
+ await hookFn(context)
+ } catch (error) {
+ logger.error(`Plugin '${pluginName}' ${hookName} hook failed`, {
+ error: (error as Error).message
+ })
+ }
+ }
+ }
+ }
+
+ private async executePluginBeforeRouteHooks(requestContext: any): Promise {
+ const loadOrder = this.pluginRegistry.getLoadOrder()
+
+ for (const pluginName of loadOrder) {
+ const plugin = this.pluginRegistry.get(pluginName)
+ if (!plugin) continue
+
+ const onBeforeRouteFn = (plugin as any).onBeforeRoute
+ if (typeof onBeforeRouteFn === 'function') {
+ try {
+ await onBeforeRouteFn(requestContext)
+
+ // If this plugin handled the request, return the response
+ if (requestContext.handled && requestContext.response) {
+ return requestContext.response
+ }
+ } catch (error) {
+ logger.error(`Plugin '${pluginName}' onBeforeRoute hook failed`, {
+ error: (error as Error).message
+ })
+ }
+ }
+ }
+
+ return null
+ }
+
+ private async executePluginErrorHooks(errorContext: any): Promise {
+ const loadOrder = this.pluginRegistry.getLoadOrder()
+
+ for (const pluginName of loadOrder) {
+ const plugin = this.pluginRegistry.get(pluginName)
+ if (!plugin) continue
+
+ const onErrorFn = (plugin as any).onError
+ if (typeof onErrorFn === 'function') {
+ try {
+ await onErrorFn(errorContext)
+
+ // If this plugin handled the error, check if it provides a response
+ if (errorContext.handled) {
+ // For Vite plugin, we'll handle the proxy here
+ if (pluginName === 'vite' && errorContext.error.constructor.name === 'NotFoundError') {
+ return await this.handleViteProxy(errorContext)
+ }
+
+ // For other plugins, return a basic success response
+ return new Response('OK', { status: 200 })
+ }
+ } catch (error) {
+ logger.error(`Plugin '${pluginName}' onError hook failed`, {
+ error: (error as Error).message
+ })
+ }
+ }
+ }
+
+ return null
+ }
+
+ private async handleViteProxy(errorContext: any): Promise {
+ const vitePort = this.context.config.client?.port || 5173
+ const url = new URL(errorContext.request.url)
+
+ try {
+ const viteUrl = `http://localhost:${vitePort}${url.pathname}${url.search}`
+
+ // Forward request to Vite
+ const response = await fetch(viteUrl, {
+ method: errorContext.method,
+ headers: errorContext.headers
+ })
+
+ // Return a proper Response object with all headers and status
+ const body = await response.arrayBuffer()
+
+ return new Response(body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers: response.headers
+ })
+
+ } catch (viteError) {
+ // If Vite fails, return error response
+ return new Response(`Vite server not ready on port ${vitePort}. Error: ${viteError}`, {
+ status: 503,
+ headers: { 'Content-Type': 'text/plain' }
+ })
+ }
+ }
+
use(plugin: Plugin) {
try {
// Use the registry's public register method, but don't await it since we need sync operation
diff --git a/core/plugins/types.ts b/core/plugins/types.ts
index dfc859cd..e277a8ea 100644
--- a/core/plugins/types.ts
+++ b/core/plugins/types.ts
@@ -6,6 +6,7 @@ export type PluginHook =
| 'onServerStart'
| 'onServerStop'
| 'onRequest'
+ | 'onBeforeRoute'
| 'onResponse'
| 'onError'
| 'onBuild'
@@ -43,6 +44,8 @@ export interface RequestContext {
body?: any
user?: any
startTime: number
+ handled?: boolean
+ response?: Response
}
export interface ResponseContext extends RequestContext {
@@ -85,11 +88,15 @@ export interface Plugin {
onServerStart?: (context: PluginContext) => void | Promise
onServerStop?: (context: PluginContext) => void | Promise
onRequest?: (context: RequestContext) => void | Promise
+ onBeforeRoute?: (context: RequestContext) => void | Promise
onResponse?: (context: ResponseContext) => void | Promise
onError?: (context: ErrorContext) => void | Promise
onBuild?: (context: BuildContext) => void | Promise
onBuildComplete?: (context: BuildContext) => void | Promise
+ // CLI commands
+ commands?: CliCommand[]
+
// Configuration
configSchema?: PluginConfigSchema
defaultConfig?: any
@@ -200,4 +207,48 @@ export type PluginLifecycleEvent =
| 'plugin:error'
| 'hook:before'
| 'hook:after'
- | 'hook:error'
\ No newline at end of file
+ | 'hook:error'
+
+// CLI Command interfaces
+export interface CliArgument {
+ name: string
+ description: string
+ required?: boolean
+ type?: 'string' | 'number' | 'boolean'
+ default?: any
+ choices?: string[]
+}
+
+export interface CliOption {
+ name: string
+ short?: string
+ description: string
+ type?: 'string' | 'number' | 'boolean' | 'array'
+ default?: any
+ required?: boolean
+ choices?: string[]
+}
+
+export interface CliCommand {
+ name: string
+ description: string
+ usage?: string
+ examples?: string[]
+ arguments?: CliArgument[]
+ options?: CliOption[]
+ aliases?: string[]
+ category?: string
+ hidden?: boolean
+ handler: (args: any[], options: any, context: CliContext) => Promise | void
+}
+
+export interface CliContext {
+ config: FluxStackConfig
+ logger: Logger
+ utils: PluginUtils
+ workingDir: string
+ packageInfo: {
+ name: string
+ version: string
+ }
+}
\ No newline at end of file
diff --git a/core/server/plugins/database.ts b/core/server/plugins/database.ts
new file mode 100644
index 00000000..b6cb2aca
--- /dev/null
+++ b/core/server/plugins/database.ts
@@ -0,0 +1,182 @@
+import type { Plugin, PluginContext, CliCommand } from "../../plugins/types"
+
+// Database plugin with CLI commands
+export const databasePlugin: Plugin = {
+ name: "database",
+ version: "1.0.0",
+ description: "Database management plugin with CLI commands",
+ author: "FluxStack Team",
+ category: "data",
+
+ setup: (context: PluginContext) => {
+ context.logger.info("Database plugin initialized")
+ },
+
+ commands: [
+ {
+ name: "migrate",
+ description: "Run database migrations",
+ category: "Database",
+ usage: "flux database:migrate [options]",
+ examples: [
+ "flux database:migrate # Run all pending migrations",
+ "flux database:migrate --rollback # Rollback last migration",
+ "flux database:migrate --to 001 # Migrate to specific version"
+ ],
+ options: [
+ {
+ name: "rollback",
+ short: "r",
+ description: "Rollback the last migration",
+ type: "boolean"
+ },
+ {
+ name: "to",
+ description: "Migrate to specific version",
+ type: "string"
+ },
+ {
+ name: "dry-run",
+ description: "Show what would be migrated without executing",
+ type: "boolean"
+ }
+ ],
+ handler: async (args, options, context) => {
+ if (options["dry-run"]) {
+ console.log("🔍 Dry run mode - showing planned migrations:")
+ }
+
+ if (options.rollback) {
+ console.log("⬇️ Rolling back last migration...")
+ // Simulate rollback
+ await new Promise(resolve => setTimeout(resolve, 1000))
+ console.log("✅ Rollback completed")
+ } else if (options.to) {
+ console.log(`📈 Migrating to version: ${options.to}`)
+ // Simulate migration to version
+ await new Promise(resolve => setTimeout(resolve, 1500))
+ console.log(`✅ Migrated to version ${options.to}`)
+ } else {
+ console.log("📈 Running all pending migrations...")
+ // Simulate migration
+ await new Promise(resolve => setTimeout(resolve, 2000))
+ console.log("✅ All migrations completed")
+ }
+ }
+ },
+ {
+ name: "seed",
+ description: "Seed the database with initial data",
+ category: "Database",
+ usage: "flux database:seed [seeder]",
+ examples: [
+ "flux database:seed # Run all seeders",
+ "flux database:seed users # Run specific seeder"
+ ],
+ arguments: [
+ {
+ name: "seeder",
+ description: "Specific seeder to run",
+ required: false,
+ type: "string"
+ }
+ ],
+ options: [
+ {
+ name: "force",
+ short: "f",
+ description: "Force seeding even if data exists",
+ type: "boolean"
+ }
+ ],
+ handler: async (args, options, context) => {
+ const [seeder] = args
+
+ if (seeder) {
+ console.log(`🌱 Running seeder: ${seeder}`)
+ console.log(` Force mode: ${options.force ? 'ON' : 'OFF'}`)
+ } else {
+ console.log("🌱 Running all seeders...")
+ }
+
+ // Simulate seeding
+ await new Promise(resolve => setTimeout(resolve, 1500))
+ console.log("✅ Database seeded successfully")
+ }
+ },
+ {
+ name: "reset",
+ description: "Reset the database (drop all tables and recreate)",
+ category: "Database",
+ usage: "flux database:reset [options]",
+ examples: [
+ "flux database:reset # Reset and migrate",
+ "flux database:reset --seed # Reset, migrate and seed"
+ ],
+ options: [
+ {
+ name: "seed",
+ short: "s",
+ description: "Run seeders after reset",
+ type: "boolean"
+ },
+ {
+ name: "confirm",
+ description: "Skip confirmation prompt",
+ type: "boolean"
+ }
+ ],
+ handler: async (args, options, context) => {
+ if (!options.confirm) {
+ console.log("⚠️ WARNING: This will delete all data in the database!")
+ console.log("Use --confirm to skip this prompt.")
+ return
+ }
+
+ console.log("🗑️ Dropping all tables...")
+ await new Promise(resolve => setTimeout(resolve, 1000))
+
+ console.log("📈 Running migrations...")
+ await new Promise(resolve => setTimeout(resolve, 1500))
+
+ if (options.seed) {
+ console.log("🌱 Running seeders...")
+ await new Promise(resolve => setTimeout(resolve, 1000))
+ }
+
+ console.log("✅ Database reset completed")
+ }
+ },
+ {
+ name: "status",
+ description: "Show database migration status",
+ category: "Database",
+ aliases: ["info"],
+ handler: async (args, options, context) => {
+ console.log("📊 Database Status:")
+ console.log("------------------")
+ console.log("Connected: ✅ Yes")
+ console.log("Tables: 15")
+ console.log("Last migration: 2024_01_15_create_users_table")
+ console.log("Pending migrations: 2")
+ console.log("Database size: 2.3 MB")
+ }
+ }
+ ]
+}
+
+// Utility functions that could be used by the plugin
+export async function runMigration(version?: string): Promise {
+ // Actual migration logic would go here
+ console.log(`Running migration ${version || 'all'}`)
+}
+
+export async function rollbackMigration(): Promise {
+ // Actual rollback logic would go here
+ console.log("Rolling back migration")
+}
+
+export async function seedDatabase(seeder?: string): Promise {
+ // Actual seeding logic would go here
+ console.log(`Seeding database ${seeder || 'all'}`)
+}
\ No newline at end of file
diff --git a/core/server/plugins/static.ts b/core/server/plugins/static.ts
index f0cad1ef..ab947127 100644
--- a/core/server/plugins/static.ts
+++ b/core/server/plugins/static.ts
@@ -16,7 +16,7 @@ export const staticPlugin: Plugin = {
} else {
// Servir arquivos estáticos em produção
const url = new URL(request.url)
- const clientDistPath = join(process.cwd(), context.config.client?.build?.outDir || "app/client/dist")
+ const clientDistPath = join(process.cwd(), context.config.client?.build?.outDir || "dist/client")
const filePath = join(clientDistPath, url.pathname)
// Servir index.html para rotas SPA
diff --git a/core/server/plugins/vite.ts b/core/server/plugins/vite.ts
index 9c7d8cc1..11cf8ce6 100644
--- a/core/server/plugins/vite.ts
+++ b/core/server/plugins/vite.ts
@@ -1,8 +1,8 @@
-import type { Plugin, PluginContext } from "../../types"
+import type { Plugin, PluginContext, RequestContext } from "../../types"
export const vitePlugin: Plugin = {
name: "vite",
- setup: async (context: PluginContext) => {
+ setup: (context: PluginContext) => {
if (!context.utils.isDevelopment()) return
const vitePort = context.config.client?.port || 5173
@@ -12,16 +12,54 @@ export const vitePlugin: Plugin = {
const isViteRunning = await checkViteRunning(vitePort)
if (isViteRunning) {
- console.log(` ✅ Vite detectado na porta ${vitePort}`)
- console.log(" 🔄 Hot reload coordenado via concurrently")
+ context.logger.info(`✓ Vite server detected on localhost:${vitePort}`)
+ context.logger.info('Hot reload coordination active')
}
}, 2000)
- // Don't block server startup
- console.log(` 🔄 Aguardando Vite na porta ${vitePort}...`)
+ context.logger.info(`Setting up Vite integration on localhost:${vitePort}`)
+ },
+
+ onBeforeRoute: async (context: RequestContext) => {
+ // Skip API routes and swagger - let them be handled by backend
+ if (context.path.startsWith("/api") || context.path.startsWith("/swagger")) {
+ return
+ }
+
+ // For all other routes, try to proxy to Vite
+ const vitePort = 5173 // TODO: Get from config context
+
+ try {
+ const url = new URL(context.request.url)
+ const viteUrl = `http://localhost:${vitePort}${context.path}${url.search}`
+
+ // Forward request to Vite
+ const response = await fetch(viteUrl, {
+ method: context.method,
+ headers: context.headers
+ })
+
+ // If Vite responds successfully, handle the request
+ if (response.ok || response.status < 500) {
+ // Return a proper Response object with all headers and status
+ const body = await response.arrayBuffer()
+
+ context.handled = true
+ context.response = new Response(body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers: response.headers
+ })
+ }
+
+ } catch (viteError) {
+ // If Vite fails, let the request continue to normal routing (will become 404)
+ console.warn(`Vite proxy error: ${viteError}`)
+ }
}
}
+
async function checkViteRunning(port: number): Promise {
try {
const response = await fetch(`http://localhost:${port}`, {
diff --git a/package.json b/package.json
index 5c67afe4..bc6a01e0 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
},
"devDependencies": {
"@eslint/js": "^9.30.1",
+ "@tailwindcss/vite": "^4.1.13",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
@@ -55,6 +56,7 @@
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"jsdom": "^26.1.0",
+ "tailwindcss": "^4.1.13",
"typescript": "^5.8.3",
"typescript-eslint": "^8.35.1",
"vitest": "^3.2.4"
@@ -62,9 +64,11 @@
"dependencies": {
"@elysiajs/eden": "^1.3.2",
"@elysiajs/swagger": "^1.3.1",
+ "@types/http-proxy-middleware": "^1.0.0",
"@vitejs/plugin-react": "^4.6.0",
"chokidar": "^4.0.3",
"elysia": "^1.4.6",
+ "http-proxy-middleware": "^3.0.5",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"vite": "^7.0.4"
diff --git a/tests/unit/core/plugins/vite.test.ts b/tests/unit/core/plugins/vite.test.ts
index 3d4207d4..c5e211d8 100644
--- a/tests/unit/core/plugins/vite.test.ts
+++ b/tests/unit/core/plugins/vite.test.ts
@@ -53,18 +53,12 @@ describe('Vite Plugin', () => {
describe('Development Mode', () => {
it('should set up plugin in development mode', async () => {
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
-
await vitePlugin.setup!(mockContext)
- expect(consoleSpy).toHaveBeenCalledWith(' 🔄 Aguardando Vite na porta 5173...')
-
- consoleSpy.mockRestore()
+ expect(mockContext.logger.info).toHaveBeenCalledWith('Setting up Vite integration on localhost:5173')
})
it('should check for Vite after timeout', async () => {
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
-
// Mock successful Vite check
vi.mocked(fetch).mockResolvedValueOnce({
status: 200,
@@ -83,15 +77,11 @@ describe('Vite Plugin', () => {
signal: expect.any(AbortSignal)
})
- expect(consoleSpy).toHaveBeenCalledWith(' ✅ Vite detectado na porta 5173')
- expect(consoleSpy).toHaveBeenCalledWith(' 🔄 Hot reload coordenado via concurrently')
-
- consoleSpy.mockRestore()
+ expect(mockContext.logger.info).toHaveBeenCalledWith('✓ Vite server detected on localhost:5173')
+ expect(mockContext.logger.info).toHaveBeenCalledWith('Hot reload coordination active')
})
it('should handle Vite check failure silently', async () => {
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
-
// Mock failed Vite check
vi.mocked(fetch).mockRejectedValueOnce(new Error('Connection refused'))
@@ -106,14 +96,10 @@ describe('Vite Plugin', () => {
})
// Should not log success messages when Vite is not running
- expect(consoleSpy).not.toHaveBeenCalledWith(' ✅ Vite detectado na porta 5173')
-
- consoleSpy.mockRestore()
+ expect(mockContext.logger.info).not.toHaveBeenCalledWith('✓ Vite server detected on localhost:5173')
})
it('should use custom vite port from context', async () => {
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
-
const customContext = {
...mockContext,
config: {
@@ -129,7 +115,7 @@ describe('Vite Plugin', () => {
await vitePlugin.setup!(customContext)
- expect(consoleSpy).toHaveBeenCalledWith(' 🔄 Aguardando Vite na porta 3001...')
+ expect(customContext.logger.info).toHaveBeenCalledWith('Setting up Vite integration on localhost:3001')
vi.advanceTimersByTime(2000)
await vi.runAllTimersAsync()
@@ -137,15 +123,11 @@ describe('Vite Plugin', () => {
expect(fetch).toHaveBeenCalledWith('http://localhost:3001', {
signal: expect.any(AbortSignal)
})
-
- consoleSpy.mockRestore()
})
})
describe('Production Mode', () => {
it('should skip plugin setup in production mode', async () => {
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
-
const productionContext = {
...mockContext,
utils: {
@@ -157,10 +139,8 @@ describe('Vite Plugin', () => {
await vitePlugin.setup!(productionContext)
- expect(consoleSpy).not.toHaveBeenCalled()
+ expect(productionContext.logger.info).not.toHaveBeenCalled()
expect(fetch).not.toHaveBeenCalled()
-
- consoleSpy.mockRestore()
})
})
@@ -183,15 +163,11 @@ describe('Vite Plugin', () => {
// We need to access the checkViteRunning function
// Since it's not exported, we'll test it indirectly through the plugin
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
-
await vitePlugin.setup!(mockContext)
vi.advanceTimersByTime(2000)
await vi.runAllTimersAsync()
- expect(consoleSpy).toHaveBeenCalledWith(' ✅ Vite detectado na porta 5173')
-
- consoleSpy.mockRestore()
+ expect(mockContext.logger.info).toHaveBeenCalledWith('✓ Vite server detected on localhost:5173')
})
it('should return false for connection timeout', async () => {
@@ -200,17 +176,13 @@ describe('Vite Plugin', () => {
setTimeout(() => reject(new Error('timeout')), 1500)
})
)
-
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await vitePlugin.setup!(mockContext)
vi.advanceTimersByTime(2000)
await vi.runAllTimersAsync()
// Should not show success message when timeout occurs
- expect(consoleSpy).not.toHaveBeenCalledWith(' ✅ Vite detectado na porta 5173')
-
- consoleSpy.mockRestore()
+ expect(mockContext.logger.info).not.toHaveBeenCalledWith('✓ Vite server detected on localhost:5173')
})
})
})
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
index c057e1f8..5f1ebbb8 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,10 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
import { resolve } from 'path'
// https://vite.dev/config/
export default defineConfig({
- plugins: [react()],
+ plugins: [react(), tailwindcss()],
root: 'app/client',
server: {
port: 5173,