@gati-framework/runtime
Version:
Gati runtime execution engine for running handler-based applications
167 lines • 5.92 kB
JavaScript
/**
* @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