diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2c3939e8..29341606 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -81,7 +81,14 @@ "Read(/C:\\Users\\Marcos\\Documents\\GitHub\\aviso-projeto\\test-temporal-bridge/**)", "Read(//c/Users/Marcos/Documents/GitHub/PROJETO-OHANA/BackEnd/src/utils/**)", "Bash(bunx tsc:*)", - "Bash(echo:*)" + "Bash(echo:*)", + "Bash(PORT=3000 DEBUG=false bun build test-build-inlining.ts --outfile=test-output.js --minify)", + "Bash(PORT=3000 DEBUG=false node test-output.js)", + "Bash(PORT=9999 DEBUG=true node test-output.js)", + "Bash(PORT=5555 DEBUG=false TEST_NEW_VAR=hello node test-output.js)", + "Bash(bunx prettier:*)", + "Read(//c/c/Users/Marcos/Documents/GitHub/aviso-projeto/FluxStack/**)", + "Read(//c/**)" ], "deny": [] } diff --git a/CLAUDE.md b/CLAUDE.md index 489250ab..19314293 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,9 +50,17 @@ FluxStack/ ├── core/ # 🔒 FRAMEWORK (read-only) │ ├── server/ # Framework Elysia + plugins -│ ├── config/ # Sistema de configuração +│ ├── config/ # Sistema base de configuração +│ ├── utils/ # Utilitários (env.ts, config-schema.ts) │ ├── types/ # Types do framework │ └── build/ # Sistema de build +├── config/ # ⚙️ CONFIGURAÇÕES DA APLICAÇÃO +│ ├── app.config.ts # Configuração principal +│ ├── server.config.ts # Servidor e CORS +│ ├── logger.config.ts # Sistema de logs +│ ├── database.config.ts # Banco de dados +│ ├── system.config.ts # Informações do sistema +│ └── index.ts # Exports centralizados ├── app/ # 👨‍💻 CÓDIGO DA APLICAÇÃO │ ├── server/ # Backend (controllers, routes) │ │ ├── controllers/ # Lógica de negócio @@ -108,10 +116,99 @@ bun run dev:clean # ✅ Output limpo (sem logs HEAD do Elysia) - **Users CRUD**: `GET|POST|PUT|DELETE /api/users` ✅ - **Swagger Docs**: `GET /swagger` ✅ -### ✅ **4. Environment Variables Dinâmicas** -- **Sistema robusto**: Precedência clara -- **Testing endpoint**: `/api/env-test` -- **Validação automática**: Environment vars +### ✅ **4. Sistema de Configuração Declarativa (Laravel-inspired)** + +FluxStack usa um sistema de configuração declarativa com validação automática e inferência de tipos completa. + +#### 📁 **Estrutura de Configuração** +``` +config/ +├── app.config.ts # Configuração da aplicação +├── server.config.ts # Configuração do servidor +├── logger.config.ts # Configuração de logs +├── database.config.ts # Configuração do banco de dados +├── system.config.ts # Informações do sistema +└── index.ts # Exports centralizados +``` + +#### 🎯 **Como Usar** + +**1. Definir Schema de Configuração:** +```typescript +// config/app.config.ts +import { defineConfig, config } from '@/core/utils/config-schema' + +const appConfigSchema = { + name: config.string('APP_NAME', 'FluxStack', true), + port: config.number('PORT', 3000, true), + env: config.enum('NODE_ENV', ['development', 'production', 'test'] as const, 'development', true), + debug: config.boolean('DEBUG', false), +} as const + +export const appConfig = defineConfig(appConfigSchema) +``` + +**2. Usar Configuração com Type Safety:** +```typescript +import { appConfig } from '@/config/app.config' + +// ✅ Type inference automática +const port = appConfig.port // number +const env = appConfig.env // "development" | "production" | "test" +const debug = appConfig.debug // boolean + +// ✅ Validação em tempo de boot +if (appConfig.env === 'production') { + // TypeScript sabe que env é exatamente 'production' +} +``` + +**3. Validação e Transformação:** +```typescript +const schema = { + port: { + type: 'number' as const, + env: 'PORT', + default: 3000, + required: true, + validate: (value: number) => { + if (value < 1 || value > 65535) { + return 'Port must be between 1 and 65535' + } + return true + } + } +} +``` + +#### ⚡ **Benefícios** +- ✅ **Type Safety Total**: Inferência automática de tipos literais +- ✅ **Validação em Boot**: Falha rápida com mensagens claras +- ✅ **Zero Tipos `any`**: TypeScript infere tudo corretamente +- ✅ **Hot Reload Seguro**: Configs podem ser recarregadas em runtime +- ✅ **Documentação Automática**: Schema serve como documentação + +#### 🔧 **Helpers Disponíveis** +```typescript +import { config } from '@/core/utils/config-schema' + +config.string(envVar, defaultValue, required) +config.number(envVar, defaultValue, required) +config.boolean(envVar, defaultValue, required) +config.array(envVar, defaultValue, required) +config.enum(envVar, values, defaultValue, required) +``` + +#### 🚫 **Não Fazer** +- ❌ Usar `process.env` diretamente no código da aplicação +- ❌ Acessar variáveis de ambiente sem validação +- ❌ Criar configs sem schema + +#### ✅ **Sempre Fazer** +- ✅ Usar configs declarativos de `config/` +- ✅ Definir schemas com validação +- ✅ Usar helpers `config.*` para type safety +- ✅ Adicionar `as const` nos schemas para preservar tipos literais ## 🚨 **Regras Críticas (Atualizadas)** @@ -172,6 +269,19 @@ curl http://localhost:3000/api/health # ✅ Health check - **Hot reload independente**: Backend e frontend separados - **Build otimizado**: Sistema unificado +### **✅ Sistema de Configuração Declarativa (Janeiro 2025)** +- **Problema resolvido**: Uso direto de `process.env` sem validação +- **Solução implementada**: Sistema Laravel-inspired com schemas +- **Arquitetura**: 3 camadas (env loader → config schema → app configs) +- **Benefícios**: + - ✅ Type inference completa com tipos literais + - ✅ Validação em boot time com mensagens claras + - ✅ Zero tipos `any` em configurações + - ✅ Hot reload seguro de configs + - ✅ Pasta `config/` centralizada e organizada +- **Build**: Pasta `config/` copiada automaticamente para produção +- **CLI**: `create-fluxstack` inclui configs automaticamente + ## 🎯 **Próximos Passos Sugeridos** ### **Funcionalidades Pendentes** diff --git a/app/server/backend-only.ts b/app/server/backend-only.ts index fc96693a..ba0cd7a2 100644 --- a/app/server/backend-only.ts +++ b/app/server/backend-only.ts @@ -1,15 +1,15 @@ // Backend standalone entry point import { startBackendOnly } from "@/core/server/standalone" import { apiRoutes } from "./routes" -import { env } from "@/core/utils/env-runtime" +import { serverConfig } from "@/config/server.config" -// Configuração para backend standalone com env dinâmico +// Configuração para backend standalone usando config declarativo const backendConfig = { - port: env.get('BACKEND_PORT', 3001), // Casting automático para number - apiPrefix: env.API_PREFIX // Direto! (string) + port: serverConfig.backendPort, + apiPrefix: serverConfig.apiPrefix } -console.log(`🚀 Backend standalone: ${env.HOST}:${backendConfig.port}`) +console.log(`🚀 Backend standalone: ${serverConfig.host}:${backendConfig.port}`) // Iniciar apenas o backend startBackendOnly(apiRoutes, backendConfig) \ No newline at end of file diff --git a/app/server/index.ts b/app/server/index.ts index 9f2295db..ef4896d9 100644 --- a/app/server/index.ts +++ b/app/server/index.ts @@ -3,45 +3,46 @@ import { FluxStackFramework, vitePlugin, swaggerPlugin, staticPlugin, liveCompon import { isDevelopment } from "@/core/utils/helpers" import { DEBUG } from "@/core/utils/logger" import { apiRoutes } from "./routes" -// Import sistema de env dinâmico simplificado -import { env, helpers } from "@/core/utils/env-runtime-v2" -// Import live components registration +import { helpers } from "@/core/utils/env" +import { serverConfig } from "@/config/server.config" +import { appConfig } from "@/config/app.config" +import { loggerConfig } from "@/config/logger.config" import "./live/register-components" // Startup info moved to DEBUG level (set LOG_LEVEL=debug to see details) -DEBUG('🔧 Loading dynamic environment configuration...') -DEBUG(`📊 Environment: ${env.NODE_ENV}`) -DEBUG(`🚀 Port: ${env.PORT}`) -DEBUG(`🌐 Host: ${env.HOST}`) +DEBUG('🔧 Loading declarative configuration...') +DEBUG(`📊 Environment: ${appConfig.env}`) +DEBUG(`🚀 Port: ${serverConfig.port}`) +DEBUG(`🌐 Host: ${serverConfig.host}`) -// Criar aplicação com configuração dinâmica simplificada +// Criar aplicação com configuração declarativa const app = new FluxStackFramework({ server: { - port: env.PORT, // Direto! (number) - host: env.HOST, // Direto! (string) - apiPrefix: env.API_PREFIX, // Direto! (string) + port: serverConfig.port, + host: serverConfig.host, + apiPrefix: serverConfig.apiPrefix, cors: { - origins: env.CORS_ORIGINS, // Direto! (string[]) - methods: env.get('CORS_METHODS', ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']), - headers: env.get('CORS_HEADERS', ['*']), - credentials: env.get('CORS_CREDENTIALS', false) + origins: serverConfig.corsOrigins, + methods: serverConfig.corsMethods, + headers: serverConfig.corsHeaders, + credentials: serverConfig.corsCredentials }, middleware: [] }, app: { - name: env.FLUXSTACK_APP_NAME, // Direto! (string) - version: env.FLUXSTACK_APP_VERSION // Direto! (string) + name: serverConfig.appName, + version: serverConfig.appVersion }, client: { - port: env.VITE_PORT, // Direto! (number) + port: serverConfig.clientPort, proxy: { - target: helpers.getServerUrl() // Helper inteligente + target: helpers.getServerUrl() }, build: { - sourceMaps: env.get('CLIENT_SOURCEMAPS', env.NODE_ENV !== 'production'), - minify: env.get('CLIENT_MINIFY', env.NODE_ENV === 'production'), - target: env.get('CLIENT_TARGET', 'es2020'), - outDir: env.get('CLIENT_OUTDIR', 'dist') + sourceMaps: serverConfig.clientSourceMaps, + minify: false, + target: serverConfig.clientTarget as any, + outDir: serverConfig.clientOutDir } } }) @@ -61,30 +62,47 @@ app.use(staticFilesPlugin) // Add Static Files support app.use(liveComponentsPlugin) // Add Live Components support -// Adicionar rota de teste para mostrar env dinâmico (antes das rotas) +// Adicionar rota de teste para mostrar config declarativo (antes das rotas) app.getApp().get('/api/env-test', () => { return { - message: '🔥 Environment Variables Simplificado!', + message: '⚡ Declarative Config System!', timestamp: new Date().toISOString(), + serverConfig: { + port: serverConfig.port, + host: serverConfig.host, + apiPrefix: serverConfig.apiPrefix, + appName: serverConfig.appName, + appVersion: serverConfig.appVersion, + cors: { + origins: serverConfig.corsOrigins, + methods: serverConfig.corsMethods, + credentials: serverConfig.corsCredentials + }, + client: { + port: serverConfig.clientPort, + target: serverConfig.clientTarget, + sourceMaps: serverConfig.clientSourceMaps + }, + features: { + enableSwagger: serverConfig.enableSwagger, + enableMetrics: serverConfig.enableMetrics, + enableMonitoring: serverConfig.enableMonitoring + } + }, environment: { - NODE_ENV: env.NODE_ENV, // Direto! - PORT: env.PORT, // Direto! - HOST: env.HOST, // Direto! - DEBUG: env.DEBUG, // Direto! - CORS_ORIGINS: env.CORS_ORIGINS, // Direto! - ENABLE_SWAGGER: env.ENABLE_SWAGGER, // Direto! - - // Vars customizadas com casting automático - CUSTOM_VAR: env.get('CUSTOM_VAR', 'not-set'), - MAX_RETRIES: env.get('MAX_RETRIES', 3), // number - ENABLE_CACHE: env.get('ENABLE_CACHE', false), // boolean - ALLOWED_IPS: env.get('ALLOWED_IPS', []) // string[] + NODE_ENV: appConfig.env, + DEBUG: appConfig.debug, + LOG_LEVEL: loggerConfig.level }, urls: { - server: helpers.getServerUrl(), // Helper! + server: helpers.getServerUrl(), + client: helpers.getClientUrl(), swagger: `${helpers.getServerUrl()}/swagger` }, - note: 'API simplificada com casting automático! 🚀' + system: { + version: 'declarative-config', + features: ['type-safe', 'validated', 'declarative', 'runtime-reload'] + } } }) diff --git a/app/server/live/FluxStackConfig.ts b/app/server/live/FluxStackConfig.ts index 8e50a5a8..33093467 100644 --- a/app/server/live/FluxStackConfig.ts +++ b/app/server/live/FluxStackConfig.ts @@ -1,6 +1,10 @@ // 🔥 FluxStack Configuration Live Component import { LiveComponent } from '@/core/types/types' +import { appConfig } from '@/config/app.config' +import { serverConfig } from '@/config/server.config' +import { loggerConfig } from '@/config/logger.config' +import { systemConfig, systemRuntimeInfo } from '@/config/system.config' export interface FluxStackConfigState { // Environment Configuration @@ -154,10 +158,10 @@ export class FluxStackConfig extends LiveComponent { // Set default state with real configuration this.state = { - environment: (process.env.NODE_ENV as any) || 'development', - port: parseInt(process.env.PORT || '3000'), - host: process.env.HOST || 'localhost', - apiPrefix: '/api', + environment: appConfig.env, + port: serverConfig.port, + host: serverConfig.host, + apiPrefix: serverConfig.apiPrefix, framework: { name: 'FluxStack', @@ -195,7 +199,7 @@ export class FluxStackConfig extends LiveComponent { enabled: true, dependencies: [], config: { - level: process.env.LOG_LEVEL || 'info', + level: loggerConfig.level, format: 'pretty' } }, @@ -244,17 +248,15 @@ export class FluxStackConfig extends LiveComponent { // Get runtime configuration private getRuntimeConfiguration() { - const os = require('os') - return { - nodeVersion: process.version, - bunVersion: process.versions.bun || 'N/A', - platform: process.platform, - architecture: process.arch, - cpuCount: os.cpus().length, - totalMemory: Math.round(os.totalmem() / 1024 / 1024 / 1024 * 100) / 100, // GB - workingDirectory: process.cwd(), - executablePath: process.execPath + nodeVersion: systemRuntimeInfo.nodeVersion, + bunVersion: systemRuntimeInfo.bunVersion, + platform: systemRuntimeInfo.platform, + architecture: systemRuntimeInfo.architecture, + cpuCount: systemRuntimeInfo.cpuCount, + totalMemory: systemRuntimeInfo.totalMemory, + workingDirectory: systemRuntimeInfo.workingDirectory, + executablePath: systemRuntimeInfo.executablePath } } @@ -355,17 +357,17 @@ export class FluxStackConfig extends LiveComponent { // Get Logging configuration private getLoggingConfiguration() { return { - level: (process.env.LOG_LEVEL as any) || 'info', - format: 'pretty', + level: loggerConfig.level as 'debug' | 'info' | 'warn' | 'error', + format: 'pretty' as 'json' | 'pretty' | 'compact', file: { - enabled: false, - path: undefined, - maxSize: undefined, - maxFiles: undefined + enabled: loggerConfig.logToFile, + path: loggerConfig.logToFile ? 'logs/app.log' : undefined, + maxSize: loggerConfig.maxSize, + maxFiles: parseInt(loggerConfig.maxFiles) || undefined }, console: { enabled: true, - colors: true + colors: loggerConfig.enableColors } } } @@ -408,16 +410,18 @@ export class FluxStackConfig extends LiveComponent { // Update specific configuration section async updateConfiguration(data: { section: string; config: Record }) { const { section, config } = data - + if (!this.state[section as keyof FluxStackConfigState]) { throw new Error(`Invalid configuration section: ${section}`) } - + + const currentSection = this.state[section as keyof FluxStackConfigState] + const updatedSection = typeof currentSection === 'object' && currentSection !== null + ? { ...currentSection as object, ...config } + : config + this.setState({ - [section]: { - ...this.state[section as keyof FluxStackConfigState], - ...config - }, + [section]: updatedSection, lastUpdated: Date.now() } as Partial) @@ -433,22 +437,22 @@ export class FluxStackConfig extends LiveComponent { // Get environment variables async getEnvironmentVariables() { const envVars = { - NODE_ENV: process.env.NODE_ENV, - PORT: process.env.PORT, - HOST: process.env.HOST, - LOG_LEVEL: process.env.LOG_LEVEL, - // Add other non-sensitive env vars - PWD: process.env.PWD, - PATH: process.env.PATH ? '***truncated***' : undefined, - USER: process.env.USER || process.env.USERNAME, - HOME: process.env.HOME || process.env.USERPROFILE + NODE_ENV: appConfig.env, + PORT: serverConfig.port.toString(), + HOST: serverConfig.host, + LOG_LEVEL: loggerConfig.level, + // Add other non-sensitive env vars from system config + PWD: systemConfig.pwd || undefined, + PATH: systemConfig.path ? '***truncated***' : undefined, + USER: systemConfig.currentUser, + HOME: systemConfig.homeDirectory || undefined } - + this.emit('ENVIRONMENT_VARIABLES_REQUESTED', { count: Object.keys(envVars).length, timestamp: Date.now() }) - + return envVars } diff --git a/app/server/middleware/errorHandling.ts b/app/server/middleware/errorHandling.ts index f46b1624..6d9dd69d 100644 --- a/app/server/middleware/errorHandling.ts +++ b/app/server/middleware/errorHandling.ts @@ -4,6 +4,7 @@ */ import type { Context } from 'elysia' +import { appConfig } from '@/config/app.config' export interface ErrorResponse { error: string @@ -139,14 +140,15 @@ export const errorHandlingMiddleware = { } // Default to internal server error + const isProduction = appConfig.env === 'production' return createErrorResponse( 500, - process.env.NODE_ENV === 'production' - ? 'Internal server error' + isProduction + ? 'Internal server error' : error.message, 'INTERNAL_ERROR', - process.env.NODE_ENV === 'production' - ? undefined + isProduction + ? undefined : { stack: error.stack }, requestId ) diff --git a/app/server/routes/config.ts b/app/server/routes/config.ts new file mode 100644 index 00000000..0730517f --- /dev/null +++ b/app/server/routes/config.ts @@ -0,0 +1,145 @@ +/** + * Config Management Routes + * Allows runtime configuration reload and inspection + */ + +import { Elysia, t } from 'elysia' +import { appRuntimeConfig } from '@/config/runtime.config' + +export const configRoutes = new Elysia({ prefix: '/config' }) + /** + * Get current runtime configuration + */ + .get('/', () => { + return { + success: true, + config: appRuntimeConfig.values, + timestamp: new Date().toISOString() + } + }, { + detail: { + summary: 'Get current runtime configuration', + tags: ['Config'] + } + }) + + /** + * Reload configuration from environment + */ + .post('/reload', () => { + try { + const oldConfig = { ...appRuntimeConfig.values } + const newConfig = appRuntimeConfig.reload() + + // Find changed fields + const changes: Record = {} + for (const key in newConfig) { + if (oldConfig[key] !== newConfig[key]) { + changes[key] = { + old: oldConfig[key], + new: newConfig[key] + } + } + } + + return { + success: true, + message: 'Configuration reloaded successfully', + changes, + timestamp: new Date().toISOString() + } + } catch (error: any) { + return { + success: false, + error: error.message, + timestamp: new Date().toISOString() + } + } + }, { + detail: { + summary: 'Reload configuration from environment variables', + description: 'Reloads configuration without restarting the server. Validates new values before applying.', + tags: ['Config'] + } + }) + + /** + * Get specific config field + */ + .get('/:field', ({ params: { field } }) => { + const value = appRuntimeConfig.get(field as any) + + if (value === undefined) { + return { + success: false, + error: `Field '${field}' not found`, + timestamp: new Date().toISOString() + } + } + + return { + success: true, + field, + value, + type: typeof value, + timestamp: new Date().toISOString() + } + }, { + detail: { + summary: 'Get specific configuration field', + tags: ['Config'] + }, + params: t.Object({ + field: t.String() + }) + }) + + /** + * Check if config field exists + */ + .get('/:field/exists', ({ params: { field } }) => { + const exists = appRuntimeConfig.has(field as any) + + return { + success: true, + field, + exists, + timestamp: new Date().toISOString() + } + }, { + detail: { + summary: 'Check if configuration field exists', + tags: ['Config'] + }, + params: t.Object({ + field: t.String() + }) + }) + + /** + * Health check for config system + */ + .get('/health', () => { + try { + const config = appRuntimeConfig.values + + return { + success: true, + status: 'healthy', + fieldsLoaded: Object.keys(config).length, + timestamp: new Date().toISOString() + } + } catch (error: any) { + return { + success: false, + status: 'unhealthy', + error: error.message, + timestamp: new Date().toISOString() + } + } + }, { + detail: { + summary: 'Config system health check', + tags: ['Config'] + } + }) diff --git a/app/server/routes/index.ts b/app/server/routes/index.ts index 58cd5cab..eee92ae6 100644 --- a/app/server/routes/index.ts +++ b/app/server/routes/index.ts @@ -1,6 +1,7 @@ import { Elysia, t } from "elysia" import { usersRoutes } from "./users.routes" import { uploadRoutes } from "./upload" +import { configRoutes } from "./config" export const apiRoutes = new Elysia({ prefix: "/api" }) .get("/", () => ({ message: "🔥 Hot Reload funcionando! FluxStack API v1.4.0 ⚡" }), { @@ -13,8 +14,8 @@ export const apiRoutes = new Elysia({ prefix: "/api" }) description: 'Returns a welcome message from the FluxStack API' } }) - .get("/health", () => ({ - status: "🚀 Hot Reload ativo!", + .get("/health", () => ({ + status: "🚀 Hot Reload ativo!", timestamp: new Date().toISOString(), uptime: `${Math.floor(process.uptime())}s`, version: "1.4.0", @@ -34,4 +35,5 @@ export const apiRoutes = new Elysia({ prefix: "/api" }) } }) .use(usersRoutes) - .use(uploadRoutes) \ No newline at end of file + .use(uploadRoutes) + .use(configRoutes) \ No newline at end of file diff --git a/config/app.config.ts b/config/app.config.ts new file mode 100644 index 00000000..409cdb99 --- /dev/null +++ b/config/app.config.ts @@ -0,0 +1,113 @@ +/** + * Application Configuration + * Laravel-style declarative config with validation + */ + +import { defineConfig, config } from '@/core/utils/config-schema' + +/** + * App configuration schema + */ +const appConfigSchema = { + // App basics + name: config.string('APP_NAME', 'FluxStack', true), + + version: { + type: 'string' as const, + env: 'APP_VERSION', + default: '1.0.0', + validate: (value: string) => /^\d+\.\d+\.\d+$/.test(value) || 'Version must be semver format (e.g., 1.0.0)' + }, + + description: config.string('APP_DESCRIPTION', 'A FluxStack application'), + + // Environment + env: config.enum('NODE_ENV', ['development', 'production', 'test'] as const, 'development', true), + + debug: config.boolean('DEBUG', false), + + // Server + port: { + type: 'number' as const, + env: 'PORT', + default: 3000, + required: true, + validate: (value: number) => { + if (value < 1 || value > 65535) { + return 'Port must be between 1 and 65535' + } + return true + } + }, + + host: config.string('HOST', 'localhost', true), + + apiPrefix: { + type: 'string' as const, + env: 'API_PREFIX', + default: '/api', + validate: (value: string) => value.startsWith('/') || 'API prefix must start with /' + }, + + // URLs + url: config.string('APP_URL', undefined, false), + + // Features + enableSwagger: config.boolean('ENABLE_SWAGGER', true), + enableMetrics: config.boolean('ENABLE_METRICS', false), + enableMonitoring: config.boolean('ENABLE_MONITORING', false), + + // Client + clientPort: config.number('VITE_PORT', 5173), + + // Logging + logLevel: config.enum('LOG_LEVEL', ['debug', 'info', 'warn', 'error'] as const, 'info'), + logFormat: config.enum('LOG_FORMAT', ['json', 'pretty'] as const, 'pretty'), + + // CORS + corsOrigins: config.array('CORS_ORIGINS', ['*']), + corsMethods: config.array('CORS_METHODS', ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']), + corsHeaders: config.array('CORS_HEADERS', ['Content-Type', 'Authorization']), + corsCredentials: config.boolean('CORS_CREDENTIALS', false), + + // Security + trustProxy: config.boolean('TRUST_PROXY', false), + + sessionSecret: { + type: 'string' as const, + env: 'SESSION_SECRET', + default: undefined, + required: false, + validate: (value: string) => { + if (!value) return true // Optional + if (value.length < 32) { + return 'Session secret must be at least 32 characters' + } + return true + } + } +} as const + +export const appConfig = defineConfig(appConfigSchema) + +// Export type for use in other files +export type AppConfig = typeof appConfig + +/** + * Type-safe environment type + * Use this when you need the literal type explicitly + */ +export type Environment = typeof appConfig.env + +/** + * Type-safe log level type + */ +export type LogLevel = typeof appConfig.logLevel + +/** + * Type-safe log format type + */ +export type LogFormat = typeof appConfig.logFormat + +// Export default +export default appConfig diff --git a/config/build.config.ts b/config/build.config.ts new file mode 100644 index 00000000..c6c0c793 --- /dev/null +++ b/config/build.config.ts @@ -0,0 +1,24 @@ +/** + * Build & Client Configuration + * Declarative build and client config for FluxStack framework + */ + +import { defineConfig, config } from '@/core/utils/config-schema' + +export const buildConfig = defineConfig({ + // Client build settings + clientBuildDir: config.string('CLIENT_BUILD_DIR', 'dist/client'), + clientSourceMaps: config.boolean('CLIENT_SOURCEMAPS', false), + clientMinify: config.boolean('CLIENT_MINIFY', true), + clientTarget: config.string('CLIENT_TARGET', 'es2020'), + + // API proxy settings + apiUrl: config.string('API_URL', 'http://localhost:3000'), + proxyChangeOrigin: config.boolean('PROXY_CHANGE_ORIGIN', true), + + // Monitoring + monitoringEnabled: config.boolean('MONITORING_ENABLED', false) +}) + +export type BuildConfig = typeof buildConfig +export default buildConfig diff --git a/config/database.config.ts b/config/database.config.ts new file mode 100644 index 00000000..6cd3b5db --- /dev/null +++ b/config/database.config.ts @@ -0,0 +1,99 @@ +/** + * Database Configuration + * Laravel-style declarative config with validation + */ + +import { defineConfig, config } from '@/core/utils/config-schema' + +/** + * Database configuration schema + */ +export const databaseConfig = defineConfig({ + // Connection + url: { + type: 'string', + env: 'DATABASE_URL', + default: undefined, + required: false, + validate: (value) => { + if (!value) return true // Optional + if (!value.includes('://')) { + return 'DATABASE_URL must be a valid connection string (e.g., postgres://...)' + } + return true + }, + description: 'Full database connection URL (overrides individual settings)' + }, + + host: config.string('DB_HOST', 'localhost'), + + port: config.number('DB_PORT', 5432), + + database: { + type: 'string', + env: 'DB_NAME', + default: undefined, + required: false, + description: 'Database name' + }, + + user: { + type: 'string', + env: 'DB_USER', + default: undefined, + required: false + }, + + password: { + type: 'string', + env: 'DB_PASSWORD', + default: undefined, + required: false + }, + + // Connection pool + poolMin: { + type: 'number', + env: 'DB_POOL_MIN', + default: 2, + validate: (value) => value >= 0 || 'Pool min must be >= 0' + }, + + poolMax: { + type: 'number', + env: 'DB_POOL_MAX', + default: 10, + validate: (value) => value > 0 || 'Pool max must be > 0' + }, + + // SSL + ssl: config.boolean('DB_SSL', false), + + // Timeouts + connectionTimeout: { + type: 'number', + env: 'DB_CONNECTION_TIMEOUT', + default: 30000, + description: 'Connection timeout in milliseconds' + }, + + queryTimeout: { + type: 'number', + env: 'DB_QUERY_TIMEOUT', + default: 60000, + description: 'Query timeout in milliseconds' + }, + + // Features + enableLogging: config.boolean('DB_ENABLE_LOGGING', false), + + enableMigrations: config.boolean('DB_ENABLE_MIGRATIONS', true), + + migrationsTable: config.string('DB_MIGRATIONS_TABLE', 'migrations') +}) + +// Export type +export type DatabaseConfig = typeof databaseConfig + +// Export default +export default databaseConfig diff --git a/config/index.ts b/config/index.ts new file mode 100644 index 00000000..5298de50 --- /dev/null +++ b/config/index.ts @@ -0,0 +1,68 @@ +/** + * ⚡ FluxStack Configuration Index + * + * Centralized configuration using Laravel-style declarative schemas + * + * @example + * ```ts + * import { appConfig, databaseConfig, servicesConfig } from '@/config' + * + * // All configs are type-safe and validated! + * console.log(appConfig.name) // string + * console.log(appConfig.port) // number + * console.log(appConfig.debug) // boolean + * + * // Nested configs + * console.log(servicesConfig.email.host) // string + * console.log(servicesConfig.jwt.secret) // string + * ``` + */ + +export { appConfig } from './app.config' +export { databaseConfig } from './database.config' +export { servicesConfig } from './services.config' +export { serverConfig } from './server.config' +export { loggerConfig } from './logger.config' +export { buildConfig } from './build.config' +export { appRuntimeConfig } from './runtime.config' +export { systemConfig, systemRuntimeInfo } from './system.config' + +// Re-export types +export type { AppConfig } from './app.config' +export type { DatabaseConfig } from './database.config' +export type { ServerConfig } from './server.config' +export type { LoggerConfig } from './logger.config' +export type { BuildConfig } from './build.config' +export type { SystemConfig, SystemRuntimeInfo } from './system.config' +export type { + EmailConfig, + JWTConfig, + StorageConfig, + RedisConfig +} from './services.config' + +/** + * All configs in one object + */ +import { appConfig } from './app.config' +import { databaseConfig } from './database.config' +import { servicesConfig } from './services.config' +import { serverConfig } from './server.config' +import { loggerConfig } from './logger.config' +import { buildConfig } from './build.config' +import { appRuntimeConfig } from './runtime.config' +import { systemConfig, systemRuntimeInfo } from './system.config' + +export const config = { + app: appConfig, + database: databaseConfig, + services: servicesConfig, + server: serverConfig, + logger: loggerConfig, + build: buildConfig, + runtime: appRuntimeConfig, + system: systemConfig, + systemRuntime: systemRuntimeInfo +} + +export default config diff --git a/config/logger.config.ts b/config/logger.config.ts new file mode 100644 index 00000000..ce07b213 --- /dev/null +++ b/config/logger.config.ts @@ -0,0 +1,27 @@ +/** + * Logger Configuration + * Declarative logger config using FluxStack config system + */ + +import { defineConfig, config } from '@/core/utils/config-schema' + +export const loggerConfig = defineConfig({ + // Log level + level: config.enum('LOG_LEVEL', ['debug', 'info', 'warn', 'error'] as const, 'info'), + + // Format settings + dateFormat: config.string('LOG_DATE_FORMAT', 'YYYY-MM-DD HH:mm:ss'), + objectDepth: config.number('LOG_OBJECT_DEPTH', 4), + + // File logging + logToFile: config.boolean('LOG_TO_FILE', false), + maxSize: config.string('LOG_MAX_SIZE', '20m'), + maxFiles: config.string('LOG_MAX_FILES', '14d'), + + // Display options + enableColors: config.boolean('LOG_COLORS', true), + enableStackTrace: config.boolean('LOG_STACK_TRACE', true) +}) + +export type LoggerConfig = typeof loggerConfig +export default loggerConfig diff --git a/config/runtime.config.ts b/config/runtime.config.ts new file mode 100644 index 00000000..465792a6 --- /dev/null +++ b/config/runtime.config.ts @@ -0,0 +1,92 @@ +/** + * Runtime-Reloadable Configuration + * Configs that can be reloaded without server restart + */ + +import { defineReactiveConfig, config } from '@/core/utils/config-schema' + +/** + * Runtime app configuration + * Can be reloaded via appRuntimeConfig.reload() + */ +export const appRuntimeConfig = defineReactiveConfig({ + // Features that can be toggled in runtime + enableSwagger: config.boolean('ENABLE_SWAGGER', true), + enableMetrics: config.boolean('ENABLE_METRICS', false), + enableMonitoring: config.boolean('ENABLE_MONITORING', false), + enableDebugMode: config.boolean('DEBUG', false), + + // Logging level can be changed in runtime + logLevel: config.enum( + 'LOG_LEVEL', + ['debug', 'info', 'warn', 'error'] as const, + 'info' + ), + + logFormat: config.enum( + 'LOG_FORMAT', + ['json', 'pretty'] as const, + 'pretty' + ), + + // Rate limiting + rateLimitEnabled: config.boolean('RATE_LIMIT_ENABLED', true), + + rateLimitMax: { + type: 'number' as const, + env: 'RATE_LIMIT_MAX', + default: 100, + validate: (value: number) => value > 0 || 'Rate limit must be positive' + }, + + rateLimitWindow: { + type: 'number' as const, + env: 'RATE_LIMIT_WINDOW', + default: 60000, + description: 'Rate limit window in milliseconds' + }, + + // Request timeout + requestTimeout: { + type: 'number' as const, + env: 'REQUEST_TIMEOUT', + default: 30000, + validate: (value: number) => value > 0 || 'Timeout must be positive' + }, + + // Max upload size + maxUploadSize: { + type: 'number' as const, + env: 'MAX_UPLOAD_SIZE', + default: 10485760, // 10MB + validate: (value: number) => value > 0 || 'Max upload size must be positive' + }, + + // Allowed origins (can be updated in runtime) + corsOrigins: config.array('CORS_ORIGINS', ['*']), + + // Maintenance mode + maintenanceMode: config.boolean('MAINTENANCE_MODE', false), + + maintenanceMessage: config.string( + 'MAINTENANCE_MESSAGE', + 'System is under maintenance. Please try again later.' + ) +}) + +/** + * Setup config watcher with logging + */ +appRuntimeConfig.watch((newConfig) => { + console.log('🔄 Runtime config reloaded:') + console.log(' Debug:', newConfig.enableDebugMode) + console.log(' Log Level:', newConfig.logLevel) + console.log(' Maintenance:', newConfig.maintenanceMode) +}) + +/** + * Export type + */ +export type AppRuntimeConfig = typeof appRuntimeConfig.values + +export default appRuntimeConfig diff --git a/config/server.config.ts b/config/server.config.ts new file mode 100644 index 00000000..ff7d774a --- /dev/null +++ b/config/server.config.ts @@ -0,0 +1,46 @@ +/** + * Server Configuration + * Declarative server config using FluxStack config system + */ + +import { defineConfig, config } from '@/core/utils/config-schema' + +const serverConfigSchema = { + // Server basics + port: config.number('PORT', 3000, true), + host: config.string('HOST', 'localhost', true), + apiPrefix: config.string('API_PREFIX', '/api'), + + // CORS configuration + corsOrigins: config.array('CORS_ORIGINS', ['http://localhost:3000', 'http://localhost:5173']), + corsMethods: config.array('CORS_METHODS', ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']), + corsHeaders: config.array('CORS_HEADERS', ['Content-Type', 'Authorization']), + corsCredentials: config.boolean('CORS_CREDENTIALS', false), + corsMaxAge: config.number('CORS_MAX_AGE', 86400), + + // Client config + clientPort: config.number('VITE_PORT', 5173), + clientTarget: config.string('CLIENT_TARGET', 'es2020'), + clientOutDir: config.string('CLIENT_OUTDIR', 'dist'), + clientSourceMaps: config.boolean('CLIENT_SOURCEMAPS', false), + + // Backend-only mode + backendPort: config.number('BACKEND_PORT', 3001), + + // App info + appName: config.string('FLUXSTACK_APP_NAME', 'FluxStack'), + appVersion: config.string('FLUXSTACK_APP_VERSION', '1.0.0'), + + // Features + enableSwagger: config.boolean('ENABLE_SWAGGER', true), + enableMetrics: config.boolean('ENABLE_METRICS', false), + enableMonitoring: config.boolean('ENABLE_MONITORING', false), + + // Vite/Development + enableViteProxyLogs: config.boolean('ENABLE_VITE_PROXY_LOGS', false) +} as const + +export const serverConfig = defineConfig(serverConfigSchema) + +export type ServerConfig = typeof serverConfig +export default serverConfig diff --git a/config/services.config.ts b/config/services.config.ts new file mode 100644 index 00000000..00d56f70 --- /dev/null +++ b/config/services.config.ts @@ -0,0 +1,130 @@ +/** + * External Services Configuration + * Laravel-style declarative config for third-party services + */ + +import { defineConfig, defineNestedConfig, config } from '@/core/utils/config-schema' + +/** + * Email service configuration + */ +const emailSchema = { + // SMTP + host: config.string('SMTP_HOST'), + + port: { + type: 'number' as const, + env: 'SMTP_PORT', + default: 587, + validate: (value: number) => value > 0 || 'SMTP port must be positive' + }, + + user: config.string('SMTP_USER'), + password: config.string('SMTP_PASSWORD'), + + secure: config.boolean('SMTP_SECURE', false), + + from: { + type: 'string' as const, + env: 'SMTP_FROM', + default: 'noreply@example.com', + validate: (value: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + return emailRegex.test(value) || 'From email must be valid' + } + }, + + replyTo: config.string('SMTP_REPLY_TO') +} + +/** + * JWT authentication configuration + */ +const jwtSchema = { + secret: { + type: 'string' as const, + env: 'JWT_SECRET', + default: undefined, + required: false, + validate: (value: string) => { + if (!value) return true // Optional + if (value.length < 32) { + return 'JWT secret must be at least 32 characters for security' + } + return true + } + }, + + expiresIn: config.string('JWT_EXPIRES_IN', '24h'), + + algorithm: config.enum( + 'JWT_ALGORITHM', + ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512'] as const, + 'HS256' + ), + + issuer: config.string('JWT_ISSUER', 'fluxstack'), + + audience: config.string('JWT_AUDIENCE') +} + +/** + * Storage configuration + */ +const storageSchema = { + provider: config.enum( + 'STORAGE_PROVIDER', + ['local', 's3', 'gcs', 'azure'] as const, + 'local' + ), + + uploadPath: config.string('UPLOAD_PATH', './uploads'), + + maxFileSize: { + type: 'number' as const, + env: 'MAX_FILE_SIZE', + default: 10485760, // 10MB + validate: (value: number) => value > 0 || 'Max file size must be positive' + }, + + allowedTypes: config.array('ALLOWED_FILE_TYPES', ['image/*', 'application/pdf']), + + // S3 specific + s3Bucket: config.string('S3_BUCKET'), + s3Region: config.string('S3_REGION', 'us-east-1'), + s3AccessKey: config.string('S3_ACCESS_KEY'), + s3SecretKey: config.string('S3_SECRET_KEY') +} + +/** + * Redis configuration + */ +const redisSchema = { + host: config.string('REDIS_HOST', 'localhost'), + port: config.number('REDIS_PORT', 6379), + password: config.string('REDIS_PASSWORD'), + db: config.number('REDIS_DB', 0), + + keyPrefix: config.string('REDIS_KEY_PREFIX', 'fluxstack:'), + + enableTls: config.boolean('REDIS_TLS', false) +} + +/** + * Export all service configs as nested object + */ +export const servicesConfig = defineNestedConfig({ + email: emailSchema, + jwt: jwtSchema, + storage: storageSchema, + redis: redisSchema +}) + +// Export types +export type EmailConfig = typeof servicesConfig.email +export type JWTConfig = typeof servicesConfig.jwt +export type StorageConfig = typeof servicesConfig.storage +export type RedisConfig = typeof servicesConfig.redis + +// Export default +export default servicesConfig diff --git a/config/system.config.ts b/config/system.config.ts new file mode 100644 index 00000000..0164c704 --- /dev/null +++ b/config/system.config.ts @@ -0,0 +1,105 @@ +/** + * System Runtime Configuration + * System information and environment variables + */ + +import { defineConfig, config } from '@/core/utils/config-schema' + +/** + * System environment variables config + */ +export const systemConfig = defineConfig({ + // User/System info + user: config.string('USER', ''), + username: config.string('USERNAME', ''), + home: config.string('HOME', ''), + userProfile: config.string('USERPROFILE', ''), + + // Paths + pwd: config.string('PWD', ''), + path: config.string('PATH', ''), + + // Shell + shell: config.string('SHELL', ''), + term: config.string('TERM', ''), + + // Common environment variables + lang: config.string('LANG', 'en_US.UTF-8'), + tmpDir: config.string('TMPDIR', ''), + + // CI/CD detection + ci: config.boolean('CI', false), + + // Computed helpers (not from env) + get currentUser() { + return this.user || this.username || 'unknown' + }, + + get homeDirectory() { + return this.home || this.userProfile || '' + }, + + get isCI() { + return this.ci + } +}) + +/** + * System runtime info (from Node.js/Bun process) + * These are not from environment variables, but from runtime + */ +export const systemRuntimeInfo = { + get nodeVersion() { + return process.version + }, + + get bunVersion() { + return (process.versions as any).bun || 'N/A' + }, + + get platform() { + return process.platform + }, + + get architecture() { + return process.arch + }, + + get cpuCount() { + return require('os').cpus().length + }, + + get totalMemory() { + const os = require('os') + return Math.round(os.totalmem() / 1024 / 1024 / 1024 * 100) / 100 // GB + }, + + get workingDirectory() { + return process.cwd() + }, + + get executablePath() { + return process.execPath + }, + + get uptime() { + return process.uptime() + }, + + get memoryUsage() { + const usage = process.memoryUsage() + return { + rss: Math.round(usage.rss / 1024 / 1024 * 100) / 100, // MB + heapTotal: Math.round(usage.heapTotal / 1024 / 1024 * 100) / 100, + heapUsed: Math.round(usage.heapUsed / 1024 / 1024 * 100) / 100, + external: Math.round(usage.external / 1024 / 1024 * 100) / 100 + } + } +} + +// Export types +export type SystemConfig = typeof systemConfig +export type SystemRuntimeInfo = typeof systemRuntimeInfo + +// Export default +export default systemConfig diff --git a/core/build/index.ts b/core/build/index.ts index 0bc22fa9..5dd8440a 100644 --- a/core/build/index.ts +++ b/core/build/index.ts @@ -1,4 +1,4 @@ -import { copyFileSync, writeFileSync, existsSync, mkdirSync } from "fs" +import { copyFileSync, writeFileSync, existsSync, mkdirSync, readFileSync } from "fs" import { join } from "path" import type { FluxStackConfig } from "../config" import type { BuildResult, BuildManifest } from "../types/build" @@ -177,9 +177,15 @@ coverage console.log(` - target path: ${distEnvPath}`) if (existsSync(envPath)) { - console.log(`📄 Copying .env file...`) - copyFileSync(envPath, distEnvPath) - console.log("📄 Environment file copied to dist/") + console.log(`📄 Copying .env file and setting production mode...`) + // Read .env content + let envContent = readFileSync(envPath, 'utf-8') + // Replace development with production + envContent = envContent.replace(/NODE_ENV=development/g, 'NODE_ENV=production') + envContent = envContent.replace(/VITE_NODE_ENV=development/g, 'VITE_NODE_ENV=production') + // Write to dist + writeFileSync(distEnvPath, envContent) + console.log("📄 Environment file copied to dist/ (NODE_ENV=production)") } else if (existsSync(envExamplePath)) { console.log(`📄 Copying .env.example file...`) copyFileSync(envExamplePath, distEnvPath) diff --git a/core/config/env-dynamic.ts b/core/config/env-dynamic.ts deleted file mode 100644 index ad0de616..00000000 --- a/core/config/env-dynamic.ts +++ /dev/null @@ -1,326 +0,0 @@ -/** - * Dynamic Environment Configuration Adapter for FluxStack - * Integrates runtime env loader with existing configuration system - * Solves Bun build issue by using dynamic environment access - */ - -import { env, runtimeEnv, envValidation } from '../utils/env-runtime' -import type { FluxStackConfig, LogLevel, BuildTarget, LogFormat } from './schema' - -/** - * Enhanced Environment Processor that uses dynamic env access - * Replaces the original EnvironmentProcessor from env.ts - */ -export class DynamicEnvironmentProcessor { - private precedenceMap: Map = new Map() - - /** - * Process environment variables using dynamic runtime access - * This prevents Bun from fixing env values during build - */ - processEnvironmentVariables(): Partial { - const config: any = {} - - // App configuration - this.setConfigValue(config, 'app.name', - env.get('FLUXSTACK_APP_NAME') || env.get('APP_NAME'), 'string') - this.setConfigValue(config, 'app.version', - env.get('FLUXSTACK_APP_VERSION') || env.get('APP_VERSION'), 'string') - this.setConfigValue(config, 'app.description', - env.get('FLUXSTACK_APP_DESCRIPTION') || env.get('APP_DESCRIPTION'), 'string') - - // Server configuration - this.setConfigValue(config, 'server.port', - env.get('PORT') || env.get('FLUXSTACK_PORT'), 'number') - this.setConfigValue(config, 'server.host', - env.get('HOST') || env.get('FLUXSTACK_HOST'), 'string') - this.setConfigValue(config, 'server.apiPrefix', - env.get('FLUXSTACK_API_PREFIX') || env.get('API_PREFIX'), 'string') - - // CORS configuration - this.setConfigValue(config, 'server.cors.origins', - env.get('CORS_ORIGINS') || env.get('FLUXSTACK_CORS_ORIGINS'), 'array') - this.setConfigValue(config, 'server.cors.methods', - env.get('CORS_METHODS') || env.get('FLUXSTACK_CORS_METHODS'), 'array') - this.setConfigValue(config, 'server.cors.headers', - env.get('CORS_HEADERS') || env.get('FLUXSTACK_CORS_HEADERS'), 'array') - this.setConfigValue(config, 'server.cors.credentials', - env.get('CORS_CREDENTIALS') || env.get('FLUXSTACK_CORS_CREDENTIALS'), 'boolean') - this.setConfigValue(config, 'server.cors.maxAge', - env.get('CORS_MAX_AGE') || env.get('FLUXSTACK_CORS_MAX_AGE'), 'number') - - // Client configuration - this.setConfigValue(config, 'client.port', - env.get('VITE_PORT') || env.get('CLIENT_PORT') || env.get('FLUXSTACK_CLIENT_PORT'), 'number') - this.setConfigValue(config, 'client.proxy.target', - env.get('VITE_API_URL') || env.get('API_URL') || env.get('FLUXSTACK_PROXY_TARGET'), 'string') - this.setConfigValue(config, 'client.build.sourceMaps', - env.get('FLUXSTACK_CLIENT_SOURCEMAPS'), 'boolean') - this.setConfigValue(config, 'client.build.minify', - env.get('FLUXSTACK_CLIENT_MINIFY'), 'boolean') - - // Build configuration - this.setConfigValue(config, 'build.target', - env.get('BUILD_TARGET') || env.get('FLUXSTACK_BUILD_TARGET'), 'buildTarget') - this.setConfigValue(config, 'build.outDir', - env.get('BUILD_OUTDIR') || env.get('FLUXSTACK_BUILD_OUTDIR'), 'string') - this.setConfigValue(config, 'build.sourceMaps', - env.get('BUILD_SOURCEMAPS') || env.get('FLUXSTACK_BUILD_SOURCEMAPS'), 'boolean') - this.setConfigValue(config, 'build.clean', - env.get('BUILD_CLEAN') || env.get('FLUXSTACK_BUILD_CLEAN'), 'boolean') - - // Build optimization - this.setConfigValue(config, 'build.optimization.minify', - env.get('BUILD_MINIFY') || env.get('FLUXSTACK_BUILD_MINIFY'), 'boolean') - this.setConfigValue(config, 'build.optimization.treeshake', - env.get('BUILD_TREESHAKE') || env.get('FLUXSTACK_BUILD_TREESHAKE'), 'boolean') - this.setConfigValue(config, 'build.optimization.compress', - env.get('BUILD_COMPRESS') || env.get('FLUXSTACK_BUILD_COMPRESS'), 'boolean') - this.setConfigValue(config, 'build.optimization.splitChunks', - env.get('BUILD_SPLIT_CHUNKS') || env.get('FLUXSTACK_BUILD_SPLIT_CHUNKS'), 'boolean') - this.setConfigValue(config, 'build.optimization.bundleAnalyzer', - env.get('BUILD_ANALYZER') || env.get('FLUXSTACK_BUILD_ANALYZER'), 'boolean') - - // Logging configuration - this.setConfigValue(config, 'logging.level', - env.get('LOG_LEVEL') || env.get('FLUXSTACK_LOG_LEVEL'), 'logLevel') - this.setConfigValue(config, 'logging.format', - env.get('LOG_FORMAT') || env.get('FLUXSTACK_LOG_FORMAT'), 'logFormat') - - // Monitoring configuration - this.setConfigValue(config, 'monitoring.enabled', - env.get('MONITORING_ENABLED') || env.get('FLUXSTACK_MONITORING_ENABLED'), 'boolean') - this.setConfigValue(config, 'monitoring.metrics.enabled', - env.get('METRICS_ENABLED') || env.get('FLUXSTACK_METRICS_ENABLED'), 'boolean') - this.setConfigValue(config, 'monitoring.metrics.collectInterval', - env.get('METRICS_INTERVAL') || env.get('FLUXSTACK_METRICS_INTERVAL'), 'number') - this.setConfigValue(config, 'monitoring.profiling.enabled', - env.get('PROFILING_ENABLED') || env.get('FLUXSTACK_PROFILING_ENABLED'), 'boolean') - this.setConfigValue(config, 'monitoring.profiling.sampleRate', - env.get('PROFILING_SAMPLE_RATE') || env.get('FLUXSTACK_PROFILING_SAMPLE_RATE'), 'number') - - // Database configuration - this.setConfigValue(config, 'database.url', env.get('DATABASE_URL'), 'string') - this.setConfigValue(config, 'database.host', env.get('DATABASE_HOST'), 'string') - this.setConfigValue(config, 'database.port', env.get('DATABASE_PORT'), 'number') - this.setConfigValue(config, 'database.database', env.get('DATABASE_NAME'), 'string') - this.setConfigValue(config, 'database.user', env.get('DATABASE_USER'), 'string') - this.setConfigValue(config, 'database.password', env.get('DATABASE_PASSWORD'), 'string') - this.setConfigValue(config, 'database.ssl', env.get('DATABASE_SSL'), 'boolean') - this.setConfigValue(config, 'database.poolSize', env.get('DATABASE_POOL_SIZE'), 'number') - - // Auth configuration - this.setConfigValue(config, 'auth.secret', env.get('JWT_SECRET'), 'string') - this.setConfigValue(config, 'auth.expiresIn', env.get('JWT_EXPIRES_IN'), 'string') - this.setConfigValue(config, 'auth.algorithm', env.get('JWT_ALGORITHM'), 'string') - this.setConfigValue(config, 'auth.issuer', env.get('JWT_ISSUER'), 'string') - - // Email configuration - this.setConfigValue(config, 'email.host', env.get('SMTP_HOST'), 'string') - this.setConfigValue(config, 'email.port', env.get('SMTP_PORT'), 'number') - this.setConfigValue(config, 'email.user', env.get('SMTP_USER'), 'string') - this.setConfigValue(config, 'email.password', env.get('SMTP_PASSWORD'), 'string') - this.setConfigValue(config, 'email.secure', env.get('SMTP_SECURE'), 'boolean') - this.setConfigValue(config, 'email.from', env.get('SMTP_FROM'), 'string') - - // Storage configuration - this.setConfigValue(config, 'storage.uploadPath', env.get('UPLOAD_PATH'), 'string') - this.setConfigValue(config, 'storage.maxFileSize', env.get('MAX_FILE_SIZE'), 'number') - this.setConfigValue(config, 'storage.provider', env.get('STORAGE_PROVIDER'), 'string') - - // Plugin configuration - this.setConfigValue(config, 'plugins.enabled', - env.get('FLUXSTACK_PLUGINS_ENABLED'), 'array') - this.setConfigValue(config, 'plugins.disabled', - env.get('FLUXSTACK_PLUGINS_DISABLED'), 'array') - - return this.cleanEmptyObjects(config) - } - - private setConfigValue( - config: any, - path: string, - value: string | undefined, - type: string - ): void { - if (value === undefined || value === '') return - - const convertedValue = this.convertValue(value, type) - if (convertedValue !== undefined) { - this.setNestedProperty(config, path, convertedValue) - - // Track precedence - this.precedenceMap.set(path, { - source: 'environment', - path, - value: convertedValue, - priority: 3 - }) - } - } - - private convertValue(value: string, type: string): any { - switch (type) { - case 'string': - return value - case 'number': - const num = parseInt(value, 10) - return isNaN(num) ? undefined : num - case 'boolean': - return ['true', '1', 'yes', 'on'].includes(value.toLowerCase()) - case 'array': - return value.split(',').map(v => v.trim()).filter(Boolean) - case 'logLevel': - const level = value.toLowerCase() as LogLevel - return ['debug', 'info', 'warn', 'error'].includes(level) ? level : 'info' - case 'buildTarget': - const target = value.toLowerCase() as BuildTarget - return ['bun', 'node', 'docker'].includes(target) ? target : 'bun' - case 'logFormat': - const format = value.toLowerCase() as LogFormat - return ['json', 'pretty'].includes(format) ? format : 'pretty' - case 'object': - try { - return JSON.parse(value) - } catch { - return {} - } - default: - return value - } - } - - private setNestedProperty(obj: any, path: string, value: any): void { - const keys = path.split('.') - let current = obj - - for (let i = 0; i < keys.length - 1; i++) { - const key = keys[i] - if (!(key in current) || typeof current[key] !== 'object') { - current[key] = {} - } - current = current[key] - } - - current[keys[keys.length - 1]] = value - } - - private cleanEmptyObjects(obj: any): any { - if (typeof obj !== 'object' || obj === null) return obj - - const cleaned: any = {} - - for (const [key, value] of Object.entries(obj)) { - if (typeof value === 'object' && value !== null && !Array.isArray(value)) { - const cleanedValue = this.cleanEmptyObjects(value) - if (Object.keys(cleanedValue).length > 0) { - cleaned[key] = cleanedValue - } - } else if (value !== undefined && value !== null) { - cleaned[key] = value - } - } - - return cleaned - } - - getPrecedenceInfo(): Map { - return new Map(this.precedenceMap) - } - - clearPrecedence(): void { - this.precedenceMap.clear() - } -} - -/** - * Enhanced environment info with dynamic access - */ -export function getDynamicEnvironmentInfo() { - const nodeEnv = env.get('NODE_ENV', 'development') as 'development' | 'production' | 'test' - - return { - name: nodeEnv, - isDevelopment: nodeEnv === 'development', - isProduction: nodeEnv === 'production', - isTest: nodeEnv === 'test', - nodeEnv - } -} - -/** - * Runtime configuration loader that uses dynamic env access - */ -export function loadConfigFromDynamicEnv(): Partial { - const processor = new DynamicEnvironmentProcessor() - return processor.processEnvironmentVariables() -} - -/** - * Utility functions for backward compatibility - */ -export function isDevelopment(): boolean { - return getDynamicEnvironmentInfo().isDevelopment -} - -export function isProduction(): boolean { - return getDynamicEnvironmentInfo().isProduction -} - -export function isTest(): boolean { - return getDynamicEnvironmentInfo().isTest -} - -/** - * Validate critical environment variables for production - */ -export function validateProductionEnv(): void { - if (isProduction()) { - const requiredVars = ['NODE_ENV'] - const missingVars = requiredVars.filter(key => !env.has(key)) - - if (missingVars.length > 0) { - throw new Error(`Missing required production environment variables: ${missingVars.join(', ')}`) - } - - // Validate LOG_LEVEL for production - const logLevel = env.get('LOG_LEVEL') - if (logLevel === 'debug') { - console.warn('⚠️ Production environment should not use debug logging') - } - } -} - -/** - * Create environment-aware configuration - */ -export function createDynamicConfig(): Partial { - const envInfo = getDynamicEnvironmentInfo() - const envConfig = loadConfigFromDynamicEnv() - - // Add environment-specific defaults - const config: any = { ...envConfig } - - // Ensure proper defaults based on environment - if (envInfo.isDevelopment) { - config.logging = { - level: env.get('LOG_LEVEL', 'debug'), - format: env.get('LOG_FORMAT', 'pretty'), - ...config.logging - } - } else if (envInfo.isProduction) { - config.logging = { - level: env.get('LOG_LEVEL', 'warn'), - format: env.get('LOG_FORMAT', 'json'), - ...config.logging - } - } - - return config -} - -// Export singleton instance -export const dynamicEnvironmentProcessor = new DynamicEnvironmentProcessor() - -// Export runtime environment access -export { env, runtimeEnv, envValidation } from '../utils/env-runtime' \ No newline at end of file diff --git a/core/config/env.ts b/core/config/env.ts index 66260e77..db120e5f 100644 --- a/core/config/env.ts +++ b/core/config/env.ts @@ -3,6 +3,7 @@ * Handles environment variable processing and precedence */ +import { env, helpers } from '../utils/env' import type { FluxStackConfig, LogLevel, BuildTarget, LogFormat } from './schema' export interface EnvironmentInfo { @@ -24,8 +25,6 @@ export interface ConfigPrecedence { * Get current environment information */ export function getEnvironmentInfo(): EnvironmentInfo { - // Import here to avoid circular dependency - const { env } = require('../utils/env-runtime-v2') const nodeEnv = env.NODE_ENV return { @@ -99,117 +98,60 @@ export class EnvironmentProcessor { const config: any = {} // App configuration - this.setConfigValue(config, 'app.name', - process.env.FLUXSTACK_APP_NAME || process.env.APP_NAME, 'string') - this.setConfigValue(config, 'app.version', - process.env.FLUXSTACK_APP_VERSION || process.env.APP_VERSION, 'string') - this.setConfigValue(config, 'app.description', - process.env.FLUXSTACK_APP_DESCRIPTION || process.env.APP_DESCRIPTION, 'string') + this.setConfigValue(config, 'app.name', env.FLUXSTACK_APP_NAME, 'string') + this.setConfigValue(config, 'app.version', env.FLUXSTACK_APP_VERSION, 'string') // Server configuration - this.setConfigValue(config, 'server.port', - process.env.PORT || process.env.FLUXSTACK_PORT, 'number') - this.setConfigValue(config, 'server.host', - process.env.HOST || process.env.FLUXSTACK_HOST, 'string') - this.setConfigValue(config, 'server.apiPrefix', - process.env.FLUXSTACK_API_PREFIX || process.env.API_PREFIX, 'string') + this.setConfigValue(config, 'server.port', env.PORT?.toString(), 'number') + this.setConfigValue(config, 'server.host', env.HOST, 'string') + this.setConfigValue(config, 'server.apiPrefix', env.API_PREFIX, 'string') // CORS configuration - this.setConfigValue(config, 'server.cors.origins', - process.env.CORS_ORIGINS || process.env.FLUXSTACK_CORS_ORIGINS, 'array') - this.setConfigValue(config, 'server.cors.methods', - process.env.CORS_METHODS || process.env.FLUXSTACK_CORS_METHODS, 'array') - this.setConfigValue(config, 'server.cors.headers', - process.env.CORS_HEADERS || process.env.FLUXSTACK_CORS_HEADERS, 'array') - this.setConfigValue(config, 'server.cors.credentials', - process.env.CORS_CREDENTIALS || process.env.FLUXSTACK_CORS_CREDENTIALS, 'boolean') - this.setConfigValue(config, 'server.cors.maxAge', - process.env.CORS_MAX_AGE || process.env.FLUXSTACK_CORS_MAX_AGE, 'number') + const corsOriginsStr = env.has('CORS_ORIGINS') ? env.all().CORS_ORIGINS : undefined + const corsMethodsStr = env.has('CORS_METHODS') ? env.all().CORS_METHODS : undefined + const corsHeadersStr = env.has('CORS_HEADERS') ? env.all().CORS_HEADERS : undefined + + this.setConfigValue(config, 'server.cors.origins', corsOriginsStr, 'array') + this.setConfigValue(config, 'server.cors.methods', corsMethodsStr, 'array') + this.setConfigValue(config, 'server.cors.headers', corsHeadersStr, 'array') + this.setConfigValue(config, 'server.cors.credentials', env.CORS_CREDENTIALS?.toString(), 'boolean') + this.setConfigValue(config, 'server.cors.maxAge', env.CORS_MAX_AGE?.toString(), 'number') // Client configuration - this.setConfigValue(config, 'client.port', - process.env.VITE_PORT || process.env.CLIENT_PORT || process.env.FLUXSTACK_CLIENT_PORT, 'number') - this.setConfigValue(config, 'client.proxy.target', - process.env.VITE_API_URL || process.env.API_URL || process.env.FLUXSTACK_PROXY_TARGET, 'string') - this.setConfigValue(config, 'client.build.sourceMaps', - process.env.FLUXSTACK_CLIENT_SOURCEMAPS, 'boolean') - this.setConfigValue(config, 'client.build.minify', - process.env.FLUXSTACK_CLIENT_MINIFY, 'boolean') + this.setConfigValue(config, 'client.port', env.VITE_PORT?.toString(), 'number') // Build configuration - this.setConfigValue(config, 'build.target', - process.env.BUILD_TARGET || process.env.FLUXSTACK_BUILD_TARGET, 'buildTarget') - this.setConfigValue(config, 'build.outDir', - process.env.BUILD_OUTDIR || process.env.FLUXSTACK_BUILD_OUTDIR, 'string') - this.setConfigValue(config, 'build.sourceMaps', - process.env.BUILD_SOURCEMAPS || process.env.FLUXSTACK_BUILD_SOURCEMAPS, 'boolean') - this.setConfigValue(config, 'build.clean', - process.env.BUILD_CLEAN || process.env.FLUXSTACK_BUILD_CLEAN, 'boolean') - - // Build optimization - this.setConfigValue(config, 'build.optimization.minify', - process.env.BUILD_MINIFY || process.env.FLUXSTACK_BUILD_MINIFY, 'boolean') - this.setConfigValue(config, 'build.optimization.treeshake', - process.env.BUILD_TREESHAKE || process.env.FLUXSTACK_BUILD_TREESHAKE, 'boolean') - this.setConfigValue(config, 'build.optimization.compress', - process.env.BUILD_COMPRESS || process.env.FLUXSTACK_BUILD_COMPRESS, 'boolean') - this.setConfigValue(config, 'build.optimization.splitChunks', - process.env.BUILD_SPLIT_CHUNKS || process.env.FLUXSTACK_BUILD_SPLIT_CHUNKS, 'boolean') - this.setConfigValue(config, 'build.optimization.bundleAnalyzer', - process.env.BUILD_ANALYZER || process.env.FLUXSTACK_BUILD_ANALYZER, 'boolean') + const buildMinify = env.has('BUILD_MINIFY') ? env.all().BUILD_MINIFY : undefined + this.setConfigValue(config, 'build.optimization.minify', buildMinify, 'boolean') // Logging configuration - this.setConfigValue(config, 'logging.level', - process.env.LOG_LEVEL || process.env.FLUXSTACK_LOG_LEVEL, 'logLevel') - this.setConfigValue(config, 'logging.format', - process.env.LOG_FORMAT || process.env.FLUXSTACK_LOG_FORMAT, 'logFormat') + this.setConfigValue(config, 'logging.level', env.LOG_LEVEL, 'logLevel') + this.setConfigValue(config, 'logging.format', env.LOG_FORMAT, 'logFormat') // Monitoring configuration - this.setConfigValue(config, 'monitoring.enabled', - process.env.MONITORING_ENABLED || process.env.FLUXSTACK_MONITORING_ENABLED, 'boolean') - this.setConfigValue(config, 'monitoring.metrics.enabled', - process.env.METRICS_ENABLED || process.env.FLUXSTACK_METRICS_ENABLED, 'boolean') - this.setConfigValue(config, 'monitoring.metrics.collectInterval', - process.env.METRICS_INTERVAL || process.env.FLUXSTACK_METRICS_INTERVAL, 'number') - this.setConfigValue(config, 'monitoring.profiling.enabled', - process.env.PROFILING_ENABLED || process.env.FLUXSTACK_PROFILING_ENABLED, 'boolean') - this.setConfigValue(config, 'monitoring.profiling.sampleRate', - process.env.PROFILING_SAMPLE_RATE || process.env.FLUXSTACK_PROFILING_SAMPLE_RATE, 'number') + this.setConfigValue(config, 'monitoring.enabled', env.ENABLE_MONITORING?.toString(), 'boolean') + this.setConfigValue(config, 'monitoring.metrics.enabled', env.ENABLE_METRICS?.toString(), 'boolean') // Database configuration - this.setConfigValue(config, 'database.url', process.env.DATABASE_URL, 'string') - this.setConfigValue(config, 'database.host', process.env.DATABASE_HOST, 'string') - this.setConfigValue(config, 'database.port', process.env.DATABASE_PORT, 'number') - this.setConfigValue(config, 'database.database', process.env.DATABASE_NAME, 'string') - this.setConfigValue(config, 'database.user', process.env.DATABASE_USER, 'string') - this.setConfigValue(config, 'database.password', process.env.DATABASE_PASSWORD, 'string') - this.setConfigValue(config, 'database.ssl', process.env.DATABASE_SSL, 'boolean') - this.setConfigValue(config, 'database.poolSize', process.env.DATABASE_POOL_SIZE, 'number') + this.setConfigValue(config, 'database.url', env.DATABASE_URL, 'string') + this.setConfigValue(config, 'database.host', env.DB_HOST, 'string') + this.setConfigValue(config, 'database.port', env.DB_PORT?.toString(), 'number') + this.setConfigValue(config, 'database.database', env.DB_NAME, 'string') + this.setConfigValue(config, 'database.user', env.DB_USER, 'string') + this.setConfigValue(config, 'database.password', env.DB_PASSWORD, 'string') + this.setConfigValue(config, 'database.ssl', env.DB_SSL?.toString(), 'boolean') // Auth configuration - this.setConfigValue(config, 'auth.secret', process.env.JWT_SECRET, 'string') - this.setConfigValue(config, 'auth.expiresIn', process.env.JWT_EXPIRES_IN, 'string') - this.setConfigValue(config, 'auth.algorithm', process.env.JWT_ALGORITHM, 'string') - this.setConfigValue(config, 'auth.issuer', process.env.JWT_ISSUER, 'string') + this.setConfigValue(config, 'auth.secret', env.JWT_SECRET, 'string') + this.setConfigValue(config, 'auth.expiresIn', env.JWT_EXPIRES_IN, 'string') + this.setConfigValue(config, 'auth.algorithm', env.JWT_ALGORITHM, 'string') // Email configuration - this.setConfigValue(config, 'email.host', process.env.SMTP_HOST, 'string') - this.setConfigValue(config, 'email.port', process.env.SMTP_PORT, 'number') - this.setConfigValue(config, 'email.user', process.env.SMTP_USER, 'string') - this.setConfigValue(config, 'email.password', process.env.SMTP_PASSWORD, 'string') - this.setConfigValue(config, 'email.secure', process.env.SMTP_SECURE, 'boolean') - this.setConfigValue(config, 'email.from', process.env.SMTP_FROM, 'string') - - // Storage configuration - this.setConfigValue(config, 'storage.uploadPath', process.env.UPLOAD_PATH, 'string') - this.setConfigValue(config, 'storage.maxFileSize', process.env.MAX_FILE_SIZE, 'number') - this.setConfigValue(config, 'storage.provider', process.env.STORAGE_PROVIDER, 'string') - - // Plugin configuration - this.setConfigValue(config, 'plugins.enabled', - process.env.FLUXSTACK_PLUGINS_ENABLED, 'array') - this.setConfigValue(config, 'plugins.disabled', - process.env.FLUXSTACK_PLUGINS_DISABLED, 'array') + this.setConfigValue(config, 'email.host', env.SMTP_HOST, 'string') + this.setConfigValue(config, 'email.port', env.SMTP_PORT?.toString(), 'number') + this.setConfigValue(config, 'email.user', env.SMTP_USER, 'string') + this.setConfigValue(config, 'email.password', env.SMTP_PASSWORD, 'string') + this.setConfigValue(config, 'email.secure', env.SMTP_SECURE?.toString(), 'boolean') return this.cleanEmptyObjects(config) } diff --git a/core/config/runtime-config.ts b/core/config/runtime-config.ts index fcf81ae5..06a2025a 100644 --- a/core/config/runtime-config.ts +++ b/core/config/runtime-config.ts @@ -1,18 +1,14 @@ /** * Runtime Configuration System for FluxStack - * Uses dynamic environment loading to solve Bun build issues - * Drop-in replacement for process.env based configuration + * Uses declarative configuration system */ -import { env, createEnvNamespace, envValidation } from '../utils/env-runtime' -import { - dynamicEnvironmentProcessor, - createDynamicConfig, - validateProductionEnv, - getDynamicEnvironmentInfo -} from './env-dynamic' +import { env, createNamespace } from '../utils/env' import type { FluxStackConfig } from './schema' import { defaultFluxStackConfig } from './schema' +import { loggerConfig } from '../../config/logger.config' +import { buildConfig } from '../../config/build.config' +import { appConfig } from '../../config/app.config' /** * Runtime Configuration Builder @@ -35,11 +31,21 @@ export class RuntimeConfigBuilder { } /** - * Load from dynamic environment variables + * Load from environment variables */ private loadFromDynamicEnv(): this { - const envConfig = createDynamicConfig() - this.config = this.deepMerge(this.config, envConfig) + // Environment vars are loaded automatically by env loader + // Just merge common overrides here + const envOverrides: Partial = {} + + if (env.has('PORT')) { + envOverrides.server = { ...this.config.server, port: env.PORT } + } + if (env.has('LOG_LEVEL')) { + envOverrides.logging = { ...this.config.logging, level: env.LOG_LEVEL } + } + + this.config = this.deepMerge(this.config, envOverrides) return this } @@ -64,8 +70,8 @@ export class RuntimeConfigBuilder { */ build(): FluxStackConfig { // Validate production environment if needed - if (env.get('NODE_ENV') === 'production') { - validateProductionEnv() + if (env.NODE_ENV === 'production') { + env.require(['NODE_ENV']) } return this.config as FluxStackConfig @@ -140,8 +146,8 @@ export const runtimeConfig = { */ development(): FluxStackConfig { return new RuntimeConfigBuilder() - .override('logging.level', env.get('LOG_LEVEL', 'debug')) - .override('logging.format', env.get('LOG_FORMAT', 'pretty')) + .override('logging.level', loggerConfig.level) + .override('logging.format', 'pretty') .override('build.optimization.minify', false) .override('build.sourceMaps', true) .override('monitoring.enabled', false) @@ -153,11 +159,11 @@ export const runtimeConfig = { */ production(): FluxStackConfig { return new RuntimeConfigBuilder() - .override('logging.level', env.get('LOG_LEVEL', 'warn')) - .override('logging.format', env.get('LOG_FORMAT', 'json')) + .override('logging.level', loggerConfig.level) + .override('logging.format', 'json') .override('build.optimization.minify', true) .override('build.sourceMaps', false) - .override('monitoring.enabled', env.bool('MONITORING_ENABLED', true)) + .override('monitoring.enabled', buildConfig.monitoringEnabled) .build() }, @@ -166,7 +172,7 @@ export const runtimeConfig = { */ test(): FluxStackConfig { return new RuntimeConfigBuilder() - .override('logging.level', env.get('LOG_LEVEL', 'error')) + .override('logging.level', loggerConfig.level) .override('server.port', 0) // Random port for tests .override('client.port', 0) .override('monitoring.enabled', false) @@ -177,7 +183,7 @@ export const runtimeConfig = { * Auto-detect environment and create appropriate config */ auto(overrides?: Partial): FluxStackConfig { - const environment = env.get('NODE_ENV', 'development') as 'development' | 'production' | 'test' + const environment = appConfig.env let config: FluxStackConfig @@ -214,27 +220,27 @@ export const envLoaders = { /** * Database environment loader */ - database: createEnvNamespace('DATABASE_'), - + database: createNamespace('DATABASE_'), + /** * JWT environment loader */ - jwt: createEnvNamespace('JWT_'), - + jwt: createNamespace('JWT_'), + /** * SMTP environment loader */ - smtp: createEnvNamespace('SMTP_'), - + smtp: createNamespace('SMTP_'), + /** * CORS environment loader */ - cors: createEnvNamespace('CORS_'), - + cors: createNamespace('CORS_'), + /** * FluxStack specific environment loader */ - fluxstack: createEnvNamespace('FLUXSTACK_') + fluxstack: createNamespace('FLUXSTACK_') } /** @@ -245,15 +251,12 @@ export const configHelpers = { * Get database URL with validation */ getDatabaseUrl(): string | null { - const url = env.get('DATABASE_URL') as string | undefined - - if (url) { - envValidation.validate('DATABASE_URL', - (value) => value.includes('://'), - 'Must be a valid URL' - ) + const url = env.DATABASE_URL + + if (url && !url.includes('://')) { + throw new Error('DATABASE_URL must be a valid URL') } - + return url || null }, @@ -261,18 +264,18 @@ export const configHelpers = { * Get CORS origins with proper defaults */ getCorsOrigins(): string[] { - const origins = env.array('CORS_ORIGINS') - - if (origins.length === 0) { - const environment = env.get('NODE_ENV', 'development') as 'development' | 'production' | 'test' - + const origins = env.CORS_ORIGINS + + if (origins.length === 0 || (origins.length === 1 && origins[0] === '*')) { + const environment = env.NODE_ENV + if (environment === 'development') { return ['http://localhost:3000', 'http://localhost:5173'] } else if (environment === 'production') { return [] // Must be explicitly configured in production } } - + return origins }, @@ -281,15 +284,15 @@ export const configHelpers = { */ getServerConfig() { return { - port: env.num('PORT', 3000), - host: env.get('HOST', 'localhost'), - apiPrefix: env.get('API_PREFIX', '/api'), + port: env.PORT, + host: env.HOST, + apiPrefix: env.API_PREFIX, cors: { origins: this.getCorsOrigins(), - methods: env.array('CORS_METHODS', ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']), - headers: env.array('CORS_HEADERS', ['Content-Type', 'Authorization']), - credentials: env.bool('CORS_CREDENTIALS', false), - maxAge: env.num('CORS_MAX_AGE', 86400) + methods: env.CORS_METHODS, + headers: env.CORS_HEADERS, + credentials: env.CORS_CREDENTIALS, + maxAge: env.CORS_MAX_AGE } } }, @@ -299,16 +302,16 @@ export const configHelpers = { */ getClientConfig() { return { - port: env.num('VITE_PORT', 5173), + port: env.VITE_PORT, proxy: { - target: env.get('API_URL', 'http://localhost:3000'), - changeOrigin: env.bool('PROXY_CHANGE_ORIGIN', true) + target: buildConfig.apiUrl, + changeOrigin: buildConfig.proxyChangeOrigin }, build: { - outDir: env.get('CLIENT_BUILD_DIR', 'dist/client'), - sourceMaps: env.bool('CLIENT_SOURCEMAPS', env.get('NODE_ENV') === 'development'), - minify: env.bool('CLIENT_MINIFY', env.get('NODE_ENV') === 'production'), - target: env.get('CLIENT_TARGET', 'es2020') + outDir: buildConfig.clientBuildDir, + sourceMaps: buildConfig.clientSourceMaps, + minify: buildConfig.clientMinify, + target: buildConfig.clientTarget } } } diff --git a/core/config/schema.ts b/core/config/schema.ts index 094ca55c..16338714 100644 --- a/core/config/schema.ts +++ b/core/config/schema.ts @@ -46,6 +46,7 @@ export interface ProxyConfig { export interface ClientBuildConfig { sourceMaps: boolean + minify: boolean target: string outDir: string } @@ -57,6 +58,7 @@ export interface ClientConfig { } export interface OptimizationConfig { + minify: boolean treeshake: boolean compress: boolean splitChunks: boolean @@ -69,6 +71,7 @@ export interface BuildConfig { outDir: string optimization: OptimizationConfig sourceMaps: boolean + minify: boolean treeshake: boolean compress?: boolean removeUnusedCSS?: boolean diff --git a/core/plugins/built-in/vite/index.ts b/core/plugins/built-in/vite/index.ts index 9002ac12..033dd97b 100644 --- a/core/plugins/built-in/vite/index.ts +++ b/core/plugins/built-in/vite/index.ts @@ -200,7 +200,8 @@ export const vitePlugin: Plugin = { } catch (viteError) { // If Vite fails, let the request continue to normal routing (will become 404) // Only log if explicitly enabled for debugging - if (process.env.ENABLE_VITE_PROXY_LOGS === 'true') { + const { serverConfig } = await import('@/config/server.config') + if (serverConfig.enableViteProxyLogs) { console.warn(`Vite proxy error: ${viteError}`) } } @@ -238,7 +239,8 @@ export const vitePlugin: Plugin = { } catch (viteError) { // If Vite fails, let the request continue to normal routing (will become 404) // Only log if explicitly enabled for debugging - if (process.env.ENABLE_VITE_PROXY_LOGS === 'true') { + const { serverConfig } = await import('@/config/server.config') + if (serverConfig.enableViteProxyLogs) { console.warn(`Vite proxy error: ${viteError}`) } } diff --git a/core/utils/config-schema.ts b/core/utils/config-schema.ts new file mode 100644 index 00000000..9ee896f0 --- /dev/null +++ b/core/utils/config-schema.ts @@ -0,0 +1,484 @@ +/** + * ⚡ FluxStack Config Schema System + * + * Laravel-inspired declarative configuration system with: + * - Schema-based config declaration + * - Automatic validation + * - Type casting + * - Default values + * - Environment variable mapping + * + * @example + * ```ts + * const appConfig = defineConfig({ + * name: { + * type: 'string', + * env: 'APP_NAME', + * default: 'MyApp', + * required: true + * }, + * port: { + * type: 'number', + * env: 'PORT', + * default: 3000, + * validate: (value) => value > 0 && value < 65536 + * }, + * debug: { + * type: 'boolean', + * env: 'DEBUG', + * default: false + * } + * }) + * + * // Access with full type safety + * appConfig.name // string + * appConfig.port // number + * appConfig.debug // boolean + * ``` + */ + +import { env } from './env' + +/** + * Config field types + */ +export type ConfigFieldType = 'string' | 'number' | 'boolean' | 'array' | 'object' | 'enum' + +/** + * Config field definition + */ +export interface ConfigField { + /** Field type */ + type: ConfigFieldType + + /** Environment variable name */ + env?: string + + /** Default value */ + default?: T + + /** Is field required? */ + required?: boolean + + /** Custom validation function */ + validate?: (value: T) => boolean | string + + /** For enum type: allowed values */ + values?: readonly T[] + + /** Field description (for documentation) */ + description?: string + + /** Custom transformer function */ + transform?: (value: any) => T +} + +/** + * Config schema definition + */ +export type ConfigSchema = Record + +/** + * Infer TypeScript type from config schema + */ +export type InferConfig = { + [K in keyof T]: T[K]['default'] extends infer D + ? D extends undefined + ? T[K]['required'] extends true + ? InferFieldType + : InferFieldType | undefined + : InferFieldType + : InferFieldType +} + +/** + * Infer field type from field definition + * Uses the generic T from ConfigField for better type inference + */ +type InferFieldType = + F extends ConfigField + ? T extends undefined + ? ( + F extends { type: 'string' } ? string : + F extends { type: 'number' } ? number : + F extends { type: 'boolean' } ? boolean : + F extends { type: 'array' } ? string[] : + F extends { type: 'object' } ? Record : + F extends { type: 'enum'; values: readonly (infer U)[] } ? U : + any + ) + : T + : any + +/** + * Validation error + */ +export interface ValidationError { + field: string + message: string + value?: any +} + +/** + * Config validation result + */ +export interface ValidationResult { + valid: boolean + errors: ValidationError[] + warnings?: string[] +} + +/** + * Cast value to specific type + */ +function castValue(value: any, type: ConfigFieldType): any { + if (value === undefined || value === null) { + return undefined + } + + switch (type) { + case 'string': + return String(value) + + case 'number': + const num = Number(value) + return isNaN(num) ? undefined : num + + case 'boolean': + if (typeof value === 'boolean') return value + if (typeof value === 'string') { + return ['true', '1', 'yes', 'on'].includes(value.toLowerCase()) + } + return Boolean(value) + + case 'array': + if (Array.isArray(value)) return value + if (typeof value === 'string') { + return value.split(',').map(v => v.trim()).filter(Boolean) + } + return [value] + + case 'object': + if (typeof value === 'object' && value !== null) return value + if (typeof value === 'string') { + try { + return JSON.parse(value) + } catch { + return {} + } + } + return {} + + case 'enum': + return value + + default: + return value + } +} + +/** + * Validate config value + */ +function validateField( + fieldName: string, + value: any, + field: ConfigField +): ValidationError | null { + // Check required + if (field.required && (value === undefined || value === null || value === '')) { + return { + field: fieldName, + message: `Field '${fieldName}' is required but not provided` + } + } + + // Skip validation if value is undefined and not required + if (value === undefined && !field.required) { + return null + } + + // Check enum values + if (field.type === 'enum' && field.values) { + if (!field.values.includes(value)) { + return { + field: fieldName, + message: `Field '${fieldName}' must be one of: ${field.values.join(', ')}`, + value + } + } + } + + // Custom validation + if (field.validate) { + const result = field.validate(value) + if (result === false) { + return { + field: fieldName, + message: `Field '${fieldName}' failed validation`, + value + } + } + if (typeof result === 'string') { + return { + field: fieldName, + message: result, + value + } + } + } + + return null +} + +/** + * Reactive config instance that can reload in runtime + */ +export class ReactiveConfig { + private schema: T + private config: InferConfig + private watchers: Array<(config: InferConfig) => void> = [] + + constructor(schema: T) { + this.schema = schema + this.config = this.loadConfig() + } + + /** + * Load config from environment + */ + private loadConfig(): InferConfig { + const config: any = {} + const errors: ValidationError[] = [] + + for (const [fieldName, field] of Object.entries(this.schema)) { + let value: any + + // 1. Try to get from environment variable + if (field.env) { + const envValue = env.has(field.env) ? env.all()[field.env] : undefined + if (envValue !== undefined && envValue !== '') { + value = envValue + } + } + + // 2. Use default value if not found in env + if (value === undefined) { + value = field.default + } + + // 3. Apply custom transform if provided + if (value !== undefined && field.transform) { + try { + value = field.transform(value) + } catch (error) { + errors.push({ + field: fieldName, + message: `Transform failed: ${error}` + }) + continue + } + } + + // 4. Cast to correct type + if (value !== undefined) { + value = castValue(value, field.type) + } + + // 5. Validate + const validationError = validateField(fieldName, value, field) + if (validationError) { + errors.push(validationError) + continue + } + + // 6. Set value + config[fieldName] = value + } + + // Throw error if validation failed + if (errors.length > 0) { + const errorMessage = errors + .map(e => ` - ${e.message}${e.value !== undefined ? ` (got: ${JSON.stringify(e.value)})` : ''}`) + .join('\n') + + throw new Error( + `❌ Configuration validation failed:\n${errorMessage}\n\n` + + `Please check your environment variables or configuration.` + ) + } + + return config as InferConfig + } + + /** + * Get current config values + */ + get values(): InferConfig { + return this.config + } + + /** + * Reload config from environment (runtime reload) + */ + reload(): InferConfig { + // Clear env cache to get fresh values + env.clearCache() + + // Reload config + const newConfig = this.loadConfig() + this.config = newConfig + + // Notify watchers + this.watchers.forEach(watcher => watcher(newConfig)) + + return newConfig + } + + /** + * Watch for config changes (called after reload) + */ + watch(callback: (config: InferConfig) => void): () => void { + this.watchers.push(callback) + + // Return unwatch function + return () => { + const index = this.watchers.indexOf(callback) + if (index > -1) { + this.watchers.splice(index, 1) + } + } + } + + /** + * Get specific field value with runtime lookup + */ + get>(key: K): InferConfig[K] { + return this.config[key] + } + + /** + * Check if field exists + */ + has>(key: K): boolean { + return this.config[key] !== undefined + } +} + +/** + * Define and load configuration from schema + */ +export function defineConfig(schema: T): InferConfig { + const reactive = new ReactiveConfig(schema) + return reactive.values as InferConfig +} + +/** + * Define reactive configuration (can be reloaded in runtime) + */ +export function defineReactiveConfig(schema: T): ReactiveConfig { + return new ReactiveConfig(schema) +} + +/** + * Validate configuration without throwing + */ +export function validateConfig( + schema: T, + values: Partial> +): ValidationResult { + const errors: ValidationError[] = [] + + for (const [fieldName, field] of Object.entries(schema)) { + const value = (values as any)[fieldName] + const error = validateField(fieldName, value, field) + if (error) { + errors.push(error) + } + } + + return { + valid: errors.length === 0, + errors + } +} + +/** + * Create nested config schema (for grouping) + */ +export function defineNestedConfig>( + schemas: T +): { [K in keyof T]: InferConfig } { + const config: any = {} + + for (const [groupName, schema] of Object.entries(schemas)) { + config[groupName] = defineConfig(schema) + } + + return config +} + +/** + * Helper to create env field quickly + */ +export function envString(envVar: string, defaultValue?: string, required = false): ConfigField { + return { + type: 'string' as const, + env: envVar, + default: defaultValue, + required + } +} + +export function envNumber(envVar: string, defaultValue?: number, required = false): ConfigField { + return { + type: 'number' as const, + env: envVar, + default: defaultValue, + required + } +} + +export function envBoolean(envVar: string, defaultValue?: boolean, required = false): ConfigField { + return { + type: 'boolean' as const, + env: envVar, + default: defaultValue, + required + } +} + +export function envArray(envVar: string, defaultValue?: string[], required = false): ConfigField { + return { + type: 'array' as const, + env: envVar, + default: defaultValue, + required + } +} + +export function envEnum( + envVar: string, + values: T, + defaultValue?: T[number], + required = false +): ConfigField { + return { + type: 'enum' as const, + env: envVar, + values, + default: defaultValue, + required + } +} + +/** + * Export shorthand helpers + */ +export const config = { + string: envString, + number: envNumber, + boolean: envBoolean, + array: envArray, + enum: envEnum +} diff --git a/core/utils/env-runtime-v2.ts b/core/utils/env-runtime-v2.ts deleted file mode 100644 index 44ebb211..00000000 --- a/core/utils/env-runtime-v2.ts +++ /dev/null @@ -1,232 +0,0 @@ -/** - * Runtime Environment Loader V2 - Simplified API - * Mais elegante com casting automático e acesso direto - */ - -/** - * Enhanced environment variable loader with smart casting - */ -class SmartEnvLoader { - private envAccessor: () => Record - - constructor() { - this.envAccessor = this.createDynamicAccessor() - } - - private createDynamicAccessor(): () => Record { - const globalScope = globalThis as any - - return () => { - // Try Bun.env first (most reliable in Bun runtime) - if (globalScope['Bun'] && globalScope['Bun']['env']) { - return globalScope['Bun']['env'] - } - - // Fallback to process.env with dynamic access - if (globalScope['process'] && globalScope['process']['env']) { - return globalScope['process']['env'] - } - - // Final fallback - const proc = eval('typeof process !== "undefined" ? process : null') - return proc?.env || {} - } - } - - /** - * Smart get with automatic type conversion based on default value - */ - get(key: string, defaultValue?: T): T { - const env = this.envAccessor() - const value = env[key] - - if (!value || value === '') { - return defaultValue as T - } - - // Auto-detect type from default value - if (typeof defaultValue === 'number') { - const parsed = parseInt(value, 10) - return (isNaN(parsed) ? defaultValue : parsed) as T - } - - if (typeof defaultValue === 'boolean') { - return ['true', '1', 'yes', 'on'].includes(value.toLowerCase()) as T - } - - if (Array.isArray(defaultValue)) { - return value.split(',').map(v => v.trim()).filter(Boolean) as T - } - - if (typeof defaultValue === 'object' && defaultValue !== null) { - try { - return JSON.parse(value) as T - } catch { - return defaultValue - } - } - - return value as T - } - - /** - * Check if environment variable exists - */ - has(key: string): boolean { - const env = this.envAccessor() - return key in env && env[key] !== undefined && env[key] !== '' - } - - /** - * Get all environment variables - */ - all(): Record { - const env = this.envAccessor() - const result: Record = {} - - for (const [key, value] of Object.entries(env)) { - if (value !== undefined && value !== '') { - result[key] = value - } - } - - return result - } -} - -// Create singleton instance -const smartEnv = new SmartEnvLoader() - -/** - * Simplified env API with smart casting - */ -export const env = { - /** - * Smart get - automatically casts based on default value type - * Usage: - * env.get('PORT', 3000) -> number - * env.get('DEBUG', false) -> boolean - * env.get('ORIGINS', ['*']) -> string[] - * env.get('HOST', 'localhost') -> string - */ - get: (key: string, defaultValue?: T): T => smartEnv.get(key, defaultValue), - - /** - * Check if env var exists - */ - has: (key: string) => smartEnv.has(key), - - /** - * Get all env vars - */ - all: () => smartEnv.all(), - - // Common environment variables as properties with smart defaults - get NODE_ENV() { return this.get('NODE_ENV', 'development') }, - get PORT() { return this.get('PORT', 3000) }, - get HOST() { return this.get('HOST', 'localhost') }, - get DEBUG() { return this.get('DEBUG', false) }, - get LOG_LEVEL() { return this.get('LOG_LEVEL', 'info') }, - get DATABASE_URL() { return this.get('DATABASE_URL', '') }, - get JWT_SECRET() { return this.get('JWT_SECRET', '') }, - get CORS_ORIGINS() { return this.get('CORS_ORIGINS', ['*']) }, - get VITE_PORT() { return this.get('VITE_PORT', 5173) }, - get API_PREFIX() { return this.get('API_PREFIX', '/api') }, - - // App specific - get FLUXSTACK_APP_NAME() { return this.get('FLUXSTACK_APP_NAME', 'FluxStack') }, - get FLUXSTACK_APP_VERSION() { return this.get('FLUXSTACK_APP_VERSION', '1.0.0') }, - - // Monitoring - get ENABLE_MONITORING() { return this.get('ENABLE_MONITORING', false) }, - get ENABLE_SWAGGER() { return this.get('ENABLE_SWAGGER', true) }, - get ENABLE_METRICS() { return this.get('ENABLE_METRICS', false) }, - - // Database - get DB_HOST() { return this.get('DB_HOST', 'localhost') }, - get DB_PORT() { return this.get('DB_PORT', 5432) }, - get DB_NAME() { return this.get('DB_NAME', '') }, - get DB_USER() { return this.get('DB_USER', '') }, - get DB_PASSWORD() { return this.get('DB_PASSWORD', '') }, - get DB_SSL() { return this.get('DB_SSL', false) }, - - // SMTP - get SMTP_HOST() { return this.get('SMTP_HOST', '') }, - get SMTP_PORT() { return this.get('SMTP_PORT', 587) }, - get SMTP_USER() { return this.get('SMTP_USER', '') }, - get SMTP_PASSWORD() { return this.get('SMTP_PASSWORD', '') }, - get SMTP_SECURE() { return this.get('SMTP_SECURE', false) } -} - -/** - * Create namespaced environment access - * Usage: const db = createNamespace('DATABASE_') - * db.get('URL') -> reads DATABASE_URL - */ -export function createNamespace(prefix: string) { - return { - get: (key: string, defaultValue?: T): T => - smartEnv.get(`${prefix}${key}`, defaultValue), - - has: (key: string) => smartEnv.has(`${prefix}${key}`), - - all: () => { - const allEnv = smartEnv.all() - const namespaced: Record = {} - - for (const [key, value] of Object.entries(allEnv)) { - if (key.startsWith(prefix)) { - namespaced[key.slice(prefix.length)] = value - } - } - - return namespaced - } - } -} - -/** - * Environment validation - */ -export const validate = { - require(keys: string[]): void { - const missing = keys.filter(key => !smartEnv.has(key)) - if (missing.length > 0) { - throw new Error(`Missing required environment variables: ${missing.join(', ')}`) - } - }, - - oneOf(key: string, validValues: string[]): void { - const value = smartEnv.get(key, '') - if (value && !validValues.includes(value)) { - throw new Error(`${key} must be one of: ${validValues.join(', ')}, got: ${value}`) - } - } -} - -/** - * Convenience functions - */ -export const helpers = { - isDevelopment: () => env.NODE_ENV === 'development', - isProduction: () => env.NODE_ENV === 'production', - isTest: () => env.NODE_ENV === 'test', - - getDatabaseUrl: () => { - const url = env.DATABASE_URL - if (url) return url - - const { DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD } = env - if (DB_HOST && DB_NAME) { - const auth = DB_USER ? `${DB_USER}:${DB_PASSWORD}@` : '' - return `postgres://${auth}${DB_HOST}:${DB_PORT}/${DB_NAME}` - } - - return null - }, - - getServerUrl: () => `http://${env.HOST}:${env.PORT}`, - getClientUrl: () => `http://${env.HOST}:${env.VITE_PORT}` -} - -export default env \ No newline at end of file diff --git a/core/utils/env-runtime.ts b/core/utils/env-runtime.ts deleted file mode 100644 index a76e4607..00000000 --- a/core/utils/env-runtime.ts +++ /dev/null @@ -1,259 +0,0 @@ -/** - * Runtime Environment Loader V2 - Simplified API - * Mais elegante com casting automático e acesso direto - */ - -/** - * Enhanced environment variable loader with smart casting - */ -class SmartEnvLoader { - private envAccessor: () => Record - - constructor() { - this.envAccessor = this.createDynamicAccessor() - } - - private createDynamicAccessor(): () => Record { - const globalScope = globalThis as any - - return () => { - // Try Bun.env first (most reliable in Bun runtime) - if (globalScope['Bun'] && globalScope['Bun']['env']) { - return globalScope['Bun']['env'] - } - - // Fallback to process.env with dynamic access - if (globalScope['process'] && globalScope['process']['env']) { - return globalScope['process']['env'] - } - - // Final fallback - const proc = eval('typeof process !== "undefined" ? process : null') - return proc?.env || {} - } - } - - /** - * Smart get with automatic type conversion based on default value - */ - get(key: string, defaultValue?: T): T { - const env = this.envAccessor() - const value = env[key] - - if (!value || value === '') { - return defaultValue as T - } - - // Auto-detect type from default value - if (typeof defaultValue === 'number') { - const parsed = parseInt(value, 10) - return (isNaN(parsed) ? defaultValue : parsed) as T - } - - if (typeof defaultValue === 'boolean') { - return ['true', '1', 'yes', 'on'].includes(value.toLowerCase()) as T - } - - if (Array.isArray(defaultValue)) { - return value.split(',').map(v => v.trim()).filter(Boolean) as T - } - - if (typeof defaultValue === 'object' && defaultValue !== null) { - try { - return JSON.parse(value) as T - } catch { - return defaultValue - } - } - - return value as T - } - - /** - * Check if environment variable exists - */ - has(key: string): boolean { - const env = this.envAccessor() - return key in env && env[key] !== undefined && env[key] !== '' - } - - /** - * Get all environment variables - */ - all(): Record { - const env = this.envAccessor() - const result: Record = {} - - for (const [key, value] of Object.entries(env)) { - if (value !== undefined && value !== '') { - result[key] = value - } - } - - return result - } -} - -// Create singleton instance -const smartEnv = new SmartEnvLoader() - -/** - * Simplified env API with smart casting - */ -export const env = { - /** - * Smart get - automatically casts based on default value type - * Usage: - * env.get('PORT', 3000) -> number - * env.get('DEBUG', false) -> boolean - * env.get('ORIGINS', ['*']) -> string[] - * env.get('HOST', 'localhost') -> string - */ - get: (key: string, defaultValue?: T): T => smartEnv.get(key, defaultValue), - - /** - * Check if env var exists - */ - has: (key: string) => smartEnv.has(key), - - /** - * Get number value - */ - num: (key: string, defaultValue?: number) => Number(smartEnv.get(key, defaultValue?.toString() || '0')), - - /** - * Get boolean value - */ - bool: (key: string, defaultValue?: boolean) => smartEnv.get(key, defaultValue?.toString() || 'false') === 'true', - - /** - * Get array value - */ - array: (key: string, defaultValue?: string[]) => smartEnv.get(key, defaultValue?.join(',') || '').split(',').filter(Boolean), - - /** - * Get all env vars - */ - all: () => smartEnv.all(), - - // Common environment variables as properties with smart defaults - get NODE_ENV() { return this.get('NODE_ENV', 'development') }, - get PORT() { return this.get('PORT', 3000) }, - get HOST() { return this.get('HOST', 'localhost') }, - get DEBUG() { return this.get('DEBUG', false) }, - get LOG_LEVEL() { return this.get('LOG_LEVEL', 'info') }, - get DATABASE_URL() { return this.get('DATABASE_URL', '') }, - get JWT_SECRET() { return this.get('JWT_SECRET', '') }, - get CORS_ORIGINS() { return this.get('CORS_ORIGINS', ['*']) }, - get VITE_PORT() { return this.get('VITE_PORT', 5173) }, - get API_PREFIX() { return this.get('API_PREFIX', '/api') }, - - // App specific - get FLUXSTACK_APP_NAME() { return this.get('FLUXSTACK_APP_NAME', 'FluxStack') }, - get FLUXSTACK_APP_VERSION() { return this.get('FLUXSTACK_APP_VERSION', '1.0.0') }, - - // Monitoring - get ENABLE_MONITORING() { return this.get('ENABLE_MONITORING', false) }, - get ENABLE_SWAGGER() { return this.get('ENABLE_SWAGGER', true) }, - get ENABLE_METRICS() { return this.get('ENABLE_METRICS', false) }, - - // Database - get DB_HOST() { return this.get('DB_HOST', 'localhost') }, - get DB_PORT() { return this.get('DB_PORT', 5432) }, - get DB_NAME() { return this.get('DB_NAME', '') }, - get DB_USER() { return this.get('DB_USER', '') }, - get DB_PASSWORD() { return this.get('DB_PASSWORD', '') }, - get DB_SSL() { return this.get('DB_SSL', false) }, - - // SMTP - get SMTP_HOST() { return this.get('SMTP_HOST', '') }, - get SMTP_PORT() { return this.get('SMTP_PORT', 587) }, - get SMTP_USER() { return this.get('SMTP_USER', '') }, - get SMTP_PASSWORD() { return this.get('SMTP_PASSWORD', '') }, - get SMTP_SECURE() { return this.get('SMTP_SECURE', false) } -} - -/** - * Create namespaced environment access - * Usage: const db = createNamespace('DATABASE_') - * db.get('URL') -> reads DATABASE_URL - */ -export function createNamespace(prefix: string) { - return { - get: (key: string, defaultValue?: T): T => - smartEnv.get(`${prefix}${key}`, defaultValue), - - has: (key: string) => smartEnv.has(`${prefix}${key}`), - - all: () => { - const allEnv = smartEnv.all() - const namespaced: Record = {} - - for (const [key, value] of Object.entries(allEnv)) { - if (key.startsWith(prefix)) { - namespaced[key.slice(prefix.length)] = value - } - } - - return namespaced - } - } -} - -/** - * Environment validation - */ -export const validate = { - require(keys: string[]): void { - const missing = keys.filter(key => !smartEnv.has(key)) - if (missing.length > 0) { - throw new Error(`Missing required environment variables: ${missing.join(', ')}`) - } - }, - - oneOf(key: string, validValues: string[]): void { - const value = smartEnv.get(key, '') - if (value && !validValues.includes(value)) { - throw new Error(`${key} must be one of: ${validValues.join(', ')}, got: ${value}`) - } - }, - - validate(key: string, validator: (value: string) => boolean, errorMessage: string): void { - const value = smartEnv.get(key, '') - if (value && !validator(value)) { - throw new Error(`${key}: ${errorMessage}`) - } - } -} - -/** - * Convenience functions - */ -export const helpers = { - isDevelopment: () => env.NODE_ENV === 'development', - isProduction: () => env.NODE_ENV === 'production', - isTest: () => env.NODE_ENV === 'test', - - getDatabaseUrl: () => { - const url = env.DATABASE_URL - if (url) return url - - const { DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD } = env - if (DB_HOST && DB_NAME) { - const auth = DB_USER ? `${DB_USER}:${DB_PASSWORD}@` : '' - return `postgres://${auth}${DB_HOST}:${DB_PORT}/${DB_NAME}` - } - - return null - }, - - getServerUrl: () => `http://${env.HOST}:${env.PORT}`, - getClientUrl: () => `http://${env.HOST}:${env.VITE_PORT}` -} - -export default env - -// Legacy exports for compatibility -export const runtimeEnv = env -export const envValidation = validate -export const createEnvNamespace = createNamespace \ No newline at end of file diff --git a/core/utils/env.ts b/core/utils/env.ts new file mode 100644 index 00000000..20901129 --- /dev/null +++ b/core/utils/env.ts @@ -0,0 +1,306 @@ +/** + * ⚡ FluxStack Unified Environment Loader + * + * Single source of truth for environment variables with: + * - Automatic type casting + * - Build-safe dynamic access (prevents Bun inlining) + * - Simple, intuitive API + * - TypeScript type inference + * + * @example + * ```ts + * import { env } from '@/core/utils/env' + * + * const port = env.PORT // number (3000) + * const debug = env.DEBUG // boolean (false) + * const origins = env.CORS_ORIGINS // string[] (['*']) + * + * // Custom vars with smart casting + * const timeout = env.get('TIMEOUT', 5000) // number + * const enabled = env.get('FEATURE_X', false) // boolean + * const tags = env.get('TAGS', ['api']) // string[] + * ``` + */ + +/** + * Smart environment loader with dynamic access + * Uses Bun.env (runtime) → process.env (fallback) → eval (last resort) + */ +class EnvLoader { + private cache = new Map() + private accessor: () => Record + + constructor() { + this.accessor = this.createAccessor() + } + + /** + * Create dynamic accessor to prevent build-time inlining + */ + private createAccessor(): () => Record { + const global = globalThis as any + + return () => { + // Try Bun.env first (most reliable in Bun) + if (global['Bun']?.['env']) { + return global['Bun']['env'] + } + + // Fallback to process.env + if (global['process']?.['env']) { + return global['process']['env'] + } + + // Last resort: eval to bypass static analysis + try { + const proc = eval('typeof process !== "undefined" ? process : null') + return proc?.env || {} + } catch { + return {} + } + } + } + + /** + * Get environment variable with automatic type casting + * Type is inferred from defaultValue + */ + get(key: string, defaultValue?: T): T { + // Check cache first + const cacheKey = `${key}:${typeof defaultValue}` + if (this.cache.has(cacheKey)) { + return this.cache.get(cacheKey) + } + + const env = this.accessor() + const value = env[key] + + if (!value || value === '') { + this.cache.set(cacheKey, defaultValue as T) + return defaultValue as T + } + + // Auto-detect type from defaultValue + let result: any = value + + if (typeof defaultValue === 'number') { + const parsed = Number(value) + result = isNaN(parsed) ? defaultValue : parsed + } else if (typeof defaultValue === 'boolean') { + result = ['true', '1', 'yes', 'on'].includes(value.toLowerCase()) + } else if (Array.isArray(defaultValue)) { + result = value.split(',').map(v => v.trim()).filter(Boolean) + } else if (typeof defaultValue === 'object' && defaultValue !== null) { + try { + result = JSON.parse(value) + } catch { + result = defaultValue + } + } + + this.cache.set(cacheKey, result) + return result as T + } + + /** + * Check if environment variable exists and has a value + */ + has(key: string): boolean { + const env = this.accessor() + const value = env[key] + return value !== undefined && value !== '' + } + + /** + * Get all environment variables + */ + all(): Record { + const env = this.accessor() + const result: Record = {} + + for (const [key, value] of Object.entries(env)) { + if (value !== undefined && value !== '') { + result[key] = value + } + } + + return result + } + + /** + * Require specific environment variables (throws if missing) + */ + require(keys: string[]): void { + const missing = keys.filter(key => !this.has(key)) + if (missing.length > 0) { + throw new Error( + `Missing required environment variables: ${missing.join(', ')}\n` + + `Please set them in your .env file or environment.` + ) + } + } + + /** + * Validate environment variable value + */ + validate(key: string, validValues: string[]): void { + const value = this.get(key, '') + if (value && !validValues.includes(value)) { + throw new Error( + `Invalid value for ${key}: "${value}"\n` + + `Valid values are: ${validValues.join(', ')}` + ) + } + } + + /** + * Clear cache (useful for testing) + */ + clearCache(): void { + this.cache.clear() + } +} + +// Singleton instance +const loader = new EnvLoader() + +/** + * Unified environment variables API + */ +export const env = { + /** + * Get environment variable with smart type casting + * @example env.get('PORT', 3000) → number + */ + get: (key: string, defaultValue?: T): T => loader.get(key, defaultValue), + + /** + * Check if environment variable exists + */ + has: (key: string): boolean => loader.has(key), + + /** + * Get all environment variables + */ + all: (): Record => loader.all(), + + /** + * Require environment variables (throws if missing) + */ + require: (keys: string[]): void => loader.require(keys), + + /** + * Validate environment variable value + */ + validate: (key: string, validValues: string[]): void => loader.validate(key, validValues), + + /** + * Clear cache (for testing) + */ + clearCache: (): void => loader.clearCache(), + + // Common environment variables with smart defaults + get NODE_ENV() { return this.get('NODE_ENV', 'development') as 'development' | 'production' | 'test' }, + get PORT() { return this.get('PORT', 3000) }, + get HOST() { return this.get('HOST', 'localhost') }, + get DEBUG() { return this.get('DEBUG', false) }, + get LOG_LEVEL() { return this.get('LOG_LEVEL', 'info') as 'debug' | 'info' | 'warn' | 'error' }, + get LOG_FORMAT() { return this.get('LOG_FORMAT', 'pretty') as 'json' | 'pretty' }, + + // API + get API_PREFIX() { return this.get('API_PREFIX', '/api') }, + get VITE_PORT() { return this.get('VITE_PORT', 5173) }, + + // CORS + get CORS_ORIGINS() { return this.get('CORS_ORIGINS', ['*']) }, + get CORS_METHODS() { return this.get('CORS_METHODS', ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']) }, + get CORS_HEADERS() { return this.get('CORS_HEADERS', ['Content-Type', 'Authorization']) }, + get CORS_CREDENTIALS() { return this.get('CORS_CREDENTIALS', false) }, + get CORS_MAX_AGE() { return this.get('CORS_MAX_AGE', 86400) }, + + // App + get FLUXSTACK_APP_NAME() { return this.get('FLUXSTACK_APP_NAME', 'FluxStack') }, + get FLUXSTACK_APP_VERSION() { return this.get('FLUXSTACK_APP_VERSION', '1.0.0') }, + + // Features + get ENABLE_MONITORING() { return this.get('ENABLE_MONITORING', false) }, + get ENABLE_SWAGGER() { return this.get('ENABLE_SWAGGER', true) }, + get ENABLE_METRICS() { return this.get('ENABLE_METRICS', false) }, + + // Database + get DATABASE_URL() { return this.get('DATABASE_URL', '') }, + get DB_HOST() { return this.get('DB_HOST', 'localhost') }, + get DB_PORT() { return this.get('DB_PORT', 5432) }, + get DB_NAME() { return this.get('DB_NAME', '') }, + get DB_USER() { return this.get('DB_USER', '') }, + get DB_PASSWORD() { return this.get('DB_PASSWORD', '') }, + get DB_SSL() { return this.get('DB_SSL', false) }, + + // Auth + get JWT_SECRET() { return this.get('JWT_SECRET', '') }, + get JWT_EXPIRES_IN() { return this.get('JWT_EXPIRES_IN', '24h') }, + get JWT_ALGORITHM() { return this.get('JWT_ALGORITHM', 'HS256') }, + + // Email + get SMTP_HOST() { return this.get('SMTP_HOST', '') }, + get SMTP_PORT() { return this.get('SMTP_PORT', 587) }, + get SMTP_USER() { return this.get('SMTP_USER', '') }, + get SMTP_PASSWORD() { return this.get('SMTP_PASSWORD', '') }, + get SMTP_SECURE() { return this.get('SMTP_SECURE', false) }, +} + +/** + * Environment helpers + */ +export const helpers = { + isDevelopment: (): boolean => env.NODE_ENV === 'development', + isProduction: (): boolean => env.NODE_ENV === 'production', + isTest: (): boolean => env.NODE_ENV === 'test', + + getServerUrl: (): string => `http://${env.HOST}:${env.PORT}`, + getClientUrl: (): string => `http://${env.HOST}:${env.VITE_PORT}`, + + getDatabaseUrl: (): string | null => { + if (env.DATABASE_URL) return env.DATABASE_URL + + const { DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD } = env + if (DB_HOST && DB_NAME) { + const auth = DB_USER ? `${DB_USER}:${DB_PASSWORD}@` : '' + return `postgres://${auth}${DB_HOST}:${DB_PORT}/${DB_NAME}` + } + + return null + } +} + +/** + * Create namespaced environment access + * @example + * const db = createNamespace('DATABASE_') + * db.get('URL') // reads DATABASE_URL + */ +export function createNamespace(prefix: string) { + return { + get: (key: string, defaultValue?: T): T => + env.get(`${prefix}${key}`, defaultValue), + + has: (key: string): boolean => + env.has(`${prefix}${key}`), + + all: (): Record => { + const allEnv = env.all() + const namespaced: Record = {} + + for (const [key, value] of Object.entries(allEnv)) { + if (key.startsWith(prefix)) { + namespaced[key.slice(prefix.length)] = value + } + } + + return namespaced + } + } +} + +// Default export +export default env diff --git a/core/utils/helpers.ts b/core/utils/helpers.ts index 3dbd010d..b8f78065 100644 --- a/core/utils/helpers.ts +++ b/core/utils/helpers.ts @@ -87,20 +87,20 @@ export const throttle = any>( } export const isProduction = (): boolean => { - // Import here to avoid circular dependency - const { env } = require('./env-runtime-v2') + // Import here to avoid circular dependency + const { env } = require('./env') return env.NODE_ENV === 'production' } export const isDevelopment = (): boolean => { // Import here to avoid circular dependency - const { env } = require('./env-runtime-v2') + const { env } = require('./env') return env.NODE_ENV === 'development' || !env.NODE_ENV } export const isTest = (): boolean => { // Import here to avoid circular dependency - const { env } = require('./env-runtime-v2') + const { env } = require('./env') return env.NODE_ENV === 'test' } diff --git a/core/utils/logger/config.ts b/core/utils/logger/config.ts index b247526d..0e14778b 100644 --- a/core/utils/logger/config.ts +++ b/core/utils/logger/config.ts @@ -1,8 +1,10 @@ /** * FluxStack Logger Configuration - * Centralized configuration for the logging system + * Re-export from declarative config */ +import { loggerConfig } from '@/config/logger.config' + export interface LoggerConfig { level: 'debug' | 'info' | 'warn' | 'error' dateFormat: string @@ -15,18 +17,18 @@ export interface LoggerConfig { } /** - * Get logger configuration from environment variables + * Get logger configuration from declarative config */ export function getLoggerConfig(): LoggerConfig { return { - level: (process.env.LOG_LEVEL as LoggerConfig['level']) || 'info', - dateFormat: process.env.LOG_DATE_FORMAT || 'YYYY-MM-DD HH:mm:ss', - logToFile: process.env.LOG_TO_FILE === 'true', - maxSize: process.env.LOG_MAX_SIZE || '20m', - maxFiles: process.env.LOG_MAX_FILES || '14d', - objectDepth: parseInt(process.env.LOG_OBJECT_DEPTH || '4'), - enableColors: process.env.LOG_COLORS !== 'false', - enableStackTrace: process.env.LOG_STACK_TRACE !== 'false' + level: loggerConfig.level, + dateFormat: loggerConfig.dateFormat, + logToFile: loggerConfig.logToFile, + maxSize: loggerConfig.maxSize, + maxFiles: loggerConfig.maxFiles, + objectDepth: loggerConfig.objectDepth, + enableColors: loggerConfig.enableColors, + enableStackTrace: loggerConfig.enableStackTrace } } diff --git a/core/utils/logger/startup-banner.ts b/core/utils/logger/startup-banner.ts index 7a6f35f5..22dd0e79 100644 --- a/core/utils/logger/startup-banner.ts +++ b/core/utils/logger/startup-banner.ts @@ -10,6 +10,7 @@ import chalk from 'chalk' import { LOG } from './index' +import { FLUXSTACK_VERSION } from '../version' export interface StartupInfo { port: number @@ -35,7 +36,7 @@ export function displayStartupBanner(info: StartupInfo): void { swaggerPath } = info - console.log('\n' + chalk.cyan.bold('⚡ FluxStack') + chalk.gray(` v1.1.0\n`)) + console.log('\n' + chalk.cyan.bold('⚡ FluxStack') + chalk.gray(` v${FLUXSTACK_VERSION}\n`)) // Server info console.log(chalk.bold('🚀 Server')) diff --git a/core/utils/version.ts b/core/utils/version.ts new file mode 100644 index 00000000..df7e5dd2 --- /dev/null +++ b/core/utils/version.ts @@ -0,0 +1,5 @@ +/** + * FluxStack Framework Version + * Single source of truth for version number + */ +export const FLUXSTACK_VERSION = '1.2.0' diff --git a/create-fluxstack.ts b/create-fluxstack.ts index 93d0e02d..2471fa68 100644 --- a/create-fluxstack.ts +++ b/create-fluxstack.ts @@ -57,6 +57,7 @@ program const filesToCopy = [ 'core', 'app', + 'config', // ✅ CRITICAL: Copy config folder with declarative configs 'ai-context', // ✅ CRITICAL: Copy AI documentation for users 'bun.lock', // ✅ CRITICAL: Copy lockfile to maintain working versions 'package.json', // ✅ Copy real package.json from framework diff --git a/fluxstack.config.ts b/fluxstack.config.ts index 374092b1..0d0e680d 100644 --- a/fluxstack.config.ts +++ b/fluxstack.config.ts @@ -1,11 +1,11 @@ /** * FluxStack Configuration * Enhanced configuration with comprehensive settings and environment support - * Updated to use dynamic environment variables + * Uses unified environment loader */ import type { FluxStackConfig } from './core/config/schema' -import { env, helpers } from './core/utils/env-runtime-v2' +import { env, helpers } from './core/utils/env' console.log(`🔧 Loading FluxStack config for ${env.NODE_ENV} environment`) diff --git a/package.json b/package.json index 38dada66..7afc43de 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "create-fluxstack", - "version": "1.1.0", - "description": "⚡ Revolutionary full-stack TypeScript framework with Temporal Bridge Auto-Discovery, Elysia + React + Bun", + "version": "1.2.0", + "description": "⚡ Revolutionary full-stack TypeScript framework with Declarative Config System, Elysia + React + Bun", "keywords": [ "framework", "full-stack",