Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit a4e415e

Browse files
fix(@angular/ssr): support getPrerenderParams for wildcard routes
Handle `getPrerenderParams` return values when used with wildcard route paths, including support for combined routes like `/product/:id/**`. Supports returning an array of path segments (e.g., `['category', '123']`) for `**` routes and dynamic segments combined with catch-all routes. This enables more flexible prerendering configurations in server routes, including handling specific paths such as `/product/1/laptop/123`. Example: ```ts { path: '/product/:id/**', renderMode: RenderMode.Prerender, async getPrerenderParams() { return [ { id: '1', '**': 'laptop/123' }, { id: '2', '**': 'laptop/456' } ]; } } ``` Closes #30035 (cherry picked from commit cb3446e)
1 parent de52cc2 commit a4e415e

File tree

3 files changed

+103
-41
lines changed

3 files changed

+103
-41
lines changed

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

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ interface Route extends AngularRoute {
4646
*/
4747
const MODULE_PRELOAD_MAX = 10;
4848

49+
/**
50+
* Regular expression to match a catch-all route pattern in a URL path,
51+
* specifically one that ends with '/**'.
52+
*/
53+
const CATCH_ALL_REGEXP = /\/(\*\*)$/;
54+
4955
/**
5056
* Regular expression to match segments preceded by a colon in a string.
5157
*/
@@ -391,7 +397,11 @@ async function* handleSSGRoute(
391397
meta.redirectTo = resolveRedirectTo(currentRoutePath, redirectTo);
392398
}
393399

394-
if (!URL_PARAMETER_REGEXP.test(currentRoutePath)) {
400+
const isCatchAllRoute = CATCH_ALL_REGEXP.test(currentRoutePath);
401+
if (
402+
(isCatchAllRoute && !getPrerenderParams) ||
403+
(!isCatchAllRoute && !URL_PARAMETER_REGEXP.test(currentRoutePath))
404+
) {
395405
// Route has no parameters
396406
yield {
397407
...meta,
@@ -415,7 +425,9 @@ async function* handleSSGRoute(
415425

416426
if (serverConfigRouteTree) {
417427
// Automatically resolve dynamic parameters for nested routes.
418-
const catchAllRoutePath = joinUrlParts(currentRoutePath, '**');
428+
const catchAllRoutePath = isCatchAllRoute
429+
? currentRoutePath
430+
: joinUrlParts(currentRoutePath, '**');
419431
const match = serverConfigRouteTree.match(catchAllRoutePath);
420432
if (match && match.renderMode === RenderMode.Prerender && !('getPrerenderParams' in match)) {
421433
serverConfigRouteTree.insert(catchAllRoutePath, {
@@ -429,20 +441,10 @@ async function* handleSSGRoute(
429441
const parameters = await runInInjectionContext(parentInjector, () => getPrerenderParams());
430442
try {
431443
for (const params of parameters) {
432-
const routeWithResolvedParams = currentRoutePath.replace(URL_PARAMETER_REGEXP, (match) => {
433-
const parameterName = match.slice(1);
434-
const value = params[parameterName];
435-
if (typeof value !== 'string') {
436-
throw new Error(
437-
`The 'getPrerenderParams' function defined for the '${stripLeadingSlash(currentRoutePath)}' route ` +
438-
`returned a non-string value for parameter '${parameterName}'. ` +
439-
`Please make sure the 'getPrerenderParams' function returns values for all parameters ` +
440-
'specified in this route.',
441-
);
442-
}
443-
444-
return value;
445-
});
444+
const replacer = handlePrerenderParamsReplacement(params, currentRoutePath);
445+
const routeWithResolvedParams = currentRoutePath
446+
.replace(URL_PARAMETER_REGEXP, replacer)
447+
.replace(CATCH_ALL_REGEXP, replacer);
446448

447449
yield {
448450
...meta,
@@ -473,6 +475,34 @@ async function* handleSSGRoute(
473475
}
474476
}
475477

478+
/**
479+
* Creates a replacer function used for substituting parameter placeholders in a route path
480+
* with their corresponding values provided in the `params` object.
481+
*
482+
* @param params - An object mapping parameter names to their string values.
483+
* @param currentRoutePath - The current route path, used for constructing error messages.
484+
* @returns A function that replaces a matched parameter placeholder (e.g., ':id') with its corresponding value.
485+
*/
486+
function handlePrerenderParamsReplacement(
487+
params: Record<string, string>,
488+
currentRoutePath: string,
489+
): (substring: string, ...args: unknown[]) => string {
490+
return (match) => {
491+
const parameterName = match.slice(1);
492+
const value = params[parameterName];
493+
if (typeof value !== 'string') {
494+
throw new Error(
495+
`The 'getPrerenderParams' function defined for the '${stripLeadingSlash(currentRoutePath)}' route ` +
496+
`returned a non-string value for parameter '${parameterName}'. ` +
497+
`Please make sure the 'getPrerenderParams' function returns values for all parameters ` +
498+
'specified in this route.',
499+
);
500+
}
501+
502+
return parameterName === '**' ? `/${value}` : value;
503+
};
504+
}
505+
476506
/**
477507
* Resolves the `redirectTo` property for a given route.
478508
*
@@ -530,9 +560,9 @@ function buildServerConfigRouteTree({ routes, appShellRoute }: ServerRoutesConfi
530560
continue;
531561
}
532562

533-
if (path.includes('*') &&'getPrerenderParams'inmetadata) {
563+
if ('getPrerenderParams'inmetadata&&(path.includes('/*/') ||path.endsWith('/*'))) {
534564
errors.push(
535-
`Invalid '${path}' route configuration: 'getPrerenderParams' cannot be used with a '*' or '**' route.`,
565+
`Invalid '${path}' route configuration: 'getPrerenderParams' cannot be used with a '*' route.`,
536566
);
537567
continue;
538568
}

‎packages/angular/ssr/src/routes/route-config.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ export interface ServerRoutePrerenderWithParams extends Omit<ServerRoutePrerende
146146
* A function that returns a Promise resolving to an array of objects, each representing a route path with URL parameters.
147147
* This function runs in the injector context, allowing access to Angular services and dependencies.
148148
*
149+
* It also works for catch-all routes (e.g., `/**`), where the parameter name will be `**` and the return value will be
150+
* the segments of the path, such as `/foo/bar`. These routes can also be combined, e.g., `/product/:id/**`,
151+
* where both a parameterized segment (`:id`) and a catch-all segment (`**`) can be used together to handle more complex paths.
152+
*
149153
* @returns A Promise resolving to an array where each element is an object with string keys (representing URL parameter names)
150154
* and string values (representing the corresponding values for those parameters in the route path).
151155
*
@@ -159,7 +163,17 @@ export interface ServerRoutePrerenderWithParams extends Omit<ServerRoutePrerende
159163
* const productService = inject(ProductService);
160164
* const ids = await productService.getIds(); // Assuming this returns ['1', '2', '3']
161165
*
162-
* return ids.map(id => ({ id })); // Generates paths like: [{ id: '1' }, { id: '2' }, { id: '3' }]
166+
* return ids.map(id => ({ id })); // Generates paths like: ['product/1', 'product/2', 'product/3']
167+
* },
168+
* },
169+
* {
170+
* path: '/product/:id/**',
171+
* renderMode: RenderMode.Prerender,
172+
* async getPrerenderParams() {
173+
* return [
174+
* { id: '1', '**': 'laptop/3' },
175+
* { id: '2', '**': 'laptop/4' }
176+
* ]; // Generates paths like: ['product/1/laptop/3', 'product/2/laptop/4']
163177
* },
164178
* },
165179
* ];

‎packages/angular/ssr/test/routes/ng-routes_spec.ts‎

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -68,26 +68,6 @@ describe('extractRoutesAndCreateRouteTree', () => {
6868
);
6969
});
7070

71-
it("should error when 'getPrerenderParams' is used with a '**' route", async () => {
72-
setAngularAppTestingManifest(
73-
[{ path: 'home', component: DummyComponent }],
74-
[
75-
{
76-
path: '**',
77-
renderMode: RenderMode.Prerender,
78-
getPrerenderParams() {
79-
return Promise.resolve([]);
80-
},
81-
},
82-
],
83-
);
84-
85-
const { errors } = await extractRoutesAndCreateRouteTree({ url });
86-
expect(errors[0]).toContain(
87-
"Invalid '**' route configuration: 'getPrerenderParams' cannot be used with a '*' or '**' route.",
88-
);
89-
});
90-
9171
it("should error when 'getPrerenderParams' is used with a '*' route", async () => {
9272
setAngularAppTestingManifest(
9373
[{ path: 'invalid/:id', component: DummyComponent }],
@@ -104,7 +84,7 @@ describe('extractRoutesAndCreateRouteTree', () => {
10484

10585
const { errors } = await extractRoutesAndCreateRouteTree({ url });
10686
expect(errors[0]).toContain(
107-
"Invalid 'invalid/*' route configuration: 'getPrerenderParams' cannot be used with a '*' or '**' route.",
87+
"Invalid 'invalid/*' route configuration: 'getPrerenderParams' cannot be used with a '*' route.",
10888
);
10989
});
11090

@@ -259,7 +239,7 @@ describe('extractRoutesAndCreateRouteTree', () => {
259239
]);
260240
});
261241

262-
it('should resolve parameterized routes for SSG and not add a fallback route if fallback is None', async () => {
242+
it('should resolve parameterized routes for SSG add a fallback route if fallback is Server', async () => {
263243
setAngularAppTestingManifest(
264244
[
265245
{ path: 'home', component: DummyComponent },
@@ -296,6 +276,44 @@ describe('extractRoutesAndCreateRouteTree', () => {
296276
]);
297277
});
298278

279+
it('should resolve catch all routes for SSG and add a fallback route if fallback is Server', async () => {
280+
setAngularAppTestingManifest(
281+
[
282+
{ path: 'home', component: DummyComponent },
283+
{ path: 'user/:name/**', component: DummyComponent },
284+
],
285+
[
286+
{
287+
path: 'user/:name/**',
288+
renderMode: RenderMode.Prerender,
289+
fallback: PrerenderFallback.Server,
290+
async getPrerenderParams() {
291+
return [
292+
{ name: 'joe', '**': 'role/admin' },
293+
{ name: 'jane', '**': 'role/writer' },
294+
];
295+
},
296+
},
297+
{ path: '**', renderMode: RenderMode.Server },
298+
],
299+
);
300+
301+
const { routeTree, errors } = await extractRoutesAndCreateRouteTree({
302+
url,
303+
invokeGetPrerenderParams: true,
304+
});
305+
expect(errors).toHaveSize(0);
306+
expect(routeTree.toObject()).toEqual([
307+
{ route: '/home', renderMode: RenderMode.Server },
308+
{ route: '/user/joe/role/admin', renderMode: RenderMode.Prerender },
309+
{
310+
route: '/user/jane/role/writer',
311+
renderMode: RenderMode.Prerender,
312+
},
313+
{ route: '/user/*/**', renderMode: RenderMode.Server },
314+
]);
315+
});
316+
299317
it('should extract nested redirects that are not explicitly defined.', async () => {
300318
setAngularAppTestingManifest(
301319
[

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /