@angular/ssr 19.2.0-next.1 → 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
@@ -726,6 +726,73 @@ const URL_PARAMETER_REGEXP = /(?<!\\):([^/]+)/g;
726
726
  * An set of HTTP status codes that are considered valid for redirect responses.
727
727
  */
728
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
+ }
729
796
  /**
730
797
  * Traverses an array of route configurations to generate route tree node metadata.
731
798
  *
@@ -736,32 +803,60 @@ const VALID_REDIRECT_RESPONSE_CODES = new Set([301, 302, 303, 307, 308]);
736
803
  * @returns An async iterable iterator yielding either route tree node metadata or an error object with an error message.
737
804
  */
738
805
  async function* traverseRoutesConfig(options) {
739
- const { routes, compiler, parentInjector, parentRoute, serverConfigRouteTree, entryPointToBrowserMapping, parentPreloads, invokeGetPrerenderParams, includePrerenderFallbackRoutes, } = options;
740
- for (const route of routes) {
741
- try {
742
- const { path = '', matcher, redirectTo, loadChildren, loadComponent, children, ɵentryName, } = route;
743
- const currentRoutePath = joinUrlParts(parentRoute, path);
744
- // Get route metadata from the server config route tree, if available
745
- let matchedMetaData;
746
- if (serverConfigRouteTree) {
747
- if (matcher) {
748
- // Only issue this error when SSR routing is used.
749
- yield {
750
- error: `The route '${stripLeadingSlash(currentRoutePath)}' uses a route matcher that is not supported.`,
751
- };
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)) {
752
814
  continue;
753
815
  }
754
- matchedMetaData = serverConfigRouteTree.match(currentRoutePath);
755
- if (!matchedMetaData) {
816
+ foundMatch = true;
817
+ matchedMetaData.presentInClientRouter = true;
818
+ if (matchedMetaData.renderMode === RenderMode.Prerender) {
756
819
  yield {
757
- error: `The '${stripLeadingSlash(currentRoutePath)}' route does not match any route defined in the server routing configuration. ` +
758
- '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'.`,
759
822
  };
760
823
  continue;
761
824
  }
762
- 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
+ };
842
+ }
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;
763
854
  }
764
- const metadata = {
855
+ matchedMetaData.presentInClientRouter = true;
856
+ }
857
+ yield* handleRoute({
858
+ ...options,
859
+ metadata: {
765
860
  renderMode: RenderMode.Prerender,
766
861
  ...matchedMetaData,
767
862
  preload: parentPreloads,
@@ -770,64 +865,10 @@ async function* traverseRoutesConfig(options) {
770
865
  // ['one', 'two', 'three'] -> 'one/two/three'
771
866
  route: path === '' ? addTrailingSlash(currentRoutePath) : currentRoutePath,
772
867
  presentInClientRouter: undefined,
773
- };
774
- if (ɵentryName && loadComponent) {
775
- appendPreloadToMetadata(ɵentryName, entryPointToBrowserMapping, metadata, true);
776
- }
777
- if (metadata.renderMode === RenderMode.Prerender) {
778
- // Handle SSG routes
779
- yield* handleSSGRoute(serverConfigRouteTree, typeof redirectTo === 'string' ? redirectTo : undefined, metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes);
780
- }
781
- else if (typeof redirectTo === 'string') {
782
- // Handle redirects
783
- if (metadata.status && !VALID_REDIRECT_RESPONSE_CODES.has(metadata.status)) {
784
- yield {
785
- error: `The '${metadata.status}' status code is not a valid redirect response code. ` +
786
- `Please use one of the following redirect response codes: ${[...VALID_REDIRECT_RESPONSE_CODES.values()].join(', ')}.`,
787
- };
788
- continue;
789
- }
790
- yield { ...metadata, redirectTo: resolveRedirectTo(metadata.route, redirectTo) };
791
- }
792
- else {
793
- yield metadata;
794
- }
795
- // Recursively process child routes
796
- if (children?.length) {
797
- yield* traverseRoutesConfig({
798
- ...options,
799
- routes: children,
800
- parentRoute: currentRoutePath,
801
- parentPreloads: metadata.preload,
802
- });
803
- }
804
- // Load and process lazy-loaded child routes
805
- if (loadChildren) {
806
- if (ɵentryName) {
807
- // When using `loadChildren`, the entire feature area (including multiple routes) is loaded.
808
- // As a result, we do not want all dynamic-import dependencies to be preload, because it involves multiple dependencies
809
- // across different child routes. In contrast, `loadComponent` only loads a single component, which allows
810
- // for precise control over preloading, ensuring that the files preloaded are exactly those required for that specific route.
811
- appendPreloadToMetadata(ɵentryName, entryPointToBrowserMapping, metadata, false);
812
- }
813
- const loadedChildRoutes = await _loadChildren(route, compiler, parentInjector).toPromise();
814
- if (loadedChildRoutes) {
815
- const { routes: childRoutes, injector = parentInjector } = loadedChildRoutes;
816
- yield* traverseRoutesConfig({
817
- ...options,
818
- routes: childRoutes,
819
- parentInjector: injector,
820
- parentRoute: currentRoutePath,
821
- parentPreloads: metadata.preload,
822
- });
823
- }
824
- }
825
- }
826
- catch (error) {
827
- yield {
828
- error: `Error processing route '${stripLeadingSlash(route.path ?? '')}': ${error.message}`,
829
- };
830
- }
868
+ },
869
+ currentRoutePath,
870
+ route,
871
+ });
831
872
  }
832
873
  }
833
874
  /**
@@ -838,18 +879,26 @@ async function* traverseRoutesConfig(options) {
838
879
  * preloads to a predefined maximum.
839
880
  */
840
881
  function appendPreloadToMetadata(entryName, entryPointToBrowserMapping, metadata, includeDynamicImports) {
841
- if (!entryPointToBrowserMapping) {
882
+ const existingPreloads = metadata.preload ?? [];
883
+ if (!entryPointToBrowserMapping || existingPreloads.length >= MODULE_PRELOAD_MAX) {
842
884
  return;
843
885
  }
844
886
  const preload = entryPointToBrowserMapping[entryName];
845
- if (preload?.length) {
846
- // Merge existing preloads with new ones, ensuring uniqueness and limiting the total to the maximum allowed.
847
- const preloadPaths = preload
848
- .filter(({ dynamicImport }) => includeDynamicImports || !dynamicImport)
849
- .map(({ path }) => path) ?? [];
850
- const combinedPreloads = [...(metadata.preload ?? []), ...preloadPaths];
851
- 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
+ }
852
900
  }
901
+ metadata.preload = Array.from(combinedPreloads);
853
902
  }
854
903
  /**
855
904
  * Handles SSG (Static Site Generation) routes by invoking `getPrerenderParams` and yielding
@@ -1054,13 +1103,10 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke
1054
1103
  router.navigationTransitions.afterPreactivation()?.next?.();
1055
1104
  // Wait until the application is stable.
1056
1105
  await applicationRef.whenStable();
1057
- const routesResults = [];
1058
1106
  const errors = [];
1059
- let baseHref = injector.get(APP_BASE_HREF, null, { optional: true }) ??
1107
+ const rawBaseHref = injector.get(APP_BASE_HREF, null, { optional: true }) ??
1060
1108
  injector.get(PlatformLocation).getBaseHrefFromDOM();
1061
- if (baseHref.startsWith('./')) {
1062
- baseHref = baseHref.slice(2);
1063
- }
1109
+ const { pathname: baseHref } = new URL(rawBaseHref, 'http://localhost');
1064
1110
  const compiler = injector.get(Compiler);
1065
1111
  const serverRoutesConfig = injector.get(SERVER_ROUTES_CONFIG, null, { optional: true });
1066
1112
  let serverConfigRouteTree;
@@ -1072,10 +1118,11 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke
1072
1118
  if (errors.length) {
1073
1119
  return {
1074
1120
  baseHref,
1075
- routes: routesResults,
1121
+ routes: [],
1076
1122
  errors,
1077
1123
  };
1078
1124
  }
1125
+ const routesResults = [];
1079
1126
  if (router.config.length) {
1080
1127
  // Retrieve all routes from the Angular router configuration.
1081
1128
  const traverseRoutes = traverseRoutesConfig({
@@ -1088,12 +1135,18 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke
1088
1135
  includePrerenderFallbackRoutes,
1089
1136
  entryPointToBrowserMapping,
1090
1137
  });
1091
- for await (const result of traverseRoutes) {
1092
- if ('error' in result) {
1093
- 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;
1094
1143
  }
1095
- else {
1096
- 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);
1097
1150
  }
1098
1151
  }
1099
1152
  // This timeout is necessary to prevent 'adev' from hanging in production builds.