UNPKG

@legend-ssr/router

Version:

High-performance radix tree router for Legend framework

209 lines (187 loc) 5.55 kB
export interface RouteHandler { method: string; handler: Function; controller: any; controllerPath: string; fullPath: string; originalRoute?: any; [key: string]: any; } export interface RouteMatch { handler: RouteHandler; params: Record<string, string>; path?: string; } // Interface declarations moved to top of file export interface RouteHandler { method: string; handler: Function; controller: any; controllerPath: string; fullPath: string; originalRoute?: any; } export interface RouteMatch { handler: RouteHandler; params: Record<string, string>; path?: string; } export interface RadixNode { path: string; handler: RouteHandler | null; children: RadixNode[]; isParam?: boolean; isWildcard?: boolean; paramName?: string; } export class RadixTree { private root: RadixNode; constructor() { this.root = { path: '', handler: null, children: [], }; } /** * Insert a route into the radix tree */ insert(path: string, handler: RouteHandler): void { this.insertNode(this.root, path, handler); } /** * Search for a route in the radix tree */ search(path: string): RouteMatch | null { const result = this.searchNode(this.root, path, {}); if (result) { return { handler: result.handler, params: result.params, path: path, }; } return null; } /** * Get all routes from the tree */ getAllRoutes(): RouteHandler[] { const routes: RouteHandler[] = []; this.collectRoutes(this.root, routes); return routes; } private insertNode(node: RadixNode, path: string, handler: RouteHandler): void { if (path === '') { node.handler = handler; return; } // Split the path into segments const segments = path.split('/').filter(Boolean); this.insertSegments(node, segments, handler); } private insertSegments(node: RadixNode, segments: string[], handler: RouteHandler): void { if (segments.length === 0) { node.handler = handler; return; } const segment = segments[0]; let child: RadixNode | undefined; let isParam = false; let isWildcard = false; let paramName: string | undefined; if (segment.startsWith(':')) { isParam = true; paramName = segment.replace(/^:/, '').replace(/\?$/, ''); } else if (segment.startsWith('[') && segment.endsWith(']')) { // Next.js style: [param] or [...param] if (segment.startsWith('[...')) { isWildcard = true; paramName = segment.slice(4, -1); } else { isParam = true; paramName = segment.slice(1, -1).replace(/\?$/, ''); } } else if (segment === '*') { isWildcard = true; paramName = '*'; } // Try to find an existing child node that matches child = node.children.find(c => (isParam && c.isParam && c.paramName === paramName) || (isWildcard && c.isWildcard && c.paramName === paramName) || (!isParam && !isWildcard && c.path === segment) ); if (!child) { child = { path: segment, handler: null, children: [], isParam, isWildcard, paramName, }; node.children.push(child); } this.insertSegments(child, segments.slice(1), handler); } private searchNode(node: RadixNode, path: string, params: Record<string, string>): { handler: RouteHandler, params: Record<string, string> } | null { if (path === '' || path === '/') { if (node.handler) { return { handler: node.handler, params }; } return null; } const segments = path.split('/').filter(Boolean); return this.searchSegments(node, segments, params); } private searchSegments(node: RadixNode, segments: string[], params: Record<string, string>): { handler: RouteHandler, params: Record<string, string> } | null { if (segments.length === 0) { if (node.handler) { return { handler: node.handler, params }; } return null; } const segment = segments[0]; // 1. Try static match first let child = node.children.find(c => !c.isParam && !c.isWildcard && c.path === segment); if (child) { const result = this.searchSegments(child, segments.slice(1), params); if (result) return result; } // 2. Try param match child = node.children.find(c => c.isParam); if (child && child.paramName) { const newParams = { ...params, [child.paramName]: segment }; const result = this.searchSegments(child, segments.slice(1), newParams); if (result) return result; } // 3. Try wildcard/catch-all match child = node.children.find(c => c.isWildcard); if (child && child.paramName) { const newParams = { ...params, [child.paramName]: segments.join('/') }; if (child.handler) { return { handler: child.handler, params: newParams }; } // Continue searching in case of nested wildcards const result = this.searchSegments(child, [], newParams); if (result) return result; } return null; } private findCommonPrefix(str1: string, str2: string): string { let i = 0; while (i < str1.length && i < str2.length && str1[i] === str2[i]) { i++; } return str1.slice(0, i); } private collectRoutes(node: RadixNode, routes: RouteHandler[]): void { if (node.handler) { routes.push(node.handler); } for (const child of node.children) { this.collectRoutes(child, routes); } } }