Skip to content

Commit ffea3ad

Browse files
committed
fix(@angular/ssr): enhance dynamic route matching for better performance and accuracy
Updated route matching logic to prioritize closest matches, improving the accuracy of dynamic route resolution. Also we optimized performance by eliminating unnecessary recursive checks, reducing overhead during route matching. Closes #29452
1 parent f5dc745 commit ffea3ad

File tree

4 files changed

+180
-183
lines changed

4 files changed

+180
-183
lines changed

packages/angular/ssr/src/routes/ng-routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,13 @@ function buildServerConfigRouteTree({ routes, appShellRoute }: ServerRoutesConfi
458458
continue;
459459
}
460460

461+
if (path.includes('**') && 'getPrerenderParams' in metadata) {
462+
errors.push(
463+
`Invalid '${path}' route configuration: 'getPrerenderParams' cannot be used with a '**' route.`,
464+
);
465+
continue;
466+
}
467+
461468
serverConfigRouteTree.insert(path, metadata);
462469
}
463470

packages/angular/ssr/src/routes/route-tree.ts

Lines changed: 38 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { addLeadingSlash, stripTrailingSlash } from '../utils/url';
9+
import { addLeadingSlash } from '../utils/url';
1010
import { RenderMode } from './route-config';
1111

1212
/**
@@ -78,13 +78,6 @@ export interface RouteTreeNodeMetadata {
7878
* The `AdditionalMetadata` type parameter allows for extending the node metadata with custom data.
7979
*/
8080
interface RouteTreeNode<AdditionalMetadata extends Record<string, unknown>> {
81-
/**
82-
* The index indicating the order in which the route was inserted into the tree.
83-
* This index helps determine the priority of routes during matching, with lower indexes
84-
* indicating earlier inserted routes.
85-
*/
86-
insertionIndex: number;
87-
8881
/**
8982
* A map of child nodes, keyed by their corresponding route segment or wildcard.
9083
*/
@@ -110,13 +103,6 @@ export class RouteTree<AdditionalMetadata extends Record<string, unknown> = {}>
110103
*/
111104
private readonly root = this.createEmptyRouteTreeNode();
112105

113-
/**
114-
* A counter that tracks the order of route insertion.
115-
* This ensures that routes are matched in the order they were defined,
116-
* with earlier routes taking precedence.
117-
*/
118-
private insertionIndexCounter = 0;
119-
120106
/**
121107
* Inserts a new route into the route tree.
122108
* The route is broken down into segments, and each segment is added to the tree.
@@ -128,29 +114,28 @@ export class RouteTree<AdditionalMetadata extends Record<string, unknown> = {}>
128114
insert(route: string, metadata: RouteTreeNodeMetadataWithoutRoute & AdditionalMetadata): void {
129115
let node = this.root;
130116
const segments = this.getPathSegments(route);
131-
const normalizedSegments: string[] = [];
132117

133-
for (const segment of segments) {
134-
// Replace parameterized segments (e.g., :id) with a wildcard (*) for matching
135-
const normalizedSegment = segment[0] === ':' ? '*' : segment;
136-
let childNode = node.children.get(normalizedSegment);
118+
for (let index = 0; index < segments.length; index++) {
119+
let segment = segments[index];
120+
if (segment[0] === ':') {
121+
// Replace parameterized segments (e.g., :id) with a wildcard (*) for matching
122+
segments[index] = segment = '*';
123+
}
137124

125+
let childNode = node.children.get(segment);
138126
if (!childNode) {
139127
childNode = this.createEmptyRouteTreeNode();
140-
node.children.set(normalizedSegment, childNode);
128+
node.children.set(segment, childNode);
141129
}
142130

143131
node = childNode;
144-
normalizedSegments.push(normalizedSegment);
145132
}
146133

147134
// At the leaf node, store the full route and its associated metadata
148135
node.metadata = {
149136
...metadata,
150-
route: addLeadingSlash(normalizedSegments.join('/')),
137+
route: addLeadingSlash(segments.join('/')),
151138
};
152-
153-
node.insertionIndex = this.insertionIndexCounter++;
154139
}
155140

156141
/**
@@ -222,7 +207,7 @@ export class RouteTree<AdditionalMetadata extends Record<string, unknown> = {}>
222207
* @returns An array of path segments.
223208
*/
224209
private getPathSegments(route: string): string[] {
225-
return stripTrailingSlash(route).split('/');
210+
return route.split('/').filter(Boolean);
226211
}
227212

228213
/**
@@ -232,74 +217,48 @@ export class RouteTree<AdditionalMetadata extends Record<string, unknown> = {}>
232217
* This function prioritizes exact segment matches first, followed by wildcard matches (`*`),
233218
* and finally deep wildcard matches (`**`) that consume all segments.
234219
*
235-
* @param remainingSegments - The remaining segments of the route path to match.
236-
* @param node - The current node in the route tree to start traversal from.
220+
* @param segments - The array of route path segments to match against the route tree.
221+
* @param node - The current node in the route tree to start traversal from. Defaults to the root node.
222+
* @param currentIndex - The index of the segment in `remainingSegments` currently being matched.
223+
* Defaults to `0` (the first segment).
237224
*
238225
* @returns The node that best matches the remaining segments or `undefined` if no match is found.
239226
*/
240227
private traverseBySegments(
241-
remainingSegments: string[],
228+
segments: string[],
242229
node = this.root,
230+
currentIndex = 0,
243231
): RouteTreeNode<AdditionalMetadata> | undefined {
244-
const { metadata, children } = node;
245-
246-
// If there are no remaining segments and the node has metadata, return this node
247-
if (!remainingSegments.length) {
248-
return metadata ? node : node.children.get('**');
232+
if (currentIndex >= segments.length) {
233+
return node.metadata ? node : node.children.get('**');
249234
}
250235

251-
// If the node has no children, end the traversal
252-
if (!children.size) {
253-
return;
236+
if (!node.children.size) {
237+
return undefined;
254238
}
255239

256-
const [segment, ...restSegments] = remainingSegments;
257-
let currentBestMatchNode: RouteTreeNode<AdditionalMetadata> | undefined;
258-
259-
// 1. Exact segment match
260-
const exactMatchNode = node.children.get(segment);
261-
currentBestMatchNode = this.getHigherPriorityNode(
262-
currentBestMatchNode,
263-
this.traverseBySegments(restSegments, exactMatchNode),
264-
);
240+
const segment = segments[currentIndex];
265241

266-
// 2. Wildcard segment match (`*`)
267-
const wildcardNode = node.children.get('*');
268-
currentBestMatchNode = this.getHigherPriorityNode(
269-
currentBestMatchNode,
270-
this.traverseBySegments(restSegments, wildcardNode),
271-
);
272-
273-
// 3. Deep wildcard segment match (`**`)
274-
const deepWildcardNode = node.children.get('**');
275-
currentBestMatchNode = this.getHigherPriorityNode(currentBestMatchNode, deepWildcardNode);
276-
277-
return currentBestMatchNode;
278-
}
279-
280-
/**
281-
* Compares two nodes and returns the node with higher priority based on insertion index.
282-
* A node with a lower insertion index is prioritized as it was defined earlier.
283-
*
284-
* @param currentBestMatchNode - The current best match node.
285-
* @param candidateNode - The node being evaluated for higher priority based on insertion index.
286-
* @returns The node with higher priority (i.e., lower insertion index). If one of the nodes is `undefined`, the other node is returned.
287-
*/
288-
private getHigherPriorityNode(
289-
currentBestMatchNode: RouteTreeNode<AdditionalMetadata> | undefined,
290-
candidateNode: RouteTreeNode<AdditionalMetadata> | undefined,
291-
): RouteTreeNode<AdditionalMetadata> | undefined {
292-
if (!candidateNode) {
293-
return currentBestMatchNode;
242+
// 1. Attempt exact match with the current segment.
243+
const exactMatch = node.children.get(segment);
244+
if (exactMatch) {
245+
const match = this.traverseBySegments(segments, exactMatch, currentIndex + 1);
246+
if (match) {
247+
return match;
248+
}
294249
}
295250

296-
if (!currentBestMatchNode) {
297-
return candidateNode;
251+
// 2. Attempt wildcard match ('*').
252+
const wildcardMatch = node.children.get('*');
253+
if (wildcardMatch) {
254+
const match = this.traverseBySegments(segments, wildcardMatch, currentIndex + 1);
255+
if (match) {
256+
return match;
257+
}
298258
}
299259

300-
return candidateNode.insertionIndex < currentBestMatchNode.insertionIndex
301-
? candidateNode
302-
: currentBestMatchNode;
260+
// 3. Attempt double wildcard match ('**').
261+
return node.children.get('**');
303262
}
304263

305264
/**
@@ -310,7 +269,6 @@ export class RouteTree<AdditionalMetadata extends Record<string, unknown> = {}>
310269
*/
311270
private createEmptyRouteTreeNode(): RouteTreeNode<AdditionalMetadata> {
312271
return {
313-
insertionIndex: -1,
314272
children: new Map(),
315273
};
316274
}

0 commit comments

Comments
 (0)