// src/config/ConfigService.ts import dotenv from 'dotenv'; import dotenvExpand from 'dotenv-expand/lib/main.js'; import path from 'path'; import { Injectable } from '../@eyjs-di'; import { defaultFileOrder, existing } from './utils'; export type ConfigOptions = { /** * Raíz del proyecto donde residen los .env */ cwd?: string; /** * Entorno (NODE_ENV). Si no se pasa, usa process.env.NODE_ENV || 'development' */ env?: 'development' | 'test' | 'production' | string; /** * Permite que los .env sobrescriban variables existentes del proceso. * Recomendación: false en producción/CI. */ override?: boolean; /** * Carga archivos .env en este orden. Si no se define, se usa el orden por defecto. */ files?: string[]; }; const ROOT = process.env.PROJECT_ROOT || process.cwd(); @Injectable() export class ConfigService { private readonly envCache: NodeJS.ProcessEnv; constructor() { this.envCache = this.loadWithDotenv(); } private loadWithDotenv(options: ConfigOptions = {}) { const env = options.env ?? process.env.NODE_ENV ?? 'development'; const cwd = options.cwd ?? ROOT; const override = options.override ?? false; const files = options.files ?? defaultFileOrder(cwd, env); const toLoad = existing(files); for (const file of toLoad) { const result = dotenv.config({ path: file, override }); if (result.error) { // si un archivo está malformado, puedes lanzar o loggear console.error(`Error loading "${file}": ${String(result.error)}`); continue; } if (result.parsed) { // Interpolación: API_URL=${HOST}/v1 dotenvExpand.expand({ parsed: result.parsed, }); } console.debug('env:load', `.env loaded: ${path.relative(cwd, file)}`); } // snapshot para lecturas estables return { ...process.env }; } public get(key: string): string | undefined { return this.envCache[key]; } /** * Helper con fallback y error claro (útil para obligatorias) */ public require(key: string): string { const v = this.get(key); if (v == null || v === '') { throw new Error(`ENV "${key}" is required and not defined.`); } return v; } }