Skip to content

Commit 267064c

Browse files
authored
refactor(@angular/ssr): parse route paths and redirect URLs into structured segments and query params
1 parent 52d3d69 commit 267064c

4 files changed

Lines changed: 335 additions & 60 deletions

File tree

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

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,15 @@ import { AngularAppManifest, getAngularAppManifest } from '../manifest';
3131
import { AngularBootstrap, isNgModule } from '../utils/ng';
3232
import { promiseWithAbort } from '../utils/promise';
3333
import { VALID_REDIRECT_RESPONSE_CODES, isValidRedirectResponseCode } from '../utils/redirect';
34-
import { joinUrlParts, stripLeadingSlash } from '../utils/url';
34+
import {
35+
type PathSegment,
36+
WILDCARD_SEGMENT,
37+
joinUrlParts,
38+
parsePathSegments,
39+
parseUrl,
40+
serializePathSegments,
41+
stripLeadingSlash,
42+
} from '../utils/url';
3543
import {
3644
PrerenderFallback,
3745
RenderMode,
@@ -179,11 +187,17 @@ async function* handleRoute(options: {
179187
const parentRoutes = new Map<string, RouteTreeNodeMetadata>();
180188

181189
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('/');
190+
// Reference the route parameters by their 0-based position in the URL (see `buildUrlWithParams`).
191+
const routeSegments = parsePathSegments(joinUrlParts(baseHref, currentRoutePath));
192+
metadata.redirectTo = resolveRedirectTo(parentRoute, route, redirectTo, (paramName) => {
193+
for (let i = routeSegments.length - 1; i >= 0; i--) {
194+
const segment = routeSegments[i];
195+
if (segment.type === 'param' && segment.name === paramName) {
196+
return { type: 'wildcard', index: i };
197+
}
198+
}
185199

186-
return `*${routeSegments.lastIndexOf(parameter)}`;
200+
return WILDCARD_SEGMENT;
187201
});
188202
metadata.relativeRedirect = redirectTo[0] !== '/' ? true : undefined;
189203
}
@@ -518,7 +532,7 @@ async function* handleSSGRoute(
518532
parentRoute.replace(URL_PARAMETER_GLOBAL_REGEXP, replacer),
519533
route,
520534
redirectTo,
521-
(parameter) => params[parameter.slice(1)],
535+
(paramName) => ({ type: 'static', value: params[paramName] }),
522536
),
523537
};
524538
}
@@ -581,45 +595,48 @@ function handlePrerenderParamsReplacement(
581595
* @param parentRoute - The path of the parent route.
582596
* @param route - The redirecting route.
583597
* @param redirectTo - The target path for redirection.
584-
* @param resolveParam - A function that returns the replacement for a parameter, such as `:id`.
598+
* @param resolveParam - A function that returns the replacement `PathSegment` for a parameter name.
585599
* @returns The resolved redirect path as a string.
586600
* @throws If `redirectTo` uses a parameter that is not defined in the path of the route.
587601
*/
588602
function resolveRedirectTo(
589603
parentRoute: string,
590604
route: Route,
591605
redirectTo: string,
592-
resolveParam: (parameter: string) => string,
606+
resolveParam: (paramName: string) => PathSegment,
593607
): string {
594608
if (redirectTo[0] === '/' && (route.matcher || !redirectTo.includes('/:'))) {
595609
return redirectTo;
596610
}
597611

598-
const [path] = redirectTo.split(/[?#]/, 1);
599-
let resolvedPath = path;
612+
const parsedRedirect = parseUrl(redirectTo);
613+
let resolvedSegments = parsedRedirect.segments;
600614

601615
// 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-
}
616+
if (!route.matcher && parsedRedirect.hasDynamicSegments) {
617+
const routeSegments = parsePathSegments(route.path ?? '');
618+
resolvedSegments = resolvedSegments.map((segment) => {
619+
if (segment.type !== 'param') {
620+
return segment;
621+
}
610622

611-
if (!routeSegments.includes(segment)) {
612-
throw new Error(`Cannot redirect to '${redirectTo}'. Cannot find '${segment}'.`);
613-
}
623+
if (!routeSegments.some((s) => s.type === 'param' && s.name === segment.name)) {
624+
throw new Error(`Cannot redirect to '${redirectTo}'. Cannot find ':${segment.name}'.`);
625+
}
614626

615-
return resolveParam(segment);
616-
})
617-
.join('/');
627+
return resolveParam(segment.name);
628+
});
618629
}
619630

620-
return redirectTo[0] === '/'
621-
? resolvedPath + redirectTo.slice(path.length)
622-
: joinUrlParts(parentRoute.replace(URL_PARAMETER_GLOBAL_REGEXP, '*'), resolvedPath);
631+
if (parsedRedirect.isAbsolute) {
632+
return serializePathSegments(resolvedSegments) + parsedRedirect.rawSuffix;
633+
}
634+
635+
const parentSegments = parsePathSegments(parentRoute).map((segment) =>
636+
segment.type === 'param' ? WILDCARD_SEGMENT : segment,
637+
);
638+
639+
return serializePathSegments([...parentSegments, ...resolvedSegments]);
623640
}
624641

625642
/**

0 commit comments

Comments
 (0)