@angular/ssr 22.2.2 → 22.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/fesm2022/ssr.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { normalizeTrustProxyHeaders, sanitizeRequestHeaders, validateRequest } from './_validation-chunk.mjs';
1
+ import { getFirstHeaderValue, normalizeTrustProxyHeaders, sanitizeRequestHeaders, validateRequest } from './_validation-chunk.mjs';
2
2
  import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
3
3
  import { ɵConsole as _Console, ApplicationRef, REQUEST, makeEnvironmentProviders, provideEnvironmentInitializer, inject, InjectionToken, ɵENABLE_ROOT_COMPONENT_BOOTSTRAP as _ENABLE_ROOT_COMPONENT_BOOTSTRAP, Compiler, createEnvironmentInjector, EnvironmentInjector, runInInjectionContext, ɵresetCompiledComponents as _resetCompiledComponents, REQUEST_CONTEXT, RESPONSE_INIT, LOCALE_ID } from '@angular/core';
4
4
  import { platformServer, INITIAL_CONFIG, ɵSERVER_CONTEXT as _SERVER_CONTEXT, ɵrenderInternal as _renderInternal, provideServerRendering as provideServerRendering$1 } from '@angular/platform-server';
@@ -95,6 +95,7 @@ function stripIndexHtmlFromURL(url) {
95
95
  }
96
96
  return url;
97
97
  }
