@angular/ssr 19.2.0-next.0 → 19.2.0-next.2

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,7 +1,7 @@
1
1
  import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
2
2
  import { ɵConsole as _Console, InjectionToken, makeEnvironmentProviders, runInInjectionContext, ɵENABLE_ROOT_COMPONENT_BOOTSTRAP as _ENABLE_ROOT_COMPONENT_BOOTSTRAP, ApplicationRef, Compiler, REQUEST, REQUEST_CONTEXT, RESPONSE_INIT, LOCALE_ID, ɵresetCompiledComponents as _resetCompiledComponents } from '@angular/core';
3
3
  import { ɵSERVER_CONTEXT as _SERVER_CONTEXT, renderModule, renderApplication, platformServer, INITIAL_CONFIG } from '@angular/platform-server';
4
- import { ɵloadChildren as _loadChildren, Router } from '@angular/router';
4
+ import { ROUTES, ɵloadChildren as _loadChildren, Router } from '@angular/router';
5
5
  import Beasties from '../third_party/beasties/index.js';
6
6
 
7
7
  /**
@@ -399,9 +399,23 @@ function promiseWithAbort(promise, signal, errorMessagePrefix) {
399
399
  });
400
400
  }
401
401
 
402
+ /**
403
+ * The internal path used for the app shell route.
404
+ * @internal
405
+ */
406
+ const APP_SHELL_ROUTE = 'ng-app-shell';
407
+ /**
408
+ * Identifies a particular kind of `ServerRoutesFeatureKind`.
409
+ * @see {@link ServerRoutesFeature}
410
+ * @developerPreview
411
+ */
412
+ var ServerRoutesFeatureKind;
413
+ (function (ServerRoutesFeatureKind) {
414
+ ServerRoutesFeatureKind[ServerRoutesFeatureKind["AppShell"] = 0] = "AppShell";
415
+ })(ServerRoutesFeatureKind || (ServerRoutesFeatureKind = {}));
402
416
  /**
403
417
  * Different rendering modes for server routes.
404
- * @see {@link provideServerRoutesConfig}
418
+ * @see {@link provideServerRouting}
405
419
  * @see {@link ServerRoute}
406
420
  * @developerPreview
407
421
  */
@@ -455,6 +469,8 @@ const SERVER_ROUTES_CONFIG = new InjectionToken('SERVER_ROUTES_CONFIG');
455
469
  *
456
470
  * @see {@link ServerRoute}
457
471
  * @see {@link ServerRoutesConfigOptions}
472
+ * @see {@link provideServerRouting}
473
+ * @deprecated use `provideServerRouting`. This will be removed in version 20.
458
474
  * @developerPreview
459
475
  */
460
476
  function provideServerRoutesConfig(routes, options) {
@@ -468,6 +484,75 @@ function provideServerRoutesConfig(routes, options) {
468
484
  },
469
485
  ]);
470
486
  }
