Skip to content

Commit 85e4a03

Browse files
committed
refactor(@angular/ssr): combine redirect URL parameter helpers and use DefaultUrlSerializer
Combine `buildPathWithParams` and `buildQueryWithParams` into a single `buildUrlWithParams` helper that uses `DefaultUrlSerializer` from `@angular/router` for query parameter parsing and serialization. (cherry picked from commit 92033ce)
1 parent 0c4bd19 commit 85e4a03

4 files changed

Lines changed: 136 additions & 222 deletions

File tree

‎packages/angular/ssr/src/app.ts‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,7 @@ import { ServerRouter } from './routes/router';
2424
import { AngularBootstrap, renderAngular } from './utils/ng';
2525
import { promiseWithAbort } from './utils/promise';
2626
import { createRedirectResponse } from './utils/redirect';
27-
import {
28-
buildPathWithParams,
29-
buildQueryWithParams,
30-
joinUrlParts,
31-
stripLeadingSlash,
32-
} from './utils/url';
27+
import { buildUrlWithParams, joinUrlParts, stripLeadingSlash } from './utils/url';
3328
import { getFirstHeaderValue } from './utils/validation';
3429

3530
/**
@@ -174,12 +169,12 @@ export class AngularServerApp {
174169
if (redirectTo !== undefined) {
175170
const location = joinUrlParts(
176171
getFirstHeaderValue(request.headers.get('X-Forwarded-Prefix')) ?? '',
177-
buildPathWithParams(redirectTo, url.pathname),
172+
buildUrlWithParams(redirectTo, url.pathname, relativeRedirect ? undefined : url.search),
178173
);
179174

180175
// Match Angular router behavior: relative redirects keep the query string.
181176
return createRedirectResponse(
182-
relativeRedirect ? location + url.search : buildQueryWithParams(location, url.searchParams),
177+
relativeRedirect ? location + url.search : location,
183178
status,
184179
headers,
185180
);

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

Lines changed: 56 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,21 @@ async function* handleRoute(options: {
178178

179179
const parentRoutes = new Map<string, RouteTreeNodeMetadata>();
180180

181+
if (typeof redirectTo === 'string') {
182+
// Reference the route parameters by their position in the URL (see `buildUrlWithParams`).
183+
metadata.redirectTo = resolveRedirectTo(parentRoute, route, redirectTo, (parameter) => {
184+
const routeSegments = joinUrlParts(baseHref, currentRoutePath).split('/');
185+
186+
return `*${routeSegments.lastIndexOf(parameter)}`;
187+
});
188+
metadata.relativeRedirect = redirectTo[0] !== '/' ? true : undefined;
189+
}
190+
181191
if (metadata.renderMode === RenderMode.Prerender) {
182192
for await (const result of handleSSGRoute(
183193
serverConfigRouteTree,
184194
route,
185195
parentRoute,
186-
baseHref,
187196
metadata,
188197
parentInjector,
189198
invokeGetPrerenderParams,
@@ -195,31 +204,16 @@ async function* handleRoute(options: {
195204
parentRoutes.set(result.route, result);
196205
}
197206
}
198-
} else if (redirectTo !== undefined) {
199-
if (metadata.status && !isValidRedirectResponseCode(metadata.status)) {
200-
yield {
201-
error:
202-
`The '${metadata.status}' status code is not a valid redirect response code. ` +
203-
`Please use one of the following redirect response codes: ${[...VALID_REDIRECT_RESPONSE_CODES.values()].join(', ')}.`,
204-
};
205-
} else if (typeof redirectTo === 'string') {
206-
// Reference the route parameters by their position in the URL (see `buildPathWithParams`).
207-
const routeSegments = joinUrlParts(baseHref, currentRoutePath).split('/');
208-
parentRoutes.set(metadata.route, {
209-
...metadata,
210-
redirectTo: resolveRedirectTo(
211-
parentRoute,
212-
replaceRedirectParams(
213-
route,
214-
redirectTo,
215-
(parameter) => `*${routeSegments.lastIndexOf(parameter)}`,
216-
),
217-
),
218-
relativeRedirect: redirectTo[0] !== '/' ? true : undefined,
219-
});
220-
} else {
221-
parentRoutes.set(metadata.route, metadata);
222-
}
207+
} else if (
208+
redirectTo !== undefined &&
209+
metadata.status &&
210+
!isValidRedirectResponseCode(metadata.status)
211+
) {
212+
yield {
213+
error:
214+
`The '${metadata.status}' status code is not a valid redirect response code. ` +
215+
`Please use one of the following redirect response codes: ${[...VALID_REDIRECT_RESPONSE_CODES.values()].join(', ')}.`,
216+
};
223217
} else {
224218
parentRoutes.set(metadata.route, metadata);
225219
}
@@ -436,7 +430,6 @@ function appendPreloadToMetadata(
436430
* @param serverConfigRouteTree - The tree representing the server's routing setup.
437431
* @param route - The route configuration.
438432
* @param parentRoute - The path of the parent route.
439-
* @param baseHref - The base href of the application.
440433
* @param metadata - The metadata associated with the route tree node.
441434
* @param parentInjector - The dependency injection container for the parent route.
442435
* @param invokeGetPrerenderParams - A flag indicating whether to invoke the `getPrerenderParams` function.
@@ -447,7 +440,6 @@ async function* handleSSGRoute(
447440
serverConfigRouteTree: RouteTree<ServerConfigRouteTreeAdditionalMetadata> | undefined,
448441
route: Route,
449442
parentRoute: string,
450-
baseHref: string,
451443
metadata: ServerConfigRouteTreeNodeMetadata,
452444
parentInjector: Injector,
453445
invokeGetPrerenderParams: boolean,
@@ -466,21 +458,6 @@ async function* handleSSGRoute(
466458
delete meta['getPrerenderParams'];
467459
}
468460

469-
const redirectTo = typeof route.redirectTo === 'string' ? route.redirectTo : undefined;
470-
if (redirectTo !== undefined) {
471-
// Reference the route parameters by their position in the URL (see `buildPathWithParams`).
472-
const routeSegments = joinUrlParts(baseHref, currentRoutePath).split('/');
473-
meta.redirectTo = resolveRedirectTo(
474-
parentRoute,
475-
replaceRedirectParams(
476-
route,
477-
redirectTo,
478-
(parameter) => `*${routeSegments.lastIndexOf(parameter)}`,
479-
),
480-
);
481-
meta.relativeRedirect = redirectTo[0] !== '/' ? true : undefined;
482-
}
483-
484461
const isCatchAllRoute = CATCH_ALL_REGEXP.test(currentRoutePath);
485462
if (
486463
(isCatchAllRoute && !getPrerenderParams) ||
@@ -522,6 +499,7 @@ async function* handleSSGRoute(
522499
}
523500
}
524501

502+
const redirectTo = typeof route.redirectTo === 'string' ? route.redirectTo : undefined;
525503
const parameters = await runInInjectionContext(parentInjector, () => getPrerenderParams());
526504
try {
527505
for (const params of parameters) {
@@ -538,11 +516,9 @@ async function* handleSSGRoute(
538516
? undefined
539517
: resolveRedirectTo(
540518
parentRoute.replace(URL_PARAMETER_GLOBAL_REGEXP, replacer),
541-
replaceRedirectParams(
542-
route,
543-
redirectTo,
544-
(parameter) => params[parameter.slice(1)],
545-
),
519+
route,
520+
redirectTo,
521+
(parameter) => params[parameter.slice(1)],
546522
),
547523
};
548524
}
@@ -595,69 +571,55 @@ function handlePrerenderParamsReplacement(
595571
}
596572

597573
/**
598-
* Resolves the `redirectTo` property for a given route.
574+
* Resolves the `redirectTo` property for a given route, replacing any `:id` route parameters
575+
* in its path and resolving relative paths against `parentRoute`.
599576
*
600-
* This function processes the `redirectTo` property to ensure that it correctly
601-
* resolves relative to the parent route path. If `redirectTo` is an absolute path,
602-
* it is returned as is. If it is a relative path, it is resolved based on the parent route path,
603-
* and, like in the Angular router, its query and fragment are ignored.
577+
* Like in the Angular router, only whole path segments starting with `:` are parameters, only
578+
* parameters defined in the path of the redirecting route itself can be used, and the query and
579+
* fragment of relative redirects are ignored.
604580
*
605581
* @param parentRoute - The path of the parent route.
606-
* @param redirectTo - The target path for redirection.
607-
* @returns The resolved redirect path as a string.
608-
*/
609-
function resolveRedirectTo(parentRoute: string, redirectTo: string): string {
610-
if (redirectTo[0] === '/') {
611-
// If the redirectTo path is absolute, return it as is.
612-
return redirectTo;
613-
}
614-
615-
// Match Angular router behavior: replace all segments matched by the route.
616-
const [path] = redirectTo.split(/[?#]/, 1);
617-
618-
return joinUrlParts(parentRoute.replace(URL_PARAMETER_GLOBAL_REGEXP, '*'), path);
619-
}
620-
621-
/**
622-
* Replaces the route parameters, such as `:id`, in the path of `redirectTo`.
623-
*
624-
* Like in the Angular router, only whole path segments starting with `:` are parameters, and only
625-
* the parameters defined in the path of the redirecting route itself can be used.
626-
*
627582
* @param route - The redirecting route.
628583
* @param redirectTo - The target path for redirection.
629-
* @param resolve - A function that returns the replacement for a parameter, such as `:id`.
630-
* @returns The redirect path with its route parameters replaced.
584+
* @param resolveParam - A function that returns the replacement for a parameter, such as `:id`.
585+
* @returns The resolved redirect path as a string.
631586
* @throws If `redirectTo` uses a parameter that is not defined in the path of the route.
632587
*/
633-
function replaceRedirectParams(
588+
function resolveRedirectTo(
589+
parentRoute: string,
634590
route: Route,
635591
redirectTo: string,
636-
resolve: (parameter: string) => string,
592+
resolveParam: (parameter: string) => string,
637593
): string {
638-
if (route.matcher) {
639-
// The parameters of a custom matcher are only known at runtime.
594+
if (redirectTo[0] === '/' && (route.matcher || !redirectTo.includes('/:'))) {
640595
return redirectTo;
641596
}
642597

643-
const routeSegments = route.path?.split('/') ?? [];
644598
const [path] = redirectTo.split(/[?#]/, 1);
645-
const resolvedPath = path
646-
.split('/')
647-
.map((segment) => {
648-
if (segment[0] !== ':') {
649-
return segment;
650-
}
599+
let resolvedPath = path;
600+
601+
// The parameters of a custom matcher are only known at runtime.
602+
if (!route.matcher && path.includes(':')) {
603+
const routeSegments = route.path?.split('/') ?? [];
604+
resolvedPath = path
605+
.split('/')
606+
.map((segment) => {
607+
if (segment[0] !== ':') {
608+
return segment;
609+
}
651610

652-
if (!routeSegments.includes(segment)) {
653-
throw new Error(`Cannot redirect to '${redirectTo}'. Cannot find '${segment}'.`);
654-
}
611+
if (!routeSegments.includes(segment)) {
612+
throw new Error(`Cannot redirect to '${redirectTo}'. Cannot find '${segment}'.`);
613+
}
655614

656-
return resolve(segment);
657-
})
658-
.join('/');
615+
return resolveParam(segment);
616+
})
617+
.join('/');
618+
}
659619

660-
return resolvedPath + redirectTo.slice(path.length);
620+
return redirectTo[0] === '/'
621+
? resolvedPath + redirectTo.slice(path.length)
622+
: joinUrlParts(parentRoute.replace(URL_PARAMETER_GLOBAL_REGEXP, '*'), resolvedPath);
661623
}
662624

663625
/**

0 commit comments

Comments
 (0)