yml-mvc-router
Version:
A configurable, Express-compatible routing module that maps routes from YAML to controllers following the MVC pattern
137 lines (116 loc) • 4.18 kB
JavaScript
const path = require('path');
const fs = require('fs');
/**
* Resolves controllers and middlewares from filesystem
*/
class Resolver {
constructor(config) {
this.config = config;
this.controllerCache = new Map();
this.middlewareCache = new Map();
}
/**
* Resolve controller by name
* @param {string} controllerName - Controller name (e.g., "UserController")
* @returns {Object} Controller module exports
*/
resolveController(controllerName) {
if (this.controllerCache.has(controllerName)) {
return this.controllerCache.get(controllerName);
}
// Use custom resolver if provided
if (this.config.resolveController) {
const controller = this.config.resolveController(controllerName);
this.controllerCache.set(controllerName, controller);
return controller;
}
// Default resolution: look for file in controllers directory
const controllerPath = this.getControllerPath(controllerName);
if (!fs.existsSync(controllerPath)) {
throw new Error(`Controller file not found: ${controllerPath}`);
}
try {
// Clear require cache in development for hot reload
if (this.config.watch) {
delete require.cache[require.resolve(controllerPath)];
}
const controller = require(controllerPath);
this.controllerCache.set(controllerName, controller);
return controller;
} catch (error) {
throw new Error(`Failed to load controller ${controllerName}: ${error.message}`);
}
}
/**
* Get full path to controller file
* @param {string} controllerName - Controller name
* @returns {string} Full file path
*/
getControllerPath(controllerName) {
// Try each allowed extension
for (const ext of this.config.controllerExt) {
const filePath = path.join(this.config.controllers, `${controllerName}${ext}`);
if (fs.existsSync(filePath)) {
return filePath;
}
}
// Default to .js if not found
return path.join(this.config.controllers, `${controllerName}.js`);
}
/**
* Resolve middleware by name or configuration object
* @param {string|Object} middlewareRef - Middleware name or config object
* @returns {Function} Middleware function
*/
resolveMiddleware(middlewareRef) {
let name, options = {};
if (typeof middlewareRef === 'string') {
name = middlewareRef;
} else if (typeof middlewareRef === 'object' && middlewareRef.name) {
name = middlewareRef.name;
options = middlewareRef.options || {};
} else {
throw new Error('Invalid middleware reference');
}
const cacheKey = `${name}:${JSON.stringify(options)}`;
if (this.middlewareCache.has(cacheKey)) {
return this.middlewareCache.get(cacheKey);
}
const middlewarePath = path.join(this.config.middlewares, `${name}.js`);
if (!fs.existsSync(middlewarePath)) {
throw new Error(`Middleware file not found: ${middlewarePath}`);
}
try {
// Clear require cache in development
if (this.config.watch) {
delete require.cache[require.resolve(middlewarePath)];
}
const middlewareModule = require(middlewarePath);
let middleware;
if (typeof middlewareModule === 'function') {
// If module exports a function, call it with options
middleware = middlewareModule(options);
} else if (middlewareModule.handler && typeof middlewareModule.handler === 'function') {
// If module exports { handler, ... }
middleware = middlewareModule.handler(options);
} else {
throw new Error(`Invalid middleware export in ${name}.js`);
}
if (typeof middleware !== 'function') {
throw new Error(`Middleware ${name} must export or return a function`);
}
this.middlewareCache.set(cacheKey, middleware);
return middleware;
} catch (error) {
throw new Error(`Failed to load middleware ${name}: ${error.message}`);
}
}
/**
* Clear caches (useful for hot reload)
*/
clearCache() {
this.controllerCache.clear();
this.middlewareCache.clear();
}
}
module.exports = Resolver;