@angular/ssr 19.0.0-next.0 → 19.0.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,354 +1,8 @@
1
- import { ɵSERVER_CONTEXT as _SERVER_CONTEXT, renderApplication, renderModule, INITIAL_CONFIG, ɵINTERNAL_SERVER_PLATFORM_PROVIDERS as _INTERNAL_SERVER_PLATFORM_PROVIDERS } from '@angular/platform-server';
2
- import * as fs from 'node:fs';
3
- import { dirname, join, normalize, resolve } from 'node:path';
4
- import { URL as URL$1 } from 'node:url';
5
- import Critters from 'critters';
6
- import { readFile } from 'node:fs/promises';
7
1
  import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
8
- import { ɵConsole as _Console, ɵresetCompiledComponents as _resetCompiledComponents, createPlatformFactory, platformCore, ApplicationRef, ɵwhenStable as _whenStable, Compiler } from '@angular/core';
2
+ import { ɵConsole as _Console, ɵresetCompiledComponents as _resetCompiledComponents, createPlatformFactory, platformCore, ApplicationRef, ɵwhenStable as _whenStable, Compiler, InjectionToken } from '@angular/core';
3
+ import { renderModule, renderApplication, INITIAL_CONFIG, ɵINTERNAL_SERVER_PLATFORM_PROVIDERS as _INTERNAL_SERVER_PLATFORM_PROVIDERS, ɵSERVER_CONTEXT as _SERVER_CONTEXT } from '@angular/platform-server';
9
4
  import { ɵloadChildren as _loadChildren, Router } from '@angular/router';