98
+ const POSITIONAL_PLACEHOLDER_REGEXP = /^\*(\d+)$/;
98
99
  function buildPathWithParams(toPath, fromPath) {
99
100
  if (toPath[0] !== '/') {
100
101
  throw new Error(`Invalid toPath: The string must start with a '/'. Received: '${toPath}'`);
@@ -102,13 +103,35 @@ function buildPathWithParams(toPath, fromPath) {
102
103
  if (fromPath[0] !== '/') {
103
104
  throw new Error(`Invalid fromPath: The string must start with a '/'. Received: '${fromPath}'`);
104
105
  }
105
- if (!toPath.includes('/*')) {
106
+ const [path] = toPath.split(/[?#]/, 1);
107
+ if (!path.includes('/*')) {
106
108
  return toPath;
107
109
  }
108
110
  const fromPathParts = fromPath.split('/');
109
- const toPathParts = toPath.split('/');
110
- const resolvedParts = toPathParts.map((part, index) => toPathParts[index] === '*' ? fromPathParts[index] : part);
111
- return joinUrlParts(...resolvedParts);
111
+ const toPathParts = path.split('/');
112
+ const resolvedParts = toPathParts.map((part, index) => part === '*' ? fromPathParts[index] : part.replace(POSITIONAL_PLACEHOLDER_REGEXP, (_, position) => fromPathParts[position]));
113
+ return joinUrlParts(...resolvedParts) + toPath.slice(path.length);
114
+ }
115
+ function buildQueryWithParams(toUrl, fromParams) {
116
+ const [, path, query, fragment] = /^([^?#]*)\?([^#]*)(.*)$/.exec(toUrl) ?? [];
117
+ if (query === undefined) {
118
+ return toUrl;
119
+ }
120
+ const params = query.split('&');
121
+ const names = params.map(param => param.split('=', 1)[0]);
122
+ const resolvedQuery = params.map((param, index) => {
123
+ const name = names[index];
124
+ const value = param.slice(name.length + 1);
125
+ if (value[0] !== ':' || names.indexOf(name) !== names.lastIndexOf(name)) {
126
+ return param;
127
+ }
128
+ const sourceValues = fromParams.getAll(value.slice(1));
129
+ return (sourceValues.length ? sourceValues : ['undefined']).map(sourceValue => `${name}=${encodeQueryValue(sourceValue)}`).join('&');
130
+ }).join('&');
131
+ return `${path}?${resolvedQuery}${fragment}`;
132
+ }
133
+ function encodeQueryValue(value) {
134
+ return encodeURIComponent(value).replace(/%(40|3A|24|2C|3B)/g, match => decodeURIComponent(match));
112
135
  }
113
136
  const MATRIX_PARAMS_REGEX = /;[^/]+/g;
114
137
  function stripMatrixParams(pathname) {
@@ -158,9 +181,9 @@ async function renderAngular(html, bootstrap, url, platformProviders, serverCont
158
181
  hasNavigationError = false;
159
182
  const requestPrefix = envInjector.get(APP_BASE_HREF, null, {
160
183
  optional: true
161
- }) ?? envInjector.get(REQUEST, null, {
184
+ }) ?? getFirstHeaderValue(envInjector.get(REQUEST, null, {
162
185
  optional: true
163
- })?.headers.get('X-Forwarded-Prefix');
186
+ })?.headers.get('X-Forwarded-Prefix'));
164
187
  const {
165
188
  pathname,
166
189
  search,
@@ -348,7 +371,7 @@ function provideServerRendering(...args) {
348
371
  providers.push(...ɵproviders);
349
372
  }
350
373
  if (!hasServerRoutes && hasAppShell) {
351
- throw new Error(`Configuration error: found 'withAppShell()' without 'withRoutes()' in the same call to 'provideServerRendering()'.` + `The 'withAppShell()' function requires 'withRoutes()' to be used.`);
374
+ throw new Error(`Configuration error: found 'withAppShell()' without 'withRoutes()' in the same call to 'provideServerRendering()'. ` + `The 'withAppShell()' function requires 'withRoutes()' to be used.`);
352
375
  }
353
376
  return makeEnvironmentProviders(providers);
354
377
  }
@@ -450,6 +473,8 @@ async function* handleRoute(options) {
450
473
  const {
451
474
  metadata,
452
475
  currentRoutePath,
476
+ parentRoute,
477
+ baseHref,
453
478
  route,
454
479
  compiler,
455
480
  parentInjector,
@@ -465,55 +490,75 @@ async function* handleRoute(options) {
465
490
  children,
466
491
  ɵentryName
467
492
  } = route;
468
- if (ɵentryName && loadComponent) {
493
+ if (ɵentryName && (loadComponent || loadChildren)) {
469
494
  appendPreloadToMetadata(ɵentryName, entryPointToBrowserMapping, metadata);
470
495
  }
496
+ const parentRoutes = new Map();
471
497
  if (metadata.renderMode === RenderMode.Prerender) {
472
- yield* handleSSGRoute(serverConfigRouteTree, typeof redirectTo === 'string' ? redirectTo : undefined, metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes);
498
+ for await (const result of handleSSGRoute(serverConfigRouteTree, route, parentRoute, baseHref, metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes)) {
499
+ if ('error' in result) {
500
+ yield result;
501
+ } else {
502
+ parentRoutes.set(result.route, result);
503
+ }
504
+ }
473
505
  } else if (redirectTo !== undefined) {
474
506
  if (metadata.status && !isValidRedirectResponseCode(metadata.status)) {
475
507
  yield {
476
508
  error: `The '${metadata.status}' status code is not a valid redirect response code. ` + `Please use one of the following redirect response codes: ${[...VALID_REDIRECT_RESPONSE_CODES.values()].join(', ')}.`
477
509
  };
478
510
  } else if (typeof redirectTo === 'string') {
479
- yield {
511
+ const routeSegments = joinUrlParts(baseHref, currentRoutePath).split('/');
512
+ parentRoutes.set(metadata.route, {
480
513
  ...metadata,
481
- redirectTo: resolveRedirectTo(metadata.route, redirectTo)
482
- };
514
+ redirectTo: resolveRedirectTo(parentRoute, replaceRedirectParams(route, redirectTo, parameter => `*${routeSegments.lastIndexOf(parameter)}`)),
515
+ relativeRedirect: redirectTo[0] !== '/' ? true : undefined
516
+ });
483
517
  } else {
484
- yield metadata;
518
+ parentRoutes.set(metadata.route, metadata);
485
519
  }
486
520
  } else {
487
- yield metadata;
488
- }
489
- if (children?.length) {
490
- yield* traverseRoutesConfig({
491
- ...options,
492
- routes: children,
493
- parentRoute: currentRoutePath,
494
- parentPreloads: metadata.preload
495
- });
521
+ parentRoutes.set(metadata.route, metadata);
496
522
  }
497
- if (loadChildren) {
498
- if (ɵentryName) {
499
- appendPreloadToMetadata(ɵentryName, entryPointToBrowserMapping, metadata);
500
- }
523
+ let childRoutes = children;
524
+ let childInjector = parentInjector;
525
+ if (!childRoutes?.length && loadChildren) {
501
526
  const routeInjector = route.providers ? createEnvironmentInjector(route.providers, parentInjector.get(EnvironmentInjector), `Route: ${route.path}`) : parentInjector;
502
527
  const loadedChildRoutes = await _loadChildren(route, compiler, routeInjector);
503
528
  if (loadedChildRoutes) {
504
- const {
505
- routes: childRoutes,
506
- injector = routeInjector
507
- } = loadedChildRoutes;
508
- yield* traverseRoutesConfig({
509
- ...options,
510
- routes: childRoutes,
511
- parentInjector: injector,
512
- parentRoute: currentRoutePath,
513
- parentPreloads: metadata.preload
514
- });
529
+ childRoutes = loadedChildRoutes.routes;
530
+ childInjector = loadedChildRoutes.injector ?? routeInjector;
531
+ }
532
+ }
533
+ if (!childRoutes?.length) {
534
+ yield* parentRoutes.values();
535
+ return;
536
+ }
537
+ const childResults = [];
538
+ const overriddenParentRoutes = new Set();
539
+ const traverseChildRoutes = traverseRoutesConfig({
540
+ ...options,
541
+ routes: childRoutes,
542
+ parentInjector: childInjector,
543
+ parentRoute: currentRoutePath,
544
+ parentPreloads: metadata.preload
545
+ });
546
+ for await (const result of traverseChildRoutes) {
547
+ if ('error' in result) {
548
+ childResults.push(result);
549
+ continue;
550
+ }
551
+ if (parentRoutes.has(result.route)) {
552
+ if (!overriddenParentRoutes.has(result.route)) {
553
+ overriddenParentRoutes.add(result.route);
554
+ parentRoutes.set(result.route, result);
555
+ }
556
+ } else {
557
+ childResults.push(result);
515
558
  }
516
559
  }
560
+ yield* parentRoutes.values();
561
+ yield* childResults;
517
562
  } catch (error) {
518
563
  yield {
519
564
  error: `Error in handleRoute for '${options.currentRoutePath}': ${error.message}`
@@ -590,7 +635,7 @@ async function* traverseRoutesConfig(options) {
590
635
  renderMode: RenderMode.Prerender,
591
636
  ...matchedMetaData,
592
637
  preload: parentPreloads,
593
- route: path === '' ? addTrailingSlash(currentRoutePath) : currentRoutePath,
638
+ route: currentRoutePath,
594
639
  presentInClientRouter: undefined
595
640
  },
596
641
  currentRoutePath,
@@ -616,7 +661,7 @@ function appendPreloadToMetadata(entryName, entryPointToBrowserMapping, metadata
616
661
  }
617
662
  metadata.preload = Array.from(combinedPreloads);
618
663
  }
619
- async function* handleSSGRoute(serverConfigRouteTree, redirectTo, metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes) {
664
+ async function* handleSSGRoute(serverConfigRouteTree, route, parentRoute, baseHref, metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes) {
620
665
  if (metadata.renderMode !== RenderMode.Prerender) {
621
666
  throw new Error(`'handleSSGRoute' was called for a route which rendering mode is not prerender.`);
622
667
  }
@@ -629,8 +674,11 @@ async function* handleSSGRoute(serverConfigRouteTree, redirectTo, metadata, pare
629
674
  if ('getPrerenderParams' in meta) {
630
675
  delete meta['getPrerenderParams'];
631
676
  }
677
+ const redirectTo = typeof route.redirectTo === 'string' ? route.redirectTo : undefined;
632
678
  if (redirectTo !== undefined) {
633
- meta.redirectTo = resolveRedirectTo(currentRoutePath, redirectTo);
679
+ const routeSegments = joinUrlParts(baseHref, currentRoutePath).split('/');
680
+ meta.redirectTo = resolveRedirectTo(parentRoute, replaceRedirectParams(route, redirectTo, parameter => `*${routeSegments.lastIndexOf(parameter)}`));
681
+ meta.relativeRedirect = redirectTo[0] !== '/' ? true : undefined;
634
682
  }
635
683
  const isCatchAllRoute = CATCH_ALL_REGEXP.test(currentRoutePath);
636
684
  if (isCatchAllRoute && !getPrerenderParams || !isCatchAllRoute && !URL_PARAMETER_REGEXP.test(currentRoutePath)) {
@@ -666,7 +714,7 @@ async function* handleSSGRoute(serverConfigRouteTree, redirectTo, metadata, pare
666
714
  yield {
667
715
  ...meta,
668
716
  route: routeWithResolvedParams,
669
- redirectTo: redirectTo === undefined ? undefined : resolveRedirectTo(routeWithResolvedParams, redirectTo)
717
+ redirectTo: redirectTo === undefined ? undefined : resolveRedirectTo(parentRoute.replace(URL_PARAMETER_GLOBAL_REGEXP, replacer), replaceRedirectParams(route, redirectTo, parameter => params[parameter.slice(1)]))
670
718
  };
671
719
  }
672
720
  } catch (error) {
@@ -694,13 +742,29 @@ function handlePrerenderParamsReplacement(params, currentRoutePath) {
694
742
  return parameterName === '**' ? `/${value}` : value;
695
743
  };
696
744
  }
697
- function resolveRedirectTo(routePath, redirectTo) {
745
+ function resolveRedirectTo(parentRoute, redirectTo) {
698
746
  if (redirectTo[0] === '/') {
699
747
  return redirectTo;
700
748
  }
701
- const segments = routePath.replace(URL_PARAMETER_GLOBAL_REGEXP, '*').split('/');
702
- segments.pop();
703
- return joinUrlParts(...segments, redirectTo);
749
+ const [path] = redirectTo.split(/[?#]/, 1);
750
+ return joinUrlParts(parentRoute.replace(URL_PARAMETER_GLOBAL_REGEXP, '*'), path);
751
+ }
752
+ function replaceRedirectParams(route, redirectTo, resolve) {
753
+ if (route.matcher) {
754
+ return redirectTo;
755
+ }
756
+ const routeSegments = route.path?.split('/') ?? [];
757
+ const [path] = redirectTo.split(/[?#]/, 1);
758
+ const resolvedPath = path.split('/').map(segment => {
759
+ if (segment[0] !== ':') {
760
+ return segment;
761
+ }
762
+ if (!routeSegments.includes(segment)) {
763
+ throw new Error(`Cannot redirect to '${redirectTo}'. Cannot find '${segment}'.`);
764
+ }
765
+ return resolve(segment);
766
+ }).join('/');
767
+ return resolvedPath + redirectTo.slice(path.length);
704
768
  }
705
769
  function buildServerConfigRouteTree({
706
770
  routes,
@@ -800,6 +864,7 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke
800
864
  compiler,
801
865
  parentInjector: injector,
802
866
  parentRoute: '',
867
+ baseHref,
803
868
  serverConfigRouteTree,
804
869
  invokeGetPrerenderParams,
805
870
  includePrerenderFallbackRoutes,
@@ -995,12 +1060,14 @@ class AngularServerApp {
995
1060
  }
996
1061
  const {
997
1062
  redirectTo,
1063
+ relativeRedirect,
998
1064
  status,
999
1065
  renderMode,
1000
1066
  headers
1001
1067
  } = matchedRoute;
1002
1068
  if (redirectTo !== undefined) {
1003
- return createRedirectResponse(joinUrlParts(request.headers.get('X-Forwarded-Prefix') ?? '', buildPathWithParams(redirectTo, url.pathname)), status, headers);
1069
+ const location = joinUrlParts(getFirstHeaderValue(request.headers.get('X-Forwarded-Prefix')) ?? '', buildPathWithParams(redirectTo, url.pathname));
1070
+ return createRedirectResponse(relativeRedirect ? location + url.search : buildQueryWithParams(location, url.searchParams), status, headers);
1004
1071
  }
1005
1072
  if (renderMode === RenderMode.Prerender) {
1006
1073
  const response = await this.handleServe(request, matchedRoute);
@@ -1341,7 +1408,7 @@ class AngularAppEngine {
1341
1408
  if (preferredLocale) {
1342
1409
  const subPath = supportedLocales[preferredLocale];
1343
1410
  if (subPath !== undefined) {
1344
- const prefix = request.headers.get('X-Forwarded-Prefix') ?? '';
1411
+ const prefix = getFirstHeaderValue(request.headers.get('X-Forwarded-Prefix')) ?? '';
1345
1412
  return createRedirectResponse(joinUrlParts(prefix, pathname, subPath), 302, {
1346
1413
  'Vary': 'Accept-Language'
1347
1414
  });