Skip to content

Commit dc35d1e

Browse files
committed
fixup! refactor(@angular/ssr): combine redirect URL parameter helpers and use DefaultUrlSerializer
1 parent b39ec95 commit dc35d1e

3 files changed

Lines changed: 63 additions & 119 deletions

File tree

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

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -167,21 +167,11 @@ export class AngularServerApp {
167167
const { redirectTo, relativeRedirect, status, renderMode, headers } = matchedRoute;
168168

169169
if (redirectTo !== undefined) {
170-
const prefix = getFirstHeaderValue(request.headers.get('X-Forwarded-Prefix'));
171-
let location = buildUrlWithParams(
172-
redirectTo,
173-
url.pathname,
174-
relativeRedirect ? undefined : url.search,
170+
const location = joinUrlParts(
171+
getFirstHeaderValue(request.headers.get('X-Forwarded-Prefix')) ?? '',
172+
buildUrlWithParams(redirectTo, url.pathname, relativeRedirect ? undefined : url.search),
175173
);
176174

177-
if (prefix) {
178-
const searchIndex = location.search(/[?#]/);
179-
location =
180-
searchIndex === -1
181-
? joinUrlParts(prefix, location)
182-
: joinUrlParts(prefix, location.slice(0, searchIndex)) + location.slice(searchIndex);
183-
}
184-
185175
// Match Angular router behavior: relative redirects keep the query string.
186176
return createRedirectResponse(
187177
relativeRedirect ? location + url.search : location,

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

Lines changed: 46 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -206,15 +206,12 @@ async function* handleRoute(options: {
206206
let routeSegments: string[] | undefined;
207207
parentRoutes.set(metadata.route, {
208208
...metadata,
209-
redirectTo: resolveRedirectTo(
210-
parentRoute,
211-
// Reference the route parameters by their position in the URL (see `buildUrlWithParams`).
212-
replaceRedirectParams(route, redirectTo, (parameter) => {
213-
routeSegments ??= joinUrlParts(baseHref, currentRoutePath).split('/');
214-
215-
return `*${routeSegments.lastIndexOf(parameter)}`;
216-
}),
217-
),
209+
// Reference the route parameters by their position in the URL (see `buildUrlWithParams`).
210+
redirectTo: resolveRedirectTo(parentRoute, route, redirectTo, (parameter) => {
211+
routeSegments ??= joinUrlParts(baseHref, currentRoutePath).split('/');
212+
213+
return `*${routeSegments.lastIndexOf(parameter)}`;
214+
}),
218215
relativeRedirect: redirectTo[0] !== '/' ? true : undefined,
219216
});
220217
} else {
@@ -469,15 +466,12 @@ async function* handleSSGRoute(
469466
const redirectTo = typeof route.redirectTo === 'string' ? route.redirectTo : undefined;
470467
if (redirectTo !== undefined) {
471468
let routeSegments: string[] | undefined;
472-
meta.redirectTo = resolveRedirectTo(
473-
parentRoute,
474-
// Reference the route parameters by their position in the URL (see `buildUrlWithParams`).
475-
replaceRedirectParams(route, redirectTo, (parameter) => {
476-
routeSegments ??= joinUrlParts(baseHref, currentRoutePath).split('/');
469+
// Reference the route parameters by their position in the URL (see `buildUrlWithParams`).
470+
meta.redirectTo = resolveRedirectTo(parentRoute, route, redirectTo, (parameter) => {
471+
routeSegments ??= joinUrlParts(baseHref, currentRoutePath).split('/');
477472

478-
return `*${routeSegments.lastIndexOf(parameter)}`;
479-
}),
480-
);
473+
return `*${routeSegments.lastIndexOf(parameter)}`;
474+
});
481475
meta.relativeRedirect = redirectTo[0] !== '/' ? true : undefined;
482476
}
483477

@@ -538,11 +532,9 @@ async function* handleSSGRoute(
538532
? undefined
539533
: resolveRedirectTo(
540534
parentRoute.replace(URL_PARAMETER_GLOBAL_REGEXP, replacer),
541-
replaceRedirectParams(
542-
route,
543-
redirectTo,
544-
(parameter) => params[parameter.slice(1)],
545-
),
535+
route,
536+
redirectTo,
537+
(parameter) => params[parameter.slice(1)],
546538
),
547539
};
548540
}
@@ -595,73 +587,51 @@ function handlePrerenderParamsReplacement(
595587
}
596588

597589
/**
598-
* Resolves the `redirectTo` property for a given route.
590+
* Resolves the `redirectTo` property for a given route, replacing any `:id` route parameters
591+
* in its path and resolving relative paths against `parentRoute`.
599592
*
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.
593+
* Like in the Angular router, only whole path segments starting with `:` are parameters, only
594+
* parameters defined in the path of the redirecting route itself can be used, and the query and
595+
* fragment of relative redirects are ignored.
604596
*
605597
* @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-
*
627598
* @param route - The redirecting route.
628599
* @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.
600+
* @param resolveParam - A function that returns the replacement for a parameter, such as `:id`.
601+
* @returns The resolved redirect path as a string.
631602
* @throws If `redirectTo` uses a parameter that is not defined in the path of the route.
632603
*/
633-
function replaceRedirectParams(
604+
function resolveRedirectTo(
605+
parentRoute: string,
634606
route: Route,
635607
redirectTo: string,
636-
resolve: (parameter: string) => string,
608+
resolveParam: (parameter: string) => string,
637609
): string {
638-
if (route.matcher || !redirectTo.includes(':')) {
639-
// The parameters of a custom matcher are only known at runtime.
640-
return redirectTo;
641-
}
642-
643610
const [path] = redirectTo.split(/[?#]/, 1);
644-
if (!path.includes(':')) {
645-
return redirectTo;
646-
}
647-
648-
const routeSegments = route.path?.split('/') ?? [];
649-
const resolvedPath = path
650-
.split('/')
651-
.map((segment) => {
652-
if (segment[0] !== ':') {
653-
return segment;
654-
}
611+
let resolvedPath = path;
612+
613+
// The parameters of a custom matcher are only known at runtime.
614+
if (!route.matcher && path.includes(':')) {
615+
const routeSegments = route.path?.split('/') ?? [];
616+
resolvedPath = path
617+
.split('/')
618+
.map((segment) => {
619+
if (segment[0] !== ':') {
620+
return segment;
621+
}
655622

656-
if (!routeSegments.includes(segment)) {
657-
throw new Error(`Cannot redirect to '${redirectTo}'. Cannot find '${segment}'.`);
658-
}
623+
if (!routeSegments.includes(segment)) {
624+
throw new Error(`Cannot redirect to '${redirectTo}'. Cannot find '${segment}'.`);
625+
}
659626

660-
return resolve(segment);
661-
})
662-
.join('/');
627+
return resolveParam(segment);
628+
})
629+
.join('/');
630+
}
663631

664-
return resolvedPath + redirectTo.slice(path.length);
632+
return redirectTo[0] === '/'
633+
? resolvedPath + redirectTo.slice(path.length)
634+
: joinUrlParts(parentRoute.replace(URL_PARAMETER_GLOBAL_REGEXP, '*'), resolvedPath);
665635
}
666636

667637
/**
@@ -939,12 +909,7 @@ export function extractRoutesAndCreateRouteTree(options: {
939909

940910
for (const { route, ...metadata } of routes) {
941911
if (metadata.redirectTo !== undefined) {
942-
const searchIndex = metadata.redirectTo.search(/[?#]/);
943-
metadata.redirectTo =
944-
searchIndex === -1
945-
? joinUrlParts(baseHref, metadata.redirectTo)
946-
: joinUrlParts(baseHref, metadata.redirectTo.slice(0, searchIndex)) +
947-
metadata.redirectTo.slice(searchIndex);
912+
metadata.redirectTo = joinUrlParts(baseHref, metadata.redirectTo);
948913
}
949914

950915
// Remove undefined fields

‎packages/angular/ssr/src/utils/url.ts‎

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -183,37 +183,26 @@ export function buildUrlWithParams(toUrl: string, fromPath: string, fromSearch?:
183183
throw new Error(`Invalid fromPath: The string must start with a '/'. Received: '${fromPath}'`);
184184
}
185185

186-
const hasPathParams = toUrl.includes('/*');
187-
const hasQueryParams = Boolean(fromSearch !== undefined && toUrl.includes('=:'));
188-
if (!hasPathParams && !hasQueryParams) {
186+
if (!toUrl.includes('/*') && (fromSearch === undefined || !toUrl.includes('=:'))) {
189187
return toUrl;
190188
}
191189

192-
const searchIndex = toUrl.search(/[?#]/);
193-
let path = searchIndex === -1 ? toUrl : toUrl.slice(0, searchIndex);
194-
let suffix = searchIndex === -1 ? '' : toUrl.slice(searchIndex);
190+
const [path] = toUrl.split(/[?#]/, 1);
191+
let resolvedPath = path;
192+
let suffix = toUrl.slice(path.length);
195193

196-
if (hasPathParams && path.includes('/*')) {
194+
if (path.includes('/*')) {
197195
const fromPathParts = fromPath.split('/');
198-
const toPathParts = path.split('/');
199-
const resolvedParts = toPathParts.map((part, index) => {
200-
if (part[0] !== '*') {
201-
return part;
202-
}
203-
204-
if (part.length === 1) {
205-
return fromPathParts[index];
206-
}
207-
208-
const position = Number(part.slice(1));
209-
210-
return Number.isInteger(position) ? fromPathParts[position] : part;
211-
});
212-
213-
path = joinUrlParts(...resolvedParts);
196+
resolvedPath = joinUrlParts(
197+
...path
198+
.split('/')
199+
.map((part, index) =>
200+
part[0] === '*' ? fromPathParts[part.length === 1 ? index : +part.slice(1)] : part,
201+
),
202+
);
214203
}
215204

216-
if (hasQueryParams && suffix.includes('=:')) {
205+
if (fromSearch !== undefined && suffix.includes('=:')) {
217206
urlSerializer ??= new DefaultUrlSerializer();
218207
const toTree = urlSerializer.parse(suffix);
219208
const actualParams = fromSearch ? urlSerializer.parse(fromSearch).queryParams : {};
@@ -228,7 +217,7 @@ export function buildUrlWithParams(toUrl: string, fromPath: string, fromSearch?:
228217
suffix = urlSerializer.serialize(toTree).slice(1);
229218
}
230219

231-
return path + suffix;
220+
return resolvedPath + suffix;
232221
}
233222

234223
const MATRIX_PARAMS_REGEX = /;[^/]+/g;

0 commit comments

Comments
 (0)