Skip to content

Commit 6ae797d

Browse files
author
Angular Builds
committed
8bfcae4 fix(@angular/ssr): support getPrerenderParams for wildcard routes
1 parent 3d8eaf6 commit 6ae797d

File tree

5 files changed

+56
-19
lines changed

5 files changed

+56
-19
lines changed

fesm2022/ssr.mjs

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,11 @@ class RouteTree {
818818
* initial scripts.
819819
*/
820820
const MODULE_PRELOAD_MAX = 10;
821+
/**
822+
* Regular expression to match a catch-all route pattern in a URL path,
823+
* specifically one that ends with '/**'.
824+
*/
825+
const CATCH_ALL_REGEXP = /\/(\*\*)$/;
821826
/**
822827
* Regular expression to match segments preceded by a colon in a string.
823828
*/
@@ -1024,7 +1029,9 @@ async function* handleSSGRoute(serverConfigRouteTree, redirectTo, metadata, pare
10241029
if (redirectTo !== undefined) {
10251030
meta.redirectTo = resolveRedirectTo(currentRoutePath, redirectTo);
10261031
}
1027-
if (!URL_PARAMETER_REGEXP.test(currentRoutePath)) {
1032+
const isCatchAllRoute = CATCH_ALL_REGEXP.test(currentRoutePath);
1033+
if ((isCatchAllRoute && !getPrerenderParams) ||
1034+
(!isCatchAllRoute && !URL_PARAMETER_REGEXP.test(currentRoutePath))) {
10281035
// Route has no parameters
10291036
yield {
10301037
...meta,
@@ -1043,7 +1050,9 @@ async function* handleSSGRoute(serverConfigRouteTree, redirectTo, metadata, pare
10431050
}
10441051
if (serverConfigRouteTree) {
10451052
// Automatically resolve dynamic parameters for nested routes.
1046-
const catchAllRoutePath = joinUrlParts(currentRoutePath, '**');
1053+
const catchAllRoutePath = isCatchAllRoute
1054+
? currentRoutePath
1055+
: joinUrlParts(currentRoutePath, '**');
10471056
const match = serverConfigRouteTree.match(catchAllRoutePath);
10481057
if (match && match.renderMode === RenderMode.Prerender && !('getPrerenderParams' in match)) {
10491058
serverConfigRouteTree.insert(catchAllRoutePath, {
@@ -1056,17 +1065,10 @@ async function* handleSSGRoute(serverConfigRouteTree, redirectTo, metadata, pare
10561065
const parameters = await runInInjectionContext(parentInjector, () => getPrerenderParams());
10571066
try {
10581067
for (const params of parameters) {
1059-
const routeWithResolvedParams = currentRoutePath.replace(URL_PARAMETER_REGEXP, (match) => {
1060-
const parameterName = match.slice(1);
1061-
const value = params[parameterName];
1062-
if (typeof value !== 'string') {
1063-
throw new Error(`The 'getPrerenderParams' function defined for the '${stripLeadingSlash(currentRoutePath)}' route ` +
1064-
`returned a non-string value for parameter '${parameterName}'. ` +
1065-
`Please make sure the 'getPrerenderParams' function returns values for all parameters ` +
1066-
'specified in this route.');
1067-
}
1068-
return value;
1069-
});
1068+
const replacer = handlePrerenderParamsReplacement(params, currentRoutePath);
1069+
const routeWithResolvedParams = currentRoutePath
1070+
.replace(URL_PARAMETER_REGEXP, replacer)
1071+
.replace(CATCH_ALL_REGEXP, replacer);
10701072
yield {
10711073
...meta,
10721074
route: routeWithResolvedParams,
@@ -1091,6 +1093,27 @@ async function* handleSSGRoute(serverConfigRouteTree, redirectTo, metadata, pare
10911093
};
10921094
}
10931095
}
1096+
/**
1097+
* Creates a replacer function used for substituting parameter placeholders in a route path
1098+
* with their corresponding values provided in the `params` object.
1099+
*
1100+
* @param params - An object mapping parameter names to their string values.
1101+
* @param currentRoutePath - The current route path, used for constructing error messages.
1102+
* @returns A function that replaces a matched parameter placeholder (e.g., ':id') with its corresponding value.
1103+
*/
1104+
function handlePrerenderParamsReplacement(params, currentRoutePath) {
1105+
return (match) => {
1106+
const parameterName = match.slice(1);
1107+
const value = params[parameterName];
1108+
if (typeof value !== 'string') {
1109+
throw new Error(`The 'getPrerenderParams' function defined for the '${stripLeadingSlash(currentRoutePath)}' route ` +
1110+
`returned a non-string value for parameter '${parameterName}'. ` +
1111+
`Please make sure the 'getPrerenderParams' function returns values for all parameters ` +
1112+
'specified in this route.');
1113+
}
1114+
return parameterName === '**' ? `/${value}` : value;
1115+
};
1116+
}
10941117
/**
10951118
* Resolves the `redirectTo` property for a given route.
10961119
*
@@ -1138,8 +1161,8 @@ function buildServerConfigRouteTree({ routes, appShellRoute }) {
11381161
errors.push(`Invalid '${path}' route configuration: the path cannot start with a slash.`);
11391162
continue;
11401163
}
1141-
if (path.includes('*') && 'getPrerenderParams' in metadata) {
1142-
errors.push(`Invalid '${path}' route configuration: 'getPrerenderParams' cannot be used with a '*' or '**' route.`);
1164+
if ('getPrerenderParams' in metadata && (path.includes('/*/') || path.endsWith('/*'))) {
1165+
errors.push(`Invalid '${path}' route configuration: 'getPrerenderParams' cannot be used with a '*' route.`);
11431166
continue;
11441167
}
11451168
serverConfigRouteTree.insert(path, metadata);

fesm2022/ssr.mjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

index.d.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ interface ServerRoutePrerenderWithParams extends Omit<ServerRoutePrerender, 'fal
105105
* A function that returns a Promise resolving to an array of objects, each representing a route path with URL parameters.
106106
* This function runs in the injector context, allowing access to Angular services and dependencies.
107107
*
108+
* It also works for catch-all routes (e.g., `/**`), where the parameter name will be `**` and the return value will be
109+
* the segments of the path, such as `/foo/bar`. These routes can also be combined, e.g., `/product/:id/**`,
110+
* where both a parameterized segment (`:id`) and a catch-all segment (`**`) can be used together to handle more complex paths.
111+
*
108112
* @returns A Promise resolving to an array where each element is an object with string keys (representing URL parameter names)
109113
* and string values (representing the corresponding values for those parameters in the route path).
110114
*
@@ -118,7 +122,17 @@ interface ServerRoutePrerenderWithParams extends Omit<ServerRoutePrerender, 'fal
118122
* const productService = inject(ProductService);
119123
* const ids = await productService.getIds(); // Assuming this returns ['1', '2', '3']
120124
*
121-
* return ids.map(id => ({ id })); // Generates paths like: [{ id: '1' }, { id: '2' }, { id: '3' }]
125+
* return ids.map(id => ({ id })); // Generates paths like: ['product/1', 'product/2', 'product/3']
126+
* },
127+
* },
128+
* {
129+
* path: '/product/:id/**',
130+
* renderMode: RenderMode.Prerender,
131+
* async getPrerenderParams() {
132+
* return [
133+
* { id: '1', '**': 'laptop/3' },
134+
* { id: '2', '**': 'laptop/4' }
135+
* ]; // Generates paths like: ['product/1/laptop/3', 'product/2/laptop/4']
122136
* },
123137
* },
124138
* ];

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@angular/ssr",
3-
"version": "20.0.0-next.6+sha-e03f2b8",
3+
"version": "20.0.0-next.6+sha-8bfcae4",
44
"description": "Angular server side rendering utilities",
55
"type": "module",
66
"license": "MIT",

uniqueId

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Wed Apr 23 2025 06:30:28 GMT+0000 (Coordinated Universal Time)
1+
Wed Apr 23 2025 07:49:17 GMT+0000 (Coordinated Universal Time)

0 commit comments

Comments
 (0)