@angular/ssr 19.0.1 → 19.0.3

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,5 +1,5 @@
1
1
  import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
2
- import { ɵConsole as _Console, InjectionToken, makeEnvironmentProviders, runInInjectionContext, ApplicationRef, ɵwhenStable as _whenStable, Compiler, REQUEST, REQUEST_CONTEXT, RESPONSE_INIT, LOCALE_ID, ɵresetCompiledComponents as _resetCompiledComponents } from '@angular/core';
2
+ import { ɵConsole as _Console, InjectionToken, makeEnvironmentProviders, runInInjectionContext, 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
4
  import { ɵloadChildren as _loadChildren, Router } from '@angular/router';
5
5
  import Beasties from '../third_party/beasties/index.js';
@@ -25,7 +25,7 @@ class ServerAssets {
25
25
  * @throws Error - Throws an error if the asset does not exist.
26
26
  */
27
27
  getServerAsset(path) {
28
- const asset = this.manifest.assets.get(path);
28
+ const asset = this.manifest.assets[path];
29
29
  if (!asset) {
30
30
  throw new Error(`Server asset '${path}' does not exist.`);
31
31
  }
@@ -38,7 +38,7 @@ class ServerAssets {
38
38
  * @returns A boolean indicating whether the asset exists.
39
39
  */
40
40
  hasServerAsset(path) {
41
- return this.manifest.assets.has(path);
41
+ return !!this.manifest.assets[path];
42
42
  }
43
43
  /**
44
44
  * Retrieves the asset for 'index.server.html'.
@@ -183,6 +183,22 @@ function addLeadingSlash(url) {
183
183
  // Check if the URL already starts with a slash
184
184
  return url[0] === '/' ? url : `/${url}`;
185
185
  }
186
+ /**
187
+ * Adds a trailing slash to a URL if it does not already have one.
188
+ *
189
+ * @param url - The URL string to which the trailing slash will be added.
190
+ * @returns The URL string with a trailing slash.
191
+ *
192
+ * @example
193
+ * ```js
194
+ * addTrailingSlash('path'); // 'path/'
195
+ * addTrailingSlash('path/'); // 'path/'
196
+ * ```
197
+ */
198
+ function addTrailingSlash(url) {
199
+ // Check if the URL already end with a slash
200
+ return url[url.length - 1] === '/' ? url : `${url}/`;
201
+ }
186
202
  /**
187
203
  * Joins URL parts into a single URL string.
188
204
  *
@@ -245,6 +261,50 @@ function stripIndexHtmlFromURL(url) {
245
261
  }
246
262
  return url;
247
263
  }
264
+ /**
265
+ * Resolves `*` placeholders in a path template by mapping them to corresponding segments
266
+ * from a base path. This is useful for constructing paths dynamically based on a given base path.
267
+ *
268
+ * The function processes the `toPath` string, replacing each `*` placeholder with
269
+ * the corresponding segment from the `fromPath`. If the `toPath` contains no placeholders,
270
+ * it is returned as-is. Invalid `toPath` formats (not starting with `/`) will throw an error.
271
+ *
272
+ * @param toPath - A path template string that may contain `*` placeholders. Each `*` is replaced
273
+ * by the corresponding segment from the `fromPath`. Static paths (e.g., `/static/path`) are returned
274
+ * directly without placeholder replacement.
275
+ * @param fromPath - A base path string, split into segments, that provides values for
276
+ * replacing `*` placeholders in the `toPath`.
277
+ * @returns A resolved path string with `*` placeholders replaced by segments from the `fromPath`,
278
+ * or the `toPath` returned unchanged if it contains no placeholders.
279
+ *
280
+ * @throws If the `toPath` does not start with a `/`, indicating an invalid path format.
281
+ *
282
+ * @example
283
+ * ```typescript
284
+ * // Example with placeholders resolved
285
+ * const resolvedPath = buildPathWithParams('/*\/details', '/123/abc');
286
+ * console.log(resolvedPath); // Outputs: '/123/details'
287
+ *
288
+ * // Example with a static path
289
+ * const staticPath = buildPathWithParams('/static/path', '/base/unused');
290
+ * console.log(staticPath); // Outputs: '/static/path'
291
+ * ```
292
+ */
293
+ function buildPathWithParams(toPath, fromPath) {
294
+ if (toPath[0] !== '/') {
295
+ throw new Error(`Invalid toPath: The string must start with a '/'. Received: '${toPath}'`);
296
+ }
297
+ if (fromPath[0] !== '/') {
298
+ throw new Error(`Invalid fromPath: The string must start with a '/'. Received: '${fromPath}'`);
299
+ }
300
+ if (!toPath.includes('/*')) {
301
+ return toPath;
302
+ }
303
+ const fromPathParts = fromPath.split('/');
304
+ const toPathParts = toPath.split('/');
305
+ const resolvedParts = toPathParts.map((part, index) => toPathParts[index] === '*' ? fromPathParts[index] : part);
306
+ return joinUrlParts(...resolvedParts);
307
+ }
248
308
 
249
309
  /**
250
310
  * Renders an Angular application or module to an HTML string.
@@ -306,6 +366,39 @@ function isNgModule(value) {
306
366
  return 'ɵmod' in value;
307
367
  }
308
368
 
369
+ /**
370
+ * Creates a promise that resolves with the result of the provided `promise` or rejects with an
371
+ * `AbortError` if the `AbortSignal` is triggered before the promise resolves.
372
+ *
373
+ * @param promise - The promise to monitor for completion.
374
+ * @param signal - An `AbortSignal` used to monitor for an abort event. If the signal is aborted,
375
+ * the returned promise will reject.
376
+ * @param errorMessagePrefix - A custom message prefix to include in the error message when the operation is aborted.
377
+ * @returns A promise that either resolves with the value of the provided `promise` or rejects with
378
+ * an `AbortError` if the `AbortSignal` is triggered.
379
+ *
380
+ * @throws {AbortError} If the `AbortSignal` is triggered before the `promise` resolves.
381
+ */
382
+ function promiseWithAbort(promise, signal, errorMessagePrefix) {
383
+ return new Promise((resolve, reject) => {
384
+ const abortHandler = () => {
385
+ reject(new DOMException(`${errorMessagePrefix} was aborted.\n${signal.reason}`, 'AbortError'));
386
+ };
387
+ // Check for abort signal
388
+ if (signal.aborted) {
389
+ abortHandler();
390
+ return;
391
+ }
392
+ signal.addEventListener('abort', abortHandler, { once: true });
393
+ promise
394
+ .then(resolve)
395
+ .catch(reject)
396
+ .finally(() => {
397
+ signal.removeEventListener('abort', abortHandler);
398
+ });
399
+ });
400
+ }
401
+
309
402
  /**
310
403
  * Different rendering modes for server routes.
311
404
  * @see {@link provideServerRoutesConfig}
@@ -409,6 +502,7 @@ class RouteTree {
409
502
  insert(route, metadata) {
410
503
  let node = this.root;
411
504
  const segments = this.getPathSegments(route);
505
+ const normalizedSegments = [];
412
506
  for (const segment of segments) {
413
507
  // Replace parameterized segments (e.g., :id) with a wildcard (*) for matching
414
508
  const normalizedSegment = segment[0] === ':' ? '*' : segment;
@@ -418,11 +512,12 @@ class RouteTree {
418
512
  node.children.set(normalizedSegment, childNode);
419
513
  }
420
514
  node = childNode;
515
+ normalizedSegments.push(normalizedSegment);
421
516
  }
422
517
  // At the leaf node, store the full route and its associated metadata
423
518
  node.metadata = {
424
519
  ...metadata,
425
- route: segments.join('/'),
520
+ route: normalizedSegments.join('/'),
426
521
  };
427
522
  node.insertionIndex = this.insertionIndexCounter++;
428
523
  }
@@ -601,12 +696,18 @@ async function* traverseRoutesConfig(options) {
601
696
  const metadata = {
602
697
  renderMode: RenderMode.Prerender,
603
698
  ...matchedMetaData,
604
- route: currentRoutePath,
699
+ // Match Angular router behavior
700
+ // ['one', 'two', ''] -> 'one/two/'
701
+ // ['one', 'two', 'three'] -> 'one/two/three'
702
+ route: path === '' ? addTrailingSlash(currentRoutePath) : currentRoutePath,
605
703
  };
606
704
  delete metadata.presentInClientRouter;
607
- // Handle redirects
608
- if (typeof redirectTo === 'string') {
609
- const redirectToResolved = resolveRedirectTo(currentRoutePath, redirectTo);
705
+ if (metadata.renderMode === RenderMode.Prerender) {
706
+ // Handle SSG routes
707
+ yield* handleSSGRoute(typeof redirectTo === 'string' ? redirectTo : undefined, metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes);
708
+ }
709
+ else if (typeof redirectTo === 'string') {
710
+ // Handle redirects
610
711
  if (metadata.status && !VALID_REDIRECT_RESPONSE_CODES.has(metadata.status)) {
611
712
  yield {
612
713
  error: `The '${metadata.status}' status code is not a valid redirect response code. ` +
@@ -614,11 +715,7 @@ async function* traverseRoutesConfig(options) {
614
715
  };
615
716
  continue;
616
717
  }
617
- yield { ...metadata, redirectTo: redirectToResolved };
618
- }
619
- else if (metadata.renderMode === RenderMode.Prerender) {
620
- // Handle SSG routes
621
- yield* handleSSGRoute(metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes);
718
+ yield { ...metadata, redirectTo: resolveRedirectTo(metadata.route, redirectTo) };
622
719
  }
623
720
  else {
624
721
  yield metadata;
@@ -656,13 +753,14 @@ async function* traverseRoutesConfig(options) {
656
753
  * Handles SSG (Static Site Generation) routes by invoking `getPrerenderParams` and yielding
657
754
  * all parameterized paths, returning any errors encountered.
658
755
  *
756
+ * @param redirectTo - Optional path to redirect to, if specified.
659
757
  * @param metadata - The metadata associated with the route tree node.
660
758
  * @param parentInjector - The dependency injection container for the parent route.
661
759
  * @param invokeGetPrerenderParams - A flag indicating whether to invoke the `getPrerenderParams` function.
662
760
  * @param includePrerenderFallbackRoutes - A flag indicating whether to include fallback routes in the result.
663
761
  * @returns An async iterable iterator that yields route tree node metadata for each SSG path or errors.
664
762
  */
665
- async function* handleSSGRoute(metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes) {
763
+ async function* handleSSGRoute(redirectTo, metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes) {
666
764
  if (metadata.renderMode !== RenderMode.Prerender) {
667
765
  throw new Error(`'handleSSGRoute' was called for a route which rendering mode is not prerender.`);
668
766
  }
@@ -671,6 +769,9 @@ async function* handleSSGRoute(metadata, parentInjector, invokeGetPrerenderParam
671
769
  if ('getPrerenderParams' in meta) {
672
770
  delete meta['getPrerenderParams'];
673
771
  }
772
+ if (redirectTo !== undefined) {
773
+ meta.redirectTo = resolveRedirectTo(currentRoutePath, redirectTo);
774
+ }
674
775
  if (!URL_PARAMETER_REGEXP.test(currentRoutePath)) {
675
776
  // Route has no parameters
676
777
  yield {
@@ -702,7 +803,13 @@ async function* handleSSGRoute(metadata, parentInjector, invokeGetPrerenderParam
702
803
  }
703
804
  return value;
704
805
  });
705
- yield { ...meta, route: routeWithResolvedParams };
806
+ yield {
807
+ ...meta,
808
+ route: routeWithResolvedParams,
809
+ redirectTo: redirectTo === undefined
810
+ ? undefined
811
+ : resolveRedirectTo(routeWithResolvedParams, redirectTo),
812
+ };
706
813
  }
707
814
  }
708
815
  catch (error) {
@@ -737,7 +844,7 @@ function resolveRedirectTo(routePath, redirectTo) {
737
844
  return redirectTo;
738
845
  }
739
846
  // Resolve relative redirectTo based on the current route path.
740
- const segments = routePath.split('/');
847
+ const segments = routePath.replace(URL_PARAMETER_REGEXP, '*').split('/');
741
848
  segments.pop(); // Remove the last segment to make it relative.
742
849
  return joinUrlParts(...segments, redirectTo);
743
850
  }
@@ -812,7 +919,7 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke
812
919
  applicationRef = await bootstrap();
813
920
  }
814
921
  // Wait until the application is stable.
815
- await _whenStable(applicationRef);
922
+ await applicationRef.whenStable();
816
923
  const injector = applicationRef.injector;
817
924
  const router = injector.get(Router);
818
925
  const routesResults = [];
@@ -848,7 +955,6 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke
848
955
  invokeGetPrerenderParams,
849
956
  includePrerenderFallbackRoutes,
850
957
  });
851
- let seenAppShellRoute;
852
958
  for await (const result of traverseRoutes) {
853
959
  if ('error' in result) {
854
960
  errors.push(result.error);
@@ -896,39 +1002,53 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke
896
1002
  * Asynchronously extracts routes from the Angular application configuration
897
1003
  * and creates a `RouteTree` to manage server-side routing.
898
1004
  *
899
- * @param url - The URL for server-side rendering. The URL is used to configure `ServerPlatformLocation`. This configuration is crucial
900
- * for ensuring that API requests for relative paths succeed, which is essential for accurate route extraction.
901
- * See:
902
- * - https://github.com/angular/angular/blob/d608b857c689d17a7ffa33bbb510301014d24a17/packages/platform-server/src/location.ts#L51
903
- * - https://github.com/angular/angular/blob/6882cc7d9eed26d3caeedca027452367ba25f2b9/packages/platform-server/src/http.ts#L44
904
- * @param manifest - An optional `AngularAppManifest` that contains the application's routing and configuration details.
905
- * If not provided, the default manifest is retrieved using `getAngularAppManifest()`.
906
- * @param invokeGetPrerenderParams - A boolean flag indicating whether to invoke `getPrerenderParams` for parameterized SSG routes
907
- * to handle prerendering paths. Defaults to `false`.
908
- * @param includePrerenderFallbackRoutes - A flag indicating whether to include fallback routes in the result. Defaults to `true`.
1005
+ * @param options - An object containing the following options:
1006
+ * - `url`: The URL for server-side rendering. The URL is used to configure `ServerPlatformLocation`. This configuration is crucial
1007
+ * for ensuring that API requests for relative paths succeed, which is essential for accurate route extraction.
1008
+ * See:
1009
+ * - https://github.com/angular/angular/blob/d608b857c689d17a7ffa33bbb510301014d24a17/packages/platform-server/src/location.ts#L51
1010
+ * - https://github.com/angular/angular/blob/6882cc7d9eed26d3caeedca027452367ba25f2b9/packages/platform-server/src/http.ts#L44
1011
+ * - `manifest`: An optional `AngularAppManifest` that contains the application's routing and configuration details.
1012
+ * If not provided, the default manifest is retrieved using `getAngularAppManifest()`.
1013
+ * - `invokeGetPrerenderParams`: A boolean flag indicating whether to invoke `getPrerenderParams` for parameterized SSG routes
1014
+ * to handle prerendering paths. Defaults to `false`.
1015
+ * - `includePrerenderFallbackRoutes`: A flag indicating whether to include fallback routes in the result. Defaults to `true`.
1016
+ * - `signal`: An optional `AbortSignal` that can be used to abort the operation.
909
1017
  *
910
1018
  * @returns A promise that resolves to an object containing:
911
1019
  * - `routeTree`: A populated `RouteTree` containing all extracted routes from the Angular application.
912
1020
  * - `appShellRoute`: The specified route for the app-shell, if configured.
913
1021
  * - `errors`: An array of strings representing any errors encountered during the route extraction process.
914
1022
  */
915
- async function extractRoutesAndCreateRouteTree(url, manifest = getAngularAppManifest(), invokeGetPrerenderParams = false, includePrerenderFallbackRoutes = true) {
916
- const routeTree = new RouteTree();
917
- const document = await new ServerAssets(manifest).getIndexServerHtml().text();
918
- const bootstrap = await manifest.bootstrap();
919
- const { baseHref, appShellRoute, routes, errors } = await getRoutesFromAngularRouterConfig(bootstrap, document, url, invokeGetPrerenderParams, includePrerenderFallbackRoutes);
920
- for (const { route, ...metadata } of routes) {
921
- if (metadata.redirectTo !== undefined) {
922
- metadata.redirectTo = joinUrlParts(baseHref, metadata.redirectTo);
923
- }
924
- const fullRoute = joinUrlParts(baseHref, route);
925
- routeTree.insert(fullRoute, metadata);
926
- }
927
- return {
928
- appShellRoute,
929
- routeTree,
930
- errors,
931
- };
1023
+ function extractRoutesAndCreateRouteTree(options) {
1024
+ const { url, manifest = getAngularAppManifest(), invokeGetPrerenderParams = false, includePrerenderFallbackRoutes = true, signal, } = options;
1025
+ async function extract() {
1026
+ const routeTree = new RouteTree();
1027
+ const document = await new ServerAssets(manifest).getIndexServerHtml().text();
1028
+ const bootstrap = await manifest.bootstrap();
1029
+ const { baseHref, appShellRoute, routes, errors } = await getRoutesFromAngularRouterConfig(bootstrap, document, url, invokeGetPrerenderParams, includePrerenderFallbackRoutes);
1030
+ for (const { route, ...metadata } of routes) {
1031
+ if (metadata.redirectTo !== undefined) {
1032
+ metadata.redirectTo = joinUrlParts(baseHref, metadata.redirectTo);
1033
+ }
1034
+ // Remove undefined fields
1035
+ // Helps avoid unnecessary test updates
1036
+ for (const [key, value] of Object.entries(metadata)) {
1037
+ if (value === undefined) {
1038
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1039
+ delete metadata[key];
1040
+ }
1041
+ }
1042
+ const fullRoute = joinUrlParts(baseHref, route);
1043
+ routeTree.insert(fullRoute, metadata);
1044
+ }
1045
+ return {
1046
+ appShellRoute,
1047
+ routeTree,
1048
+ errors,
1049
+ };
1050
+ }
1051
+ return signal ? promiseWithAbort(extract(), signal, 'Routes extraction') : extract();
932
1052
  }
933
1053
 
934
1054
  /**
@@ -1061,7 +1181,7 @@ class ServerRouter {
1061
1181
  }
1062
1182
  // Create and store a new promise for the build process.
1063
1183
  // This prevents concurrent builds by re-using the same promise.
1064
- ServerRouter.#extractionPromise ??= extractRoutesAndCreateRouteTree(url, manifest)
1184
+ ServerRouter.#extractionPromise ??= extractRoutesAndCreateRouteTree({ url, manifest })
1065
1185
  .then(({ routeTree, errors }) => {
1066
1186
  if (errors.length > 0) {
1067
1187
  throw new Error('Error(s) occurred while extracting routes:\n' +
@@ -1493,11 +1613,12 @@ class AngularServerApp {
1493
1613
  }
1494
1614
  const { redirectTo, status, renderMode } = matchedRoute;
1495
1615
  if (redirectTo !== undefined) {
1616
+ return Response.redirect(new URL(buildPathWithParams(redirectTo, url.pathname), url),
1496
1617
  // Note: The status code is validated during route extraction.
1497
1618
  // 302 Found is used by default for redirections
1498
1619
  // See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status
1499
1620
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1500
- return Response.redirect(new URL(redirectTo, url), status ?? 302);
1621
+ status ?? 302);
1501
1622
  }
1502
1623
  if (renderMode === RenderMode.Prerender) {
1503
1624
  const response = await this.handleServe(request, matchedRoute);
@@ -1505,10 +1626,7 @@ class AngularServerApp {
1505
1626
  return response;
1506
1627
  }
1507
1628
  }
1508
- return Promise.race([
1509
- this.waitForRequestAbort(request),
1510
- this.handleRendering(request, matchedRoute, requestContext),
1511
- ]);
1629
+ return promiseWithAbort(this.handleRendering(request, matchedRoute, requestContext), request.signal, `Request for: ${request.url}`);
1512
1630
  }
1513
1631
  /**
1514
1632
  * Handles serving a prerendered static asset if available for the matched route.
@@ -1529,8 +1647,7 @@ class AngularServerApp {
1529
1647
  if (method !== 'GET' && method !== 'HEAD') {
1530
1648
  return null;
1531
1649
  }
1532
- const { pathname } = stripIndexHtmlFromURL(new URL(request.url));
1533
- const assetPath = stripLeadingSlash(joinUrlParts(pathname, 'index.html'));
1650
+ const assetPath = this.buildServerAssetPathFromRequest(request);
1534
1651
  if (!this.assets.hasServerAsset(assetPath)) {
1535
1652
  return null;
1536
1653
  }
@@ -1559,18 +1676,11 @@ class AngularServerApp {
1559
1676
  * @returns A promise that resolves to the rendered response, or null if no matching route is found.
1560
1677
  */
1561
1678
  async handleRendering(request, matchedRoute, requestContext) {
1562
- const { redirectTo, status } = matchedRoute;
1563
- if (redirectTo !== undefined) {
1564
- // Note: The status code is validated during route extraction.
1565
- // 302 Found is used by default for redirections
1566
- // See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status
1567
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1568
- return Response.redirect(new URL(redirectTo, new URL(request.url)), status ?? 302);
1569
- }
1570
- const { renderMode, headers } = matchedRoute;
1679
+ const { renderMode, headers, status } = matchedRoute;
1571
1680
  if (!this.allowStaticRouteRender && renderMode === RenderMode.Prerender) {
1572
1681
  return null;
1573
1682
  }
1683
+ const url = new URL(request.url);
1574
1684
  const platformProviders = [];
1575
1685
  // Initialize the response with status and headers if available.
1576
1686
  const responseInit = {
@@ -1595,7 +1705,9 @@ class AngularServerApp {
1595
1705
  }
1596
1706
  else if (renderMode === RenderMode.Client) {
1597
1707
  // Serve the client-side rendered version if the route is configured for CSR.
1598
- return new Response(await this.assets.getServerAsset('index.csr.html').text(), responseInit);
1708
+ let html = await this.assets.getServerAsset('index.csr.html').text();
1709
+ html = await this.runTransformsOnHtml(html, url);
1710
+ return new Response(html, responseInit);
1599
1711
  }
1600
1712
  const { manifest: { bootstrap, inlineCriticalCss, locale }, hooks, assets, } = this;
1601
1713
  if (locale !== undefined) {
@@ -1604,13 +1716,9 @@ class AngularServerApp {
1604
1716
  useValue: locale,
1605
1717
  });
1606
1718
  }
1607
- const url = new URL(request.url);
1608
- let html = await assets.getIndexServerHtml().text();
1609
- // Skip extra microtask if there are no pre hooks.
1610
- if (hooks.has('html:transform:pre')) {
1611
- html = await hooks.run('html:transform:pre', { html, url });
1612
- }
1613
1719
  this.boostrap ??= await bootstrap();
1720
+ let html = await assets.getIndexServerHtml().text();
1721
+ html = await this.runTransformsOnHtml(html, url);
1614
1722
  html = await renderAngular(html, this.boostrap, url, platformProviders, SERVER_CONTEXT_VALUE[renderMode]);
1615
1723
  if (inlineCriticalCss) {
1616
1724
  // Optionally inline critical CSS.
@@ -1642,20 +1750,41 @@ class AngularServerApp {
1642
1750
  return new Response(html, responseInit);
1643
1751
  }
1644
1752
  /**
1645
- * Returns a promise that rejects if the request is aborted.
1753
+ * Constructs the asset path on the server based on the provided HTTP request.
1646
1754
  *
1647
- * @param request - The HTTP request object being monitored for abortion.
1648
- * @returns A promise that never resolves and rejects with an `AbortError`
1649
- * if the request is aborted.
1650
- */
1651
- waitForRequestAbort(request) {
1652
- return new Promise((_, reject) => {
1653
- request.signal.addEventListener('abort', () => {
1654
- const abortError = new Error(`Request for: ${request.url} was aborted.\n${request.signal.reason}`);
1655
- abortError.name = 'AbortError';
1656
- reject(abortError);
1657
- }, { once: true });
1658
- });
1755
+ * This method processes the incoming request URL to derive a path corresponding
1756
+ * to the requested asset. It ensures the path points to the correct file (e.g.,
1757
+ * `index.html`) and removes any base href if it is not part of the asset path.
1758
+ *
1759
+ * @param request - The incoming HTTP request object.
1760
+ * @returns The server-relative asset path derived from the request.
1761
+ */
1762
+ buildServerAssetPathFromRequest(request) {
1763
+ let { pathname: assetPath } = new URL(request.url);
1764
+ if (!assetPath.endsWith('/index.html')) {
1765
+ // Append "index.html" to build the default asset path.
1766
+ assetPath = joinUrlParts(assetPath, 'index.html');
1767
+ }
1768
+ const { baseHref } = this.manifest;
1769
+ // Check if the asset path starts with the base href and the base href is not (`/` or ``).
1770
+ if (baseHref.length > 1 && assetPath.startsWith(baseHref)) {
1771
+ // Remove the base href from the start of the asset path to align with server-asset expectations.
1772
+ assetPath = assetPath.slice(baseHref.length);
1773
+ }
1774
+ return stripLeadingSlash(assetPath);
1775
+ }
1776
+ /**
1777
+ * Runs the registered transform hooks on the given HTML content.
1778
+ *
1779
+ * @param html - The raw HTML content to be transformed.
1780
+ * @param url - The URL associated with the HTML content, used for context during transformations.
1781
+ * @returns A promise that resolves to the transformed HTML string.
1782
+ */
1783
+ async runTransformsOnHtml(html, url) {
1784
+ if (this.hooks.has('html:transform:pre')) {
1785
+ html = await this.hooks.run('html:transform:pre', { html, url });
1786
+ }
1787
+ return html;
1659
1788
  }
1660
1789
  }
1661
1790
  let angularServerApp;
@@ -1757,6 +1886,10 @@ class AngularAppEngine {
1757
1886
  * The manifest for the server application.
1758
1887
  */
1759
1888
  manifest = getAngularAppEngineManifest();
1889
+ /**
1890
+ * The number of entry points available in the server application's manifest.
1891
+ */
1892
+ entryPointsCount = Object.keys(this.manifest.entryPoints).length;
1760
1893
  /**
1761
1894
  * A cache that holds entry points, keyed by their potential locale string.
1762
1895
  */
@@ -1815,7 +1948,7 @@ class AngularAppEngine {
1815
1948
  return cachedEntryPoint;
1816
1949
  }
1817
1950
  const { entryPoints } = this.manifest;
1818
- const entryPoint = entryPoints.get(potentialLocale);
1951
+ const entryPoint = entryPoints[potentialLocale];
1819
1952
  if (!entryPoint) {
1820
1953
  return undefined;
1821
1954
  }
@@ -1835,8 +1968,8 @@ class AngularAppEngine {
1835
1968
  * @returns A promise that resolves to the entry point exports or `undefined` if not found.
1836
1969
  */
1837
1970
  getEntryPointExportsForUrl(url) {
1838
- const { entryPoints, basePath } = this.manifest;
1839
- if (entryPoints.size === 1) {
1971
+ const { basePath } = this.manifest;
1972
+ if (this.entryPointsCount === 1) {
1840
1973
  return this.getEntryPointExports('');
1841
1974
  }
1842
1975
  const potentialLocale = getPotentialLocaleIdFromUrl(url, basePath);