10
-
11
- /**
12
- * Pattern used to extract the media query set by Critters in an `onload` handler.
13
- */
14
- const MEDIA_SET_HANDLER_PATTERN = /^this\.media=["'](.*)["'];?$/;
15
- /**
16
- * Name of the attribute used to save the Critters media query so it can be re-assigned on load.
17
- */
18
- const CSP_MEDIA_ATTR = 'ngCspMedia';
19
- /**
20
- * Script text used to change the media value of the link tags.
21
- *
22
- * NOTE:
23
- * We do not use `document.querySelectorAll('link').forEach((s) => s.addEventListener('load', ...)`
24
- * because this does not always fire on Chome.
25
- * See: https://github.com/angular/angular-cli/issues/26932 and https://crbug.com/1521256
26
- */
27
- const LINK_LOAD_SCRIPT_CONTENT = [
28
- '(() => {',
29
- ` const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';`,
30
- ' const documentElement = document.documentElement;',
31
- ' const listener = (e) => {',
32
- ' const target = e.target;',
33
- ` if (!target || target.tagName !== 'LINK' || !target.hasAttribute(CSP_MEDIA_ATTR)) {`,
34
- ' return;',
35
- ' }',
36
- ' target.media = target.getAttribute(CSP_MEDIA_ATTR);',
37
- ' target.removeAttribute(CSP_MEDIA_ATTR);',
38
- // Remove onload listener when there are no longer styles that need to be loaded.
39
- ' if (!document.head.querySelector(`link[${CSP_MEDIA_ATTR}]`)) {',
40
- ` documentElement.removeEventListener('load', listener);`,
41
- ' }',
42
- ' };',
43
- // We use an event with capturing (the true parameter) because load events don't bubble.
44
- ` documentElement.addEventListener('load', listener, true);`,
45
- '})();',
46
- ].join('\n');
47
- class CrittersExtended extends Critters {
48
- optionsExtended;
49
- resourceCache;
50
- warnings = [];
51
- errors = [];
52
- initialEmbedLinkedStylesheet;
53
- addedCspScriptsDocuments = new WeakSet();
54
- documentNonces = new WeakMap();
55
- constructor(optionsExtended, resourceCache) {
56
- super({
57
- logger: {
58
- warn: (s) => this.warnings.push(s),
59
- error: (s) => this.errors.push(s),
60
- info: () => { },
61
- },
62
- logLevel: 'warn',
63
- path: optionsExtended.outputPath,
64
- publicPath: optionsExtended.deployUrl,
65
- compress: !!optionsExtended.minify,
66
- pruneSource: false,
67
- reduceInlineStyles: false,
68
- mergeStylesheets: false,
69
- // Note: if `preload` changes to anything other than `media`, the logic in
70
- // `embedLinkedStylesheetOverride` will have to be updated.
71
- preload: 'media',
72
- noscriptFallback: true,
73
- inlineFonts: true,
74
- });
75
- this.optionsExtended = optionsExtended;
76
- this.resourceCache = resourceCache;
77
- // We can't use inheritance to override `embedLinkedStylesheet`, because it's not declared in
78
- // the `Critters` .d.ts which means that we can't call the `super` implementation. TS doesn't
79
- // allow for `super` to be cast to a different type.
80
- this.initialEmbedLinkedStylesheet = this.embedLinkedStylesheet;
81
- this.embedLinkedStylesheet = this.embedLinkedStylesheetOverride;
82
- }
83
- async readFile(path) {
84
- let resourceContent = this.resourceCache.get(path);
85
- if (resourceContent === undefined) {
86
- resourceContent = await readFile(path, 'utf-8');
87
- this.resourceCache.set(path, resourceContent);
88
- }
89
- return resourceContent;
90
- }
91
- /**
92
- * Override of the Critters `embedLinkedStylesheet` method
93
- * that makes it work with Angular's CSP APIs.
94
- */
95
- embedLinkedStylesheetOverride = async (link, document) => {
96
- if (link.getAttribute('media') === 'print' && link.next?.name === 'noscript') {
97
- // Workaround for https://github.com/GoogleChromeLabs/critters/issues/64
98
- // NB: this is only needed for the webpack based builders.
99
- const media = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN);
100
- if (media) {
101
- link.removeAttribute('onload');
102
- link.setAttribute('media', media[1]);
103
- link?.next?.remove();
104
- }
105
- }
106
- const returnValue = await this.initialEmbedLinkedStylesheet(link, document);
107
- const cspNonce = this.findCspNonce(document);
108
- if (cspNonce) {
109
- const crittersMedia = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN);
110
- if (crittersMedia) {
111
- // If there's a Critters-generated `onload` handler and the file has an Angular CSP nonce,
112
- // we have to remove the handler, because it's incompatible with CSP. We save the value
113
- // in a different attribute and we generate a script tag with the nonce that uses
114
- // `addEventListener` to apply the media query instead.
115
- link.removeAttribute('onload');
116
- link.setAttribute(CSP_MEDIA_ATTR, crittersMedia[1]);
117
- this.conditionallyInsertCspLoadingScript(document, cspNonce, link);
118
- }
119
- // Ideally we would hook in at the time Critters inserts the `style` tags, but there isn't
120
- // a way of doing that at the moment so we fall back to doing it any time a `link` tag is
121
- // inserted. We mitigate it by only iterating the direct children of the `<head>` which
122
- // should be pretty shallow.
123
- document.head.children.forEach((child) => {
124
- if (child.tagName === 'style' && !child.hasAttribute('nonce')) {
125
- child.setAttribute('nonce', cspNonce);
126
- }
127
- });
128
- }
129
- return returnValue;
130
- };
131
- /**
132
- * Finds the CSP nonce for a specific document.
133
- */
134
- findCspNonce(document) {
135
- if (this.documentNonces.has(document)) {
136
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
137
- return this.documentNonces.get(document);
138
- }
139
- // HTML attribute are case-insensitive, but the parser used by Critters is case-sensitive.
140
- const nonceElement = document.querySelector('[ngCspNonce], [ngcspnonce]');
141
- const cspNonce = nonceElement?.getAttribute('ngCspNonce') || nonceElement?.getAttribute('ngcspnonce') || null;
142
- this.documentNonces.set(document, cspNonce);
143
- return cspNonce;
144
- }
145
- /**
146
- * Inserts the `script` tag that swaps the critical CSS at runtime,
147
- * if one hasn't been inserted into the document already.
148
- */
149
- conditionallyInsertCspLoadingScript(document, nonce, link) {
150
- if (this.addedCspScriptsDocuments.has(document)) {
151
- return;
152
- }
153
- if (document.head.textContent.includes(LINK_LOAD_SCRIPT_CONTENT)) {
154
- // Script was already added during the build.
155
- this.addedCspScriptsDocuments.add(document);
156
- return;
157
- }
158
- const script = document.createElement('script');
159
- script.setAttribute('nonce', nonce);
160
- script.textContent = LINK_LOAD_SCRIPT_CONTENT;
161
- // Prepend the script to the head since it needs to
162
- // run as early as possible, before the `link` tags.
163
- document.head.insertBefore(script, link);
164
- this.addedCspScriptsDocuments.add(document);
165
- }
166
- }
167
- class InlineCriticalCssProcessor {
168
- options;
169
- resourceCache = new Map();
170
- constructor(options) {
171
- this.options = options;
172
- }
173
- async process(html, options) {
174
- const critters = new CrittersExtended({ ...this.options, ...options }, this.resourceCache);
175
- const content = await critters.process(html);
176
- return {
177
- content,
178
- errors: critters.errors.length ? critters.errors : undefined,
179
- warnings: critters.warnings.length ? critters.warnings : undefined,
180
- };
181
- }
182
- }
183
-
184
- const PERFORMANCE_MARK_PREFIX = '🅰️';
185
- function printPerformanceLogs() {
186
- let maxWordLength = 0;
187
- const benchmarks = [];
188
- for (const { name, duration } of performance.getEntriesByType('measure')) {
189
- if (!name.startsWith(PERFORMANCE_MARK_PREFIX)) {
190
- continue;
191
- }
192
- // `🅰️:Retrieve SSG Page` -> `Retrieve SSG Page:`
193
- const step = name.slice(PERFORMANCE_MARK_PREFIX.length + 1) + ':';
194
- if (step.length > maxWordLength) {
195
- maxWordLength = step.length;
196
- }
197
- benchmarks.push([step, `${duration.toFixed(1)}ms`]);
198
- performance.clearMeasures(name);
199
- }
200
- /* eslint-disable no-console */
201
- console.log('********** Performance results **********');
202
- for (const [step, value] of benchmarks) {
203
- const spaces = maxWordLength - step.length + 5;
204
- console.log(step + ' '.repeat(spaces) + value);
205
- }
206
- console.log('*****************************************');
207
- /* eslint-enable no-console */
208
- }
209
- async function runMethodAndMeasurePerf(label, asyncMethod) {
210
- const labelName = `${PERFORMANCE_MARK_PREFIX}:${label}`;
211
- const startLabel = `start:${labelName}`;
212
- const endLabel = `end:${labelName}`;
213
- try {
214
- performance.mark(startLabel);
215
- return await asyncMethod();
216
- }
217
- finally {
218
- performance.mark(endLabel);
219
- performance.measure(labelName, startLabel, endLabel);
220
- performance.clearMarks(startLabel);
221
- performance.clearMarks(endLabel);
222
- }
223
- }
224
- function noopRunMethodAndMeasurePerf(label, asyncMethod) {
225
- return asyncMethod();
226
- }
227
-
228
- const SSG_MARKER_REGEXP = /ng-server-context=["']\w*\|?ssg\|?\w*["']/;
229
- /**
230
- * A common engine to use to server render an application.
231
- */
232
- class CommonEngine {
233
- options;
234
- templateCache = new Map();
235
- inlineCriticalCssProcessor;
236
- pageIsSSG = new Map();
237
- constructor(options) {
238
- this.options = options;
239
- this.inlineCriticalCssProcessor = new InlineCriticalCssProcessor({
240
- minify: false,
241
- });
242
- }
243
- /**
244
- * Render an HTML document for a specific URL with specified
245
- * render options
246
- */
247
- async render(opts) {
248
- const enablePerformanceProfiler = this.options?.enablePerformanceProfiler;
249
- const runMethod = enablePerformanceProfiler
250
- ? runMethodAndMeasurePerf
251
- : noopRunMethodAndMeasurePerf;
252
- let html = await runMethod('Retrieve SSG Page', () => this.retrieveSSGPage(opts));
253
- if (html === undefined) {
254
- html = await runMethod('Render Page', () => this.renderApplication(opts));
255
- if (opts.inlineCriticalCss !== false) {
256
- const { content, errors, warnings } = await runMethod('Inline Critical CSS', () =>
257
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
258
- this.inlineCriticalCss(html, opts));
259
- html = content;
260
- // eslint-disable-next-line no-console
261
- warnings?.forEach((m) => console.warn(m));
262
- // eslint-disable-next-line no-console
263
- errors?.forEach((m) => console.error(m));
264
- }
265
- }
266
- if (enablePerformanceProfiler) {
267
- printPerformanceLogs();
268
- }
269
- return html;
270
- }
271
- inlineCriticalCss(html, opts) {
272
- return this.inlineCriticalCssProcessor.process(html, {
273
- outputPath: opts.publicPath ?? (opts.documentFilePath ? dirname(opts.documentFilePath) : ''),
274
- });
275
- }
276
- async retrieveSSGPage(opts) {
277
- const { publicPath, documentFilePath, url } = opts;
278
- if (!publicPath || !documentFilePath || url === undefined) {
279
- return undefined;
280
- }
281
- const { pathname } = new URL$1(url, 'resolve://');
282
- // Do not use `resolve` here as otherwise it can lead to path traversal vulnerability.
283
- // See: https://portswigger.net/web-security/file-path-traversal
284
- const pagePath = join(publicPath, pathname, 'index.html');
285
- if (this.pageIsSSG.get(pagePath)) {
286
- // Serve pre-rendered page.
287
- return fs.promises.readFile(pagePath, 'utf-8');
288
- }
289
- if (!pagePath.startsWith(normalize(publicPath))) {
290
- // Potential path traversal detected.
291
- return undefined;
292
- }
293
- if (pagePath === resolve(documentFilePath) || !(await exists(pagePath))) {
294
- // View matches with prerender path or file does not exist.
295
- this.pageIsSSG.set(pagePath, false);
296
- return undefined;
297
- }
298
- // Static file exists.
299
- const content = await fs.promises.readFile(pagePath, 'utf-8');
300
- const isSSG = SSG_MARKER_REGEXP.test(content);
301
- this.pageIsSSG.set(pagePath, isSSG);
302
- return isSSG ? content : undefined;
303
- }
304
- async renderApplication(opts) {
305
- const moduleOrFactory = this.options?.bootstrap ?? opts.bootstrap;
306
- if (!moduleOrFactory) {
307
- throw new Error('A module or bootstrap option must be provided.');
308
- }
309
- const extraProviders = [
310
- { provide: _SERVER_CONTEXT, useValue: 'ssr' },
311
- ...(opts.providers ?? []),
312
- ...(this.options?.providers ?? []),
313
- ];
314
- let document = opts.document;
315
- if (!document && opts.documentFilePath) {
316
- document = await this.getDocument(opts.documentFilePath);
317
- }
318
- const commonRenderingOptions = {
319
- url: opts.url,
320
- document,
321
- };
322
- return isBootstrapFn(moduleOrFactory)
323
- ? renderApplication(moduleOrFactory, {
324
- platformProviders: extraProviders,
325
- ...commonRenderingOptions,
326
- })
327
- : renderModule(moduleOrFactory, { extraProviders, ...commonRenderingOptions });
328
- }
329
- /** Retrieve the document from the cache or the filesystem */
330
- async getDocument(filePath) {
331
- let doc = this.templateCache.get(filePath);
332
- if (!doc) {
333
- doc = await fs.promises.readFile(filePath, 'utf-8');
334
- this.templateCache.set(filePath, doc);
335
- }
336
- return doc;
337
- }
338
- }
339
- async function exists(path) {
340
- try {
341
- await fs.promises.access(path, fs.constants.F_OK);
342
- return true;
343
- }
344
- catch {
345
- return false;
346
- }
347
- }
348
- function isBootstrapFn(value) {
349
- // We can differentiate between a module and a bootstrap function by reading compiler-generated `ɵmod` static property:
350
- return typeof value === 'function' && !('ɵmod' in value);
351
- }
5
+ import Critters from '../third_party/critters/index.js';
352
6
 
353
7
  /**
354
8
  * Custom implementation of the Angular Console service that filters out specific log messages.
@@ -357,10 +11,13 @@ function isBootstrapFn(value) {
357
11
  * It overrides the `log` method to suppress logs that match certain predefined messages.
358
12
  */
359
13
  class Console extends _Console {
360
- /**
361
- * A set of log messages that should be ignored and not printed to the console.
362
- */
363
- ignoredLogs = new Set(['Angular is running in development mode.']);
14
+ constructor() {
15
+ super(...arguments);
16
+ /**
17
+ * A set of log messages that should be ignored and not printed to the console.
18
+ */
19
+ this.ignoredLogs = new Set(['Angular is running in development mode.']);
20
+ }
364
21
  /**
365
22
  * Logs a message to the console if it is not in the set of ignored messages.
366
23
  *
@@ -582,10 +239,12 @@ function resolveRedirectTo(routePath, redirectTo) {
582
239
  * @returns A promise that resolves to an object of type `AngularRouterConfigResult`.
583
240
  */
584
241
  async function getRoutesFromAngularRouterConfig(bootstrap, document, url) {
585
- // Need to clean up GENERATED_COMP_IDS map in `@angular/core`.
586
- // Otherwise an incorrect component ID generation collision detected warning will be displayed in development.
587
- // See: https://github.com/angular/angular-cli/issues/25924
588
- _resetCompiledComponents();
242
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
243
+ // Need to clean up GENERATED_COMP_IDS map in `@angular/core`.
244
+ // Otherwise an incorrect component ID generation collision detected warning will be displayed in development.
245
+ // See: https://github.com/angular/angular-cli/issues/25924
246
+ _resetCompiledComponents();
247
+ }
589
248
  const { protocol, host } = url;
590
249
  // Create and initialize the Angular platform for server-side rendering.
591
250
  const platformRef = createPlatformFactory(platformCore, 'server', [
@@ -641,5 +300,907 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url) {
641
300
  }
642
301
  }
643
302
 
644
- export { CommonEngine, getRoutesFromAngularRouterConfig as ɵgetRoutesFromAngularRouterConfig };
303
+ /**
304
+ * Manages server-side assets.
305
+ */
306
+ class ServerAssets {
307
+ /**
308
+ * Creates an instance of ServerAsset.
309
+ *
310
+ * @param manifest - The manifest containing the server assets.
311
+ */
312
+ constructor(manifest) {
313
+ this.manifest = manifest;
314
+ }
315
+ /**
316
+ * Retrieves the content of a server-side asset using its path.
317
+ *
318
+ * @param path - The path to the server asset.
319
+ * @returns A promise that resolves to the asset content as a string.
320
+ * @throws Error If the asset path is not found in the manifest, an error is thrown.
321
+ */
322
+ async getServerAsset(path) {
323
+ const asset = this.manifest.assets.get(path);
324
+ if (!asset) {
325
+ throw new Error(`Server asset '${path}' does not exist.`);
326
+ }
327
+ return asset();
328
+ }
329
+ /**
330
+ * Retrieves and caches the content of 'index.server.html'.
331
+ *
332
+ * @returns A promise that resolves to the content of 'index.server.html'.
333
+ * @throws Error If there is an issue retrieving the asset.
334
+ */
335
+ getIndexServerHtml() {
336
+ return this.getServerAsset('index.server.html');
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Manages a collection of hooks and provides methods to register and execute them.
342
+ * Hooks are functions that can be invoked with specific arguments to allow modifications or enhancements.
343
+ */
344
+ class Hooks {
345
+ constructor() {
346
+ /**
347
+ * A map of hook names to arrays of hook functions.
348
+ * Each hook name can have multiple associated functions, which are executed in sequence.
349
+ */
350
+ this.store = new Map();
351
+ }
352
+ /**
353
+ * Executes all hooks associated with the specified name, passing the given argument to each hook function.
354
+ * The hooks are invoked sequentially, and the argument may be modified by each hook.
355
+ *
356
+ * @template Hook - The type of the hook name. It should be one of the keys of `HooksMapping`.
357
+ * @param name - The name of the hook whose functions will be executed.
358
+ * @param context - The input value to be passed to each hook function. The value is mutated by each hook function.
359
+ * @returns A promise that resolves once all hook functions have been executed.
360
+ *
361
+ * @example
362
+ * ```typescript
363
+ * const hooks = new Hooks();
364
+ * hooks.on('html:transform:pre', async (ctx) => {
365
+ * ctx.html = ctx.html.replace(/foo/g, 'bar');
366
+ * return ctx.html;
367
+ * });
368
+ * const result = await hooks.run('html:transform:pre', { html: '<div>foo</div>' });
369
+ * console.log(result); // '<div>bar</div>'
370
+ * ```
371
+ * @internal
372
+ */
373
+ async run(name, context) {
374
+ const hooks = this.store.get(name);
375
+ switch (name) {
376
+ case 'html:transform:pre': {
377
+ if (!hooks) {
378
+ return context.html;
379
+ }
380
+ const ctx = { ...context };
381
+ for (const hook of hooks) {
382
+ ctx.html = await hook(ctx);
383
+ }
384
+ return ctx.html;
385
+ }
386
+ default:
387
+ throw new Error(`Running hook "${name}" is not supported.`);
388
+ }
389
+ }
390
+ /**
391
+ * Registers a new hook function under the specified hook name.
392
+ * This function should be a function that takes an argument of type `T` and returns a `string` or `Promise<string>`.
393
+ *
394
+ * @template Hook - The type of the hook name. It should be one of the keys of `HooksMapping`.
395
+ * @param name - The name of the hook under which the function will be registered.
396
+ * @param handler - A function to be executed when the hook is triggered. The handler will be called with an argument
397
+ * that may be modified by the hook functions.
398
+ *
399
+ * @remarks
400
+ * - If there are existing handlers registered under the given hook name, the new handler will be added to the list.
401
+ * - If no handlers are registered under the given hook name, a new list will be created with the handler as its first element.
402
+ *
403
+ * @example
404
+ * ```typescript
405
+ * hooks.on('html:transform:pre', async (ctx) => {
406
+ * return ctx.html.replace(/foo/g, 'bar');
407
+ * });
408
+ * ```
409
+ */
410
+ on(name, handler) {
411
+ const hooks = this.store.get(name);
412
+ if (hooks) {
413
+ hooks.push(handler);
414
+ }
415
+ else {
416
+ this.store.set(name, [handler]);
417
+ }
418
+ }
419
+ /**
420
+ * Checks if there are any hooks registered under the specified name.
421
+ *
422
+ * @param name - The name of the hook to check.
423
+ * @returns `true` if there are hooks registered under the specified name, otherwise `false`.
424
+ */
425
+ has(name) {
426
+ return !!this.store.get(name)?.length;
427
+ }
428
+ }
429
+
430
+ /**
431
+ * The Angular app manifest object.
432
+ * This is used internally to store the current Angular app manifest.
433
+ */
434
+ let angularAppManifest;
435
+ /**
436
+ * Sets the Angular app manifest.
437
+ *
438
+ * @param manifest - The manifest object to set for the Angular application.
439
+ */
440
+ function setAngularAppManifest(manifest) {
441
+ angularAppManifest = manifest;
442
+ }
443
+ /**
444
+ * Gets the Angular app manifest.
445
+ *
446
+ * @returns The Angular app manifest.
447
+ * @throws Will throw an error if the Angular app manifest is not set.
448
+ */
449
+ function getAngularAppManifest() {
450
+ if (!angularAppManifest) {
451
+ throw new Error('Angular app manifest is not set. ' +
452
+ `Please ensure you are using the '@angular/build:application' builder to build your server application.`);
453
+ }
454
+ return angularAppManifest;
455
+ }
456
+ /**
457
+ * The Angular app engine manifest object.
458
+ * This is used internally to store the current Angular app engine manifest.
459
+ */
460
+ let angularAppEngineManifest;
461
+ /**
462
+ * Sets the Angular app engine manifest.
463
+ *
464
+ * @param manifest - The engine manifest object to set.
465
+ */
466
+ function setAngularAppEngineManifest(manifest) {
467
+ angularAppEngineManifest = manifest;
468
+ }
469
+ /**
470
+ * Gets the Angular app engine manifest.
471
+ *
472
+ * @returns The Angular app engine manifest.
473
+ * @throws Will throw an error if the Angular app engine manifest is not set.
474
+ */
475
+ function getAngularAppEngineManifest() {
476
+ if (!angularAppEngineManifest) {
477
+ throw new Error('Angular app engine manifest is not set. ' +
478
+ `Please ensure you are using the '@angular/build:application' builder to build your server application.`);
479
+ }
480
+ return angularAppEngineManifest;
481
+ }
482
+
483
+ /**
484
+ * A route tree implementation that supports efficient route matching, including support for wildcard routes.
485
+ * This structure is useful for organizing and retrieving routes in a hierarchical manner,
486
+ * enabling complex routing scenarios with nested paths.
487
+ */
488
+ class RouteTree {
489
+ constructor() {
490
+ /**
491
+ * The root node of the route tree.
492
+ * All routes are stored and accessed relative to this root node.
493
+ */
494
+ this.root = this.createEmptyRouteTreeNode('');
495
+ /**
496
+ * A counter that tracks the order of route insertion.
497
+ * This ensures that routes are matched in the order they were defined,
498
+ * with earlier routes taking precedence.
499
+ */
500
+ this.insertionIndexCounter = 0;
501
+ }
502
+ /**
503
+ * Inserts a new route into the route tree.
504
+ * The route is broken down into segments, and each segment is added to the tree.
505
+ * Parameterized segments (e.g., :id) are normalized to wildcards (*) for matching purposes.
506
+ *
507
+ * @param route - The route path to insert into the tree.
508
+ * @param metadata - Metadata associated with the route, excluding the route path itself.
509
+ */
510
+ insert(route, metadata) {
511
+ let node = this.root;
512
+ const normalizedRoute = stripTrailingSlash(route);
513
+ const segments = normalizedRoute.split('/');
514
+ for (const segment of segments) {
515
+ // Replace parameterized segments (e.g., :id) with a wildcard (*) for matching
516
+ const normalizedSegment = segment[0] === ':' ? '*' : segment;
517
+ let childNode = node.children.get(normalizedSegment);
518
+ if (!childNode) {
519
+ childNode = this.createEmptyRouteTreeNode(normalizedSegment);
520
+ node.children.set(normalizedSegment, childNode);
521
+ }
522
+ node = childNode;
523
+ }
524
+ // At the leaf node, store the full route and its associated metadata
525
+ node.metadata = {
526
+ ...metadata,
527
+ route: normalizedRoute,
528
+ };
529
+ node.insertionIndex = this.insertionIndexCounter++;
530
+ }
531
+ /**
532
+ * Matches a given route against the route tree and returns the best matching route's metadata.
533
+ * The best match is determined by the lowest insertion index, meaning the earliest defined route
534
+ * takes precedence.
535
+ *
536
+ * @param route - The route path to match against the route tree.
537
+ * @returns The metadata of the best matching route or `undefined` if no match is found.
538
+ */
539
+ match(route) {
540
+ const segments = stripTrailingSlash(route).split('/');
541
+ return this.traverseBySegments(segments)?.metadata;
542
+ }
543
+ /**
544
+ * Converts the route tree into a serialized format representation.
545
+ * This method converts the route tree into an array of metadata objects that describe the structure of the tree.
546
+ * The array represents the routes in a nested manner where each entry includes the route and its associated metadata.
547
+ *
548
+ * @returns An array of `RouteTreeNodeMetadata` objects representing the route tree structure.
549
+ * Each object includes the `route` and associated metadata of a route.
550
+ */
551
+ toObject() {
552
+ return Array.from(this.traverse());
553
+ }
554
+ /**
555
+ * Constructs a `RouteTree` from an object representation.
556
+ * This method is used to recreate a `RouteTree` instance from an array of metadata objects.
557
+ * The array should be in the format produced by `toObject`, allowing for the reconstruction of the route tree
558
+ * with the same routes and metadata.
559
+ *
560
+ * @param value - An array of `RouteTreeNodeMetadata` objects that represent the serialized format of the route tree.
561
+ * Each object should include a `route` and its associated metadata.
562
+ * @returns A new `RouteTree` instance constructed from the provided metadata objects.
563
+ */
564
+ static fromObject(value) {
565
+ const tree = new RouteTree();
566
+ for (const { route, ...metadata } of value) {
567
+ tree.insert(route, metadata);
568
+ }
569
+ return tree;
570
+ }
571
+ /**
572
+ * A generator function that recursively traverses the route tree and yields the metadata of each node.
573
+ * This allows for easy and efficient iteration over all nodes in the tree.
574
+ *
575
+ * @param node - The current node to start the traversal from. Defaults to the root node of the tree.
576
+ */
577
+ *traverse(node = this.root) {
578
+ if (node.metadata) {
579
+ yield node.metadata;
580
+ }
581
+ for (const childNode of node.children.values()) {
582
+ yield* this.traverse(childNode);
583
+ }
584
+ }
585
+ /**
586
+ * Recursively traverses the route tree from a given node, attempting to match the remaining route segments.
587
+ * If the node is a leaf node (no more segments to match) and contains metadata, the node is yielded.
588
+ *
589
+ * This function prioritizes exact segment matches first, followed by wildcard matches (`*`),
590
+ * and finally deep wildcard matches (`**`) that consume all segments.
591
+ *
592
+ * @param remainingSegments - The remaining segments of the route path to match.
593
+ * @param node - The current node in the route tree to start traversal from.
594
+ *
595
+ * @returns The node that best matches the remaining segments or `undefined` if no match is found.
596
+ */
597
+ traverseBySegments(remainingSegments, node = this.root) {
598
+ const { metadata, children } = node;
599
+ // If there are no remaining segments and the node has metadata, return this node
600
+ if (!remainingSegments?.length) {
601
+ if (metadata) {
602
+ return node;
603
+ }
604
+ return;
605
+ }
606
+ // If the node has no children, end the traversal
607
+ if (!children.size) {
608
+ return;
609
+ }
610
+ const [segment, ...restSegments] = remainingSegments;
611
+ let currentBestMatchNode;
612
+ // 1. Exact segment match
613
+ const exactMatchNode = node.children.get(segment);
614
+ currentBestMatchNode = this.getHigherPriorityNode(currentBestMatchNode, this.traverseBySegments(restSegments, exactMatchNode));
615
+ // 2. Wildcard segment match (`*`)
616
+ const wildcardNode = node.children.get('*');
617
+ currentBestMatchNode = this.getHigherPriorityNode(currentBestMatchNode, this.traverseBySegments(restSegments, wildcardNode));
618
+ // 3. Deep wildcard segment match (`**`)
619
+ const deepWildcardNode = node.children.get('**');
620
+ currentBestMatchNode = this.getHigherPriorityNode(currentBestMatchNode, deepWildcardNode);
621
+ return currentBestMatchNode;
622
+ }
623
+ /**
624
+ * Compares two nodes and returns the node with higher priority based on insertion index.
625
+ * A node with a lower insertion index is prioritized as it was defined earlier.
626
+ *
627
+ * @param currentBestMatchNode - The current best match node.
628
+ * @param candidateNode - The node being evaluated for higher priority based on insertion index.
629
+ * @returns The node with higher priority (i.e., lower insertion index). If one of the nodes is `undefined`, the other node is returned.
630
+ */
631
+ getHigherPriorityNode(currentBestMatchNode, candidateNode) {
632
+ if (!candidateNode) {
633
+ return currentBestMatchNode;
634
+ }
635
+ if (!currentBestMatchNode) {
636
+ return candidateNode;
637
+ }
638
+ return candidateNode.insertionIndex < currentBestMatchNode.insertionIndex
639
+ ? candidateNode
640
+ : currentBestMatchNode;
641
+ }
642
+ /**
643
+ * Creates an empty route tree node with the specified segment.
644
+ * This helper function is used during the tree construction.
645
+ *
646
+ * @param segment - The route segment that this node represents.
647
+ * @returns A new, empty route tree node.
648
+ */
649
+ createEmptyRouteTreeNode(segment) {
650
+ return {
651
+ segment,
652
+ insertionIndex: -1,
653
+ children: new Map(),
654
+ };
655
+ }
656
+ }
657
+
658
+ /**
659
+ * Manages the application's server routing logic by building and maintaining a route tree.
660
+ *
661
+ * This class is responsible for constructing the route tree from the Angular application
662
+ * configuration and using it to match incoming requests to the appropriate routes.
663
+ */
664
+ class ServerRouter {
665
+ /**
666
+ * Creates an instance of the `ServerRouter`.
667
+ *
668
+ * @param routeTree - An instance of `RouteTree` that holds the routing information.
669
+ * The `RouteTree` is used to match request URLs to the appropriate route metadata.
670
+ */
671
+ constructor(routeTree) {
672
+ this.routeTree = routeTree;
673
+ }
674
+ /**
675
+ * Static property to track the ongoing build promise.
676
+ */
677
+ static #extractionPromise;
678
+ /**
679
+ * Creates or retrieves a `ServerRouter` instance based on the provided manifest and URL.
680
+ *
681
+ * If the manifest contains pre-built routes, a new `ServerRouter` is immediately created.
682
+ * Otherwise, it builds the router by extracting routes from the Angular configuration
683
+ * asynchronously. This method ensures that concurrent builds are prevented by re-using
684
+ * the same promise.
685
+ *
686
+ * @param manifest - An instance of `AngularAppManifest` that contains the route information.
687
+ * @param url - The URL for server-side rendering. The URL is needed to configure `ServerPlatformLocation`.
688
+ * This is necessary to ensure that API requests for relative paths succeed, which is crucial for correct route extraction.
689
+ * [Reference](https://github.com/angular/angular/blob/d608b857c689d17a7ffa33bbb510301014d24a17/packages/platform-server/src/location.ts#L51)
690
+ * @returns A promise resolving to a `ServerRouter` instance.
691
+ */
692
+ static from(manifest, url) {
693
+ if (manifest.routes) {
694
+ const routeTree = RouteTree.fromObject(manifest.routes);
695
+ return Promise.resolve(new ServerRouter(routeTree));
696
+ }
697
+ // Create and store a new promise for the build process.
698
+ // This prevents concurrent builds by re-using the same promise.
699
+ ServerRouter.#extractionPromise ??= (async () => {
700
+ try {
701
+ const routeTree = new RouteTree();
702
+ const document = await new ServerAssets(manifest).getIndexServerHtml();
703
+ const { baseHref, routes } = await getRoutesFromAngularRouterConfig(manifest.bootstrap(), document, url);
704
+ for (let { route, redirectTo } of routes) {
705
+ route = joinUrlParts(baseHref, route);
706
+ redirectTo = redirectTo === undefined ? undefined : joinUrlParts(baseHref, redirectTo);
707
+ routeTree.insert(route, { redirectTo });
708
+ }
709
+ return new ServerRouter(routeTree);
710
+ }
711
+ finally {
712
+ ServerRouter.#extractionPromise = undefined;
713
+ }
714
+ })();
715
+ return ServerRouter.#extractionPromise;
716
+ }
717
+ /**
718
+ * Matches a request URL against the route tree to retrieve route metadata.
719
+ *
720
+ * This method strips 'index.html' from the URL if it is present and then attempts
721
+ * to find a match in the route tree. If a match is found, it returns the associated
722
+ * route metadata; otherwise, it returns `undefined`.
723
+ *
724
+ * @param url - The URL to be matched against the route tree.
725
+ * @returns The metadata for the matched route or `undefined` if no match is found.
726
+ */
727
+ match(url) {
728
+ // Strip 'index.html' from URL if present.
729
+ // A request to `http://www.example.com/page/index.html` will render the Angular route corresponding to `http://www.example.com/page`.
730
+ const { pathname } = stripIndexHtmlFromURL(url);
731
+ return this.routeTree.match(decodeURIComponent(pathname));
732
+ }
733
+ }
734
+
735
+ /**
736
+ * Injection token for the current request.
737
+ */
738
+ const REQUEST = new InjectionToken('REQUEST');
739
+ /**
740
+ * Injection token for the response initialization options.
741
+ */
742
+ const RESPONSE_INIT = new InjectionToken('RESPONSE_INIT');
743
+ /**
744
+ * Injection token for additional request context.
745
+ */
746
+ const REQUEST_CONTEXT = new InjectionToken('REQUEST_CONTEXT');
747
+
748
+ /**
749
+ * Pattern used to extract the media query set by Critters in an `onload` handler.
750
+ */
751
+ const MEDIA_SET_HANDLER_PATTERN = /^this\.media=["'](.*)["'];?$/;
752
+ /**
753
+ * Name of the attribute used to save the Critters media query so it can be re-assigned on load.
754
+ */
755
+ const CSP_MEDIA_ATTR = 'ngCspMedia';
756
+ /**
757
+ * Script that dynamically updates the `media` attribute of `<link>` tags based on a custom attribute (`CSP_MEDIA_ATTR`).
758
+ *
759
+ * NOTE:
760
+ * We do not use `document.querySelectorAll('link').forEach((s) => s.addEventListener('load', ...)`
761
+ * because load events are not always triggered reliably on Chrome.
762
+ * See: https://github.com/angular/angular-cli/issues/26932 and https://crbug.com/1521256
763
+ *
764
+ * The script:
765
+ * - Ensures the event target is a `<link>` tag with the `CSP_MEDIA_ATTR` attribute.
766
+ * - Updates the `media` attribute with the value of `CSP_MEDIA_ATTR` and then removes the attribute.
767
+ * - Removes the event listener when all relevant `<link>` tags have been processed.
768
+ * - Uses event capturing (the `true` parameter) since load events do not bubble up the DOM.
769
+ */
770
+ const LINK_LOAD_SCRIPT_CONTENT = `
771
+ (() => {
772
+ const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';
773
+ const documentElement = document.documentElement;
774
+
775
+ // Listener for load events on link tags.
776
+ const listener = (e) => {
777
+ const target = e.target;
778
+ if (
779
+ !target ||
780
+ target.tagName !== 'LINK' ||
781
+ !target.hasAttribute(CSP_MEDIA_ATTR)
782
+ ) {
783
+ return;
784
+ }
785
+
786
+ target.media = target.getAttribute(CSP_MEDIA_ATTR);
787
+ target.removeAttribute(CSP_MEDIA_ATTR);
788
+
789
+ if (!document.head.querySelector(\`link[\${CSP_MEDIA_ATTR}]\`)) {
790
+ documentElement.removeEventListener('load', listener);
791
+ }
792
+ };
793
+
794
+ documentElement.addEventListener('load', listener, true);
795
+ })();
796
+ `.trim();
797
+ class CrittersBase extends Critters {
798
+ }
799
+ /* eslint-enable @typescript-eslint/no-unsafe-declaration-merging */
800
+ class InlineCriticalCssProcessor extends CrittersBase {
801
+ constructor(readFile, outputPath) {
802
+ super({
803
+ logger: {
804
+ // eslint-disable-next-line no-console
805
+ warn: (s) => console.warn(s),
806
+ // eslint-disable-next-line no-console
807
+ error: (s) => console.error(s),
808
+ info: () => { },
809
+ },
810
+ logLevel: 'warn',
811
+ path: outputPath,
812
+ publicPath: undefined,
813
+ compress: false,
814
+ pruneSource: false,
815
+ reduceInlineStyles: false,
816
+ mergeStylesheets: false,
817
+ // Note: if `preload` changes to anything other than `media`, the logic in
818
+ // `embedLinkedStylesheet` will have to be updated.
819
+ preload: 'media',
820
+ noscriptFallback: true,
821
+ inlineFonts: true,
822
+ });
823
+ this.readFile = readFile;
824
+ this.outputPath = outputPath;
825
+ this.addedCspScriptsDocuments = new WeakSet();
826
+ this.documentNonces = new WeakMap();
827
+ }
828
+ /**
829
+ * Override of the Critters `embedLinkedStylesheet` method
830
+ * that makes it work with Angular's CSP APIs.
831
+ */
832
+ async embedLinkedStylesheet(link, document) {
833
+ if (link.getAttribute('media') === 'print' && link.next?.name === 'noscript') {
834
+ // Workaround for https://github.com/GoogleChromeLabs/critters/issues/64
835
+ // NB: this is only needed for the webpack based builders.
836
+ const media = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN);
837
+ if (media) {
838
+ link.removeAttribute('onload');
839
+ link.setAttribute('media', media[1]);
840
+ link?.next?.remove();
841
+ }
842
+ }
843
+ const returnValue = await super.embedLinkedStylesheet(link, document);
844
+ const cspNonce = this.findCspNonce(document);
845
+ if (cspNonce) {
846
+ const crittersMedia = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN);
847
+ if (crittersMedia) {
848
+ // If there's a Critters-generated `onload` handler and the file has an Angular CSP nonce,
849
+ // we have to remove the handler, because it's incompatible with CSP. We save the value
850
+ // in a different attribute and we generate a script tag with the nonce that uses
851
+ // `addEventListener` to apply the media query instead.
852
+ link.removeAttribute('onload');
853
+ link.setAttribute(CSP_MEDIA_ATTR, crittersMedia[1]);
854
+ this.conditionallyInsertCspLoadingScript(document, cspNonce, link);
855
+ }
856
+ // Ideally we would hook in at the time Critters inserts the `style` tags, but there isn't
857
+ // a way of doing that at the moment so we fall back to doing it any time a `link` tag is
858
+ // inserted. We mitigate it by only iterating the direct children of the `<head>` which
859
+ // should be pretty shallow.
860
+ document.head.children.forEach((child) => {
861
+ if (child.tagName === 'style' && !child.hasAttribute('nonce')) {
862
+ child.setAttribute('nonce', cspNonce);
863
+ }
864
+ });
865
+ }
866
+ return returnValue;
867
+ }
868
+ /**
869
+ * Finds the CSP nonce for a specific document.
870
+ */
871
+ findCspNonce(document) {
872
+ if (this.documentNonces.has(document)) {
873
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
874
+ return this.documentNonces.get(document);
875
+ }
876
+ // HTML attribute are case-insensitive, but the parser used by Critters is case-sensitive.
877
+ const nonceElement = document.querySelector('[ngCspNonce], [ngcspnonce]');
878
+ const cspNonce = nonceElement?.getAttribute('ngCspNonce') || nonceElement?.getAttribute('ngcspnonce') || null;
879
+ this.documentNonces.set(document, cspNonce);
880
+ return cspNonce;
881
+ }
882
+ /**
883
+ * Inserts the `script` tag that swaps the critical CSS at runtime,
884
+ * if one hasn't been inserted into the document already.
885
+ */
886
+ conditionallyInsertCspLoadingScript(document, nonce, link) {
887
+ if (this.addedCspScriptsDocuments.has(document)) {
888
+ return;
889
+ }
890
+ if (document.head.textContent.includes(LINK_LOAD_SCRIPT_CONTENT)) {
891
+ // Script was already added during the build.
892
+ this.addedCspScriptsDocuments.add(document);
893
+ return;
894
+ }
895
+ const script = document.createElement('script');
896
+ script.setAttribute('nonce', nonce);
897
+ script.textContent = LINK_LOAD_SCRIPT_CONTENT;
898
+ // Prepend the script to the head since it needs to
899
+ // run as early as possible, before the `link` tags.
900
+ document.head.insertBefore(script, link);
901
+ this.addedCspScriptsDocuments.add(document);
902
+ }
903
+ }
904
+
905
+ /**
906
+ * Enum representing the different contexts in which server rendering can occur.
907
+ */
908
+ var ServerRenderContext;
909
+ (function (ServerRenderContext) {
910
+ ServerRenderContext["SSR"] = "ssr";
911
+ ServerRenderContext["SSG"] = "ssg";
912
+ ServerRenderContext["AppShell"] = "app-shell";
913
+ })(ServerRenderContext || (ServerRenderContext = {}));
914
+ /**
915
+ * Represents a locale-specific Angular server application managed by the server application engine.
916
+ *
917
+ * The `AngularServerApp` class handles server-side rendering and asset management for a specific locale.
918
+ */
919
+ class AngularServerApp {
920
+ constructor() {
921
+ /**
922
+ * Hooks for extending or modifying the behavior of the server application.
923
+ * This instance can be used to attach custom functionality to various events in the server application lifecycle.
924
+ */
925
+ this.hooks = new Hooks();
926
+ /**
927
+ * The manifest associated with this server application.
928
+ */
929
+ this.manifest = getAngularAppManifest();
930
+ /**
931
+ * An instance of ServerAsset that handles server-side asset.
932
+ */
933
+ this.assets = new ServerAssets(this.manifest);
934
+ }
935
+ /**
936
+ * Renders a response for the given HTTP request using the server application.
937
+ *
938
+ * This method processes the request and returns a response based on the specified rendering context.
939
+ *
940
+ * @param request - The incoming HTTP request to be rendered.
941
+ * @param requestContext - Optional additional context for rendering, such as request metadata.
942
+ * @param serverContext - The rendering context.
943
+ *
944
+ * @returns A promise that resolves to the HTTP response object resulting from the rendering, or null if no match is found.
945
+ */
946
+ render(request, requestContext, serverContext = ServerRenderContext.SSR) {
947
+ return Promise.race([
948
+ this.createAbortPromise(request),
949
+ this.handleRendering(request, requestContext, serverContext),
950
+ ]);
951
+ }
952
+ /**
953
+ * Creates a promise that rejects when the request is aborted.
954
+ *
955
+ * @param request - The HTTP request to monitor for abortion.
956
+ * @returns A promise that never resolves but rejects with an `AbortError` if the request is aborted.
957
+ */
958
+ createAbortPromise(request) {
959
+ return new Promise((_, reject) => {
960
+ request.signal.addEventListener('abort', () => {
961
+ const abortError = new Error(`Request for: ${request.url} was aborted.\n${request.signal.reason}`);
962
+ abortError.name = 'AbortError';
963
+ reject(abortError);
964
+ }, { once: true });
965
+ });
966
+ }
967
+ /**
968
+ * Handles the server-side rendering process for the given HTTP request.
969
+ * This method matches the request URL to a route and performs rendering if a matching route is found.
970
+ *
971
+ * @param request - The incoming HTTP request to be processed.
972
+ * @param requestContext - Optional additional context for rendering, such as request metadata.
973
+ * @param serverContext - The rendering context. Defaults to server-side rendering (SSR).
974
+ *
975
+ * @returns A promise that resolves to the rendered response, or null if no matching route is found.
976
+ */
977
+ async handleRendering(request, requestContext, serverContext = ServerRenderContext.SSR) {
978
+ const url = new URL(request.url);
979
+ this.router ??= await ServerRouter.from(this.manifest, url);
980
+ const matchedRoute = this.router.match(url);
981
+ if (!matchedRoute) {
982
+ // Not a known Angular route.
983
+ return null;
984
+ }
985
+ const { redirectTo } = matchedRoute;
986
+ if (redirectTo !== undefined) {
987
+ // 302 Found is used by default for redirections
988
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status
989
+ return Response.redirect(new URL(redirectTo, url), 302);
990
+ }
991
+ const platformProviders = [
992
+ {
993
+ provide: _SERVER_CONTEXT,
994
+ useValue: serverContext,
995
+ },
996
+ {
997
+ // An Angular Console Provider that does not print a set of predefined logs.
998
+ provide: _Console,
999
+ // Using `useClass` would necessitate decorating `Console` with `@Injectable`,
1000
+ // which would require switching from `ts_library` to `ng_module`. This change
1001
+ // would also necessitate various patches of `@angular/bazel` to support ESM.
1002
+ useFactory: () => new Console(),
1003
+ },
1004
+ ];
1005
+ const isSsrMode = serverContext === ServerRenderContext.SSR;
1006
+ const responseInit = {};
1007
+ if (isSsrMode) {
1008
+ platformProviders.push({
1009
+ provide: REQUEST,
1010
+ useValue: request,
1011
+ }, {
1012
+ provide: REQUEST_CONTEXT,
1013
+ useValue: requestContext,
1014
+ }, {
1015
+ provide: RESPONSE_INIT,
1016
+ useValue: responseInit,
1017
+ });
1018
+ }
1019
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
1020
+ // Need to clean up GENERATED_COMP_IDS map in `@angular/core`.
1021
+ // Otherwise an incorrect component ID generation collision detected warning will be displayed in development.
1022
+ // See: https://github.com/angular/angular-cli/issues/25924
1023
+ _resetCompiledComponents();
1024
+ }
1025
+ const { manifest, hooks, assets } = this;
1026
+ let html = await assets.getIndexServerHtml();
1027
+ // Skip extra microtask if there are no pre hooks.
1028
+ if (hooks.has('html:transform:pre')) {
1029
+ html = await hooks.run('html:transform:pre', { html });
1030
+ }
1031
+ html = await renderAngular(html, manifest.bootstrap(), new URL(request.url), platformProviders);
1032
+ if (manifest.inlineCriticalCss) {
1033
+ // Optionally inline critical CSS.
1034
+ this.inlineCriticalCssProcessor ??= new InlineCriticalCssProcessor((path) => {
1035
+ const fileName = path.split('/').pop() ?? path;
1036
+ return this.assets.getServerAsset(fileName);
1037
+ });
1038
+ html = await this.inlineCriticalCssProcessor.process(html);
1039
+ }
1040
+ return new Response(html, responseInit);
1041
+ }
1042
+ }
1043
+ let angularServerApp;
1044
+ /**
1045
+ * Retrieves or creates an instance of `AngularServerApp`.
1046
+ * - If an instance of `AngularServerApp` already exists, it will return the existing one.
1047
+ * - If no instance exists, it will create a new one with the provided options.
1048
+ * @returns The existing or newly created instance of `AngularServerApp`.
1049
+ */
1050
+ function getOrCreateAngularServerApp() {
1051
+ return (angularServerApp ??= new AngularServerApp());
1052
+ }
1053
+ /**
1054
+ * Destroys the existing `AngularServerApp` instance, releasing associated resources and resetting the
1055
+ * reference to `undefined`.
1056
+ *
1057
+ * This function is primarily used to enable the recreation of the `AngularServerApp` instance,
1058
+ * typically when server configuration or application state needs to be refreshed.
1059
+ */
1060
+ function destroyAngularServerApp() {
1061
+ angularServerApp = undefined;
1062
+ }
1063
+
1064
+ /**
1065
+ * Extracts a potential locale ID from a given URL based on the specified base path.
1066
+ *
1067
+ * This function parses the URL to locate a potential locale identifier that immediately
1068
+ * follows the base path segment in the URL's pathname. If the URL does not contain a valid
1069
+ * locale ID, an empty string is returned.
1070
+ *
1071
+ * @param url - The full URL from which to extract the locale ID.
1072
+ * @param basePath - The base path used as the reference point for extracting the locale ID.
1073
+ * @returns The extracted locale ID if present, or an empty string if no valid locale ID is found.
1074
+ *
1075
+ * @example
1076
+ * ```js
1077
+ * const url = new URL('https://example.com/base/en/page');
1078
+ * const basePath = '/base';
1079
+ * const localeId = getPotentialLocaleIdFromUrl(url, basePath);
1080
+ * console.log(localeId); // Output: 'en'
1081
+ * ```
1082
+ */
1083
+ function getPotentialLocaleIdFromUrl(url, basePath) {
1084
+ const { pathname } = url;
1085
+ // Move forward of the base path section.
1086
+ let start = basePath.length;
1087
+ if (pathname[start] === '/') {
1088
+ start++;
1089
+ }
1090
+ // Find the next forward slash.
1091
+ let end = pathname.indexOf('/', start);
1092
+ if (end === -1) {
1093
+ end = pathname.length;
1094
+ }
1095
+ // Extract the potential locale id.
1096
+ return pathname.slice(start, end);
1097
+ }
1098
+
1099
+ /**
1100
+ * Angular server application engine.
1101
+ * Manages Angular server applications (including localized ones), handles rendering requests,
1102
+ * and optionally transforms index HTML before rendering.
1103
+ */
1104
+ class AngularAppEngine {
1105
+ constructor() {
1106
+ /**
1107
+ * The manifest for the server application.
1108
+ */
1109
+ this.manifest = getAngularAppEngineManifest();
1110
+ }
1111
+ /**
1112
+ * Hooks for extending or modifying the behavior of the server application.
1113
+ * These hooks are used by the Angular CLI when running the development server and
1114
+ * provide extensibility points for the application lifecycle.
1115
+ *
1116
+ * @private
1117
+ */
1118
+ static { this.ɵhooks = new Hooks(); }
1119
+ /**
1120
+ * Provides access to the hooks for extending or modifying the server application's behavior.
1121
+ * This allows attaching custom functionality to various server application lifecycle events.
1122
+ *
1123
+ * @internal
1124
+ */
1125
+ get hooks() {
1126
+ return AngularAppEngine.ɵhooks;
1127
+ }
1128
+ /**
1129
+ * Renders a response for the given HTTP request using the server application.
1130
+ *
1131
+ * This method processes the request, determines the appropriate route and rendering context,
1132
+ * and returns an HTTP response.
1133
+ *
1134
+ * If the request URL appears to be for a file (excluding `/index.html`), the method returns `null`.
1135
+ * A request to `https://www.example.com/page/index.html` will render the Angular route
1136
+ * corresponding to `https://www.example.com/page`.
1137
+ *
1138
+ * @param request - The incoming HTTP request object to be rendered.
1139
+ * @param requestContext - Optional additional context for the request, such as metadata.
1140
+ * @returns A promise that resolves to a Response object, or `null` if the request URL represents a file (e.g., `./logo.png`)
1141
+ * rather than an application route.
1142
+ */
1143
+ async render(request, requestContext) {
1144
+ // Skip if the request looks like a file but not `/index.html`.
1145
+ const url = new URL(request.url);
1146
+ const entryPoint = this.getEntryPointFromUrl(url);
1147
+ if (!entryPoint) {
1148
+ return null;
1149
+ }
1150
+ const { ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp } = await entryPoint();
1151
+ // Note: Using `instanceof` is not feasible here because `AngularServerApp` will
1152
+ // be located in separate bundles, making `instanceof` checks unreliable.
1153
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
1154
+ const serverApp = getOrCreateAngularServerApp();
1155
+ serverApp.hooks = this.hooks;
1156
+ return serverApp.render(request, requestContext);
1157
+ }
1158
+ /**
1159
+ * Retrieves the entry point path and locale for the Angular server application based on the provided URL.
1160
+ *
1161
+ * This method determines the appropriate entry point and locale for rendering the application by examining the URL.
1162
+ * If there is only one entry point available, it is returned regardless of the URL.
1163
+ * Otherwise, the method extracts a potential locale identifier from the URL and looks up the corresponding entry point.
1164
+ *
1165
+ * @param url - The URL used to derive the locale and determine the appropriate entry point.
1166
+ * @returns A function that returns a promise resolving to an object with the `EntryPointExports` type,
1167
+ * or `undefined` if no matching entry point is found for the extracted locale.
1168
+ */
1169
+ getEntryPointFromUrl(url) {
1170
+ const { entryPoints, basePath } = this.manifest;
1171
+ if (entryPoints.size === 1) {
1172
+ return entryPoints.values().next().value;
1173
+ }
1174
+ const potentialLocale = getPotentialLocaleIdFromUrl(url, basePath);
1175
+ return entryPoints.get(potentialLocale);
1176
+ }
1177
+ }
1178
+ let angularAppEngine;
1179
+ /**
1180
+ * Retrieves an existing `AngularAppEngine` instance or creates a new one if none exists.
1181
+ *
1182
+ * This method ensures that only a single instance of `AngularAppEngine` is created and reused across
1183
+ * the application lifecycle, providing efficient resource management. If the instance does not exist,
1184
+ * it will be instantiated upon the first call.
1185
+ *
1186
+ * @developerPreview
1187
+ * @returns The existing or newly created instance of `AngularAppEngine`.
1188
+ */
1189
+ function getOrCreateAngularAppEngine() {
1190
+ return (angularAppEngine ??= new AngularAppEngine());
1191
+ }
1192
+ /**
1193
+ * Destroys the current `AngularAppEngine` instance, releasing any associated resources.
1194
+ *
1195
+ * This method resets the reference to the `AngularAppEngine` instance to `undefined`, allowing
1196
+ * a new instance to be created on the next call to `getOrCreateAngularAppEngine()`. It is typically
1197
+ * used when reinitializing the server environment or refreshing the application state is necessary.
1198
+ *
1199
+ * @developerPreview
1200
+ */
1201
+ function destroyAngularAppEngine() {
1202
+ angularAppEngine = undefined;
1203
+ }
1204
+
1205
+ export { destroyAngularAppEngine, getOrCreateAngularAppEngine, AngularAppEngine as ɵAngularAppEngine, InlineCriticalCssProcessor as ɵInlineCriticalCssProcessor, ServerRenderContext as ɵServerRenderContext, destroyAngularServerApp as ɵdestroyAngularServerApp, getOrCreateAngularServerApp as ɵgetOrCreateAngularServerApp, getRoutesFromAngularRouterConfig as ɵgetRoutesFromAngularRouterConfig, setAngularAppEngineManifest as ɵsetAngularAppEngineManifest, setAngularAppManifest as ɵsetAngularAppManifest };
645
1206
  //# sourceMappingURL=ssr.mjs.map