UNPKG

@gati-framework/runtime

Version:

Gati runtime execution engine for running handler-based applications

167 lines 5.92 kB
/** * @module runtime/loader * @description Automatic handler discovery and registration */ import { readdirSync, statSync } from 'fs'; import { join, extname } from 'path'; import { logger } from './logger.js'; /** * Discover all handler files in a directory * * @param dir - Directory to search (e.g., './src/handlers') * @returns Array of handler file paths * * @example * ```typescript * const handlers = await discoverHandlers('./src/handlers'); * // Returns: ['./src/handlers/hello.ts', './src/handlers/users/create.ts'] * ``` */ export async function discoverHandlers(dir) { const handlers = []; try { const files = readdirSync(dir); for (const file of files) { const fullPath = join(dir, file); const stat = statSync(fullPath); if (stat.isDirectory()) { // Recursively search subdirectories const nestedHandlers = await discoverHandlers(fullPath); handlers.push(...nestedHandlers); } else if (stat.isFile() && ['.ts', '.js', '.mts', '.mjs'].includes(extname(file))) { // Only include TypeScript/JavaScript files handlers.push(fullPath); } } } catch (error) { // Directory doesn't exist or not accessible logger.warn({ dir, error: error instanceof Error ? error.message : 'Unknown error' }, 'Could not discover handlers'); } return handlers; } /** * Load and register handlers from a directory * * @param app - GatiApp instance * @param handlersDir - Directory containing handlers (e.g., './src/handlers') * @param options - Loading options * * @example * ```typescript * const app = createApp(); * await loadHandlers(app, './src/handlers'); * // All handlers automatically registered * ``` */ export async function loadHandlers(app, handlersDir, options = {}) { const { basePath = '', verbose = false } = options; // Discover all handler files const handlerPaths = await discoverHandlers(handlersDir); if (verbose) { logger.info({ count: handlerPaths.length }, 'Found handler files'); } // Load and register each handler for (const handlerPath of handlerPaths) { try { // Dynamic import the handler module const mod = await import(handlerPath); // Extract handler function (named export `handler` or default export) const modRecord = mod; let possible = modRecord['handler']; if (possible === undefined) { const maybeDefault = modRecord['default']; if (typeof maybeDefault === 'function') { possible = maybeDefault; } else if (maybeDefault && typeof maybeDefault === 'object') { const h = maybeDefault['handler']; if (typeof h === 'function') possible = h; } } const handlerFn = typeof possible === 'function' ? possible : undefined; if (!handlerFn) { logger.warn({ handlerPath }, 'No valid handler function found, skipping'); continue; } // Extract route metadata (from explicit export or filename) const metadata = extractMetadata(handlerPath, mod); // Register the handler const method = (metadata.method || 'GET').toUpperCase(); const route = basePath + (metadata.route || inferRouteFromPath(handlerPath, handlersDir)); switch (method) { case 'GET': app.get(route, handlerFn); break; case 'POST': app.post(route, handlerFn); break; case 'PUT': app.put(route, handlerFn); break; case 'PATCH': app.patch(route, handlerFn); break; case 'DELETE': app.delete(route, handlerFn); break; default: logger.warn({ method, handlerPath }, 'Unknown HTTP method'); } // Verbose logging intentionally noop to satisfy no-console rule at runtime build time } catch (error) { logger.error({ handlerPath, error: error instanceof Error ? error.message : 'Unknown error' }, 'Failed to load handler'); } } } /** * Extract metadata from handler module */ function extractMetadata(_filePath, module) { const m = module; let meta = m['metadata']; if (meta === undefined) { const def = m['default']; if (def && typeof def === 'object' && 'metadata' in (def)) { meta = def['metadata']; } } if (meta && typeof meta === 'object') { const result = {}; const mm = meta; if (typeof mm['method'] === 'string') result.method = mm['method']; if (typeof mm['route'] === 'string') result.route = mm['route']; return result; } // TODO: Parse file for JSDoc in future versions return {}; } /** * Infer route from file path * * @example * ``` * './src/handlers/hello.ts' → '/hello' * './src/handlers/users/create.ts' → '/users/create' * './src/handlers/api/v1/posts.ts' → '/api/v1/posts' * ``` */ function inferRouteFromPath(filePath, baseDir) { // Remove base directory and extension let route = filePath .replace(baseDir, '') .replace(/\\/g, '/') // Normalize path separators .replace(/\.(ts|js|mts|mjs)$/, ''); // Remove leading slash if present if (route.startsWith('/')) { route = route.slice(1); } // Convert to route format return '/' + route; } //# sourceMappingURL=loader.js.map