// @eyjs/bootstrap - Service Loader import { readFile } from 'fs/promises' import { glob } from 'glob' import path from 'path' import type { LoaderOptions } from '../@eyjs-di' export interface LoadResult { loadedCount: number skippedCount: number errorCount: number loadedFiles: string[] skippedFiles: string[] errorFiles: string[] } export class ServiceLoader { private static readonly defaultIgnorePatterns = [ '**/*.spec.{ts,js}', '**/*.test.{ts,js}', '**/__test__/**', '**/__tests__/**', '**/node_modules/**', '**/auto-load/**', '**/create-app/**', '**/*.d.ts', ] // Decoradores que buscamos private static readonly defaultDecoratorPatterns = [ '@Injectable', '@Controller', '@Middleware', '@Component', '@Service', '@Repository', '@Bootstrap', '@Module', ] static async load(options: LoaderOptions): Promise { const { rootDir, extraDirs = [], ignorePatterns = this.defaultIgnorePatterns, decoratorPatterns = this.defaultDecoratorPatterns, } = options const allowedDirs = [rootDir, ...extraDirs] // console.debug(`🔍 Scanning directories: ${allowedDirs.join(', ')}`) const result: LoadResult = { loadedCount: 0, skippedCount: 0, errorCount: 0, loadedFiles: [], skippedFiles: [], errorFiles: [], } for (const dir of allowedDirs) { const pattern = path.resolve(dir, '**/*.{ts,js}') await this.loadDirectory( pattern, allowedDirs, ignorePatterns, decoratorPatterns, result, dir, ) } // console.info( // `📊 Load Summary: ${result.loadedCount} loaded, ${result.skippedCount} skipped, ${result.errorCount} errors`, // ) return result } private static async loadDirectory( pattern: string, allowedDirs: string[], ignorePatterns: string[], decoratorPatterns: string[], result: LoadResult, dir: string, ): Promise { const files = await glob(pattern, { ignore: ignorePatterns, nodir: true, }) // console.debug( // `🔍 Found ${files.length} files to scan in ${path.basename(dir)}`, // ) for (const file of files) { const resolvedPath = path.resolve(file) const relativePath = path.relative(process.cwd(), resolvedPath) || '/' // Si hay directorios permitidos, filtramos if (!this.isInsideAllowedDirs(resolvedPath, allowedDirs)) { // console.debug(`🚫 Skipped (outside allowed dirs): ${relativePath}`) result.skippedFiles.push(resolvedPath) result.skippedCount++ continue } // Verificar si el archivo contiene decoradores if (!(await this.hasDecorators(resolvedPath, decoratorPatterns))) { // console.debug(`⏭️ No decorators found: ${relativePath}`) result.skippedFiles.push(resolvedPath) result.skippedCount++ continue } try { await import(resolvedPath) // console.debug(`✅ Loaded: ${relativePath}`) result.loadedFiles.push(resolvedPath) result.loadedCount++ } catch (err) { console.warn(`❌ Failed to import ${relativePath}: ${err}`) result.errorFiles.push(resolvedPath) result.errorCount++ } } } private static async hasDecorators( filePath: string, decoratorPatterns: string[], ): Promise { try { const content = await readFile(filePath, 'utf-8') return decoratorPatterns.some((pattern) => content.includes(pattern)) } catch (err) { const relativePath = path.relative(process.cwd(), filePath) || '/' console.debug(`⚠️ Could not read file ${relativePath}: ${err}`) return false } } private static isInsideAllowedDirs( filePath: string, allowedDirs: string[], ): boolean { const normalizedFile = path.normalize(filePath) return allowedDirs.some((dir) => normalizedFile.startsWith(path.normalize(dir)), ) } }