487
+ /**
488
+ * Sets up the necessary providers for configuring server routes.
489
+ * This function accepts an array of server routes and optional configuration
490
+ * options, returning an `EnvironmentProviders` object that encapsulates
491
+ * the server routes and configuration settings.
492
+ *
493
+ * @param routes - An array of server routes to be provided.
494
+ * @param features - (Optional) server routes features.
495
+ * @returns An `EnvironmentProviders` instance with the server routes configuration.
496
+ *
497
+ * @see {@link ServerRoute}
498
+ * @see {@link withAppShell}
499
+ * @developerPreview
500
+ */
501
+ function provideServerRouting(routes, ...features) {
502
+ const config = { routes };
503
+ const hasAppShell = features.some((f) => f.ɵkind === ServerRoutesFeatureKind.AppShell);
504
+ if (hasAppShell) {
505
+ config.appShellRoute = APP_SHELL_ROUTE;
506
+ }
507
+ const providers = [
508
+ {
509
+ provide: SERVER_ROUTES_CONFIG,
510
+ useValue: config,
511
+ },
512
+ ];
513
+ for (const feature of features) {
514
+ providers.push(...feature.ɵproviders);
515
+ }
516
+ return makeEnvironmentProviders(providers);
517
+ }
518
+ /**
519
+ * Configures the app shell route with the provided component.
520
+ *
521
+ * The app shell serves as the main entry point for the application and is commonly used
522
+ * to enable server-side rendering (SSR) of the application shell. It handles requests
523
+ * that do not match any specific server route, providing a fallback mechanism and improving
524
+ * perceived performance during navigation.
525
+ *
526
+ * This configuration is particularly useful in applications leveraging Progressive Web App (PWA)
527
+ * patterns, such as service workers, to deliver a seamless user experience.
528
+ *
529
+ * @param component The Angular component to render for the app shell route.
530
+ * @returns A server routes feature configuration for the app shell.
531
+ *
532
+ * @see {@link provideServerRouting}
533
+ * @see {@link https://angular.dev/ecosystem/service-workers/app-shell | App shell pattern on Angular.dev}
534
+ */
535
+ function withAppShell(component) {
536
+ const routeConfig = {
537
+ path: APP_SHELL_ROUTE,
538
+ };
539
+ if ('ɵcmp' in component) {
540
+ routeConfig.component = component;
541
+ }
542
+ else {
543
+ routeConfig.loadComponent = component;
544
+ }
545
+ return {
546
+ ɵkind: ServerRoutesFeatureKind.AppShell,
547
+ ɵproviders: [
548
+ {
549
+ provide: ROUTES,
550
+ useValue: routeConfig,
551
+ multi: true,
552
+ },
553
+ ],
554
+ };
555
+ }
471
556
 
472
557
  /**
473
558
  * A route tree implementation that supports efficient route matching, including support for wildcard routes.
@@ -482,12 +567,6 @@ class RouteTree {
482
567
  * All routes are stored and accessed relative to this root node.
483
568
  */
484
569
  root = this.createEmptyRouteTreeNode();
485
- /**
486
- * A counter that tracks the order of route insertion.
487
- * This ensures that routes are matched in the order they were defined,
488
- * with earlier routes taking precedence.
489
- */
490
- insertionIndexCounter = 0;
491
570
  /**
492
571
  * Inserts a new route into the route tree.
493
572
  * The route is broken down into segments, and each segment is added to the tree.
@@ -516,7 +595,6 @@ class RouteTree {
516
595
  ...metadata,
517
596
  route: addLeadingSlash(normalizedSegments.join('/')),
518
597
  };
519
- node.insertionIndex = this.insertionIndexCounter++;
520
598
  }
521
599
  /**
522
600
  * Matches a given route against the route tree and returns the best matching route's metadata.
@@ -579,7 +657,7 @@ class RouteTree {
579
657
  * @returns An array of path segments.
580
658
  */
581
659
  getPathSegments(route) {
582
- return stripTrailingSlash(route).split('/');
660
+ return route.split('/').filter(Boolean);
583
661
  }
584
662
  /**
585
663
  * Recursively traverses the route tree from a given node, attempting to match the remaining route segments.
@@ -588,52 +666,39 @@ class RouteTree {
588
666
  * This function prioritizes exact segment matches first, followed by wildcard matches (`*`),
589
667
  * and finally deep wildcard matches (`**`) that consume all segments.
590
668
  *
591
- * @param remainingSegments - The remaining segments of the route path to match.
592
- * @param node - The current node in the route tree to start traversal from.
669
+ * @param segments - The array of route path segments to match against the route tree.
670
+ * @param node - The current node in the route tree to start traversal from. Defaults to the root node.
671
+ * @param currentIndex - The index of the segment in `remainingSegments` currently being matched.
672
+ * Defaults to `0` (the first segment).
593
673
  *
594
674
  * @returns The node that best matches the remaining segments or `undefined` if no match is found.
595
675
  */
