@angular/ssr 19.0.0-next.7 → 19.0.0-next.9

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, createPlatformFactory, platformCore, ApplicationRef, ɵwhenStable as _whenStable, Compiler, ɵresetCompiledComponents as _resetCompiledComponents } from '@angular/core';
2
+ import { ɵConsole as _Console, InjectionToken, makeEnvironmentProviders, runInInjectionContext, createPlatformFactory, platformCore, ApplicationRef, ɵwhenStable as _whenStable, Compiler, LOCALE_ID, ɵresetCompiledComponents as _resetCompiledComponents } from '@angular/core';
3
3
  import { ɵSERVER_CONTEXT as _SERVER_CONTEXT, renderModule, renderApplication, INITIAL_CONFIG, ɵINTERNAL_SERVER_PLATFORM_PROVIDERS as _INTERNAL_SERVER_PLATFORM_PROVIDERS } from '@angular/platform-server';
4
4
  import { ɵloadChildren as _loadChildren, Router } from '@angular/router';
5
5
  import Critters from '../third_party/critters/index.js';
@@ -573,16 +573,18 @@ async function* traverseRoutesConfig(options) {
573
573
  matchedMetaData = serverConfigRouteTree.match(currentRoutePath);
574
574
  if (!matchedMetaData) {
575
575
  yield {
576
- error: `The '${currentRoutePath}' route does not match any route defined in the server routing configuration. ` +
576
+ error: `The '${stripLeadingSlash(currentRoutePath)}' route does not match any route defined in the server routing configuration. ` +
577
577
  'Please ensure this route is added to the server routing configuration.',
578
578
  };
579
579
  continue;
580
580
  }
581
+ matchedMetaData.presentInClientRouter = true;
581
582
  }
582
583
  const metadata = {
583
584
  ...matchedMetaData,
584
585
  route: currentRoutePath,
585
586
  };
587
+ delete metadata.presentInClientRouter;
586
588
  // Handle redirects
587
589
  if (typeof redirectTo === 'string') {
588
590
  const redirectToResolved = resolveRedirectTo(currentRoutePath, redirectTo);
@@ -625,7 +627,9 @@ async function* traverseRoutesConfig(options) {
625
627
  }
626
628
  }
627
629
  catch (error) {
628
- yield { error: `Error processing route '${route.path}': ${error.message}` };
630
+ yield {
631
+ error: `Error processing route '${stripLeadingSlash(route.path ?? '')}': ${error.message}`,
632
+ };
629
633
  }
630
634
  }
631
635
  }
