Skip to content

Commit c663e81

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. Also avoid unnecessary regex/string splitting when redirect targets do not contain parameter placeholders, and only apply `joinUrlParts` to the path portion of redirect URLs so trailing slashes in query parameters or fragments are preserved.
1 parent 8aa16b6 commit c663e81

4 files changed

Lines changed: 137 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: 57 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,22 @@ async function* handleRoute(options: {
178178

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

181+
if (typeof redirectTo === 'string') {
182+
let routeSegments: string[] | undefined;
183+
// Reference the route parameters by their position in the URL (see `buildUrlWithParams`).
184+
metadata.redirectTo = resolveRedirectTo(parentRoute, route, redirectTo, (parameter) => {
185+
routeSegments ??= joinUrlParts(baseHref, currentRoutePath).split('/');
186+
187+
return `*${routeSegments.lastIndexOf(parameter)}`;
188+
});
189+
metadata.relativeRedirect = redirectTo[0] !== '/' ? true : undefined;
190+
}
191+
181192
if (metadata.renderMode === RenderMode.Prerender) {
182193
for await (const result of handleSSGRoute(
183194
serverConfigRouteTree,
184195
route,
185196
parentRoute,
186-
baseHref,
187197
metadata,
188198
parentInjector,
189199
invokeGetPrerenderParams,
@@ -195,31 +205,16 @@ async function* handleRoute(options: {
195205
parentRoutes.set(result.route, result);
196206
}
197207
}
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-
}
208+
} else if (
209+
redirectTo !== undefined &&
210+
metadata.status &&
211+
!isValidRedirectResponseCode(metadata.status)
212+
) {
213+
yield {
214+
error:
215+
`The '${metadata.status}' status code is not a valid redirect response code. ` +
216+
`Please use one of the following redirect response codes: ${[...VALID_REDIRECT_RESPONSE_CODES.values()].join(', ')}.`,
217+
};
223218
} else {
224219
parentRoutes.set(metadata.route, metadata);
225220
}
@@ -436,7 +431,6 @@ function appendPreloadToMetadata(
436431
* @param serverConfigRouteTree - The tree representing the server's routing setup.
437432
* @param route - The route configuration.
438433
* @param parentRoute - The path of the parent route.
439-
* @param baseHref - The base href of the application.
440434
* @param metadata - The metadata associated with the route tree node.
441435
* @param parentInjector - The dependency injection container for the parent route.
442436
* @param invokeGetPrerenderParams - A flag indicating whether to invoke the `getPrerenderParams` function.
@@ -447,7 +441,6 @@ async function* handleSSGRoute(
447441
serverConfigRouteTree: RouteTree<ServerConfigRouteTreeAdditionalMetadata> | undefined,
448442
route: Route,
449443
parentRoute: string,
450-
baseHref: string,
451444
metadata: ServerConfigRouteTreeNodeMetadata,
452445
parentInjector: Injector,
453446
invokeGetPrerenderParams: boolean,
@@ -466,21 +459,6 @@ async function* handleSSGRoute(
466459
delete meta['getPrerenderParams'];
467460
}
468461

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-
484462
const isCatchAllRoute = CATCH_ALL_REGEXP.test(currentRoutePath);
485463
if (
486464
(isCatchAllRoute && !getPrerenderParams) ||
@@ -522,6 +500,7 @@ async function* handleSSGRoute(
522500
}
523501
}
524502

503+
const redirectTo = typeof route.redirectTo === 'string' ? route.redirectTo : undefined;
525504
const parameters = await runInInjectionContext(parentInjector, () => getPrerenderParams());
526505
try {
527506
for (const params of parameters) {
@@ -538,11 +517,9 @@ async function* handleSSGRoute(
538517
? undefined
539518
: resolveRedirectTo(
540519
parentRoute.replace(URL_PARAMETER_GLOBAL_REGEXP, replacer),
541-
replaceRedirectParams(
542-
route,
543-
redirectTo,
544-
(parameter) => params[parameter.slice(1)],
545-
),
520+
route,
521+
redirectTo,
522+
(parameter) => params[parameter.slice(1)],
546523
),
547524
};
548525
}
@@ -595,69 +572,55 @@ function handlePrerenderParamsReplacement(
595572
}
596573

597574
/**
598-
* Resolves the `redirectTo` property for a given route.
575+
* Resolves the `redirectTo` property for a given route, replacing any `:id` route parameters
576+
* in its path and resolving relative paths against `parentRoute`.
599577
*
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.
578+
* Like in the Angular router, only whole path segments starting with `:` are parameters, only
579+
* parameters defined in the path of the redirecting route itself can be used, and the query and
580+
* fragment of relative redirects are ignored.
604581
*
605582
* @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-
*
627583
* @param route - The redirecting route.
628584
* @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.
585+
* @param resolveParam - A function that returns the replacement for a parameter, such as `:id`.
586+
* @returns The resolved redirect path as a string.
631587
* @throws If `redirectTo` uses a parameter that is not defined in the path of the route.
632588
*/
633-
function replaceRedirectParams(
589+
function resolveRedirectTo(
590+
parentRoute: string,
634591
route: Route,
635592
redirectTo: string,
636-
resolve: (parameter: string) => string,
593+
resolveParam: (parameter: string) => string,
637594
): string {
638-
if (route.matcher) {
639-
// The parameters of a custom matcher are only known at runtime.
595+
if (redirectTo[0] === '/' && (route.matcher || !redirectTo.includes('/:'))) {
640596
return redirectTo;
641597
}
642598

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

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

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

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

663626
/**

0 commit comments

Comments
 (0)