596
- traverseBySegments(remainingSegments, node = this.root) {
597
- const { metadata, children } = node;
598
- // If there are no remaining segments and the node has metadata, return this node
599
- if (!remainingSegments.length) {
600
- return metadata ? node : node.children.get('**');
601
- }
602
- // If the node has no children, end the traversal
603
- if (!children.size) {
604
- return;
676
+ traverseBySegments(segments, node = this.root, currentIndex = 0) {
677
+ if (currentIndex >= segments.length) {
678
+ return node.metadata ? node : node.children.get('**');
605
679
  }
606
- const [segment, ...restSegments] = remainingSegments;
607
- let currentBestMatchNode;
608
- // 1. Exact segment match
609
- const exactMatchNode = node.children.get(segment);
610
- currentBestMatchNode = this.getHigherPriorityNode(currentBestMatchNode, this.traverseBySegments(restSegments, exactMatchNode));
611
- // 2. Wildcard segment match (`*`)
612
- const wildcardNode = node.children.get('*');
613
- currentBestMatchNode = this.getHigherPriorityNode(currentBestMatchNode, this.traverseBySegments(restSegments, wildcardNode));
614
- // 3. Deep wildcard segment match (`**`)
615
- const deepWildcardNode = node.children.get('**');
616
- currentBestMatchNode = this.getHigherPriorityNode(currentBestMatchNode, deepWildcardNode);
617
- return currentBestMatchNode;
618
- }
619
- /**
620
- * Compares two nodes and returns the node with higher priority based on insertion index.
621
- * A node with a lower insertion index is prioritized as it was defined earlier.
622
- *
623
- * @param currentBestMatchNode - The current best match node.
624
- * @param candidateNode - The node being evaluated for higher priority based on insertion index.
625
- * @returns The node with higher priority (i.e., lower insertion index). If one of the nodes is `undefined`, the other node is returned.
626
- */
627
- getHigherPriorityNode(currentBestMatchNode, candidateNode) {
628
- if (!candidateNode) {
629
- return currentBestMatchNode;
680
+ if (!node.children.size) {
681
+ return undefined;
630
682
  }
631
- if (!currentBestMatchNode) {
632
- return candidateNode;
683
+ const segment = segments[currentIndex];
684
+ // 1. Attempt exact match with the current segment.
685
+ const exactMatch = node.children.get(segment);
686
+ if (exactMatch) {
687
+ const match = this.traverseBySegments(segments, exactMatch, currentIndex + 1);
688
+ if (match) {
689
+ return match;
690
+ }
633
691
  }
634
- return candidateNode.insertionIndex < currentBestMatchNode.insertionIndex
635
- ? candidateNode
636
- : currentBestMatchNode;
692
+ // 2. Attempt wildcard match ('*').
693
+ const wildcardMatch = node.children.get('*');
694
+ if (wildcardMatch) {
695
+ const match = this.traverseBySegments(segments, wildcardMatch, currentIndex + 1);
696
+ if (match) {
697
+ return match;
698
+ }
699
+ }
700
+ // 3. Attempt double wildcard match ('**').
701
+ return node.children.get('**');
637
702
  }
638
703
  /**
639
704
  * Creates an empty route tree node.
@@ -643,7 +708,6 @@ class RouteTree {
643
708
  */
644
709
  createEmptyRouteTreeNode() {
645
710
  return {
646
- insertionIndex: -1,
647
711
  children: new Map(),
648
712
  };
649
713
  }
@@ -662,6 +726,73 @@ const URL_PARAMETER_REGEXP = /(?<!\\):([^/]+)/g;
662
726
  * An set of HTTP status codes that are considered valid for redirect responses.
663
727
  */
664
728
  const VALID_REDIRECT_RESPONSE_CODES = new Set([301, 302, 303, 307, 308]);
729
+ /**
730
+ * Handles a single route within the route tree and yields metadata or errors.
731
+ *
732
+ * @param options - Configuration options for handling the route.
733
+ * @returns An async iterable iterator yielding `RouteTreeNodeMetadata` or an error object.
734
+ */
735
+ async function* handleRoute(options) {
736
+ try {
737
+ const { metadata, currentRoutePath, route, compiler, parentInjector, serverConfigRouteTree, entryPointToBrowserMapping, invokeGetPrerenderParams, includePrerenderFallbackRoutes, } = options;
738
+ const { redirectTo, loadChildren, loadComponent, children, ɵentryName } = route;
739
+ if (ɵentryName && loadComponent) {
740
+ appendPreloadToMetadata(ɵentryName, entryPointToBrowserMapping, metadata, true);
741
+ }
742
+ if (metadata.renderMode === RenderMode.Prerender) {
743
+ yield* handleSSGRoute(serverConfigRouteTree, typeof redirectTo === 'string' ? redirectTo : undefined, metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes);
744
+ }
745
+ else if (typeof redirectTo === 'string') {
746
+ if (metadata.status && !VALID_REDIRECT_RESPONSE_CODES.has(metadata.status)) {
747
+ yield {
748
+ error: `The '${metadata.status}' status code is not a valid redirect response code. ` +
749
+ `Please use one of the following redirect response codes: ${[...VALID_REDIRECT_RESPONSE_CODES.values()].join(', ')}.`,
750
+ };
751
+ }
752
+ else {
753
+ yield { ...metadata, redirectTo: resolveRedirectTo(metadata.route, redirectTo) };
754
+ }
755
+ }
756
+ else {
757
+ yield metadata;
758
+ }
759
+ // Recursively process child routes
760
+ if (children?.length) {
761
+ yield* traverseRoutesConfig({
762
+ ...options,
763
+ routes: children,
764
+ parentRoute: currentRoutePath,
765
+ parentPreloads: metadata.preload,
766
+ });
767
+ }
768
+ // Load and process lazy-loaded child routes
769
+ if (loadChildren) {
770
+ if (ɵentryName) {
771
+ // When using `loadChildren`, the entire feature area (including multiple routes) is loaded.
772
+ // As a result, we do not want all dynamic-import dependencies to be preload, because it involves multiple dependencies
773
+ // across different child routes. In contrast, `loadComponent` only loads a single component, which allows
774
+ // for precise control over preloading, ensuring that the files preloaded are exactly those required for that specific route.
775
+ appendPreloadToMetadata(ɵentryName, entryPointToBrowserMapping, metadata, false);
776
+ }
777
+ const loadedChildRoutes = await _loadChildren(route, compiler, parentInjector).toPromise();
778
+ if (loadedChildRoutes) {
779
+ const { routes: childRoutes, injector = parentInjector } = loadedChildRoutes;
780
+ yield* traverseRoutesConfig({
781
+ ...options,
782
+ routes: childRoutes,
783
+ parentInjector: injector,
784
+ parentRoute: currentRoutePath,
785
+ parentPreloads: metadata.preload,
786
+ });
787
+ }
788
+ }
789
+ }
790
+ catch (error) {
791
+ yield {
792
+ error: `Error in handleRoute for '${options.currentRoutePath}': ${error.message}`,
793
+ };
794
+ }
795
+ }
665
796
  /**
666
797
  * Traverses an array of route configurations to generate route tree node metadata.
667
798
  *
@@ -672,32 +803,60 @@ const VALID_REDIRECT_RESPONSE_CODES = new Set([301, 302, 303, 307, 308]);
672
803
  * @returns An async iterable iterator yielding either route tree node metadata or an error object with an error message.
673
804
  */
674
805
  async function* traverseRoutesConfig(options) {
675
- const { routes, compiler, parentInjector, parentRoute, serverConfigRouteTree, entryPointToBrowserMapping, parentPreloads, invokeGetPrerenderParams, includePrerenderFallbackRoutes, } = options;
676
- for (const route of routes) {
677
- try {
678
- const { path = '', matcher, redirectTo, loadChildren, loadComponent, children, ɵentryName, } = route;
679
- const currentRoutePath = joinUrlParts(parentRoute, path);
680
- // Get route metadata from the server config route tree, if available
681
- let matchedMetaData;
682
- if (serverConfigRouteTree) {
683
- if (matcher) {
684
- // Only issue this error when SSR routing is used.
685
- yield {
686
- error: `The route '${stripLeadingSlash(currentRoutePath)}' uses a route matcher that is not supported.`,
687
- };
806
+ const { routes: routeConfigs, parentPreloads, parentRoute, serverConfigRouteTree } = options;
807
+ for (const route of routeConfigs) {
808
+ const { matcher, path = matcher ? '**' : '' } = route;
809
+ const currentRoutePath = joinUrlParts(parentRoute, path);
810
+ if (matcher && serverConfigRouteTree) {
811
+ let foundMatch = false;
812
+ for (const matchedMetaData of serverConfigRouteTree.traverse()) {
813
+ if (!matchedMetaData.route.startsWith(currentRoutePath)) {
688
814
  continue;
689
815
  }
690
- matchedMetaData = serverConfigRouteTree.match(currentRoutePath);
691
- if (!matchedMetaData) {
816
+ foundMatch = true;
817
+ matchedMetaData.presentInClientRouter = true;
818
+ if (matchedMetaData.renderMode === RenderMode.Prerender) {
692
819
  yield {
693
- error: `The '${stripLeadingSlash(currentRoutePath)}' route does not match any route defined in the server routing configuration. ` +
694
- 'Please ensure this route is added to the server routing configuration.',
820
+ error: `The route '${stripLeadingSlash(currentRoutePath)}' is set for prerendering but has a defined matcher. ` +
821
+ `Routes with matchers cannot use prerendering. Please specify a different 'renderMode'.`,
695
822
  };
696
823
  continue;
697
824
  }
698
- matchedMetaData.presentInClientRouter = true;
825
+ yield* handleRoute({
826
+ ...options,
827
+ currentRoutePath,
828
+ route,
829
+ metadata: {
830
+ ...matchedMetaData,
831
+ preload: parentPreloads,
832
+ route: matchedMetaData.route,
833
+ presentInClientRouter: undefined,
834
+ },
835
+ });
836
+ }
837
+ if (!foundMatch) {
838
+ yield {
839
+ error: `The route '${stripLeadingSlash(currentRoutePath)}' has a defined matcher but does not ` +
840
+ 'match any route in the server routing configuration. Please ensure this route is added to the server routing configuration.',
841
+ };
699
842
  }
700
- const metadata = {
843
+ continue;
844
+ }
845
+ let matchedMetaData;
846
+ if (serverConfigRouteTree) {
847
+ matchedMetaData = serverConfigRouteTree.match(currentRoutePath);
848
+ if (!matchedMetaData) {
849
+ yield {
850
+ error: `The '${stripLeadingSlash(currentRoutePath)}' route does not match any route defined in the server routing configuration. ` +
851
+ 'Please ensure this route is added to the server routing configuration.',
852
+ };
853
+ continue;
854
+ }
855
+ matchedMetaData.presentInClientRouter = true;
856
+ }
857
+ yield* handleRoute({
858
+ ...options,
859
+ metadata: {
701
860
  renderMode: RenderMode.Prerender,
702
861
  ...matchedMetaData,
703
862
  preload: parentPreloads,
@@ -706,64 +865,10 @@ async function* traverseRoutesConfig(options) {
706
865
  // ['one', 'two', 'three'] -> 'one/two/three'
707
866
  route: path === '' ? addTrailingSlash(currentRoutePath) : currentRoutePath,
708
867
  presentInClientRouter: undefined,
709
- };
710
- if (ɵentryName && loadComponent) {
711
- appendPreloadToMetadata(ɵentryName, entryPointToBrowserMapping, metadata, true);
712
- }
713
- if (metadata.renderMode === RenderMode.Prerender) {
714
- // Handle SSG routes
715
- yield* handleSSGRoute(typeof redirectTo === 'string' ? redirectTo : undefined, metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes);
716
- }
717
- else if (typeof redirectTo === 'string') {
718
- // Handle redirects
719
- if (metadata.status && !VALID_REDIRECT_RESPONSE_CODES.has(metadata.status)) {
720
- yield {
721
- error: `The '${metadata.status}' status code is not a valid redirect response code. ` +
722
- `Please use one of the following redirect response codes: ${[...VALID_REDIRECT_RESPONSE_CODES.values()].join(', ')}.`,
723
- };
724
- continue;
725
- }
726
- yield { ...metadata, redirectTo: resolveRedirectTo(metadata.route, redirectTo) };
727
- }
728
- else {
729
- yield metadata;
730
- }
731
- // Recursively process child routes
732
- if (children?.length) {
733
- yield* traverseRoutesConfig({
734
- ...options,
735
- routes: children,
736
- parentRoute: currentRoutePath,
737
- parentPreloads: metadata.preload,
738
- });
739
- }
740
- // Load and process lazy-loaded child routes
741
- if (loadChildren) {
742
- if (ɵentryName) {
743
- // When using `loadChildren`, the entire feature area (including multiple routes) is loaded.
744
- // As a result, we do not want all dynamic-import dependencies to be preload, because it involves multiple dependencies
745
- // across different child routes. In contrast, `loadComponent` only loads a single component, which allows
746
- // for precise control over preloading, ensuring that the files preloaded are exactly those required for that specific route.
747
- appendPreloadToMetadata(ɵentryName, entryPointToBrowserMapping, metadata, false);
748
- }
749
- const loadedChildRoutes = await _loadChildren(route, compiler, parentInjector).toPromise();
750
- if (loadedChildRoutes) {
751
- const { routes: childRoutes, injector = parentInjector } = loadedChildRoutes;
752
- yield* traverseRoutesConfig({
753
- ...options,
754
- routes: childRoutes,
755
- parentInjector: injector,
756
- parentRoute: currentRoutePath,
757
- parentPreloads: metadata.preload,
758
- });
759
- }
760
- }
761
- }
762
- catch (error) {
763
- yield {
764
- error: `Error processing route '${stripLeadingSlash(route.path ?? '')}': ${error.message}`,
765
- };
766
- }
868
+ },
869
+ currentRoutePath,
870
+ route,
871
+ });
767
872
  }
768
873
  }
769
874
  /**
@@ -774,23 +879,32 @@ async function* traverseRoutesConfig(options) {
774
879
  * preloads to a predefined maximum.
775
880
  */
776
881
  function appendPreloadToMetadata(entryName, entryPointToBrowserMapping, metadata, includeDynamicImports) {
777
- if (!entryPointToBrowserMapping) {
882
+ const existingPreloads = metadata.preload ?? [];
883
+ if (!entryPointToBrowserMapping || existingPreloads.length >= MODULE_PRELOAD_MAX) {
778
884
  return;
779
885
  }
780
886
  const preload = entryPointToBrowserMapping[entryName];
781
- if (preload?.length) {
782
- // Merge existing preloads with new ones, ensuring uniqueness and limiting the total to the maximum allowed.
783
- const preloadPaths = preload
784
- .filter(({ dynamicImport }) => includeDynamicImports || !dynamicImport)
785
- .map(({ path }) => path) ?? [];
786
- const combinedPreloads = [...(metadata.preload ?? []), ...preloadPaths];
787
- metadata.preload = Array.from(new Set(combinedPreloads)).slice(0, MODULE_PRELOAD_MAX);
887
+ if (!preload?.length) {
888
+ return;
889
+ }
890
+ // Merge existing preloads with new ones, ensuring uniqueness and limiting the total to the maximum allowed.
891
+ const combinedPreloads = new Set(existingPreloads);
892
+ for (const { dynamicImport, path } of preload) {
893
+ if (dynamicImport && !includeDynamicImports) {
894
+ continue;
895
+ }
896
+ combinedPreloads.add(path);
897
+ if (combinedPreloads.size === MODULE_PRELOAD_MAX) {
898
+ break;
899
+ }
788
900
  }
901
+ metadata.preload = Array.from(combinedPreloads);
789
902
  }
790
903
  /**
791
904
  * Handles SSG (Static Site Generation) routes by invoking `getPrerenderParams` and yielding
792
905
  * all parameterized paths, returning any errors encountered.
793
906
  *
907
+ * @param serverConfigRouteTree - The tree representing the server's routing setup.
794
908
  * @param redirectTo - Optional path to redirect to, if specified.
795
909
  * @param metadata - The metadata associated with the route tree node.
796
910
  * @param parentInjector - The dependency injection container for the parent route.
@@ -798,7 +912,7 @@ function appendPreloadToMetadata(entryName, entryPointToBrowserMapping, metadata
798
912
  * @param includePrerenderFallbackRoutes - A flag indicating whether to include fallback routes in the result.
799
913
  * @returns An async iterable iterator that yields route tree node metadata for each SSG path or errors.
800
914
  */
801
- async function* handleSSGRoute(redirectTo, metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes) {
915
+ async function* handleSSGRoute(serverConfigRouteTree, redirectTo, metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes) {
802
916
  if (metadata.renderMode !== RenderMode.Prerender) {
803
917
  throw new Error(`'handleSSGRoute' was called for a route which rendering mode is not prerender.`);
804
918
  }
@@ -827,6 +941,18 @@ async function* handleSSGRoute(redirectTo, metadata, parentInjector, invokeGetPr
827
941
  };
828
942
  return;
829
943
  }
944
+ if (serverConfigRouteTree) {
945
+ // Automatically resolve dynamic parameters for nested routes.
946
+ const catchAllRoutePath = joinUrlParts(currentRoutePath, '**');
947
+ const match = serverConfigRouteTree.match(catchAllRoutePath);
948
+ if (match && match.renderMode === RenderMode.Prerender && !('getPrerenderParams' in match)) {
949
+ serverConfigRouteTree.insert(catchAllRoutePath, {
950
+ ...match,
951
+ presentInClientRouter: true,
952
+ getPrerenderParams,
953
+ });
954
+ }
955
+ }
830
956
  const parameters = await runInInjectionContext(parentInjector, () => getPrerenderParams());
831
957
  try {
832
958
  for (const params of parameters) {
@@ -912,6 +1038,10 @@ function buildServerConfigRouteTree({ routes, appShellRoute }) {
912
1038
  errors.push(`Invalid '${path}' route configuration: the path cannot start with a slash.`);
913
1039
  continue;
914
1040
  }
1041
+ if (path.includes('*') && 'getPrerenderParams' in metadata) {
1042
+ errors.push(`Invalid '${path}' route configuration: 'getPrerenderParams' cannot be used with a '*' or '**' route.`);
1043
+ continue;
1044
+ }
915
1045
  serverConfigRouteTree.insert(path, metadata);
916
1046
  }
917
1047
  return { serverConfigRouteTree, errors };
@@ -973,13 +1103,10 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke
973
1103
  router.navigationTransitions.afterPreactivation()?.next?.();
974
1104
  // Wait until the application is stable.
975
1105
  await applicationRef.whenStable();
976
- const routesResults = [];
977
1106
  const errors = [];
978
- let baseHref = injector.get(APP_BASE_HREF, null, { optional: true }) ??
1107
+ const rawBaseHref = injector.get(APP_BASE_HREF, null, { optional: true }) ??
979
1108
  injector.get(PlatformLocation).getBaseHrefFromDOM();
980
- if (baseHref.startsWith('./')) {
981
- baseHref = baseHref.slice(2);
982
- }
1109
+ const { pathname: baseHref } = new URL(rawBaseHref, 'http://localhost');
983
1110
  const compiler = injector.get(Compiler);
984
1111
  const serverRoutesConfig = injector.get(SERVER_ROUTES_CONFIG, null, { optional: true });
985
1112
  let serverConfigRouteTree;
@@ -991,10 +1118,11 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke
991
1118
  if (errors.length) {
992
1119
  return {
993
1120
  baseHref,
994
- routes: routesResults,
1121
+ routes: [],
995
1122
  errors,
996
1123
  };
997
1124
  }
1125
+ const routesResults = [];
998
1126
  if (router.config.length) {
999
1127
  // Retrieve all routes from the Angular router configuration.
1000
1128
  const traverseRoutes = traverseRoutesConfig({
@@ -1007,12 +1135,18 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke
1007
1135
  includePrerenderFallbackRoutes,
1008
1136
  entryPointToBrowserMapping,
1009
1137
  });
1010
- for await (const result of traverseRoutes) {
1011
- if ('error' in result) {
1012
- errors.push(result.error);
1138
+ const seenRoutes = new Set();
1139
+ for await (const routeMetadata of traverseRoutes) {
1140
+ if ('error' in routeMetadata) {
1141
+ errors.push(routeMetadata.error);
1142
+ continue;
1013
1143
  }
1014
- else {
1015
- routesResults.push(result);
1144
+ // If a result already exists for the exact same route, subsequent matches should be ignored.
1145
+ // This aligns with Angular's app router behavior, which prioritizes the first route.
1146
+ const routePath = routeMetadata.route;
1147
+ if (!seenRoutes.has(routePath)) {
1148
+ routesResults.push(routeMetadata);
1149
+ seenRoutes.add(routePath);
1016
1150
  }
1017
1151
  }
1018
1152
  // This timeout is necessary to prevent 'adev' from hanging in production builds.
@@ -2154,8 +2288,8 @@ class AngularAppEngine {
2154
2288
  redirectBasedOnAcceptLanguage(request) {
2155
2289
  const { basePath, supportedLocales } = this.manifest;
2156
2290
  // If the request is not for the base path, it's not our responsibility to handle it.
2157
- const url = new URL(request.url);
2158
- if (url.pathname !== basePath) {
2291
+ const { pathname } = new URL(request.url);
2292
+ if (pathname !== basePath) {
2159
2293
  return null;
2160
2294
  }
2161
2295
  // For requests to the base path (typically '/'), attempt to extract the preferred locale
@@ -2164,11 +2298,10 @@ class AngularAppEngine {
2164
2298
  if (preferredLocale) {
2165
2299
  const subPath = supportedLocales[preferredLocale];
2166
2300
  if (subPath !== undefined) {
2167
- url.pathname = joinUrlParts(url.pathname, subPath);
2168
2301
  return new Response(null, {
2169
2302
  status: 302, // Use a 302 redirect as language preference may change.
2170
2303
  headers: {
2171
- 'Location': url.toString(),
2304
+ 'Location': joinUrlParts(pathname, subPath),
2172
2305
  'Vary': 'Accept-Language',
2173
2306
  },
2174
2307
  });
@@ -2272,5 +2405,5 @@ function createRequestHandler(handler) {
2272
2405
  return handler;
2273
2406
  }
2274
2407
 
2275
- export { AngularAppEngine, PrerenderFallback, RenderMode, createRequestHandler, provideServerRoutesConfig, InlineCriticalCssProcessor as ɵInlineCriticalCssProcessor, destroyAngularServerApp as ɵdestroyAngularServerApp, extractRoutesAndCreateRouteTree as ɵextractRoutesAndCreateRouteTree, getOrCreateAngularServerApp as ɵgetOrCreateAngularServerApp, getRoutesFromAngularRouterConfig as ɵgetRoutesFromAngularRouterConfig, setAngularAppEngineManifest as ɵsetAngularAppEngineManifest, setAngularAppManifest as ɵsetAngularAppManifest };
2408
+ export { AngularAppEngine, PrerenderFallback, RenderMode, createRequestHandler, provideServerRoutesConfig, provideServerRouting, withAppShell, InlineCriticalCssProcessor as ɵInlineCriticalCssProcessor, destroyAngularServerApp as ɵdestroyAngularServerApp, extractRoutesAndCreateRouteTree as ɵextractRoutesAndCreateRouteTree, getOrCreateAngularServerApp as ɵgetOrCreateAngularServerApp, getRoutesFromAngularRouterConfig as ɵgetRoutesFromAngularRouterConfig, setAngularAppEngineManifest as ɵsetAngularAppEngineManifest, setAngularAppManifest as ɵsetAngularAppManifest };
2276
2409
  //# sourceMappingURL=ssr.mjs.map