@legend-ssr/router
Version:
High-performance radix tree router for Legend framework
211 lines (172 loc) • 6.4 kB
text/typescript
import { LegendApplication } from '@legend-ssr/common';
import { RadixTree, RouteHandler, RouteMatch } from './radix-tree';
export interface LegendRouterOptions {
caseSensitive?: boolean;
strict?: boolean;
mergeParams?: boolean;
}
// Updated interface to match Legend application's route structure
export interface ControllerRoute {
controller: any;
controllerPath: string;
method: string;
path: string;
handler: Function;
fullPath: string;
}
export class LegendRouter {
private trees: Map<string, RadixTree> = new Map();
private options: LegendRouterOptions;
private registeredRoutes: ControllerRoute[] = [];
constructor(options: LegendRouterOptions = {}) {
this.options = {
caseSensitive: false,
strict: false,
mergeParams: true,
...options,
};
}
/**
* Build router from Legend application
*/
static fromLegendApplication(app: LegendApplication, options?: LegendRouterOptions): LegendRouter {
const router = new LegendRouter(options);
console.log('[LegendRouter] Building router from Legend application...', app);
// Get all controller routes from Legend application without stripping basePath
const routes: ControllerRoute[] = app.getControllerRoutes();
console.log('[LegendRouter] Raw routes from Legend app:', routes);
console.log('[LegendRouter] Number of routes found:' , routes.length);
if (routes.length === 0) {
console.warn('[LegendRouter] WARNING: No routes found in Legend application!');
console.log('[LegendRouter] Checking controllers directly...');
// Debug: Check if controllers are registered
const controllers: any[] = app.getAllControllers();
console.log('[LegendRouter] Controllers found:', controllers.length);
controllers.forEach((controller: any, index: number) => {
console.log(`[LegendRouter] Controller ${index}:`, controller.constructor.name);
});
}
// Store routes for reference
router.registeredRoutes = routes;
// Add each route to the appropriate method tree
routes.forEach((route: ControllerRoute, index: number) => {
console.log(`[LegendRouter] Registering route ${index + 1}/${routes.length}:`, {
method: route.method,
fullPath: route.fullPath,
controllerPath: route.controllerPath
});
router.addRoute(route.method, route.fullPath, {
method: route.method,
handler: route.handler,
controller: route.controller,
controllerPath: route.controllerPath,
fullPath: route.fullPath,
originalRoute: route
});
console.log(`[LegendRouter] Registering route: method=${route.method}, fullPath=${route.fullPath}`);
});
console.log('[LegendRouter] Router built successfully with', routes.length, 'routes');
console.log('[LegendRouter] Route trees created for methods:', Array.from(router.trees.keys()));
console.log(`[LegendRouter] Route trees after registration:`, Array.from(router.trees.keys()));
return router;
}
/**
* Add a route to the router
*/
addRoute(method: string, path: string, handler: RouteHandler): void {
const normalizedMethod = method.toUpperCase();
if (!this.trees.has(normalizedMethod)) {
this.trees.set(normalizedMethod, new RadixTree());
console.log(`[LegendRouter] Created new tree for method: ${normalizedMethod}`);
}
const tree = this.trees.get(normalizedMethod)!;
const normalizedPath = this.normalizePath(path);
console.log(`[LegendRouter] Adding route: ${normalizedMethod} ${normalizedPath}`);
tree.insert(normalizedPath, handler);
}
/**
* Find a matching route
*/
match(method: string, path: string): RouteMatch | null {
const normalizedMethod = method.toUpperCase();
const tree = this.trees.get(normalizedMethod);
console.log(`[LegendRouter] Looking for route: ${normalizedMethod} ${path}`);
console.log(`[LegendRouter] Available trees:`, Array.from(this.trees.keys()));
if (!tree) {
console.log(`[LegendRouter] No tree found for method: ${normalizedMethod}`);
return null;
}
const normalizedPath = this.normalizePath(path);
console.log(`[LegendRouter] Normalized path: ${normalizedPath}`);
const result = tree.search(normalizedPath);
if (result) {
console.log(`[LegendRouter] Route matched:`, {
method: normalizedMethod,
path: normalizedPath,
params: result.params
});
} else {
console.log(`[LegendRouter] No match found for: ${normalizedMethod} ${normalizedPath}`);
}
return result;
}
/**
* Get all registered routes
*/
getAllRoutes(): Array<{ method: string; routes: RouteHandler[] }> {
const allRoutes: Array<{ method: string; routes: RouteHandler[] }> = [];
for (const [method, tree] of this.trees) {
allRoutes.push({
method,
routes: tree.getAllRoutes(),
});
}
return allRoutes;
}
/**
* Get original controller routes
*/
getControllerRoutes(): ControllerRoute[] {
return this.registeredRoutes;
}
/**
* Get route statistics
*/
getStats(): {
totalRoutes: number;
methodCounts: Record<string, number>;
methods: string[];
routes: Array<{ method: string; path: string; controller: string }>;
} {
let totalRoutes = 0;
const methodCounts: Record<string, number> = {};
for (const [method, tree] of this.trees) {
const routes = tree.getAllRoutes();
methodCounts[method] = routes.length;
totalRoutes += routes.length;
}
// Create detailed route list
const routes = this.registeredRoutes.map(route => ({
method: route.method,
path: route.fullPath,
controller: route.controller?.constructor?.name || 'Unknown'
}));
return {
totalRoutes,
methodCounts,
methods: Array.from(this.trees.keys()),
routes
};
}
private normalizePath(path: string): string {
// Remove query parameters
const cleanPath = path.split('?')[0];
// Handle case sensitivity
const normalizedPath = this.options.caseSensitive ? cleanPath : cleanPath.toLowerCase();
// Handle trailing slash
if (!this.options.strict && normalizedPath.length > 1 && normalizedPath.endsWith('/')) {
return normalizedPath.slice(0, -1);
}
return normalizedPath || '/';
}
}