@legend-ssr/router
Version:
High-performance radix tree router for Legend framework
209 lines (187 loc) • 5.55 kB
text/typescript
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);
}
}
}