@@ -659,8 +663,8 @@ async function* handleSSGRoute(metadata, parentInjector, invokeGetPrerenderParam
659
663
  if (invokeGetPrerenderParams) {
660
664
  if (!getPrerenderParams) {
661
665
  yield {
662
- error: `The '${currentRoutePath}' route uses prerendering and includes parameters, but 'getPrerenderParams' is missing. ` +
663
- `Please define 'getPrerenderParams' function for this route in your server routing configuration ` +
666
+ error: `The '${stripLeadingSlash(currentRoutePath)}' route uses prerendering and includes parameters, but 'getPrerenderParams' ` +
667
+ `is missing. Please define 'getPrerenderParams' function for this route in your server routing configuration ` +
664
668
  `or specify a different 'renderMode'.`,
665
669
  };
666
670
  return;
@@ -672,7 +676,7 @@ async function* handleSSGRoute(metadata, parentInjector, invokeGetPrerenderParam
672
676
  const parameterName = match.slice(1);
673
677
  const value = params[parameterName];
674
678
  if (typeof value !== 'string') {
675
- throw new Error(`The 'getPrerenderParams' function defined for the '${currentRoutePath}' route ` +
679
+ throw new Error(`The 'getPrerenderParams' function defined for the '${stripLeadingSlash(currentRoutePath)}' route ` +
676
680
  `returned a non-string value for parameter '${parameterName}'. ` +
677
681
  `Please make sure the 'getPrerenderParams' function returns values for all parameters ` +
678
682
  'specified in this route.');
@@ -816,14 +820,34 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke
816
820
  invokeGetPrerenderParams,
817
821
  includePrerenderFallbackRoutes,
818
822
  });
823
+ let seenAppShellRoute;
819
824
  for await (const result of traverseRoutes) {
820
825
  if ('error' in result) {
821
826
  errors.push(result.error);
822
827
  }
823
828
  else {
829
+ if (result.renderMode === RenderMode.AppShell) {
830
+ if (seenAppShellRoute !== undefined) {
831
+ errors.push(`Error: Both '${seenAppShellRoute}' and '${stripLeadingSlash(result.route)}' routes have ` +
832
+ `their 'renderMode' set to 'AppShell'. AppShell renderMode should only be assigned to one route. ` +
833
+ `Please review your route configurations to ensure that only one route is set to 'RenderMode.AppShell'.`);
834
+ }
835
+ seenAppShellRoute = stripLeadingSlash(result.route);
836
+ }
824
837
  routesResults.push(result);
825
838
  }
826
839
  }
840
+ if (serverConfigRouteTree) {
841
+ for (const { route, presentInClientRouter } of serverConfigRouteTree.traverse()) {
842
+ if (presentInClientRouter || route === '**') {
843
+ // Skip if matched or it's the catch-all route.
844
+ continue;
845
+ }
846
+ errors.push(`The '${route}' server route does not match any routes defined in the Angular ` +
847
+ `routing configuration (typically provided as a part of the 'provideRouter' call). ` +
848
+ 'Please make sure that the mentioned server route is present in the Angular routing configuration.');
849
+ }
850
+ }
827
851
  }
828
852
  else {
829
853
  routesResults.push({ route: '', renderMode: RenderMode.Prerender });
@@ -1039,17 +1063,43 @@ class ServerRouter {
1039
1063
 
1040
1064
  /**
1041
1065
  * Injection token for the current request.
1066
+ * @developerPreview
1042
1067
  */
1043
1068
  const REQUEST = new InjectionToken('REQUEST');
1044
1069
  /**
1045
1070
  * Injection token for the response initialization options.
1071
+ * @developerPreview
1046
1072
  */
1047
1073
  const RESPONSE_INIT = new InjectionToken('RESPONSE_INIT');
1048
1074
  /**
1049
1075
  * Injection token for additional request context.
1076
+ * @developerPreview
1050
1077
  */
1051
1078
  const REQUEST_CONTEXT = new InjectionToken('REQUEST_CONTEXT');
1052
1079
 
1080
+ /**
1081
+ * Generates a SHA-256 hash of the provided string.
1082
+ *
1083
+ * @param data - The input string to be hashed.
1084
+ * @returns A promise that resolves to the SHA-256 hash of the input,
1085
+ * represented as a hexadecimal string.
1086
+ */
1087
+ async function sha256(data) {
1088
+ if (typeof crypto === 'undefined') {
1089
+ // TODO(alanagius): remove once Node.js version 18 is no longer supported.
1090
+ throw new Error(`The global 'crypto' module is unavailable. ` +
1091
+ `If you are running on Node.js, please ensure you are using version 20 or later, ` +
1092
+ `which includes built-in support for the Web Crypto module.`);
1093
+ }
1094
+ const encodedData = new TextEncoder().encode(data);
1095
+ const hashBuffer = await crypto.subtle.digest('SHA-256', encodedData);
1096
+ const hashParts = [];
1097
+ for (const h of new Uint8Array(hashBuffer)) {
1098
+ hashParts.push(h.toString(16).padStart(2, '0'));
1099
+ }
1100
+ return hashParts.join('');
1101
+ }
1102
+
1053
1103
  /**
1054
1104
  * Pattern used to extract the media query set by Critters in an `onload` handler.
1055
1105
  */
@@ -1207,6 +1257,126 @@ class InlineCriticalCssProcessor extends CrittersBase {
1207
1257
  }
1208
1258
  }
1209
1259
 
1260
+ /**
1261
+ * A Least Recently Used (LRU) cache implementation.
1262
+ *
1263
+ * This cache stores a fixed number of key-value pairs, and when the cache exceeds its capacity,
1264
+ * the least recently accessed items are evicted.
1265
+ *
1266
+ * @template Key - The type of the cache keys.
1267
+ * @template Value - The type of the cache values.
1268
+ */
1269
+ class LRUCache {
1270
+ /**
1271
+ * Creates a new LRUCache instance.
1272
+ * @param capacity The maximum number of items the cache can hold.
1273
+ */
1274
+ constructor(capacity) {
1275
+ /**
1276
+ * Internal storage for the cache, mapping keys to their associated nodes in the linked list.
1277
+ */
1278
+ this.cache = new Map();
1279
+ this.capacity = capacity;
1280
+ }
1281
+ /**
1282
+ * Gets the value associated with the given key.
1283
+ * @param key The key to retrieve the value for.
1284
+ * @returns The value associated with the key, or undefined if the key is not found.
1285
+ */
1286
+ get(key) {
1287
+ const node = this.cache.get(key);
1288
+ if (node) {
1289
+ this.moveToHead(node);
1290
+ return node.value;
1291
+ }
1292
+ return undefined;
1293
+ }
1294
+ /**
1295
+ * Puts a key-value pair into the cache.
1296
+ * If the key already exists, the value is updated.
1297
+ * If the cache is full, the least recently used item is evicted.
1298
+ * @param key The key to insert or update.
1299
+ * @param value The value to associate with the key.
1300
+ */
1301
+ put(key, value) {
1302
+ const cachedNode = this.cache.get(key);
1303
+ if (cachedNode) {
1304
+ // Update existing node
1305
+ cachedNode.value = value;
1306
+ this.moveToHead(cachedNode);
1307
+ return;
1308
+ }
1309
+ // Create a new node
1310
+ const newNode = { key, value, prev: undefined, next: undefined };
1311
+ this.cache.set(key, newNode);
1312
+ this.addToHead(newNode);
1313
+ if (this.cache.size > this.capacity) {
1314
+ // Evict the LRU item
1315
+ const tail = this.removeTail();
1316
+ if (tail) {
1317
+ this.cache.delete(tail.key);
1318
+ }
1319
+ }
1320
+ }
1321
+ /**
1322
+ * Adds a node to the head of the linked list.
1323
+ * @param node The node to add.
1324
+ */
1325
+ addToHead(node) {
1326
+ node.next = this.head;
1327
+ node.prev = undefined;
1328
+ if (this.head) {
1329
+ this.head.prev = node;
1330
+ }
1331
+ this.head = node;
1332
+ if (!this.tail) {
1333
+ this.tail = node;
1334
+ }
1335
+ }
1336
+ /**
1337
+ * Removes a node from the linked list.
1338
+ * @param node The node to remove.
1339
+ */
1340
+ removeNode(node) {
1341
+ if (node.prev) {
1342
+ node.prev.next = node.next;
1343
+ }
1344
+ else {
1345
+ this.head = node.next;
1346
+ }
1347
+ if (node.next) {
1348
+ node.next.prev = node.prev;
1349
+ }
1350
+ else {
1351
+ this.tail = node.prev;
1352
+ }
1353
+ }
1354
+ /**
1355
+ * Moves a node to the head of the linked list.
1356
+ * @param node The node to move.
1357
+ */
1358
+ moveToHead(node) {
1359
+ this.removeNode(node);
1360
+ this.addToHead(node);
1361
+ }
1362
+ /**
1363
+ * Removes the tail node from the linked list.
1364
+ * @returns The removed tail node, or undefined if the list is empty.
1365
+ */
1366
+ removeTail() {
1367
+ const node = this.tail;
1368
+ if (node) {
1369
+ this.removeNode(node);
1370
+ }
1371
+ return node;
1372
+ }
1373
+ }
1374
+
1375
+ /**
1376
+ * Maximum number of critical CSS entries the cache can store.
1377
+ * This value determines the capacity of the LRU (Least Recently Used) cache, which stores critical CSS for pages.
1378
+ */
1379
+ const MAX_INLINE_CSS_CACHE_ENTRIES = 50;
1210
1380
  /**
1211
1381
  * A mapping of `RenderMode` enum values to corresponding string representations.
1212
1382
  *
@@ -1245,6 +1415,14 @@ class AngularServerApp {
1245
1415
  * An instance of ServerAsset that handles server-side asset.
1246
1416
  */
1247
1417
  this.assets = new ServerAssets(this.manifest);
1418
+ /**
1419
+ * Cache for storing critical CSS for pages.
1420
+ * Stores a maximum of MAX_INLINE_CSS_CACHE_ENTRIES entries.
1421
+ *
1422
+ * Uses an LRU (Least Recently Used) eviction policy, meaning that when the cache is full,
1423
+ * the least recently accessed page's critical CSS will be removed to make space for new entries.
1424
+ */
1425
+ this.criticalCssLRUCache = new LRUCache(MAX_INLINE_CSS_CACHE_ENTRIES);
1248
1426
  }
1249
1427
  /**
1250
1428
  * Renders a response for the given HTTP request using the server application.
@@ -1326,38 +1504,62 @@ class AngularServerApp {
1326
1504
  // Initialize the response with status and headers if available.
1327
1505
  responseInit = {
1328
1506
  status,
1329
- headers: headers ? new Headers(headers) : undefined,
1507
+ headers: new Headers({
1508
+ 'Content-Type': 'text/html;charset=UTF-8',
1509
+ ...headers,
1510
+ }),
1330
1511
  };
1331
- if (renderMode === RenderMode.Client) {
1512
+ if (renderMode === RenderMode.Server) {
1513
+ // Configure platform providers for request and response only for SSR.
1514
+ platformProviders.push({
1515
+ provide: REQUEST,
1516
+ useValue: request,
1517
+ }, {
1518
+ provide: REQUEST_CONTEXT,
1519
+ useValue: requestContext,
1520
+ }, {
1521
+ provide: RESPONSE_INIT,
1522
+ useValue: responseInit,
1523
+ });
1524
+ }
1525
+ else if (renderMode === RenderMode.Client) {
1332
1526
  // Serve the client-side rendered version if the route is configured for CSR.
1333
1527
  return new Response(await this.assets.getServerAsset('index.csr.html'), responseInit);
1334
1528
  }
1529
+ }
1530
+ const { manifest: { bootstrap, inlineCriticalCss, locale }, hooks, assets, } = this;
1531
+ if (locale !== undefined) {
1335
1532
  platformProviders.push({
1336
- provide: REQUEST,
1337
- useValue: request,
1338
- }, {
1339
- provide: REQUEST_CONTEXT,
1340
- useValue: requestContext,
1341
- }, {
1342
- provide: RESPONSE_INIT,
1343
- useValue: responseInit,
1533
+ provide: LOCALE_ID,
1534
+ useValue: locale,
1344
1535
  });
1345
1536
  }
1346
- const { manifest, hooks, assets } = this;
1347
1537
  let html = await assets.getIndexServerHtml();
1348
1538
  // Skip extra microtask if there are no pre hooks.
1349
1539
  if (hooks.has('html:transform:pre')) {
1350
- html = await hooks.run('html:transform:pre', { html });
1540
+ html = await hooks.run('html:transform:pre', { html, url });
1351
1541
  }
1352
- this.boostrap ??= await manifest.bootstrap();
1353
- html = await renderAngular(html, this.boostrap, new URL(request.url), platformProviders, SERVER_CONTEXT_VALUE[renderMode]);
1354
- if (manifest.inlineCriticalCss) {
1542
+ this.boostrap ??= await bootstrap();
1543
+ html = await renderAngular(html, this.boostrap, url, platformProviders, SERVER_CONTEXT_VALUE[renderMode]);
1544
+ if (inlineCriticalCss) {
1355
1545
  // Optionally inline critical CSS.
1356
1546
  this.inlineCriticalCssProcessor ??= new InlineCriticalCssProcessor((path) => {
1357
1547
  const fileName = path.split('/').pop() ?? path;
1358
1548
  return this.assets.getServerAsset(fileName);
1359
1549
  });
1360
- html = await this.inlineCriticalCssProcessor.process(html);
1550
+ if (isSsrMode) {
1551
+ // Only cache if we are running in SSR Mode.
1552
+ const cacheKey = await sha256(html);
1553
+ let htmlWithCriticalCss = this.criticalCssLRUCache.get(cacheKey);
1554
+ if (htmlWithCriticalCss === undefined) {
1555
+ htmlWithCriticalCss = await this.inlineCriticalCssProcessor.process(html);
1556
+ this.criticalCssLRUCache.put(cacheKey, htmlWithCriticalCss);
1557
+ }
1558
+ html = htmlWithCriticalCss;
1559
+ }
1560
+ else {
1561
+ html = await this.inlineCriticalCssProcessor.process(html);
1562
+ }
1361
1563
  }
1362
1564
  return new Response(html, responseInit);
1363
1565
  }
@@ -1442,6 +1644,10 @@ class AngularAppEngine {
1442
1644
  * The manifest for the server application.
1443
1645
  */
1444
1646
  this.manifest = getAngularAppEngineManifest();
1647
+ /**
1648
+ * A cache that holds entry points, keyed by their potential locale string.
1649
+ */
1650
+ this.entryPointsCache = new Map();
1445
1651
  }
1446
1652
  /**
1447
1653
  * Hooks for extending or modifying the behavior of the server application.
@@ -1478,11 +1684,11 @@ class AngularAppEngine {
1478
1684
  async render(request, requestContext) {
1479
1685
  // Skip if the request looks like a file but not `/index.html`.
1480
1686
  const url = new URL(request.url);
1481
- const entryPoint = this.getEntryPointFromUrl(url);
1687
+ const entryPoint = await this.getEntryPointExportsForUrl(url);
1482
1688
  if (!entryPoint) {
1483
1689
  return null;
1484
1690
  }
1485
- const { ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp } = await entryPoint();
1691
+ const { ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp } = entryPoint;
1486
1692
  // Note: Using `instanceof` is not feasible here because `AngularServerApp` will
1487
1693
  // be located in separate bundles, making `instanceof` checks unreliable.
1488
1694
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
@@ -1490,25 +1696,6 @@ class AngularAppEngine {
1490
1696
  serverApp.hooks = this.hooks;
1491
1697
  return serverApp.render(request, requestContext);
1492
1698
  }
1493
- /**
1494
- * Retrieves the entry point path and locale for the Angular server application based on the provided URL.
1495
- *
1496
- * This method determines the appropriate entry point and locale for rendering the application by examining the URL.
1497
- * If there is only one entry point available, it is returned regardless of the URL.
1498
- * Otherwise, the method extracts a potential locale identifier from the URL and looks up the corresponding entry point.
1499
- *
1500
- * @param url - The URL used to derive the locale and determine the appropriate entry point.
1501
- * @returns A function that returns a promise resolving to an object with the `EntryPointExports` type,
1502
- * or `undefined` if no matching entry point is found for the extracted locale.
1503
- */
1504
- getEntryPointFromUrl(url) {
1505
- const { entryPoints, basePath } = this.manifest;
1506
- if (entryPoints.size === 1) {
1507
- return entryPoints.values().next().value;
1508
- }
1509
- const potentialLocale = getPotentialLocaleIdFromUrl(url, basePath);
1510
- return entryPoints.get(potentialLocale);
1511
- }
1512
1699
  /**
1513
1700
  * Retrieves HTTP headers for a request associated with statically generated (SSG) pages,
1514
1701
  * based on the URL pathname.
@@ -1522,10 +1709,77 @@ class AngularAppEngine {
1522
1709
  return new Map();
1523
1710
  }
1524
1711
  const { pathname } = stripIndexHtmlFromURL(new URL(request.url));
1525
- const headers = this.manifest.staticPathsHeaders.get(pathname);
1712
+ const headers = this.manifest.staticPathsHeaders.get(stripTrailingSlash(pathname));
1526
1713
  return new Map(headers);
1527
1714
  }
1715
+ /**
1716
+ * Retrieves the exports for a specific entry point, caching the result.
1717
+ *
1718
+ * @param potentialLocale - The locale string used to find the corresponding entry point.
1719
+ * @returns A promise that resolves to the entry point exports or `undefined` if not found.
1720
+ */
1721
+ getEntryPointExports(potentialLocale) {
1722
+ const cachedEntryPoint = this.entryPointsCache.get(potentialLocale);
1723
+ if (cachedEntryPoint) {
1724
+ return cachedEntryPoint;
1725
+ }
1726
+ const { entryPoints } = this.manifest;
1727
+ const entryPoint = entryPoints.get(potentialLocale);
1728
+ if (!entryPoint) {
1729
+ return undefined;
1730
+ }
1731
+ const entryPointExports = entryPoint();
1732
+ this.entryPointsCache.set(potentialLocale, entryPointExports);
1733
+ return entryPointExports;
1734
+ }
1735
+ /**
1736
+ * Retrieves the entry point for a given URL by determining the locale and mapping it to
1737
+ * the appropriate application bundle.
1738
+ *
1739
+ * This method determines the appropriate entry point and locale for rendering the application by examining the URL.
1740
+ * If there is only one entry point available, it is returned regardless of the URL.
1741
+ * Otherwise, the method extracts a potential locale identifier from the URL and looks up the corresponding entry point.
1742
+ *
1743
+ * @param url - The URL of the request.
1744
+ * @returns A promise that resolves to the entry point exports or `undefined` if not found.
1745
+ */
1746
+ getEntryPointExportsForUrl(url) {
1747
+ const { entryPoints, basePath } = this.manifest;
1748
+ if (entryPoints.size === 1) {
1749
+ return this.getEntryPointExports('');
1750
+ }
1751
+ const potentialLocale = getPotentialLocaleIdFromUrl(url, basePath);
1752
+ return this.getEntryPointExports(potentialLocale);
1753
+ }
1754
+ }
1755
+
1756
+ /**
1757
+ * Annotates a request handler function with metadata, marking it as a special
1758
+ * handler.
1759
+ *
1760
+ * @param handler - The request handler function to be annotated.
1761
+ * @returns The same handler function passed in, with metadata attached.
1762
+ *
1763
+ * @example
1764
+ * Example usage in a Hono application:
1765
+ * ```ts
1766
+ * const app = new Hono();
1767
+ * export default createRequestHandler(app.fetch);
1768
+ * ```
1769
+ *
1770
+ * @example
1771
+ * Example usage in a H3 application:
1772
+ * ```ts
1773
+ * const app = createApp();
1774
+ * const handler = toWebHandler(app);
1775
+ * export default createRequestHandler(handler);
1776
+ * ```
1777
+ * @developerPreview
1778
+ */
1779
+ function createRequestHandler(handler) {
1780
+ handler['__ng_request_handler__'] = true;
1781
+ return handler;
1528
1782
  }
1529
1783
 
1530
- export { AngularAppEngine, provideServerRoutesConfig, InlineCriticalCssProcessor as ɵInlineCriticalCssProcessor, destroyAngularServerApp as ɵdestroyAngularServerApp, extractRoutesAndCreateRouteTree as ɵextractRoutesAndCreateRouteTree, getOrCreateAngularServerApp as ɵgetOrCreateAngularServerApp, getRoutesFromAngularRouterConfig as ɵgetRoutesFromAngularRouterConfig, setAngularAppEngineManifest as ɵsetAngularAppEngineManifest, setAngularAppManifest as ɵsetAngularAppManifest };
1784
+ export { AngularAppEngine, REQUEST, REQUEST_CONTEXT, RESPONSE_INIT, RenderMode, createRequestHandler, provideServerRoutesConfig, InlineCriticalCssProcessor as ɵInlineCriticalCssProcessor, destroyAngularServerApp as ɵdestroyAngularServerApp, extractRoutesAndCreateRouteTree as ɵextractRoutesAndCreateRouteTree, getOrCreateAngularServerApp as ɵgetOrCreateAngularServerApp, getRoutesFromAngularRouterConfig as ɵgetRoutesFromAngularRouterConfig, setAngularAppEngineManifest as ɵsetAngularAppEngineManifest, setAngularAppManifest as ɵsetAngularAppManifest };
1531
1785
  //# sourceMappingURL=ssr.mjs.map