@angular/ssr 21.0.0-next.9 → 21.0.0-rc.0
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/node.mjs +246 -407
- package/fesm2022/node.mjs.map +1 -1
- package/fesm2022/ssr.mjs +1298 -2448
- package/fesm2022/ssr.mjs.map +1 -1
- package/package.json +7 -7
package/fesm2022/ssr.mjs
CHANGED
|
@@ -4,1623 +4,886 @@ import { ActivatedRoute, Router, ROUTES, ɵloadChildren as _loadChildren } from
|
|
|
4
4
|
import { PlatformLocation, APP_BASE_HREF } from '@angular/common';
|
|
5
5
|
import Beasties from '../third_party/beasties/index.js';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Manages server-side assets.
|
|
9
|
-
*/
|
|
10
7
|
class ServerAssets {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const asset = this.manifest.assets[path];
|
|
29
|
-
if (!asset) {
|
|
30
|
-
throw new Error(`Server asset '${path}' does not exist.`);
|
|
31
|
-
}
|
|
32
|
-
return asset;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Checks if a specific server-side asset exists.
|
|
36
|
-
*
|
|
37
|
-
* @param path - The path to the server asset.
|
|
38
|
-
* @returns A boolean indicating whether the asset exists.
|
|
39
|
-
*/
|
|
40
|
-
hasServerAsset(path) {
|
|
41
|
-
return !!this.manifest.assets[path];
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Retrieves the asset for 'index.server.html'.
|
|
45
|
-
*
|
|
46
|
-
* @returns The `ServerAsset` object for 'index.server.html'.
|
|
47
|
-
* @throws Error - Throws an error if 'index.server.html' does not exist.
|
|
48
|
-
*/
|
|
49
|
-
getIndexServerHtml() {
|
|
50
|
-
return this.getServerAsset('index.server.html');
|
|
51
|
-
}
|
|
8
|
+
manifest;
|
|
9
|
+
constructor(manifest) {
|
|
10
|
+
this.manifest = manifest;
|
|
11
|
+
}
|
|
12
|
+
getServerAsset(path) {
|
|
13
|
+
const asset = this.manifest.assets[path];
|
|
14
|
+
if (!asset) {
|
|
15
|
+
throw new Error(`Server asset '${path}' does not exist.`);
|
|
16
|
+
}
|
|
17
|
+
return asset;
|
|
18
|
+
}
|
|
19
|
+
hasServerAsset(path) {
|
|
20
|
+
return !!this.manifest.assets[path];
|
|
21
|
+
}
|
|
22
|
+
getIndexServerHtml() {
|
|
23
|
+
return this.getServerAsset('index.server.html');
|
|
24
|
+
}
|
|
52
25
|
}
|
|
53
26
|
|
|
54
|
-
/**
|
|
55
|
-
* A set of log messages that should be ignored and not printed to the console.
|
|
56
|
-
*/
|
|
57
27
|
const IGNORED_LOGS = new Set(['Angular is running in development mode.']);
|
|
58
|
-
/**
|
|
59
|
-
* Custom implementation of the Angular Console service that filters out specific log messages.
|
|
60
|
-
*
|
|
61
|
-
* This class extends the internal Angular `ɵConsole` class to provide customized logging behavior.
|
|
62
|
-
* It overrides the `log` method to suppress logs that match certain predefined messages.
|
|
63
|
-
*/
|
|
64
28
|
class Console extends _Console {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
* @param message - The message to log to the console.
|
|
69
|
-
*
|
|
70
|
-
* This method overrides the `log` method of the `ɵConsole` class. It checks if the
|
|
71
|
-
* message is in the `IGNORED_LOGS` set. If it is not, it delegates the logging to
|
|
72
|
-
* the parent class's `log` method. Otherwise, the message is suppressed.
|
|
73
|
-
*/
|
|
74
|
-
log(message) {
|
|
75
|
-
if (!IGNORED_LOGS.has(message)) {
|
|
76
|
-
super.log(message);
|
|
77
|
-
}
|
|
29
|
+
log(message) {
|
|
30
|
+
if (!IGNORED_LOGS.has(message)) {
|
|
31
|
+
super.log(message);
|
|
78
32
|
}
|
|
33
|
+
}
|
|
79
34
|
}
|
|
80
35
|
|
|
81
|
-
/**
|
|
82
|
-
* The Angular app manifest object.
|
|
83
|
-
* This is used internally to store the current Angular app manifest.
|
|
84
|
-
*/
|
|
85
36
|
let angularAppManifest;
|
|
86
|
-
/**
|
|
87
|
-
* Sets the Angular app manifest.
|
|
88
|
-
*
|
|
89
|
-
* @param manifest - The manifest object to set for the Angular application.
|
|
90
|
-
*/
|
|
91
37
|
function setAngularAppManifest(manifest) {
|
|
92
|
-
|
|
38
|
+
angularAppManifest = manifest;
|
|
93
39
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Gets the Angular app manifest.
|
|
96
|
-
*
|
|
97
|
-
* @returns The Angular app manifest.
|
|
98
|
-
* @throws Will throw an error if the Angular app manifest is not set.
|
|
99
|
-
*/
|
|
100
40
|
function getAngularAppManifest() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return angularAppManifest;
|
|
41
|
+
if (!angularAppManifest) {
|
|
42
|
+
throw new Error('Angular app manifest is not set. ' + `Please ensure you are using the '@angular/build:application' builder to build your server application.`);
|
|
43
|
+
}
|
|
44
|
+
return angularAppManifest;
|
|
106
45
|
}
|
|
107
|
-
/**
|
|
108
|
-
* The Angular app engine manifest object.
|
|
109
|
-
* This is used internally to store the current Angular app engine manifest.
|
|
110
|
-
*/
|
|
111
46
|
let angularAppEngineManifest;
|
|
112
|
-
/**
|
|
113
|
-
* Sets the Angular app engine manifest.
|
|
114
|
-
*
|
|
115
|
-
* @param manifest - The engine manifest object to set.
|
|
116
|
-
*/
|
|
117
47
|
function setAngularAppEngineManifest(manifest) {
|
|
118
|
-
|
|
48
|
+
angularAppEngineManifest = manifest;
|
|
119
49
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Gets the Angular app engine manifest.
|
|
122
|
-
*
|
|
123
|
-
* @returns The Angular app engine manifest.
|
|
124
|
-
* @throws Will throw an error if the Angular app engine manifest is not set.
|
|
125
|
-
*/
|
|
126
50
|
function getAngularAppEngineManifest() {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return angularAppEngineManifest;
|
|
51
|
+
if (!angularAppEngineManifest) {
|
|
52
|
+
throw new Error('Angular app engine manifest is not set. ' + `Please ensure you are using the '@angular/build:application' builder to build your server application.`);
|
|
53
|
+
}
|
|
54
|
+
return angularAppEngineManifest;
|
|
132
55
|
}
|
|
133
56
|
|
|
134
|
-
/**
|
|
135
|
-
* Removes the trailing slash from a URL if it exists.
|
|
136
|
-
*
|
|
137
|
-
* @param url - The URL string from which to remove the trailing slash.
|
|
138
|
-
* @returns The URL string without a trailing slash.
|
|
139
|
-
*
|
|
140
|
-
* @example
|
|
141
|
-
* ```js
|
|
142
|
-
* stripTrailingSlash('path/'); // 'path'
|
|
143
|
-
* stripTrailingSlash('/path'); // '/path'
|
|
144
|
-
* stripTrailingSlash('/'); // '/'
|
|
145
|
-
* stripTrailingSlash(''); // ''
|
|
146
|
-
* ```
|
|
147
|
-
*/
|
|
148
57
|
function stripTrailingSlash(url) {
|
|
149
|
-
|
|
150
|
-
return url.length > 1 && url[url.length - 1] === '/' ? url.slice(0, -1) : url;
|
|
58
|
+
return url.length > 1 && url[url.length - 1] === '/' ? url.slice(0, -1) : url;
|
|
151
59
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Removes the leading slash from a URL if it exists.
|
|
154
|
-
*
|
|
155
|
-
* @param url - The URL string from which to remove the leading slash.
|
|
156
|
-
* @returns The URL string without a leading slash.
|
|
157
|
-
*
|
|
158
|
-
* @example
|
|
159
|
-
* ```js
|
|
160
|
-
* stripLeadingSlash('/path'); // 'path'
|
|
161
|
-
* stripLeadingSlash('/path/'); // 'path/'
|
|
162
|
-
* stripLeadingSlash('/'); // '/'
|
|
163
|
-
* stripLeadingSlash(''); // ''
|
|
164
|
-
* ```
|
|
165
|
-
*/
|
|
166
60
|
function stripLeadingSlash(url) {
|
|
167
|
-
|
|
168
|
-
return url.length > 1 && url[0] === '/' ? url.slice(1) : url;
|
|
61
|
+
return url.length > 1 && url[0] === '/' ? url.slice(1) : url;
|
|
169
62
|
}
|
|
170
|
-
/**
|
|
171
|
-
* Adds a leading slash to a URL if it does not already have one.
|
|
172
|
-
*
|
|
173
|
-
* @param url - The URL string to which the leading slash will be added.
|
|
174
|
-
* @returns The URL string with a leading slash.
|
|
175
|
-
*
|
|
176
|
-
* @example
|
|
177
|
-
* ```js
|
|
178
|
-
* addLeadingSlash('path'); // '/path'
|
|
179
|
-
* addLeadingSlash('/path'); // '/path'
|
|
180
|
-
* ```
|
|
181
|
-
*/
|
|
182
63
|
function addLeadingSlash(url) {
|
|
183
|
-
|
|
184
|
-
return url[0] === '/' ? url : `/${url}`;
|
|
64
|
+
return url[0] === '/' ? url : `/${url}`;
|
|
185
65
|
}
|
|
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
66
|
function addTrailingSlash(url) {
|
|
199
|
-
|
|
200
|
-
return url[url.length - 1] === '/' ? url : `${url}/`;
|
|
67
|
+
return url[url.length - 1] === '/' ? url : `${url}/`;
|
|
201
68
|
}
|
|
202
|
-
/**
|
|
203
|
-
* Joins URL parts into a single URL string.
|
|
204
|
-
*
|
|
205
|
-
* This function takes multiple URL segments, normalizes them by removing leading
|
|
206
|
-
* and trailing slashes where appropriate, and then joins them into a single URL.
|
|
207
|
-
*
|
|
208
|
-
* @param parts - The parts of the URL to join. Each part can be a string with or without slashes.
|
|
209
|
-
* @returns The joined URL string, with normalized slashes.
|
|
210
|
-
*
|
|
211
|
-
* @example
|
|
212
|
-
* ```js
|
|
213
|
-
* joinUrlParts('path/', '/to/resource'); // '/path/to/resource'
|
|
214
|
-
* joinUrlParts('/path/', 'to/resource'); // '/path/to/resource'
|
|
215
|
-
* joinUrlParts('', ''); // '/'
|
|
216
|
-
* ```
|
|
217
|
-
*/
|
|
218
69
|
function joinUrlParts(...parts) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
normalizedPart = normalizedPart.slice(0, -1);
|
|
231
|
-
}
|
|
232
|
-
if (normalizedPart !== '') {
|
|
233
|
-
normalizeParts.push(normalizedPart);
|
|
234
|
-
}
|
|
70
|
+
const normalizeParts = [];
|
|
71
|
+
for (const part of parts) {
|
|
72
|
+
if (part === '') {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
let normalizedPart = part;
|
|
76
|
+
if (part[0] === '/') {
|
|
77
|
+
normalizedPart = normalizedPart.slice(1);
|
|
78
|
+
}
|
|
79
|
+
if (part[part.length - 1] === '/') {
|
|
80
|
+
normalizedPart = normalizedPart.slice(0, -1);
|
|
235
81
|
}
|
|
236
|
-
|
|
82
|
+
if (normalizedPart !== '') {
|
|
83
|
+
normalizeParts.push(normalizedPart);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return addLeadingSlash(normalizeParts.join('/'));
|
|
237
87
|
}
|
|
238
|
-
/**
|
|
239
|
-
* Strips `/index.html` from the end of a URL's path, if present.
|
|
240
|
-
*
|
|
241
|
-
* This function is used to convert URLs pointing to an `index.html` file into their directory
|
|
242
|
-
* equivalents. For example, it transforms a URL like `http://www.example.com/page/index.html`
|
|
243
|
-
* into `http://www.example.com/page`.
|
|
244
|
-
*
|
|
245
|
-
* @param url - The URL object to process.
|
|
246
|
-
* @returns A new URL object with `/index.html` removed from the path, if it was present.
|
|
247
|
-
*
|
|
248
|
-
* @example
|
|
249
|
-
* ```typescript
|
|
250
|
-
* const originalUrl = new URL('http://www.example.com/page/index.html');
|
|
251
|
-
* const cleanedUrl = stripIndexHtmlFromURL(originalUrl);
|
|
252
|
-
* console.log(cleanedUrl.href); // Output: 'http://www.example.com/page'
|
|
253
|
-
* ```
|
|
254
|
-
*/
|
|
255
88
|
function stripIndexHtmlFromURL(url) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return url;
|
|
89
|
+
if (url.pathname.endsWith('/index.html')) {
|
|
90
|
+
const modifiedURL = new URL(url);
|
|
91
|
+
modifiedURL.pathname = modifiedURL.pathname.slice(0, -11);
|
|
92
|
+
return modifiedURL;
|
|
93
|
+
}
|
|
94
|
+
return url;
|
|
263
95
|
}
|
|
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
96
|
function buildPathWithParams(toPath, fromPath) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
97
|
+
if (toPath[0] !== '/') {
|
|
98
|
+
throw new Error(`Invalid toPath: The string must start with a '/'. Received: '${toPath}'`);
|
|
99
|
+
}
|
|
100
|
+
if (fromPath[0] !== '/') {
|
|
101
|
+
throw new Error(`Invalid fromPath: The string must start with a '/'. Received: '${fromPath}'`);
|
|
102
|
+
}
|
|
103
|
+
if (!toPath.includes('/*')) {
|
|
104
|
+
return toPath;
|
|
105
|
+
}
|
|
106
|
+
const fromPathParts = fromPath.split('/');
|
|
107
|
+
const toPathParts = toPath.split('/');
|
|
108
|
+
const resolvedParts = toPathParts.map((part, index) => toPathParts[index] === '*' ? fromPathParts[index] : part);
|
|
109
|
+
return joinUrlParts(...resolvedParts);
|
|
307
110
|
}
|
|
308
111
|
const MATRIX_PARAMS_REGEX = /;[^/]+/g;
|
|
309
|
-
/**
|
|
310
|
-
* Removes Angular matrix parameters from a given URL path.
|
|
311
|
-
*
|
|
312
|
-
* This function takes a URL path string and removes any matrix parameters.
|
|
313
|
-
* Matrix parameters are parts of a URL segment that start with a semicolon `;`.
|
|
314
|
-
*
|
|
315
|
-
* @param pathname - The URL path to remove matrix parameters from.
|
|
316
|
-
* @returns The URL path with matrix parameters removed.
|
|
317
|
-
*
|
|
318
|
-
* @example
|
|
319
|
-
* ```ts
|
|
320
|
-
* stripMatrixParams('/path;param=value'); // returns '/path'
|
|
321
|
-
* stripMatrixParams('/path;param=value/to;p=1/resource'); // returns '/path/to/resource'
|
|
322
|
-
* stripMatrixParams('/path/to/resource'); // returns '/path/to/resource'
|
|
323
|
-
* ```
|
|
324
|
-
*/
|
|
325
112
|
function stripMatrixParams(pathname) {
|
|
326
|
-
|
|
327
|
-
// This regex finds all occurrences of a semicolon followed by any characters
|
|
328
|
-
return pathname.includes(';') ? pathname.replace(MATRIX_PARAMS_REGEX, '') : pathname;
|
|
113
|
+
return pathname.includes(';') ? pathname.replace(MATRIX_PARAMS_REGEX, '') : pathname;
|
|
329
114
|
}
|
|
330
115
|
|
|
331
|
-
/**
|
|
332
|
-
* Renders an Angular application or module to an HTML string.
|
|
333
|
-
*
|
|
334
|
-
* This function determines whether the provided `bootstrap` value is an Angular module
|
|
335
|
-
* or a bootstrap function and invokes the appropriate rendering method (`renderModule` or `renderApplication`).
|
|
336
|
-
*
|
|
337
|
-
* @param html - The initial HTML document content.
|
|
338
|
-
* @param bootstrap - An Angular module type or a function returning a promise that resolves to an `ApplicationRef`.
|
|
339
|
-
* @param url - The application URL, used for route-based rendering in SSR.
|
|
340
|
-
* @param platformProviders - An array of platform providers for the rendering process.
|
|
341
|
-
* @param serverContext - A string representing the server context, providing additional metadata for SSR.
|
|
342
|
-
* @returns A promise resolving to an object containing:
|
|
343
|
-
* - `hasNavigationError`: Indicates if a navigation error occurred.
|
|
344
|
-
* - `redirectTo`: (Optional) The redirect URL if a navigation redirect occurred.
|
|
345
|
-
* - `content`: A function returning a promise that resolves to the rendered HTML string.
|
|
346
|
-
*/
|
|
347
116
|
async function renderAngular(html, bootstrap, url, platformProviders, serverContext) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
else if (lastSuccessfulNavigation) {
|
|
394
|
-
hasNavigationError = false;
|
|
395
|
-
const { pathname, search, hash } = envInjector.get(PlatformLocation);
|
|
396
|
-
const finalUrl = [stripTrailingSlash(pathname), search, hash].join('');
|
|
397
|
-
if (urlToRender.href !== new URL(finalUrl, urlToRender.origin).href) {
|
|
398
|
-
redirectTo = finalUrl;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
return {
|
|
402
|
-
hasNavigationError,
|
|
403
|
-
redirectTo,
|
|
404
|
-
content: () => new Promise((resolve, reject) => {
|
|
405
|
-
// Defer rendering to the next event loop iteration to avoid blocking, as most operations in `renderInternal` are synchronous.
|
|
406
|
-
setTimeout(() => {
|
|
407
|
-
_renderInternal(platformRef, applicationRef)
|
|
408
|
-
.then(resolve)
|
|
409
|
-
.catch(reject)
|
|
410
|
-
.finally(() => void asyncDestroyPlatform(platformRef));
|
|
411
|
-
}, 0);
|
|
412
|
-
}),
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
catch (error) {
|
|
416
|
-
await asyncDestroyPlatform(platformRef);
|
|
417
|
-
throw error;
|
|
418
|
-
}
|
|
419
|
-
finally {
|
|
420
|
-
if (hasNavigationError || redirectTo) {
|
|
421
|
-
void asyncDestroyPlatform(platformRef);
|
|
422
|
-
}
|
|
117
|
+
const urlToRender = stripIndexHtmlFromURL(url);
|
|
118
|
+
const platformRef = platformServer([{
|
|
119
|
+
provide: INITIAL_CONFIG,
|
|
120
|
+
useValue: {
|
|
121
|
+
url: urlToRender.href,
|
|
122
|
+
document: html
|
|
123
|
+
}
|
|
124
|
+
}, {
|
|
125
|
+
provide: _SERVER_CONTEXT,
|
|
126
|
+
useValue: serverContext
|
|
127
|
+
}, {
|
|
128
|
+
provide: _Console,
|
|
129
|
+
useFactory: () => new Console()
|
|
130
|
+
}, ...platformProviders]);
|
|
131
|
+
let redirectTo;
|
|
132
|
+
let hasNavigationError = true;
|
|
133
|
+
try {
|
|
134
|
+
let applicationRef;
|
|
135
|
+
if (isNgModule(bootstrap)) {
|
|
136
|
+
const moduleRef = await platformRef.bootstrapModule(bootstrap);
|
|
137
|
+
applicationRef = moduleRef.injector.get(ApplicationRef);
|
|
138
|
+
} else {
|
|
139
|
+
applicationRef = await bootstrap({
|
|
140
|
+
platformRef
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
await applicationRef.whenStable();
|
|
144
|
+
const envInjector = applicationRef.injector;
|
|
145
|
+
const routerIsProvided = !!envInjector.get(ActivatedRoute, null);
|
|
146
|
+
const router = envInjector.get(Router);
|
|
147
|
+
const lastSuccessfulNavigation = router.lastSuccessfulNavigation();
|
|
148
|
+
if (!routerIsProvided) {
|
|
149
|
+
hasNavigationError = false;
|
|
150
|
+
} else if (lastSuccessfulNavigation) {
|
|
151
|
+
hasNavigationError = false;
|
|
152
|
+
const {
|
|
153
|
+
pathname,
|
|
154
|
+
search,
|
|
155
|
+
hash
|
|
156
|
+
} = envInjector.get(PlatformLocation);
|
|
157
|
+
const finalUrl = [stripTrailingSlash(pathname), search, hash].join('');
|
|
158
|
+
if (urlToRender.href !== new URL(finalUrl, urlToRender.origin).href) {
|
|
159
|
+
redirectTo = finalUrl;
|
|
160
|
+
}
|
|
423
161
|
}
|
|
162
|
+
return {
|
|
163
|
+
hasNavigationError,
|
|
164
|
+
redirectTo,
|
|
165
|
+
content: () => new Promise((resolve, reject) => {
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
_renderInternal(platformRef, applicationRef).then(resolve).catch(reject).finally(() => void asyncDestroyPlatform(platformRef));
|
|
168
|
+
}, 0);
|
|
169
|
+
})
|
|
170
|
+
};
|
|
171
|
+
} catch (error) {
|
|
172
|
+
await asyncDestroyPlatform(platformRef);
|
|
173
|
+
throw error;
|
|
174
|
+
} finally {
|
|
175
|
+
if (hasNavigationError || redirectTo) {
|
|
176
|
+
void asyncDestroyPlatform(platformRef);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
424
179
|
}
|
|
425
|
-
/**
|
|
426
|
-
* Type guard to determine if a given value is an Angular module.
|
|
427
|
-
* Angular modules are identified by the presence of the `ɵmod` static property.
|
|
428
|
-
* This function helps distinguish between Angular modules and bootstrap functions.
|
|
429
|
-
*
|
|
430
|
-
* @param value - The value to be checked.
|
|
431
|
-
* @returns True if the value is an Angular module (i.e., it has the `ɵmod` property), false otherwise.
|
|
432
|
-
*/
|
|
433
180
|
function isNgModule(value) {
|
|
434
|
-
|
|
181
|
+
return 'ɵmod' in value;
|
|
435
182
|
}
|
|
436
|
-
/**
|
|
437
|
-
* Gracefully destroys the application in a macrotask, allowing pending promises to resolve
|
|
438
|
-
* and surfacing any potential errors to the user.
|
|
439
|
-
*
|
|
440
|
-
* @param platformRef - The platform reference to be destroyed.
|
|
441
|
-
*/
|
|
442
183
|
function asyncDestroyPlatform(platformRef) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
184
|
+
return new Promise(resolve => {
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
if (!platformRef.destroyed) {
|
|
187
|
+
platformRef.destroy();
|
|
188
|
+
}
|
|
189
|
+
resolve();
|
|
190
|
+
}, 0);
|
|
191
|
+
});
|
|
451
192
|
}
|
|
452
193
|
|
|
453
|
-
/**
|
|
454
|
-
* Creates a promise that resolves with the result of the provided `promise` or rejects with an
|
|
455
|
-
* `AbortError` if the `AbortSignal` is triggered before the promise resolves.
|
|
456
|
-
*
|
|
457
|
-
* @param promise - The promise to monitor for completion.
|
|
458
|
-
* @param signal - An `AbortSignal` used to monitor for an abort event. If the signal is aborted,
|
|
459
|
-
* the returned promise will reject.
|
|
460
|
-
* @param errorMessagePrefix - A custom message prefix to include in the error message when the operation is aborted.
|
|
461
|
-
* @returns A promise that either resolves with the value of the provided `promise` or rejects with
|
|
462
|
-
* an `AbortError` if the `AbortSignal` is triggered.
|
|
463
|
-
*
|
|
464
|
-
* @throws {AbortError} If the `AbortSignal` is triggered before the `promise` resolves.
|
|
465
|
-
*/
|
|
466
194
|
function promiseWithAbort(promise, signal, errorMessagePrefix) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
.finally(() => {
|
|
481
|
-
signal.removeEventListener('abort', abortHandler);
|
|
482
|
-
});
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
const abortHandler = () => {
|
|
197
|
+
reject(new DOMException(`${errorMessagePrefix} was aborted.\n${signal.reason}`, 'AbortError'));
|
|
198
|
+
};
|
|
199
|
+
if (signal.aborted) {
|
|
200
|
+
abortHandler();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
signal.addEventListener('abort', abortHandler, {
|
|
204
|
+
once: true
|
|
205
|
+
});
|
|
206
|
+
promise.then(resolve).catch(reject).finally(() => {
|
|
207
|
+
signal.removeEventListener('abort', abortHandler);
|
|
483
208
|
});
|
|
209
|
+
});
|
|
484
210
|
}
|
|
485
211
|
|
|
486
|
-
/**
|
|
487
|
-
* The internal path used for the app shell route.
|
|
488
|
-
* @internal
|
|
489
|
-
*/
|
|
490
212
|
const APP_SHELL_ROUTE = 'ng-app-shell';
|
|
491
|
-
/**
|
|
492
|
-
* Identifies a particular kind of `ServerRenderingFeatureKind`.
|
|
493
|
-
* @see {@link ServerRenderingFeature}
|
|
494
|
-
*/
|
|
495
213
|
var ServerRenderingFeatureKind;
|
|
496
214
|
(function (ServerRenderingFeatureKind) {
|
|
497
|
-
|
|
498
|
-
|
|
215
|
+
ServerRenderingFeatureKind[ServerRenderingFeatureKind["AppShell"] = 0] = "AppShell";
|
|
216
|
+
ServerRenderingFeatureKind[ServerRenderingFeatureKind["ServerRoutes"] = 1] = "ServerRoutes";
|
|
499
217
|
})(ServerRenderingFeatureKind || (ServerRenderingFeatureKind = {}));
|
|
500
|
-
/**
|
|
501
|
-
* Different rendering modes for server routes.
|
|
502
|
-
* @see {@link withRoutes}
|
|
503
|
-
* @see {@link ServerRoute}
|
|
504
|
-
*/
|
|
505
218
|
var RenderMode;
|
|
506
219
|
(function (RenderMode) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
RenderMode[RenderMode["Client"] = 1] = "Client";
|
|
511
|
-
/** Static Site Generation (SSG) mode, where content is pre-rendered at build time and served as static files. */
|
|
512
|
-
RenderMode[RenderMode["Prerender"] = 2] = "Prerender";
|
|
220
|
+
RenderMode[RenderMode["Server"] = 0] = "Server";
|
|
221
|
+
RenderMode[RenderMode["Client"] = 1] = "Client";
|
|
222
|
+
RenderMode[RenderMode["Prerender"] = 2] = "Prerender";
|
|
513
223
|
})(RenderMode || (RenderMode = {}));
|
|
514
|
-
/**
|
|
515
|
-
* Defines the fallback strategies for Static Site Generation (SSG) routes when a pre-rendered path is not available.
|
|
516
|
-
* This is particularly relevant for routes with parameterized URLs where some paths might not be pre-rendered at build time.
|
|
517
|
-
* @see {@link ServerRoutePrerenderWithParams}
|
|
518
|
-
*/
|
|
519
224
|
var PrerenderFallback;
|
|
520
225
|
(function (PrerenderFallback) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
*/
|
|
525
|
-
PrerenderFallback[PrerenderFallback["Server"] = 0] = "Server";
|
|
526
|
-
/**
|
|
527
|
-
* Fallback to Client-Side Rendering (CSR) if the pre-rendered path is not available.
|
|
528
|
-
* This strategy allows the page to be rendered on the client side.
|
|
529
|
-
*/
|
|
530
|
-
PrerenderFallback[PrerenderFallback["Client"] = 1] = "Client";
|
|
531
|
-
/**
|
|
532
|
-
* No fallback; if the path is not pre-rendered, the server will not handle the request.
|
|
533
|
-
* This means the application will not provide any response for paths that are not pre-rendered.
|
|
534
|
-
*/
|
|
535
|
-
PrerenderFallback[PrerenderFallback["None"] = 2] = "None";
|
|
226
|
+
PrerenderFallback[PrerenderFallback["Server"] = 0] = "Server";
|
|
227
|
+
PrerenderFallback[PrerenderFallback["Client"] = 1] = "Client";
|
|
228
|
+
PrerenderFallback[PrerenderFallback["None"] = 2] = "None";
|
|
536
229
|
})(PrerenderFallback || (PrerenderFallback = {}));
|
|
537
|
-
/**
|
|
538
|
-
* Token for providing the server routes configuration.
|
|
539
|
-
* @internal
|
|
540
|
-
*/
|
|
541
230
|
const SERVER_ROUTES_CONFIG = new InjectionToken('SERVER_ROUTES_CONFIG');
|
|
542
|
-
/**
|
|
543
|
-
* Configures server-side routing for the application.
|
|
544
|
-
*
|
|
545
|
-
* This function registers an array of `ServerRoute` definitions, enabling server-side rendering
|
|
546
|
-
* for specific URL paths. These routes are used to pre-render content on the server, improving
|
|
547
|
-
* initial load performance and SEO.
|
|
548
|
-
*
|
|
549
|
-
* @param routes - An array of `ServerRoute` objects, each defining a server-rendered route.
|
|
550
|
-
* @returns A `ServerRenderingFeature` object configuring server-side routes.
|
|
551
|
-
*
|
|
552
|
-
* @example
|
|
553
|
-
* ```ts
|
|
554
|
-
* import { provideServerRendering, withRoutes, ServerRoute, RenderMode } from '@angular/ssr';
|
|
555
|
-
*
|
|
556
|
-
* const serverRoutes: ServerRoute[] = [
|
|
557
|
-
* {
|
|
558
|
-
* path: '', // This renders the "/" route on the client (CSR)
|
|
559
|
-
* renderMode: RenderMode.Client,
|
|
560
|
-
* },
|
|
561
|
-
* {
|
|
562
|
-
* path: 'about', // This page is static, so we prerender it (SSG)
|
|
563
|
-
* renderMode: RenderMode.Prerender,
|
|
564
|
-
* },
|
|
565
|
-
* {
|
|
566
|
-
* path: 'profile', // This page requires user-specific data, so we use SSR
|
|
567
|
-
* renderMode: RenderMode.Server,
|
|
568
|
-
* },
|
|
569
|
-
* {
|
|
570
|
-
* path: '**', // All other routes will be rendered on the server (SSR)
|
|
571
|
-
* renderMode: RenderMode.Server,
|
|
572
|
-
* },
|
|
573
|
-
* ];
|
|
574
|
-
*
|
|
575
|
-
* provideServerRendering(withRoutes(serverRoutes));
|
|
576
|
-
* ```
|
|
577
|
-
*
|
|
578
|
-
* @see {@link provideServerRendering}
|
|
579
|
-
* @see {@link ServerRoute}
|
|
580
|
-
*/
|
|
581
231
|
function withRoutes(routes) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
232
|
+
const config = {
|
|
233
|
+
routes
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
ɵkind: ServerRenderingFeatureKind.ServerRoutes,
|
|
237
|
+
ɵproviders: [{
|
|
238
|
+
provide: SERVER_ROUTES_CONFIG,
|
|
239
|
+
useValue: config
|
|
240
|
+
}]
|
|
241
|
+
};
|
|
592
242
|
}
|
|
593
|
-
/**
|
|
594
|
-
* Configures the shell of the application.
|
|
595
|
-
*
|
|
596
|
-
* The app shell is a minimal, static HTML page that is served immediately, while the
|
|
597
|
-
* full Angular application loads in the background. This improves perceived performance
|
|
598
|
-
* by providing instant feedback to the user.
|
|
599
|
-
*
|
|
600
|
-
* This function configures the app shell route, which serves the provided component for
|
|
601
|
-
* requests that do not match any defined server routes.
|
|
602
|
-
*
|
|
603
|
-
* @param component - The Angular component to render for the app shell. Can be a direct
|
|
604
|
-
* component type or a dynamic import function.
|
|
605
|
-
* @returns A `ServerRenderingFeature` object configuring the app shell.
|
|
606
|
-
*
|
|
607
|
-
* @example
|
|
608
|
-
* ```ts
|
|
609
|
-
* import { provideServerRendering, withAppShell, withRoutes } from '@angular/ssr';
|
|
610
|
-
* import { AppShellComponent } from './app-shell.component';
|
|
611
|
-
*
|
|
612
|
-
* provideServerRendering(
|
|
613
|
-
* withRoutes(serverRoutes),
|
|
614
|
-
* withAppShell(AppShellComponent)
|
|
615
|
-
* );
|
|
616
|
-
* ```
|
|
617
|
-
*
|
|
618
|
-
* @example
|
|
619
|
-
* ```ts
|
|
620
|
-
* import { provideServerRendering, withAppShell, withRoutes } from '@angular/ssr';
|
|
621
|
-
*
|
|
622
|
-
* provideServerRendering(
|
|
623
|
-
* withRoutes(serverRoutes),
|
|
624
|
-
* withAppShell(() =>
|
|
625
|
-
* import('./app-shell.component').then((m) => m.AppShellComponent)
|
|
626
|
-
* )
|
|
627
|
-
* );
|
|
628
|
-
* ```
|
|
629
|
-
*
|
|
630
|
-
* @see {@link provideServerRendering}
|
|
631
|
-
* @see {@link https://angular.dev/ecosystem/service-workers/app-shell App shell pattern on Angular.dev}
|
|
632
|
-
*/
|
|
633
243
|
function withAppShell(component) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
config.appShellRoute = APP_SHELL_ROUTE;
|
|
654
|
-
}),
|
|
655
|
-
],
|
|
656
|
-
};
|
|
244
|
+
const routeConfig = {
|
|
245
|
+
path: APP_SHELL_ROUTE
|
|
246
|
+
};
|
|
247
|
+
if ('ɵcmp' in component) {
|
|
248
|
+
routeConfig.component = component;
|
|
249
|
+
} else {
|
|
250
|
+
routeConfig.loadComponent = component;
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
ɵkind: ServerRenderingFeatureKind.AppShell,
|
|
254
|
+
ɵproviders: [{
|
|
255
|
+
provide: ROUTES,
|
|
256
|
+
useValue: routeConfig,
|
|
257
|
+
multi: true
|
|
258
|
+
}, provideEnvironmentInitializer(() => {
|
|
259
|
+
const config = inject(SERVER_ROUTES_CONFIG);
|
|
260
|
+
config.appShellRoute = APP_SHELL_ROUTE;
|
|
261
|
+
})]
|
|
262
|
+
};
|
|
657
263
|
}
|
|
658
|
-
/**
|
|
659
|
-
* Configures server-side rendering for an Angular application.
|
|
660
|
-
*
|
|
661
|
-
* This function sets up the necessary providers for server-side rendering, including
|
|
662
|
-
* support for server routes and app shell. It combines features configured using
|
|
663
|
-
* `withRoutes` and `withAppShell` to provide a comprehensive server-side rendering setup.
|
|
664
|
-
*
|
|
665
|
-
* @param features - Optional features to configure additional server rendering behaviors.
|
|
666
|
-
* @returns An `EnvironmentProviders` instance with the server-side rendering configuration.
|
|
667
|
-
*
|
|
668
|
-
* @example
|
|
669
|
-
* Basic example of how you can enable server-side rendering in your application
|
|
670
|
-
* when using the `bootstrapApplication` function:
|
|
671
|
-
*
|
|
672
|
-
* ```ts
|
|
673
|
-
* import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser';
|
|
674
|
-
* import { provideServerRendering, withRoutes, withAppShell } from '@angular/ssr';
|
|
675
|
-
* import { AppComponent } from './app/app.component';
|
|
676
|
-
* import { SERVER_ROUTES } from './app/app.server.routes';
|
|
677
|
-
* import { AppShellComponent } from './app/app-shell.component';
|
|
678
|
-
*
|
|
679
|
-
* const bootstrap = (context: BootstrapContext) =>
|
|
680
|
-
* bootstrapApplication(AppComponent, {
|
|
681
|
-
* providers: [
|
|
682
|
-
* provideServerRendering(
|
|
683
|
-
* withRoutes(SERVER_ROUTES),
|
|
684
|
-
* withAppShell(AppShellComponent),
|
|
685
|
-
* ),
|
|
686
|
-
* ],
|
|
687
|
-
* }, context);
|
|
688
|
-
*
|
|
689
|
-
* export default bootstrap;
|
|
690
|
-
* ```
|
|
691
|
-
* @see {@link withRoutes} configures server-side routing
|
|
692
|
-
* @see {@link withAppShell} configures the application shell
|
|
693
|
-
*/
|
|
694
264
|
function provideServerRendering(...features) {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
265
|
+
let hasAppShell = false;
|
|
266
|
+
let hasServerRoutes = false;
|
|
267
|
+
const providers = [provideServerRendering$1()];
|
|
268
|
+
for (const {
|
|
269
|
+
ɵkind,
|
|
270
|
+
ɵproviders
|
|
271
|
+
} of features) {
|
|
272
|
+
hasAppShell ||= ɵkind === ServerRenderingFeatureKind.AppShell;
|
|
273
|
+
hasServerRoutes ||= ɵkind === ServerRenderingFeatureKind.ServerRoutes;
|
|
274
|
+
providers.push(...ɵproviders);
|
|
275
|
+
}
|
|
276
|
+
if (!hasServerRoutes && hasAppShell) {
|
|
277
|
+
throw new Error(`Configuration error: found 'withAppShell()' without 'withRoutes()' in the same call to 'provideServerRendering()'.` + `The 'withAppShell()' function requires 'withRoutes()' to be used.`);
|
|
278
|
+
}
|
|
279
|
+
return makeEnvironmentProviders(providers);
|
|
708
280
|
}
|
|
709
281
|
|
|
710
|
-
/**
|
|
711
|
-
* A route tree implementation that supports efficient route matching, including support for wildcard routes.
|
|
712
|
-
* This structure is useful for organizing and retrieving routes in a hierarchical manner,
|
|
713
|
-
* enabling complex routing scenarios with nested paths.
|
|
714
|
-
*
|
|
715
|
-
* @typeParam AdditionalMetadata - Type of additional metadata that can be associated with route nodes.
|
|
716
|
-
*/
|
|
717
282
|
class RouteTree {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
*
|
|
796
|
-
* @param node - The current node to start the traversal from. Defaults to the root node of the tree.
|
|
797
|
-
*/
|
|
798
|
-
*traverse(node = this.root) {
|
|
799
|
-
if (node.metadata) {
|
|
800
|
-
yield node.metadata;
|
|
801
|
-
}
|
|
802
|
-
for (const childNode of node.children.values()) {
|
|
803
|
-
yield* this.traverse(childNode);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* Extracts the path segments from a given route string.
|
|
808
|
-
*
|
|
809
|
-
* @param route - The route string from which to extract segments.
|
|
810
|
-
* @returns An array of path segments.
|
|
811
|
-
*/
|
|
812
|
-
getPathSegments(route) {
|
|
813
|
-
return route.split('/').filter(Boolean);
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* Recursively traverses the route tree from a given node, attempting to match the remaining route segments.
|
|
817
|
-
* If the node is a leaf node (no more segments to match) and contains metadata, the node is yielded.
|
|
818
|
-
*
|
|
819
|
-
* This function prioritizes exact segment matches first, followed by wildcard matches (`*`),
|
|
820
|
-
* and finally deep wildcard matches (`**`) that consume all segments.
|
|
821
|
-
*
|
|
822
|
-
* @param segments - The array of route path segments to match against the route tree.
|
|
823
|
-
* @param node - The current node in the route tree to start traversal from. Defaults to the root node.
|
|
824
|
-
* @param currentIndex - The index of the segment in `remainingSegments` currently being matched.
|
|
825
|
-
* Defaults to `0` (the first segment).
|
|
826
|
-
*
|
|
827
|
-
* @returns The node that best matches the remaining segments or `undefined` if no match is found.
|
|
828
|
-
*/
|
|
829
|
-
traverseBySegments(segments, node = this.root, currentIndex = 0) {
|
|
830
|
-
if (currentIndex >= segments.length) {
|
|
831
|
-
return node.metadata ? node : node.children.get('**');
|
|
832
|
-
}
|
|
833
|
-
if (!node.children.size) {
|
|
834
|
-
return undefined;
|
|
835
|
-
}
|
|
836
|
-
const segment = segments[currentIndex];
|
|
837
|
-
// 1. Attempt exact match with the current segment.
|
|
838
|
-
const exactMatch = node.children.get(segment);
|
|
839
|
-
if (exactMatch) {
|
|
840
|
-
const match = this.traverseBySegments(segments, exactMatch, currentIndex + 1);
|
|
841
|
-
if (match) {
|
|
842
|
-
return match;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
// 2. Attempt wildcard match ('*').
|
|
846
|
-
const wildcardMatch = node.children.get('*');
|
|
847
|
-
if (wildcardMatch) {
|
|
848
|
-
const match = this.traverseBySegments(segments, wildcardMatch, currentIndex + 1);
|
|
849
|
-
if (match) {
|
|
850
|
-
return match;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
// 3. Attempt double wildcard match ('**').
|
|
854
|
-
return node.children.get('**');
|
|
855
|
-
}
|
|
856
|
-
/**
|
|
857
|
-
* Creates an empty route tree node.
|
|
858
|
-
* This helper function is used during the tree construction.
|
|
859
|
-
*
|
|
860
|
-
* @returns A new, empty route tree node.
|
|
861
|
-
*/
|
|
862
|
-
createEmptyRouteTreeNode() {
|
|
863
|
-
return {
|
|
864
|
-
children: new Map(),
|
|
865
|
-
};
|
|
866
|
-
}
|
|
283
|
+
root = this.createEmptyRouteTreeNode();
|
|
284
|
+
insert(route, metadata) {
|
|
285
|
+
let node = this.root;
|
|
286
|
+
const segments = this.getPathSegments(route);
|
|
287
|
+
const normalizedSegments = [];
|
|
288
|
+
for (const segment of segments) {
|
|
289
|
+
const normalizedSegment = segment[0] === ':' ? '*' : segment;
|
|
290
|
+
let childNode = node.children.get(normalizedSegment);
|
|
291
|
+
if (!childNode) {
|
|
292
|
+
childNode = this.createEmptyRouteTreeNode();
|
|
293
|
+
node.children.set(normalizedSegment, childNode);
|
|
294
|
+
}
|
|
295
|
+
node = childNode;
|
|
296
|
+
normalizedSegments.push(normalizedSegment);
|
|
297
|
+
}
|
|
298
|
+
node.metadata = {
|
|
299
|
+
...metadata,
|
|
300
|
+
route: addLeadingSlash(normalizedSegments.join('/'))
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
match(route) {
|
|
304
|
+
const segments = this.getPathSegments(route);
|
|
305
|
+
return this.traverseBySegments(segments)?.metadata;
|
|
306
|
+
}
|
|
307
|
+
toObject() {
|
|
308
|
+
return Array.from(this.traverse());
|
|
309
|
+
}
|
|
310
|
+
static fromObject(value) {
|
|
311
|
+
const tree = new RouteTree();
|
|
312
|
+
for (const {
|
|
313
|
+
route,
|
|
314
|
+
...metadata
|
|
315
|
+
} of value) {
|
|
316
|
+
tree.insert(route, metadata);
|
|
317
|
+
}
|
|
318
|
+
return tree;
|
|
319
|
+
}
|
|
320
|
+
*traverse(node = this.root) {
|
|
321
|
+
if (node.metadata) {
|
|
322
|
+
yield node.metadata;
|
|
323
|
+
}
|
|
324
|
+
for (const childNode of node.children.values()) {
|
|
325
|
+
yield* this.traverse(childNode);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
getPathSegments(route) {
|
|
329
|
+
return route.split('/').filter(Boolean);
|
|
330
|
+
}
|
|
331
|
+
traverseBySegments(segments, node = this.root, currentIndex = 0) {
|
|
332
|
+
if (currentIndex >= segments.length) {
|
|
333
|
+
return node.metadata ? node : node.children.get('**');
|
|
334
|
+
}
|
|
335
|
+
if (!node.children.size) {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
const segment = segments[currentIndex];
|
|
339
|
+
const exactMatch = node.children.get(segment);
|
|
340
|
+
if (exactMatch) {
|
|
341
|
+
const match = this.traverseBySegments(segments, exactMatch, currentIndex + 1);
|
|
342
|
+
if (match) {
|
|
343
|
+
return match;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const wildcardMatch = node.children.get('*');
|
|
347
|
+
if (wildcardMatch) {
|
|
348
|
+
const match = this.traverseBySegments(segments, wildcardMatch, currentIndex + 1);
|
|
349
|
+
if (match) {
|
|
350
|
+
return match;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return node.children.get('**');
|
|
354
|
+
}
|
|
355
|
+
createEmptyRouteTreeNode() {
|
|
356
|
+
return {
|
|
357
|
+
children: new Map()
|
|
358
|
+
};
|
|
359
|
+
}
|
|
867
360
|
}
|
|
868
361
|
|
|
869
|
-
/**
|
|
870
|
-
* The maximum number of module preload link elements that should be added for
|
|
871
|
-
* initial scripts.
|
|
872
|
-
*/
|
|
873
362
|
const MODULE_PRELOAD_MAX = 10;
|
|
874
|
-
/**
|
|
875
|
-
* Regular expression to match a catch-all route pattern in a URL path,
|
|
876
|
-
* specifically one that ends with '/**'.
|
|
877
|
-
*/
|
|
878
363
|
const CATCH_ALL_REGEXP = /\/(\*\*)$/;
|
|
879
|
-
/**
|
|
880
|
-
* Regular expression to match segments preceded by a colon in a string.
|
|
881
|
-
*/
|
|
882
364
|
const URL_PARAMETER_REGEXP = /(?<!\\):([^/]+)/g;
|
|
883
|
-
/**
|
|
884
|
-
* An set of HTTP status codes that are considered valid for redirect responses.
|
|
885
|
-
*/
|
|
886
365
|
const VALID_REDIRECT_RESPONSE_CODES = new Set([301, 302, 303, 307, 308]);
|
|
887
|
-
/**
|
|
888
|
-
* Handles a single route within the route tree and yields metadata or errors.
|
|
889
|
-
*
|
|
890
|
-
* @param options - Configuration options for handling the route.
|
|
891
|
-
* @returns An async iterable iterator yielding `RouteTreeNodeMetadata` or an error object.
|
|
892
|
-
*/
|
|
893
366
|
async function* handleRoute(options) {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
else {
|
|
921
|
-
yield metadata;
|
|
922
|
-
}
|
|
923
|
-
// Recursively process child routes
|
|
924
|
-
if (children?.length) {
|
|
925
|
-
yield* traverseRoutesConfig({
|
|
926
|
-
...options,
|
|
927
|
-
routes: children,
|
|
928
|
-
parentRoute: currentRoutePath,
|
|
929
|
-
parentPreloads: metadata.preload,
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
// Load and process lazy-loaded child routes
|
|
933
|
-
if (loadChildren) {
|
|
934
|
-
if (ɵentryName) {
|
|
935
|
-
appendPreloadToMetadata(ɵentryName, entryPointToBrowserMapping, metadata);
|
|
936
|
-
}
|
|
937
|
-
const routeInjector = route.providers
|
|
938
|
-
? createEnvironmentInjector(route.providers, parentInjector.get(EnvironmentInjector), `Route: ${route.path}`)
|
|
939
|
-
: parentInjector;
|
|
940
|
-
const loadedChildRoutes = await _loadChildren(route, compiler, routeInjector);
|
|
941
|
-
if (loadedChildRoutes) {
|
|
942
|
-
const { routes: childRoutes, injector = routeInjector } = loadedChildRoutes;
|
|
943
|
-
yield* traverseRoutesConfig({
|
|
944
|
-
...options,
|
|
945
|
-
routes: childRoutes,
|
|
946
|
-
parentInjector: injector,
|
|
947
|
-
parentRoute: currentRoutePath,
|
|
948
|
-
parentPreloads: metadata.preload,
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
catch (error) {
|
|
367
|
+
try {
|
|
368
|
+
const {
|
|
369
|
+
metadata,
|
|
370
|
+
currentRoutePath,
|
|
371
|
+
route,
|
|
372
|
+
compiler,
|
|
373
|
+
parentInjector,
|
|
374
|
+
serverConfigRouteTree,
|
|
375
|
+
entryPointToBrowserMapping,
|
|
376
|
+
invokeGetPrerenderParams,
|
|
377
|
+
includePrerenderFallbackRoutes
|
|
378
|
+
} = options;
|
|
379
|
+
const {
|
|
380
|
+
redirectTo,
|
|
381
|
+
loadChildren,
|
|
382
|
+
loadComponent,
|
|
383
|
+
children,
|
|
384
|
+
ɵentryName
|
|
385
|
+
} = route;
|
|
386
|
+
if (ɵentryName && loadComponent) {
|
|
387
|
+
appendPreloadToMetadata(ɵentryName, entryPointToBrowserMapping, metadata);
|
|
388
|
+
}
|
|
389
|
+
if (metadata.renderMode === RenderMode.Prerender) {
|
|
390
|
+
yield* handleSSGRoute(serverConfigRouteTree, typeof redirectTo === 'string' ? redirectTo : undefined, metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes);
|
|
391
|
+
} else if (redirectTo !== undefined) {
|
|
392
|
+
if (metadata.status && !VALID_REDIRECT_RESPONSE_CODES.has(metadata.status)) {
|
|
954
393
|
yield {
|
|
955
|
-
|
|
394
|
+
error: `The '${metadata.status}' status code is not a valid redirect response code. ` + `Please use one of the following redirect response codes: ${[...VALID_REDIRECT_RESPONSE_CODES.values()].join(', ')}.`
|
|
956
395
|
};
|
|
396
|
+
} else if (typeof redirectTo === 'string') {
|
|
397
|
+
yield {
|
|
398
|
+
...metadata,
|
|
399
|
+
redirectTo: resolveRedirectTo(metadata.route, redirectTo)
|
|
400
|
+
};
|
|
401
|
+
} else {
|
|
402
|
+
yield metadata;
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
yield metadata;
|
|
406
|
+
}
|
|
407
|
+
if (children?.length) {
|
|
408
|
+
yield* traverseRoutesConfig({
|
|
409
|
+
...options,
|
|
410
|
+
routes: children,
|
|
411
|
+
parentRoute: currentRoutePath,
|
|
412
|
+
parentPreloads: metadata.preload
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (loadChildren) {
|
|
416
|
+
if (ɵentryName) {
|
|
417
|
+
appendPreloadToMetadata(ɵentryName, entryPointToBrowserMapping, metadata);
|
|
418
|
+
}
|
|
419
|
+
const routeInjector = route.providers ? createEnvironmentInjector(route.providers, parentInjector.get(EnvironmentInjector), `Route: ${route.path}`) : parentInjector;
|
|
420
|
+
const loadedChildRoutes = await _loadChildren(route, compiler, routeInjector);
|
|
421
|
+
if (loadedChildRoutes) {
|
|
422
|
+
const {
|
|
423
|
+
routes: childRoutes,
|
|
424
|
+
injector = routeInjector
|
|
425
|
+
} = loadedChildRoutes;
|
|
426
|
+
yield* traverseRoutesConfig({
|
|
427
|
+
...options,
|
|
428
|
+
routes: childRoutes,
|
|
429
|
+
parentInjector: injector,
|
|
430
|
+
parentRoute: currentRoutePath,
|
|
431
|
+
parentPreloads: metadata.preload
|
|
432
|
+
});
|
|
433
|
+
}
|
|
957
434
|
}
|
|
435
|
+
} catch (error) {
|
|
436
|
+
yield {
|
|
437
|
+
error: `Error in handleRoute for '${options.currentRoutePath}': ${error.message}`
|
|
438
|
+
};
|
|
439
|
+
}
|
|
958
440
|
}
|
|
959
|
-
/**
|
|
960
|
-
* Traverses an array of route configurations to generate route tree node metadata.
|
|
961
|
-
*
|
|
962
|
-
* This function processes each route and its children, handling redirects, SSG (Static Site Generation) settings,
|
|
963
|
-
* and lazy-loaded routes. It yields route metadata for each route and its potential variants.
|
|
964
|
-
*
|
|
965
|
-
* @param options - The configuration options for traversing routes.
|
|
966
|
-
* @returns An async iterable iterator yielding either route tree node metadata or an error object with an error message.
|
|
967
|
-
*/
|
|
968
441
|
async function* traverseRoutesConfig(options) {
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
preload: parentPreloads,
|
|
995
|
-
route: matchedMetaData.route,
|
|
996
|
-
presentInClientRouter: undefined,
|
|
997
|
-
},
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1000
|
-
if (!foundMatch) {
|
|
1001
|
-
yield {
|
|
1002
|
-
error: `The route '${stripLeadingSlash(currentRoutePath)}' has a defined matcher but does not ` +
|
|
1003
|
-
'match any route in the server routing configuration. Please ensure this route is added to the server routing configuration.',
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
|
-
continue;
|
|
1007
|
-
}
|
|
1008
|
-
let matchedMetaData;
|
|
1009
|
-
if (serverConfigRouteTree) {
|
|
1010
|
-
matchedMetaData = serverConfigRouteTree.match(currentRoutePath);
|
|
1011
|
-
if (!matchedMetaData) {
|
|
1012
|
-
yield {
|
|
1013
|
-
error: `The '${stripLeadingSlash(currentRoutePath)}' route does not match any route defined in the server routing configuration. ` +
|
|
1014
|
-
'Please ensure this route is added to the server routing configuration.',
|
|
1015
|
-
};
|
|
1016
|
-
continue;
|
|
1017
|
-
}
|
|
1018
|
-
matchedMetaData.presentInClientRouter = true;
|
|
442
|
+
const {
|
|
443
|
+
routes: routeConfigs,
|
|
444
|
+
parentPreloads,
|
|
445
|
+
parentRoute,
|
|
446
|
+
serverConfigRouteTree
|
|
447
|
+
} = options;
|
|
448
|
+
for (const route of routeConfigs) {
|
|
449
|
+
const {
|
|
450
|
+
matcher,
|
|
451
|
+
path = matcher ? '**' : ''
|
|
452
|
+
} = route;
|
|
453
|
+
const currentRoutePath = joinUrlParts(parentRoute, path);
|
|
454
|
+
if (matcher && serverConfigRouteTree) {
|
|
455
|
+
let foundMatch = false;
|
|
456
|
+
for (const matchedMetaData of serverConfigRouteTree.traverse()) {
|
|
457
|
+
if (!matchedMetaData.route.startsWith(currentRoutePath)) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
foundMatch = true;
|
|
461
|
+
matchedMetaData.presentInClientRouter = true;
|
|
462
|
+
if (matchedMetaData.renderMode === RenderMode.Prerender) {
|
|
463
|
+
yield {
|
|
464
|
+
error: `The route '${stripLeadingSlash(currentRoutePath)}' is set for prerendering but has a defined matcher. ` + `Routes with matchers cannot use prerendering. Please specify a different 'renderMode'.`
|
|
465
|
+
};
|
|
466
|
+
continue;
|
|
1019
467
|
}
|
|
1020
468
|
yield* handleRoute({
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
presentInClientRouter: undefined,
|
|
1031
|
-
},
|
|
1032
|
-
currentRoutePath,
|
|
1033
|
-
route,
|
|
469
|
+
...options,
|
|
470
|
+
currentRoutePath,
|
|
471
|
+
route,
|
|
472
|
+
metadata: {
|
|
473
|
+
...matchedMetaData,
|
|
474
|
+
preload: parentPreloads,
|
|
475
|
+
route: matchedMetaData.route,
|
|
476
|
+
presentInClientRouter: undefined
|
|
477
|
+
}
|
|
1034
478
|
});
|
|
479
|
+
}
|
|
480
|
+
if (!foundMatch) {
|
|
481
|
+
yield {
|
|
482
|
+
error: `The route '${stripLeadingSlash(currentRoutePath)}' has a defined matcher but does not ` + 'match any route in the server routing configuration. Please ensure this route is added to the server routing configuration.'
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
continue;
|
|
1035
486
|
}
|
|
487
|
+
let matchedMetaData;
|
|
488
|
+
if (serverConfigRouteTree) {
|
|
489
|
+
matchedMetaData = serverConfigRouteTree.match(currentRoutePath);
|
|
490
|
+
if (!matchedMetaData) {
|
|
491
|
+
yield {
|
|
492
|
+
error: `The '${stripLeadingSlash(currentRoutePath)}' route does not match any route defined in the server routing configuration. ` + 'Please ensure this route is added to the server routing configuration.'
|
|
493
|
+
};
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
matchedMetaData.presentInClientRouter = true;
|
|
497
|
+
}
|
|
498
|
+
yield* handleRoute({
|
|
499
|
+
...options,
|
|
500
|
+
metadata: {
|
|
501
|
+
renderMode: RenderMode.Prerender,
|
|
502
|
+
...matchedMetaData,
|
|
503
|
+
preload: parentPreloads,
|
|
504
|
+
route: path === '' ? addTrailingSlash(currentRoutePath) : currentRoutePath,
|
|
505
|
+
presentInClientRouter: undefined
|
|
506
|
+
},
|
|
507
|
+
currentRoutePath,
|
|
508
|
+
route
|
|
509
|
+
});
|
|
510
|
+
}
|
|
1036
511
|
}
|
|
1037
|
-
/**
|
|
1038
|
-
* Appends preload information to the metadata object based on the specified entry-point and chunk mappings.
|
|
1039
|
-
*
|
|
1040
|
-
* This function extracts preload data for a given entry-point from the provided chunk mappings. It adds the
|
|
1041
|
-
* corresponding browser bundles to the metadata's preload list, ensuring no duplicates and limiting the total
|
|
1042
|
-
* preloads to a predefined maximum.
|
|
1043
|
-
*/
|
|
1044
512
|
function appendPreloadToMetadata(entryName, entryPointToBrowserMapping, metadata) {
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
metadata.preload = Array.from(combinedPreloads);
|
|
513
|
+
const existingPreloads = metadata.preload ?? [];
|
|
514
|
+
if (!entryPointToBrowserMapping || existingPreloads.length >= MODULE_PRELOAD_MAX) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const preload = entryPointToBrowserMapping[entryName];
|
|
518
|
+
if (!preload?.length) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const combinedPreloads = new Set(existingPreloads);
|
|
522
|
+
for (const href of preload) {
|
|
523
|
+
combinedPreloads.add(href);
|
|
524
|
+
if (combinedPreloads.size === MODULE_PRELOAD_MAX) {
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
metadata.preload = Array.from(combinedPreloads);
|
|
1062
529
|
}
|
|
1063
|
-
/**
|
|
1064
|
-
* Handles SSG (Static Site Generation) routes by invoking `getPrerenderParams` and yielding
|
|
1065
|
-
* all parameterized paths, returning any errors encountered.
|
|
1066
|
-
*
|
|
1067
|
-
* @param serverConfigRouteTree - The tree representing the server's routing setup.
|
|
1068
|
-
* @param redirectTo - Optional path to redirect to, if specified.
|
|
1069
|
-
* @param metadata - The metadata associated with the route tree node.
|
|
1070
|
-
* @param parentInjector - The dependency injection container for the parent route.
|
|
1071
|
-
* @param invokeGetPrerenderParams - A flag indicating whether to invoke the `getPrerenderParams` function.
|
|
1072
|
-
* @param includePrerenderFallbackRoutes - A flag indicating whether to include fallback routes in the result.
|
|
1073
|
-
* @returns An async iterable iterator that yields route tree node metadata for each SSG path or errors.
|
|
1074
|
-
*/
|
|
1075
530
|
async function* handleSSGRoute(serverConfigRouteTree, redirectTo, metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes) {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
531
|
+
if (metadata.renderMode !== RenderMode.Prerender) {
|
|
532
|
+
throw new Error(`'handleSSGRoute' was called for a route which rendering mode is not prerender.`);
|
|
533
|
+
}
|
|
534
|
+
const {
|
|
535
|
+
route: currentRoutePath,
|
|
536
|
+
fallback,
|
|
537
|
+
...meta
|
|
538
|
+
} = metadata;
|
|
539
|
+
const getPrerenderParams = 'getPrerenderParams' in meta ? meta.getPrerenderParams : undefined;
|
|
540
|
+
if ('getPrerenderParams' in meta) {
|
|
541
|
+
delete meta['getPrerenderParams'];
|
|
542
|
+
}
|
|
543
|
+
if (redirectTo !== undefined) {
|
|
544
|
+
meta.redirectTo = resolveRedirectTo(currentRoutePath, redirectTo);
|
|
545
|
+
}
|
|
546
|
+
const isCatchAllRoute = CATCH_ALL_REGEXP.test(currentRoutePath);
|
|
547
|
+
if (isCatchAllRoute && !getPrerenderParams || !isCatchAllRoute && !URL_PARAMETER_REGEXP.test(currentRoutePath)) {
|
|
548
|
+
yield {
|
|
549
|
+
...meta,
|
|
550
|
+
route: currentRoutePath
|
|
551
|
+
};
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (invokeGetPrerenderParams) {
|
|
555
|
+
if (!getPrerenderParams) {
|
|
556
|
+
yield {
|
|
557
|
+
error: `The '${stripLeadingSlash(currentRoutePath)}' route uses prerendering and includes parameters, but 'getPrerenderParams' ` + `is missing. Please define 'getPrerenderParams' function for this route in your server routing configuration ` + `or specify a different 'renderMode'.`
|
|
558
|
+
};
|
|
559
|
+
return;
|
|
1086
560
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
if (invokeGetPrerenderParams) {
|
|
1098
|
-
if (!getPrerenderParams) {
|
|
1099
|
-
yield {
|
|
1100
|
-
error: `The '${stripLeadingSlash(currentRoutePath)}' route uses prerendering and includes parameters, but 'getPrerenderParams' ` +
|
|
1101
|
-
`is missing. Please define 'getPrerenderParams' function for this route in your server routing configuration ` +
|
|
1102
|
-
`or specify a different 'renderMode'.`,
|
|
1103
|
-
};
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
if (serverConfigRouteTree) {
|
|
1107
|
-
// Automatically resolve dynamic parameters for nested routes.
|
|
1108
|
-
const catchAllRoutePath = isCatchAllRoute
|
|
1109
|
-
? currentRoutePath
|
|
1110
|
-
: joinUrlParts(currentRoutePath, '**');
|
|
1111
|
-
const match = serverConfigRouteTree.match(catchAllRoutePath);
|
|
1112
|
-
if (match && match.renderMode === RenderMode.Prerender && !('getPrerenderParams' in match)) {
|
|
1113
|
-
serverConfigRouteTree.insert(catchAllRoutePath, {
|
|
1114
|
-
...match,
|
|
1115
|
-
presentInClientRouter: true,
|
|
1116
|
-
getPrerenderParams,
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
const parameters = await runInInjectionContext(parentInjector, () => getPrerenderParams());
|
|
1121
|
-
try {
|
|
1122
|
-
for (const params of parameters) {
|
|
1123
|
-
const replacer = handlePrerenderParamsReplacement(params, currentRoutePath);
|
|
1124
|
-
const routeWithResolvedParams = currentRoutePath
|
|
1125
|
-
.replace(URL_PARAMETER_REGEXP, replacer)
|
|
1126
|
-
.replace(CATCH_ALL_REGEXP, replacer);
|
|
1127
|
-
yield {
|
|
1128
|
-
...meta,
|
|
1129
|
-
route: routeWithResolvedParams,
|
|
1130
|
-
redirectTo: redirectTo === undefined
|
|
1131
|
-
? undefined
|
|
1132
|
-
: resolveRedirectTo(routeWithResolvedParams, redirectTo),
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
catch (error) {
|
|
1137
|
-
yield { error: `${error.message}` };
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
561
|
+
if (serverConfigRouteTree) {
|
|
562
|
+
const catchAllRoutePath = isCatchAllRoute ? currentRoutePath : joinUrlParts(currentRoutePath, '**');
|
|
563
|
+
const match = serverConfigRouteTree.match(catchAllRoutePath);
|
|
564
|
+
if (match && match.renderMode === RenderMode.Prerender && !('getPrerenderParams' in match)) {
|
|
565
|
+
serverConfigRouteTree.insert(catchAllRoutePath, {
|
|
566
|
+
...match,
|
|
567
|
+
presentInClientRouter: true,
|
|
568
|
+
getPrerenderParams
|
|
569
|
+
});
|
|
570
|
+
}
|
|
1140
571
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
572
|
+
const parameters = await runInInjectionContext(parentInjector, () => getPrerenderParams());
|
|
573
|
+
try {
|
|
574
|
+
for (const params of parameters) {
|
|
575
|
+
const replacer = handlePrerenderParamsReplacement(params, currentRoutePath);
|
|
576
|
+
const routeWithResolvedParams = currentRoutePath.replace(URL_PARAMETER_REGEXP, replacer).replace(CATCH_ALL_REGEXP, replacer);
|
|
1144
577
|
yield {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
578
|
+
...meta,
|
|
579
|
+
route: routeWithResolvedParams,
|
|
580
|
+
redirectTo: redirectTo === undefined ? undefined : resolveRedirectTo(routeWithResolvedParams, redirectTo)
|
|
1148
581
|
};
|
|
582
|
+
}
|
|
583
|
+
} catch (error) {
|
|
584
|
+
yield {
|
|
585
|
+
error: `${error.message}`
|
|
586
|
+
};
|
|
587
|
+
return;
|
|
1149
588
|
}
|
|
589
|
+
}
|
|
590
|
+
if (includePrerenderFallbackRoutes && (fallback !== PrerenderFallback.None || !invokeGetPrerenderParams)) {
|
|
591
|
+
yield {
|
|
592
|
+
...meta,
|
|
593
|
+
route: currentRoutePath,
|
|
594
|
+
renderMode: fallback === PrerenderFallback.Client ? RenderMode.Client : RenderMode.Server
|
|
595
|
+
};
|
|
596
|
+
}
|
|
1150
597
|
}
|
|
1151
|
-
/**
|
|
1152
|
-
* Creates a replacer function used for substituting parameter placeholders in a route path
|
|
1153
|
-
* with their corresponding values provided in the `params` object.
|
|
1154
|
-
*
|
|
1155
|
-
* @param params - An object mapping parameter names to their string values.
|
|
1156
|
-
* @param currentRoutePath - The current route path, used for constructing error messages.
|
|
1157
|
-
* @returns A function that replaces a matched parameter placeholder (e.g., ':id') with its corresponding value.
|
|
1158
|
-
*/
|
|
1159
598
|
function handlePrerenderParamsReplacement(params, currentRoutePath) {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
}
|
|
1169
|
-
return parameterName === '**' ? `/${value}` : value;
|
|
1170
|
-
};
|
|
599
|
+
return match => {
|
|
600
|
+
const parameterName = match.slice(1);
|
|
601
|
+
const value = params[parameterName];
|
|
602
|
+
if (typeof value !== 'string') {
|
|
603
|
+
throw new Error(`The 'getPrerenderParams' function defined for the '${stripLeadingSlash(currentRoutePath)}' route ` + `returned a non-string value for parameter '${parameterName}'. ` + `Please make sure the 'getPrerenderParams' function returns values for all parameters ` + 'specified in this route.');
|
|
604
|
+
}
|
|
605
|
+
return parameterName === '**' ? `/${value}` : value;
|
|
606
|
+
};
|
|
1171
607
|
}
|
|
1172
|
-
/**
|
|
1173
|
-
* Resolves the `redirectTo` property for a given route.
|
|
1174
|
-
*
|
|
1175
|
-
* This function processes the `redirectTo` property to ensure that it correctly
|
|
1176
|
-
* resolves relative to the current route path. If `redirectTo` is an absolute path,
|
|
1177
|
-
* it is returned as is. If it is a relative path, it is resolved based on the current route path.
|
|
1178
|
-
*
|
|
1179
|
-
* @param routePath - The current route path.
|
|
1180
|
-
* @param redirectTo - The target path for redirection.
|
|
1181
|
-
* @returns The resolved redirect path as a string.
|
|
1182
|
-
*/
|
|
1183
608
|
function resolveRedirectTo(routePath, redirectTo) {
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
segments.pop(); // Remove the last segment to make it relative.
|
|
1191
|
-
return joinUrlParts(...segments, redirectTo);
|
|
609
|
+
if (redirectTo[0] === '/') {
|
|
610
|
+
return redirectTo;
|
|
611
|
+
}
|
|
612
|
+
const segments = routePath.replace(URL_PARAMETER_REGEXP, '*').split('/');
|
|
613
|
+
segments.pop();
|
|
614
|
+
return joinUrlParts(...segments, redirectTo);
|
|
1192
615
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
}
|
|
1225
|
-
return { serverConfigRouteTree, errors };
|
|
616
|
+
function buildServerConfigRouteTree({
|
|
617
|
+
routes,
|
|
618
|
+
appShellRoute
|
|
619
|
+
}) {
|
|
620
|
+
const serverRoutes = [...routes];
|
|
621
|
+
if (appShellRoute !== undefined) {
|
|
622
|
+
serverRoutes.unshift({
|
|
623
|
+
path: appShellRoute,
|
|
624
|
+
renderMode: RenderMode.Prerender
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
const serverConfigRouteTree = new RouteTree();
|
|
628
|
+
const errors = [];
|
|
629
|
+
for (const {
|
|
630
|
+
path,
|
|
631
|
+
...metadata
|
|
632
|
+
} of serverRoutes) {
|
|
633
|
+
if (path[0] === '/') {
|
|
634
|
+
errors.push(`Invalid '${path}' route configuration: the path cannot start with a slash.`);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if ('getPrerenderParams' in metadata && (path.includes('/*/') || path.endsWith('/*'))) {
|
|
638
|
+
errors.push(`Invalid '${path}' route configuration: 'getPrerenderParams' cannot be used with a '*' route.`);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
serverConfigRouteTree.insert(path, metadata);
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
serverConfigRouteTree,
|
|
645
|
+
errors
|
|
646
|
+
};
|
|
1226
647
|
}
|
|
1227
|
-
/**
|
|
1228
|
-
* Retrieves routes from the given Angular application.
|
|
1229
|
-
*
|
|
1230
|
-
* This function initializes an Angular platform, bootstraps the application or module,
|
|
1231
|
-
* and retrieves routes from the Angular router configuration. It handles both module-based
|
|
1232
|
-
* and function-based bootstrapping. It yields the resulting routes as `RouteTreeNodeMetadata` objects or errors.
|
|
1233
|
-
*
|
|
1234
|
-
* @param bootstrap - A function that returns a promise resolving to an `ApplicationRef` or an Angular module to bootstrap.
|
|
1235
|
-
* @param document - The initial HTML document used for server-side rendering.
|
|
1236
|
-
* This document is necessary to render the application on the server.
|
|
1237
|
-
* @param url - The URL for server-side rendering. The URL is used to configure `ServerPlatformLocation`. This configuration is crucial
|
|
1238
|
-
* for ensuring that API requests for relative paths succeed, which is essential for accurate route extraction.
|
|
1239
|
-
* @param invokeGetPrerenderParams - A boolean flag indicating whether to invoke `getPrerenderParams` for parameterized SSG routes
|
|
1240
|
-
* to handle prerendering paths. Defaults to `false`.
|
|
1241
|
-
* @param includePrerenderFallbackRoutes - A flag indicating whether to include fallback routes in the result. Defaults to `true`.
|
|
1242
|
-
* @param entryPointToBrowserMapping - Maps the entry-point name to the associated JavaScript browser bundles.
|
|
1243
|
-
*
|
|
1244
|
-
* @returns A promise that resolves to an object of type `AngularRouterConfigResult` or errors.
|
|
1245
|
-
*/
|
|
1246
648
|
async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invokeGetPrerenderParams = false, includePrerenderFallbackRoutes = true, entryPointToBrowserMapping = undefined) {
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
route: '',
|
|
1348
|
-
renderMode: RenderMode.Prerender,
|
|
1349
|
-
};
|
|
1350
|
-
routesResults.push({
|
|
1351
|
-
...rootRouteMetadata,
|
|
1352
|
-
// Matched route might be `/*` or `/**`, which would make Angular serve all routes rather than just `/`.
|
|
1353
|
-
// So we limit to just `/` for the empty app router case.
|
|
1354
|
-
route: '',
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
return {
|
|
1358
|
-
baseHref,
|
|
1359
|
-
routes: routesResults,
|
|
1360
|
-
errors,
|
|
1361
|
-
appShellRoute: serverRoutesConfig?.appShellRoute,
|
|
1362
|
-
};
|
|
1363
|
-
}
|
|
1364
|
-
finally {
|
|
1365
|
-
platformRef.destroy();
|
|
649
|
+
const {
|
|
650
|
+
protocol,
|
|
651
|
+
host
|
|
652
|
+
} = url;
|
|
653
|
+
const platformRef = platformServer([{
|
|
654
|
+
provide: INITIAL_CONFIG,
|
|
655
|
+
useValue: {
|
|
656
|
+
document,
|
|
657
|
+
url: `${protocol}//${host}/`
|
|
658
|
+
}
|
|
659
|
+
}, {
|
|
660
|
+
provide: _Console,
|
|
661
|
+
useFactory: () => new Console()
|
|
662
|
+
}, {
|
|
663
|
+
provide: _ENABLE_ROOT_COMPONENT_BOOTSTRAP,
|
|
664
|
+
useValue: false
|
|
665
|
+
}]);
|
|
666
|
+
try {
|
|
667
|
+
let applicationRef;
|
|
668
|
+
if (isNgModule(bootstrap)) {
|
|
669
|
+
const moduleRef = await platformRef.bootstrapModule(bootstrap);
|
|
670
|
+
applicationRef = moduleRef.injector.get(ApplicationRef);
|
|
671
|
+
} else {
|
|
672
|
+
applicationRef = await bootstrap({
|
|
673
|
+
platformRef
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
const injector = applicationRef.injector;
|
|
677
|
+
const router = injector.get(Router);
|
|
678
|
+
router.navigationTransitions.afterPreactivation()?.next?.();
|
|
679
|
+
await applicationRef.whenStable();
|
|
680
|
+
const errors = [];
|
|
681
|
+
const rawBaseHref = injector.get(APP_BASE_HREF, null, {
|
|
682
|
+
optional: true
|
|
683
|
+
}) ?? injector.get(PlatformLocation).getBaseHrefFromDOM();
|
|
684
|
+
const {
|
|
685
|
+
pathname: baseHref
|
|
686
|
+
} = new URL(rawBaseHref, 'http://localhost');
|
|
687
|
+
const compiler = injector.get(Compiler);
|
|
688
|
+
const serverRoutesConfig = injector.get(SERVER_ROUTES_CONFIG, null, {
|
|
689
|
+
optional: true
|
|
690
|
+
});
|
|
691
|
+
let serverConfigRouteTree;
|
|
692
|
+
if (serverRoutesConfig) {
|
|
693
|
+
const result = buildServerConfigRouteTree(serverRoutesConfig);
|
|
694
|
+
serverConfigRouteTree = result.serverConfigRouteTree;
|
|
695
|
+
errors.push(...result.errors);
|
|
696
|
+
}
|
|
697
|
+
if (errors.length) {
|
|
698
|
+
return {
|
|
699
|
+
baseHref,
|
|
700
|
+
routes: [],
|
|
701
|
+
errors
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
const routesResults = [];
|
|
705
|
+
if (router.config.length) {
|
|
706
|
+
const traverseRoutes = traverseRoutesConfig({
|
|
707
|
+
routes: router.config,
|
|
708
|
+
compiler,
|
|
709
|
+
parentInjector: injector,
|
|
710
|
+
parentRoute: '',
|
|
711
|
+
serverConfigRouteTree,
|
|
712
|
+
invokeGetPrerenderParams,
|
|
713
|
+
includePrerenderFallbackRoutes,
|
|
714
|
+
entryPointToBrowserMapping
|
|
715
|
+
});
|
|
716
|
+
const seenRoutes = new Set();
|
|
717
|
+
for await (const routeMetadata of traverseRoutes) {
|
|
718
|
+
if ('error' in routeMetadata) {
|
|
719
|
+
errors.push(routeMetadata.error);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const routePath = routeMetadata.route;
|
|
723
|
+
if (!seenRoutes.has(routePath)) {
|
|
724
|
+
routesResults.push(routeMetadata);
|
|
725
|
+
seenRoutes.add(routePath);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
729
|
+
if (serverConfigRouteTree) {
|
|
730
|
+
for (const {
|
|
731
|
+
route,
|
|
732
|
+
presentInClientRouter
|
|
733
|
+
} of serverConfigRouteTree.traverse()) {
|
|
734
|
+
if (presentInClientRouter || route.endsWith('/**')) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
errors.push(`The '${stripLeadingSlash(route)}' server route does not match any routes defined in the Angular ` + `routing configuration (typically provided as a part of the 'provideRouter' call). ` + 'Please make sure that the mentioned server route is present in the Angular routing configuration.');
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
} else {
|
|
741
|
+
const rootRouteMetadata = serverConfigRouteTree?.match('') ?? {
|
|
742
|
+
route: '',
|
|
743
|
+
renderMode: RenderMode.Prerender
|
|
744
|
+
};
|
|
745
|
+
routesResults.push({
|
|
746
|
+
...rootRouteMetadata,
|
|
747
|
+
route: ''
|
|
748
|
+
});
|
|
1366
749
|
}
|
|
750
|
+
return {
|
|
751
|
+
baseHref,
|
|
752
|
+
routes: routesResults,
|
|
753
|
+
errors,
|
|
754
|
+
appShellRoute: serverRoutesConfig?.appShellRoute
|
|
755
|
+
};
|
|
756
|
+
} finally {
|
|
757
|
+
platformRef.destroy();
|
|
758
|
+
}
|
|
1367
759
|
}
|
|
1368
|
-
/**
|
|
1369
|
-
* Asynchronously extracts routes from the Angular application configuration
|
|
1370
|
-
* and creates a `RouteTree` to manage server-side routing.
|
|
1371
|
-
*
|
|
1372
|
-
* @param options - An object containing the following options:
|
|
1373
|
-
* - `url`: The URL for server-side rendering. The URL is used to configure `ServerPlatformLocation`. This configuration is crucial
|
|
1374
|
-
* for ensuring that API requests for relative paths succeed, which is essential for accurate route extraction.
|
|
1375
|
-
* See:
|
|
1376
|
-
* - https://github.com/angular/angular/blob/d608b857c689d17a7ffa33bbb510301014d24a17/packages/platform-server/src/location.ts#L51
|
|
1377
|
-
* - https://github.com/angular/angular/blob/6882cc7d9eed26d3caeedca027452367ba25f2b9/packages/platform-server/src/http.ts#L44
|
|
1378
|
-
* - `manifest`: An optional `AngularAppManifest` that contains the application's routing and configuration details.
|
|
1379
|
-
* If not provided, the default manifest is retrieved using `getAngularAppManifest()`.
|
|
1380
|
-
* - `invokeGetPrerenderParams`: A boolean flag indicating whether to invoke `getPrerenderParams` for parameterized SSG routes
|
|
1381
|
-
* to handle prerendering paths. Defaults to `false`.
|
|
1382
|
-
* - `includePrerenderFallbackRoutes`: A flag indicating whether to include fallback routes in the result. Defaults to `true`.
|
|
1383
|
-
* - `signal`: An optional `AbortSignal` that can be used to abort the operation.
|
|
1384
|
-
*
|
|
1385
|
-
* @returns A promise that resolves to an object containing:
|
|
1386
|
-
* - `routeTree`: A populated `RouteTree` containing all extracted routes from the Angular application.
|
|
1387
|
-
* - `appShellRoute`: The specified route for the app-shell, if configured.
|
|
1388
|
-
* - `errors`: An array of strings representing any errors encountered during the route extraction process.
|
|
1389
|
-
*/
|
|
1390
760
|
function extractRoutesAndCreateRouteTree(options) {
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
761
|
+
const {
|
|
762
|
+
url,
|
|
763
|
+
manifest = getAngularAppManifest(),
|
|
764
|
+
invokeGetPrerenderParams = false,
|
|
765
|
+
includePrerenderFallbackRoutes = true,
|
|
766
|
+
signal
|
|
767
|
+
} = options;
|
|
768
|
+
async function extract() {
|
|
769
|
+
const routeTree = new RouteTree();
|
|
770
|
+
const document = await new ServerAssets(manifest).getIndexServerHtml().text();
|
|
771
|
+
const bootstrap = await manifest.bootstrap();
|
|
772
|
+
const {
|
|
773
|
+
baseHref,
|
|
774
|
+
appShellRoute,
|
|
775
|
+
routes,
|
|
776
|
+
errors
|
|
777
|
+
} = await getRoutesFromAngularRouterConfig(bootstrap, document, url, invokeGetPrerenderParams, includePrerenderFallbackRoutes, manifest.entryPointToBrowserMapping);
|
|
778
|
+
for (const {
|
|
779
|
+
route,
|
|
780
|
+
...metadata
|
|
781
|
+
} of routes) {
|
|
782
|
+
if (metadata.redirectTo !== undefined) {
|
|
783
|
+
metadata.redirectTo = joinUrlParts(baseHref, metadata.redirectTo);
|
|
784
|
+
}
|
|
785
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
786
|
+
if (value === undefined) {
|
|
787
|
+
delete metadata[key];
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const fullRoute = joinUrlParts(baseHref, route);
|
|
791
|
+
routeTree.insert(fullRoute, metadata);
|
|
1417
792
|
}
|
|
1418
|
-
return
|
|
793
|
+
return {
|
|
794
|
+
appShellRoute,
|
|
795
|
+
routeTree,
|
|
796
|
+
errors
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
return signal ? promiseWithAbort(extract(), signal, 'Routes extraction') : extract();
|
|
1419
800
|
}
|
|
1420
801
|
|
|
1421
|
-
/**
|
|
1422
|
-
* Manages a collection of hooks and provides methods to register and execute them.
|
|
1423
|
-
* Hooks are functions that can be invoked with specific arguments to allow modifications or enhancements.
|
|
1424
|
-
*/
|
|
1425
802
|
class Hooks {
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
}
|
|
1459
|
-
const ctx = { ...context };
|
|
1460
|
-
for (const hook of hooks) {
|
|
1461
|
-
ctx.html = await hook(ctx);
|
|
1462
|
-
}
|
|
1463
|
-
return ctx.html;
|
|
1464
|
-
}
|
|
1465
|
-
default:
|
|
1466
|
-
throw new Error(`Running hook "${name}" is not supported.`);
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
/**
|
|
1470
|
-
* Registers a new hook function under the specified hook name.
|
|
1471
|
-
* This function should be a function that takes an argument of type `T` and returns a `string` or `Promise<string>`.
|
|
1472
|
-
*
|
|
1473
|
-
* @template Hook - The type of the hook name. It should be one of the keys of `HooksMapping`.
|
|
1474
|
-
* @param name - The name of the hook under which the function will be registered.
|
|
1475
|
-
* @param handler - A function to be executed when the hook is triggered. The handler will be called with an argument
|
|
1476
|
-
* that may be modified by the hook functions.
|
|
1477
|
-
*
|
|
1478
|
-
* @remarks
|
|
1479
|
-
* - If there are existing handlers registered under the given hook name, the new handler will be added to the list.
|
|
1480
|
-
* - If no handlers are registered under the given hook name, a new list will be created with the handler as its first element.
|
|
1481
|
-
*
|
|
1482
|
-
* @example
|
|
1483
|
-
* ```typescript
|
|
1484
|
-
* hooks.on('html:transform:pre', async (ctx) => {
|
|
1485
|
-
* return ctx.html.replace(/foo/g, 'bar');
|
|
1486
|
-
* });
|
|
1487
|
-
* ```
|
|
1488
|
-
*/
|
|
1489
|
-
on(name, handler) {
|
|
1490
|
-
const hooks = this.store.get(name);
|
|
1491
|
-
if (hooks) {
|
|
1492
|
-
hooks.push(handler);
|
|
1493
|
-
}
|
|
1494
|
-
else {
|
|
1495
|
-
this.store.set(name, [handler]);
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
/**
|
|
1499
|
-
* Checks if there are any hooks registered under the specified name.
|
|
1500
|
-
*
|
|
1501
|
-
* @param name - The name of the hook to check.
|
|
1502
|
-
* @returns `true` if there are hooks registered under the specified name, otherwise `false`.
|
|
1503
|
-
*/
|
|
1504
|
-
has(name) {
|
|
1505
|
-
return !!this.store.get(name)?.length;
|
|
1506
|
-
}
|
|
803
|
+
store = new Map();
|
|
804
|
+
async run(name, context) {
|
|
805
|
+
const hooks = this.store.get(name);
|
|
806
|
+
switch (name) {
|
|
807
|
+
case 'html:transform:pre':
|
|
808
|
+
{
|
|
809
|
+
if (!hooks) {
|
|
810
|
+
return context.html;
|
|
811
|
+
}
|
|
812
|
+
const ctx = {
|
|
813
|
+
...context
|
|
814
|
+
};
|
|
815
|
+
for (const hook of hooks) {
|
|
816
|
+
ctx.html = await hook(ctx);
|
|
817
|
+
}
|
|
818
|
+
return ctx.html;
|
|
819
|
+
}
|
|
820
|
+
default:
|
|
821
|
+
throw new Error(`Running hook "${name}" is not supported.`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
on(name, handler) {
|
|
825
|
+
const hooks = this.store.get(name);
|
|
826
|
+
if (hooks) {
|
|
827
|
+
hooks.push(handler);
|
|
828
|
+
} else {
|
|
829
|
+
this.store.set(name, [handler]);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
has(name) {
|
|
833
|
+
return !!this.store.get(name)?.length;
|
|
834
|
+
}
|
|
1507
835
|
}
|
|
1508
836
|
|
|
1509
|
-
/**
|
|
1510
|
-
* Manages the application's server routing logic by building and maintaining a route tree.
|
|
1511
|
-
*
|
|
1512
|
-
* This class is responsible for constructing the route tree from the Angular application
|
|
1513
|
-
* configuration and using it to match incoming requests to the appropriate routes.
|
|
1514
|
-
*/
|
|
1515
837
|
class ServerRouter {
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
// This prevents concurrent builds by re-using the same promise.
|
|
1551
|
-
ServerRouter.#extractionPromise ??= extractRoutesAndCreateRouteTree({ url, manifest })
|
|
1552
|
-
.then(({ routeTree, errors }) => {
|
|
1553
|
-
if (errors.length > 0) {
|
|
1554
|
-
throw new Error('Error(s) occurred while extracting routes:\n' +
|
|
1555
|
-
errors.map((error) => `- ${error}`).join('\n'));
|
|
1556
|
-
}
|
|
1557
|
-
return new ServerRouter(routeTree);
|
|
1558
|
-
})
|
|
1559
|
-
.finally(() => {
|
|
1560
|
-
ServerRouter.#extractionPromise = undefined;
|
|
1561
|
-
});
|
|
1562
|
-
return ServerRouter.#extractionPromise;
|
|
1563
|
-
}
|
|
1564
|
-
/**
|
|
1565
|
-
* Matches a request URL against the route tree to retrieve route metadata.
|
|
1566
|
-
*
|
|
1567
|
-
* This method strips 'index.html' from the URL if it is present and then attempts
|
|
1568
|
-
* to find a match in the route tree. If a match is found, it returns the associated
|
|
1569
|
-
* route metadata; otherwise, it returns `undefined`.
|
|
1570
|
-
*
|
|
1571
|
-
* @param url - The URL to be matched against the route tree.
|
|
1572
|
-
* @returns The metadata for the matched route or `undefined` if no match is found.
|
|
1573
|
-
*/
|
|
1574
|
-
match(url) {
|
|
1575
|
-
// Strip 'index.html' from URL if present.
|
|
1576
|
-
// A request to `http://www.example.com/page/index.html` will render the Angular route corresponding to `http://www.example.com/page`.
|
|
1577
|
-
let { pathname } = stripIndexHtmlFromURL(url);
|
|
1578
|
-
pathname = stripMatrixParams(pathname);
|
|
1579
|
-
pathname = decodeURIComponent(pathname);
|
|
1580
|
-
return this.routeTree.match(pathname);
|
|
1581
|
-
}
|
|
838
|
+
routeTree;
|
|
839
|
+
constructor(routeTree) {
|
|
840
|
+
this.routeTree = routeTree;
|
|
841
|
+
}
|
|
842
|
+
static #extractionPromise;
|
|
843
|
+
static from(manifest, url) {
|
|
844
|
+
if (manifest.routes) {
|
|
845
|
+
const routeTree = RouteTree.fromObject(manifest.routes);
|
|
846
|
+
return Promise.resolve(new ServerRouter(routeTree));
|
|
847
|
+
}
|
|
848
|
+
ServerRouter.#extractionPromise ??= extractRoutesAndCreateRouteTree({
|
|
849
|
+
url,
|
|
850
|
+
manifest
|
|
851
|
+
}).then(({
|
|
852
|
+
routeTree,
|
|
853
|
+
errors
|
|
854
|
+
}) => {
|
|
855
|
+
if (errors.length > 0) {
|
|
856
|
+
throw new Error('Error(s) occurred while extracting routes:\n' + errors.map(error => `- ${error}`).join('\n'));
|
|
857
|
+
}
|
|
858
|
+
return new ServerRouter(routeTree);
|
|
859
|
+
}).finally(() => {
|
|
860
|
+
ServerRouter.#extractionPromise = undefined;
|
|
861
|
+
});
|
|
862
|
+
return ServerRouter.#extractionPromise;
|
|
863
|
+
}
|
|
864
|
+
match(url) {
|
|
865
|
+
let {
|
|
866
|
+
pathname
|
|
867
|
+
} = stripIndexHtmlFromURL(url);
|
|
868
|
+
pathname = stripMatrixParams(pathname);
|
|
869
|
+
pathname = decodeURIComponent(pathname);
|
|
870
|
+
return this.routeTree.match(pathname);
|
|
871
|
+
}
|
|
1582
872
|
}
|
|
1583
873
|
|
|
1584
|
-
/**
|
|
1585
|
-
* Generates a SHA-256 hash of the provided string.
|
|
1586
|
-
*
|
|
1587
|
-
* @param data - The input string to be hashed.
|
|
1588
|
-
* @returns A promise that resolves to the SHA-256 hash of the input,
|
|
1589
|
-
* represented as a hexadecimal string.
|
|
1590
|
-
*/
|
|
1591
874
|
async function sha256(data) {
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
875
|
+
const encodedData = new TextEncoder().encode(data);
|
|
876
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', encodedData);
|
|
877
|
+
const hashParts = [];
|
|
878
|
+
for (const h of new Uint8Array(hashBuffer)) {
|
|
879
|
+
hashParts.push(h.toString(16).padStart(2, '0'));
|
|
880
|
+
}
|
|
881
|
+
return hashParts.join('');
|
|
1599
882
|
}
|
|
1600
883
|
|
|
1601
|
-
/**
|
|
1602
|
-
* Pattern used to extract the media query set by Beasties in an `onload` handler.
|
|
1603
|
-
*/
|
|
1604
884
|
const MEDIA_SET_HANDLER_PATTERN = /^this\.media=["'](.*)["'];?$/;
|
|
1605
|
-
/**
|
|
1606
|
-
* Name of the attribute used to save the Beasties media query so it can be re-assigned on load.
|
|
1607
|
-
*/
|
|
1608
885
|
const CSP_MEDIA_ATTR = 'ngCspMedia';
|
|
1609
|
-
|
|
1610
|
-
* Script that dynamically updates the `media` attribute of `<link>` tags based on a custom attribute (`CSP_MEDIA_ATTR`).
|
|
1611
|
-
*
|
|
1612
|
-
* NOTE:
|
|
1613
|
-
* We do not use `document.querySelectorAll('link').forEach((s) => s.addEventListener('load', ...)`
|
|
1614
|
-
* because load events are not always triggered reliably on Chrome.
|
|
1615
|
-
* See: https://github.com/angular/angular-cli/issues/26932 and https://crbug.com/1521256
|
|
1616
|
-
*
|
|
1617
|
-
* The script:
|
|
1618
|
-
* - Ensures the event target is a `<link>` tag with the `CSP_MEDIA_ATTR` attribute.
|
|
1619
|
-
* - Updates the `media` attribute with the value of `CSP_MEDIA_ATTR` and then removes the attribute.
|
|
1620
|
-
* - Removes the event listener when all relevant `<link>` tags have been processed.
|
|
1621
|
-
* - Uses event capturing (the `true` parameter) since load events do not bubble up the DOM.
|
|
1622
|
-
*/
|
|
1623
|
-
const LINK_LOAD_SCRIPT_CONTENT = /* @__PURE__ */ (() => `(() => {
|
|
886
|
+
const LINK_LOAD_SCRIPT_CONTENT = /* @__PURE__ */(() => `(() => {
|
|
1624
887
|
const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';
|
|
1625
888
|
const documentElement = document.documentElement;
|
|
1626
889
|
|
|
@@ -1645,995 +908,582 @@ const LINK_LOAD_SCRIPT_CONTENT = /* @__PURE__ */ (() => `(() => {
|
|
|
1645
908
|
|
|
1646
909
|
documentElement.addEventListener('load', listener, true);
|
|
1647
910
|
})();`)();
|
|
1648
|
-
class BeastiesBase extends Beasties {
|
|
1649
|
-
}
|
|
1650
|
-
/* eslint-enable @typescript-eslint/no-unsafe-declaration-merging */
|
|
911
|
+
class BeastiesBase extends Beasties {}
|
|
1651
912
|
class InlineCriticalCssProcessor extends BeastiesBase {
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
child.setAttribute('nonce', cspNonce);
|
|
1716
|
-
}
|
|
1717
|
-
});
|
|
1718
|
-
}
|
|
1719
|
-
return returnValue;
|
|
1720
|
-
}
|
|
1721
|
-
/**
|
|
1722
|
-
* Finds the CSP nonce for a specific document.
|
|
1723
|
-
*/
|
|
1724
|
-
findCspNonce(document) {
|
|
1725
|
-
if (this.documentNonces.has(document)) {
|
|
1726
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1727
|
-
return this.documentNonces.get(document);
|
|
1728
|
-
}
|
|
1729
|
-
// HTML attribute are case-insensitive, but the parser used by Beasties is case-sensitive.
|
|
1730
|
-
const nonceElement = document.querySelector('[ngCspNonce], [ngcspnonce]');
|
|
1731
|
-
const cspNonce = nonceElement?.getAttribute('ngCspNonce') || nonceElement?.getAttribute('ngcspnonce') || null;
|
|
1732
|
-
this.documentNonces.set(document, cspNonce);
|
|
1733
|
-
return cspNonce;
|
|
1734
|
-
}
|
|
1735
|
-
/**
|
|
1736
|
-
* Inserts the `script` tag that swaps the critical CSS at runtime,
|
|
1737
|
-
* if one hasn't been inserted into the document already.
|
|
1738
|
-
*/
|
|
1739
|
-
conditionallyInsertCspLoadingScript(document, nonce, link) {
|
|
1740
|
-
if (this.addedCspScriptsDocuments.has(document)) {
|
|
1741
|
-
return;
|
|
1742
|
-
}
|
|
1743
|
-
if (document.head.textContent.includes(LINK_LOAD_SCRIPT_CONTENT)) {
|
|
1744
|
-
// Script was already added during the build.
|
|
1745
|
-
this.addedCspScriptsDocuments.add(document);
|
|
1746
|
-
return;
|
|
1747
|
-
}
|
|
1748
|
-
const script = document.createElement('script');
|
|
1749
|
-
script.setAttribute('nonce', nonce);
|
|
1750
|
-
script.textContent = LINK_LOAD_SCRIPT_CONTENT;
|
|
1751
|
-
// Prepend the script to the head since it needs to
|
|
1752
|
-
// run as early as possible, before the `link` tags.
|
|
1753
|
-
document.head.insertBefore(script, link);
|
|
1754
|
-
this.addedCspScriptsDocuments.add(document);
|
|
913
|
+
readFile;
|
|
914
|
+
outputPath;
|
|
915
|
+
addedCspScriptsDocuments = new WeakSet();
|
|
916
|
+
documentNonces = new WeakMap();
|
|
917
|
+
constructor(readFile, outputPath) {
|
|
918
|
+
super({
|
|
919
|
+
logger: {
|
|
920
|
+
warn: s => console.warn(s),
|
|
921
|
+
error: s => console.error(s),
|
|
922
|
+
info: () => {}
|
|
923
|
+
},
|
|
924
|
+
logLevel: 'warn',
|
|
925
|
+
path: outputPath,
|
|
926
|
+
publicPath: undefined,
|
|
927
|
+
compress: false,
|
|
928
|
+
pruneSource: false,
|
|
929
|
+
reduceInlineStyles: false,
|
|
930
|
+
mergeStylesheets: false,
|
|
931
|
+
preload: 'media',
|
|
932
|
+
noscriptFallback: true,
|
|
933
|
+
inlineFonts: true
|
|
934
|
+
});
|
|
935
|
+
this.readFile = readFile;
|
|
936
|
+
this.outputPath = outputPath;
|
|
937
|
+
}
|
|
938
|
+
async embedLinkedStylesheet(link, document) {
|
|
939
|
+
if (link.getAttribute('media') === 'print' && link.next?.name === 'noscript') {
|
|
940
|
+
const media = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN);
|
|
941
|
+
if (media) {
|
|
942
|
+
link.removeAttribute('onload');
|
|
943
|
+
link.setAttribute('media', media[1]);
|
|
944
|
+
link?.next?.remove();
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const returnValue = await super.embedLinkedStylesheet(link, document);
|
|
948
|
+
const cspNonce = this.findCspNonce(document);
|
|
949
|
+
if (cspNonce) {
|
|
950
|
+
const beastiesMedia = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN);
|
|
951
|
+
if (beastiesMedia) {
|
|
952
|
+
link.removeAttribute('onload');
|
|
953
|
+
link.setAttribute(CSP_MEDIA_ATTR, beastiesMedia[1]);
|
|
954
|
+
this.conditionallyInsertCspLoadingScript(document, cspNonce, link);
|
|
955
|
+
}
|
|
956
|
+
document.head.children.forEach(child => {
|
|
957
|
+
if (child.tagName === 'style' && !child.hasAttribute('nonce')) {
|
|
958
|
+
child.setAttribute('nonce', cspNonce);
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
return returnValue;
|
|
963
|
+
}
|
|
964
|
+
findCspNonce(document) {
|
|
965
|
+
if (this.documentNonces.has(document)) {
|
|
966
|
+
return this.documentNonces.get(document);
|
|
967
|
+
}
|
|
968
|
+
const nonceElement = document.querySelector('[ngCspNonce], [ngcspnonce]');
|
|
969
|
+
const cspNonce = nonceElement?.getAttribute('ngCspNonce') || nonceElement?.getAttribute('ngcspnonce') || null;
|
|
970
|
+
this.documentNonces.set(document, cspNonce);
|
|
971
|
+
return cspNonce;
|
|
972
|
+
}
|
|
973
|
+
conditionallyInsertCspLoadingScript(document, nonce, link) {
|
|
974
|
+
if (this.addedCspScriptsDocuments.has(document)) {
|
|
975
|
+
return;
|
|
1755
976
|
}
|
|
977
|
+
if (document.head.textContent.includes(LINK_LOAD_SCRIPT_CONTENT)) {
|
|
978
|
+
this.addedCspScriptsDocuments.add(document);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const script = document.createElement('script');
|
|
982
|
+
script.setAttribute('nonce', nonce);
|
|
983
|
+
script.textContent = LINK_LOAD_SCRIPT_CONTENT;
|
|
984
|
+
document.head.insertBefore(script, link);
|
|
985
|
+
this.addedCspScriptsDocuments.add(document);
|
|
986
|
+
}
|
|
1756
987
|
}
|
|
1757
988
|
|
|
1758
|
-
/**
|
|
1759
|
-
* A Least Recently Used (LRU) cache implementation.
|
|
1760
|
-
*
|
|
1761
|
-
* This cache stores a fixed number of key-value pairs, and when the cache exceeds its capacity,
|
|
1762
|
-
* the least recently accessed items are evicted.
|
|
1763
|
-
*
|
|
1764
|
-
* @template Key - The type of the cache keys.
|
|
1765
|
-
* @template Value - The type of the cache values.
|
|
1766
|
-
*/
|
|
1767
989
|
class LRUCache {
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
this.capacity = capacity;
|
|
1790
|
-
}
|
|
1791
|
-
/**
|
|
1792
|
-
* Gets the value associated with the given key.
|
|
1793
|
-
* @param key The key to retrieve the value for.
|
|
1794
|
-
* @returns The value associated with the key, or undefined if the key is not found.
|
|
1795
|
-
*/
|
|
1796
|
-
get(key) {
|
|
1797
|
-
const node = this.cache.get(key);
|
|
1798
|
-
if (node) {
|
|
1799
|
-
this.moveToHead(node);
|
|
1800
|
-
return node.value;
|
|
1801
|
-
}
|
|
1802
|
-
return undefined;
|
|
1803
|
-
}
|
|
1804
|
-
/**
|
|
1805
|
-
* Puts a key-value pair into the cache.
|
|
1806
|
-
* If the key already exists, the value is updated.
|
|
1807
|
-
* If the cache is full, the least recently used item is evicted.
|
|
1808
|
-
* @param key The key to insert or update.
|
|
1809
|
-
* @param value The value to associate with the key.
|
|
1810
|
-
*/
|
|
1811
|
-
put(key, value) {
|
|
1812
|
-
const cachedNode = this.cache.get(key);
|
|
1813
|
-
if (cachedNode) {
|
|
1814
|
-
// Update existing node
|
|
1815
|
-
cachedNode.value = value;
|
|
1816
|
-
this.moveToHead(cachedNode);
|
|
1817
|
-
return;
|
|
1818
|
-
}
|
|
1819
|
-
// Create a new node
|
|
1820
|
-
const newNode = { key, value, prev: undefined, next: undefined };
|
|
1821
|
-
this.cache.set(key, newNode);
|
|
1822
|
-
this.addToHead(newNode);
|
|
1823
|
-
if (this.cache.size > this.capacity) {
|
|
1824
|
-
// Evict the LRU item
|
|
1825
|
-
const tail = this.removeTail();
|
|
1826
|
-
if (tail) {
|
|
1827
|
-
this.cache.delete(tail.key);
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
/**
|
|
1832
|
-
* Adds a node to the head of the linked list.
|
|
1833
|
-
* @param node The node to add.
|
|
1834
|
-
*/
|
|
1835
|
-
addToHead(node) {
|
|
1836
|
-
node.next = this.head;
|
|
1837
|
-
node.prev = undefined;
|
|
1838
|
-
if (this.head) {
|
|
1839
|
-
this.head.prev = node;
|
|
1840
|
-
}
|
|
1841
|
-
this.head = node;
|
|
1842
|
-
if (!this.tail) {
|
|
1843
|
-
this.tail = node;
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
/**
|
|
1847
|
-
* Removes a node from the linked list.
|
|
1848
|
-
* @param node The node to remove.
|
|
1849
|
-
*/
|
|
1850
|
-
removeNode(node) {
|
|
1851
|
-
if (node.prev) {
|
|
1852
|
-
node.prev.next = node.next;
|
|
1853
|
-
}
|
|
1854
|
-
else {
|
|
1855
|
-
this.head = node.next;
|
|
1856
|
-
}
|
|
1857
|
-
if (node.next) {
|
|
1858
|
-
node.next.prev = node.prev;
|
|
1859
|
-
}
|
|
1860
|
-
else {
|
|
1861
|
-
this.tail = node.prev;
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
/**
|
|
1865
|
-
* Moves a node to the head of the linked list.
|
|
1866
|
-
* @param node The node to move.
|
|
1867
|
-
*/
|
|
1868
|
-
moveToHead(node) {
|
|
1869
|
-
this.removeNode(node);
|
|
1870
|
-
this.addToHead(node);
|
|
1871
|
-
}
|
|
1872
|
-
/**
|
|
1873
|
-
* Removes the tail node from the linked list.
|
|
1874
|
-
* @returns The removed tail node, or undefined if the list is empty.
|
|
1875
|
-
*/
|
|
1876
|
-
removeTail() {
|
|
1877
|
-
const node = this.tail;
|
|
1878
|
-
if (node) {
|
|
1879
|
-
this.removeNode(node);
|
|
1880
|
-
}
|
|
1881
|
-
return node;
|
|
990
|
+
capacity;
|
|
991
|
+
cache = new Map();
|
|
992
|
+
head;
|
|
993
|
+
tail;
|
|
994
|
+
constructor(capacity) {
|
|
995
|
+
this.capacity = capacity;
|
|
996
|
+
}
|
|
997
|
+
get(key) {
|
|
998
|
+
const node = this.cache.get(key);
|
|
999
|
+
if (node) {
|
|
1000
|
+
this.moveToHead(node);
|
|
1001
|
+
return node.value;
|
|
1002
|
+
}
|
|
1003
|
+
return undefined;
|
|
1004
|
+
}
|
|
1005
|
+
put(key, value) {
|
|
1006
|
+
const cachedNode = this.cache.get(key);
|
|
1007
|
+
if (cachedNode) {
|
|
1008
|
+
cachedNode.value = value;
|
|
1009
|
+
this.moveToHead(cachedNode);
|
|
1010
|
+
return;
|
|
1882
1011
|
}
|
|
1012
|
+
const newNode = {
|
|
1013
|
+
key,
|
|
1014
|
+
value,
|
|
1015
|
+
prev: undefined,
|
|
1016
|
+
next: undefined
|
|
1017
|
+
};
|
|
1018
|
+
this.cache.set(key, newNode);
|
|
1019
|
+
this.addToHead(newNode);
|
|
1020
|
+
if (this.cache.size > this.capacity) {
|
|
1021
|
+
const tail = this.removeTail();
|
|
1022
|
+
if (tail) {
|
|
1023
|
+
this.cache.delete(tail.key);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
addToHead(node) {
|
|
1028
|
+
node.next = this.head;
|
|
1029
|
+
node.prev = undefined;
|
|
1030
|
+
if (this.head) {
|
|
1031
|
+
this.head.prev = node;
|
|
1032
|
+
}
|
|
1033
|
+
this.head = node;
|
|
1034
|
+
if (!this.tail) {
|
|
1035
|
+
this.tail = node;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
removeNode(node) {
|
|
1039
|
+
if (node.prev) {
|
|
1040
|
+
node.prev.next = node.next;
|
|
1041
|
+
} else {
|
|
1042
|
+
this.head = node.next;
|
|
1043
|
+
}
|
|
1044
|
+
if (node.next) {
|
|
1045
|
+
node.next.prev = node.prev;
|
|
1046
|
+
} else {
|
|
1047
|
+
this.tail = node.prev;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
moveToHead(node) {
|
|
1051
|
+
this.removeNode(node);
|
|
1052
|
+
this.addToHead(node);
|
|
1053
|
+
}
|
|
1054
|
+
removeTail() {
|
|
1055
|
+
const node = this.tail;
|
|
1056
|
+
if (node) {
|
|
1057
|
+
this.removeNode(node);
|
|
1058
|
+
}
|
|
1059
|
+
return node;
|
|
1060
|
+
}
|
|
1883
1061
|
}
|
|
1884
1062
|
|
|
1885
|
-
/**
|
|
1886
|
-
* Maximum number of critical CSS entries the cache can store.
|
|
1887
|
-
* This value determines the capacity of the LRU (Least Recently Used) cache, which stores critical CSS for pages.
|
|
1888
|
-
*/
|
|
1889
1063
|
const MAX_INLINE_CSS_CACHE_ENTRIES = 50;
|
|
1890
|
-
/**
|
|
1891
|
-
* A mapping of `RenderMode` enum values to corresponding string representations.
|
|
1892
|
-
*
|
|
1893
|
-
* This record is used to map each `RenderMode` to a specific string value that represents
|
|
1894
|
-
* the server context. The string values are used internally to differentiate
|
|
1895
|
-
* between various rendering strategies when processing routes.
|
|
1896
|
-
*
|
|
1897
|
-
* - `RenderMode.Prerender` maps to `'ssg'` (Static Site Generation).
|
|
1898
|
-
* - `RenderMode.Server` maps to `'ssr'` (Server-Side Rendering).
|
|
1899
|
-
* - `RenderMode.Client` maps to an empty string `''` (Client-Side Rendering, no server context needed).
|
|
1900
|
-
*/
|
|
1901
1064
|
const SERVER_CONTEXT_VALUE = {
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1065
|
+
[RenderMode.Prerender]: 'ssg',
|
|
1066
|
+
[RenderMode.Server]: 'ssr',
|
|
1067
|
+
[RenderMode.Client]: ''
|
|
1905
1068
|
};
|
|
1906
|
-
/**
|
|
1907
|
-
* Represents a locale-specific Angular server application managed by the server application engine.
|
|
1908
|
-
*
|
|
1909
|
-
* The `AngularServerApp` class handles server-side rendering and asset management for a specific locale.
|
|
1910
|
-
*/
|
|
1911
1069
|
class AngularServerApp {
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
html = await this.runTransformsOnHtml(html, url, preload);
|
|
2084
|
-
return new Response(html, responseInit);
|
|
2085
|
-
}
|
|
2086
|
-
if (locale !== undefined) {
|
|
2087
|
-
platformProviders.push({
|
|
2088
|
-
provide: LOCALE_ID,
|
|
2089
|
-
useValue: locale,
|
|
2090
|
-
});
|
|
2091
|
-
}
|
|
2092
|
-
this.boostrap ??= await bootstrap();
|
|
2093
|
-
let html = await assets.getIndexServerHtml().text();
|
|
2094
|
-
html = await this.runTransformsOnHtml(html, url, preload);
|
|
2095
|
-
const result = await renderAngular(html, this.boostrap, url, platformProviders, SERVER_CONTEXT_VALUE[renderMode]);
|
|
2096
|
-
if (result.hasNavigationError) {
|
|
2097
|
-
return null;
|
|
2098
|
-
}
|
|
2099
|
-
if (result.redirectTo) {
|
|
2100
|
-
return createRedirectResponse(result.redirectTo, status);
|
|
2101
|
-
}
|
|
2102
|
-
if (renderMode === RenderMode.Prerender) {
|
|
2103
|
-
const renderedHtml = await result.content();
|
|
2104
|
-
const finalHtml = await this.inlineCriticalCss(renderedHtml, url);
|
|
2105
|
-
return new Response(finalHtml, responseInit);
|
|
2106
|
-
}
|
|
2107
|
-
// Use a stream to send the response before finishing rendering and inling critical CSS, improving performance via header flushing.
|
|
2108
|
-
const stream = new ReadableStream({
|
|
2109
|
-
start: async (controller) => {
|
|
2110
|
-
const renderedHtml = await result.content();
|
|
2111
|
-
const finalHtml = await this.inlineCriticalCssWithCache(renderedHtml, url);
|
|
2112
|
-
controller.enqueue(finalHtml);
|
|
2113
|
-
controller.close();
|
|
2114
|
-
},
|
|
2115
|
-
});
|
|
2116
|
-
return new Response(stream, responseInit);
|
|
2117
|
-
}
|
|
2118
|
-
/**
|
|
2119
|
-
* Inlines critical CSS into the given HTML content.
|
|
2120
|
-
*
|
|
2121
|
-
* @param html The HTML content to process.
|
|
2122
|
-
* @param url The URL associated with the request, for logging purposes.
|
|
2123
|
-
* @returns A promise that resolves to the HTML with inlined critical CSS.
|
|
2124
|
-
*/
|
|
2125
|
-
async inlineCriticalCss(html, url) {
|
|
2126
|
-
const { inlineCriticalCssProcessor } = this;
|
|
2127
|
-
if (!inlineCriticalCssProcessor) {
|
|
2128
|
-
return html;
|
|
2129
|
-
}
|
|
2130
|
-
try {
|
|
2131
|
-
return await inlineCriticalCssProcessor.process(html);
|
|
2132
|
-
}
|
|
2133
|
-
catch (error) {
|
|
2134
|
-
// eslint-disable-next-line no-console
|
|
2135
|
-
console.error(`An error occurred while inlining critical CSS for: ${url}.`, error);
|
|
2136
|
-
return html;
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
/**
|
|
2140
|
-
* Inlines critical CSS into the given HTML content.
|
|
2141
|
-
* This method uses a cache to avoid reprocessing the same HTML content multiple times.
|
|
2142
|
-
*
|
|
2143
|
-
* @param html The HTML content to process.
|
|
2144
|
-
* @param url The URL associated with the request, for logging purposes.
|
|
2145
|
-
* @returns A promise that resolves to the HTML with inlined critical CSS.
|
|
2146
|
-
*/
|
|
2147
|
-
async inlineCriticalCssWithCache(html, url) {
|
|
2148
|
-
const { inlineCriticalCssProcessor, criticalCssLRUCache, textDecoder } = this;
|
|
2149
|
-
if (!inlineCriticalCssProcessor) {
|
|
2150
|
-
return textDecoder.encode(html);
|
|
2151
|
-
}
|
|
2152
|
-
const cacheKey = url.toString();
|
|
2153
|
-
const cached = criticalCssLRUCache.get(cacheKey);
|
|
2154
|
-
const shaOfContentPreInlinedCss = await sha256(html);
|
|
2155
|
-
if (cached?.shaOfContentPreInlinedCss === shaOfContentPreInlinedCss) {
|
|
2156
|
-
return cached.contentWithCriticialCSS;
|
|
2157
|
-
}
|
|
2158
|
-
const processedHtml = await this.inlineCriticalCss(html, url);
|
|
2159
|
-
const finalHtml = textDecoder.encode(processedHtml);
|
|
2160
|
-
criticalCssLRUCache.put(cacheKey, {
|
|
2161
|
-
shaOfContentPreInlinedCss,
|
|
2162
|
-
contentWithCriticialCSS: finalHtml,
|
|
2163
|
-
});
|
|
2164
|
-
return finalHtml;
|
|
2165
|
-
}
|
|
2166
|
-
/**
|
|
2167
|
-
* Constructs the asset path on the server based on the provided HTTP request.
|
|
2168
|
-
*
|
|
2169
|
-
* This method processes the incoming request URL to derive a path corresponding
|
|
2170
|
-
* to the requested asset. It ensures the path points to the correct file (e.g.,
|
|
2171
|
-
* `index.html`) and removes any base href if it is not part of the asset path.
|
|
2172
|
-
*
|
|
2173
|
-
* @param request - The incoming HTTP request object.
|
|
2174
|
-
* @returns The server-relative asset path derived from the request.
|
|
2175
|
-
*/
|
|
2176
|
-
buildServerAssetPathFromRequest(request) {
|
|
2177
|
-
let { pathname: assetPath } = new URL(request.url);
|
|
2178
|
-
if (!assetPath.endsWith('/index.html')) {
|
|
2179
|
-
// Append "index.html" to build the default asset path.
|
|
2180
|
-
assetPath = joinUrlParts(assetPath, 'index.html');
|
|
2181
|
-
}
|
|
2182
|
-
const { baseHref } = this.manifest;
|
|
2183
|
-
// Check if the asset path starts with the base href and the base href is not (`/` or ``).
|
|
2184
|
-
if (baseHref.length > 1 && assetPath.startsWith(baseHref)) {
|
|
2185
|
-
// Remove the base href from the start of the asset path to align with server-asset expectations.
|
|
2186
|
-
assetPath = assetPath.slice(baseHref.length);
|
|
2187
|
-
}
|
|
2188
|
-
return stripLeadingSlash(assetPath);
|
|
2189
|
-
}
|
|
2190
|
-
/**
|
|
2191
|
-
* Runs the registered transform hooks on the given HTML content.
|
|
2192
|
-
*
|
|
2193
|
-
* @param html - The raw HTML content to be transformed.
|
|
2194
|
-
* @param url - The URL associated with the HTML content, used for context during transformations.
|
|
2195
|
-
* @param preload - An array of URLs representing the JavaScript resources to preload.
|
|
2196
|
-
* @returns A promise that resolves to the transformed HTML string.
|
|
2197
|
-
*/
|
|
2198
|
-
async runTransformsOnHtml(html, url, preload) {
|
|
2199
|
-
if (this.hooks.has('html:transform:pre')) {
|
|
2200
|
-
html = await this.hooks.run('html:transform:pre', { html, url });
|
|
2201
|
-
}
|
|
2202
|
-
if (preload?.length) {
|
|
2203
|
-
html = appendPreloadHintsToHtml(html, preload);
|
|
2204
|
-
}
|
|
2205
|
-
return html;
|
|
1070
|
+
options;
|
|
1071
|
+
allowStaticRouteRender;
|
|
1072
|
+
hooks;
|
|
1073
|
+
constructor(options = {}) {
|
|
1074
|
+
this.options = options;
|
|
1075
|
+
this.allowStaticRouteRender = this.options.allowStaticRouteRender ?? false;
|
|
1076
|
+
this.hooks = options.hooks ?? new Hooks();
|
|
1077
|
+
if (this.manifest.inlineCriticalCss) {
|
|
1078
|
+
this.inlineCriticalCssProcessor = new InlineCriticalCssProcessor(path => {
|
|
1079
|
+
const fileName = path.split('/').pop() ?? path;
|
|
1080
|
+
return this.assets.getServerAsset(fileName).text();
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
manifest = getAngularAppManifest();
|
|
1085
|
+
assets = new ServerAssets(this.manifest);
|
|
1086
|
+
router;
|
|
1087
|
+
inlineCriticalCssProcessor;
|
|
1088
|
+
boostrap;
|
|
1089
|
+
textDecoder = new TextEncoder();
|
|
1090
|
+
criticalCssLRUCache = new LRUCache(MAX_INLINE_CSS_CACHE_ENTRIES);
|
|
1091
|
+
async handle(request, requestContext) {
|
|
1092
|
+
const url = new URL(request.url);
|
|
1093
|
+
this.router ??= await ServerRouter.from(this.manifest, url);
|
|
1094
|
+
const matchedRoute = this.router.match(url);
|
|
1095
|
+
if (!matchedRoute) {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
const {
|
|
1099
|
+
redirectTo,
|
|
1100
|
+
status,
|
|
1101
|
+
renderMode
|
|
1102
|
+
} = matchedRoute;
|
|
1103
|
+
if (redirectTo !== undefined) {
|
|
1104
|
+
return createRedirectResponse(buildPathWithParams(redirectTo, url.pathname), status);
|
|
1105
|
+
}
|
|
1106
|
+
if (renderMode === RenderMode.Prerender) {
|
|
1107
|
+
const response = await this.handleServe(request, matchedRoute);
|
|
1108
|
+
if (response) {
|
|
1109
|
+
return response;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return promiseWithAbort(this.handleRendering(request, matchedRoute, requestContext), request.signal, `Request for: ${request.url}`);
|
|
1113
|
+
}
|
|
1114
|
+
async handleServe(request, matchedRoute) {
|
|
1115
|
+
const {
|
|
1116
|
+
headers,
|
|
1117
|
+
renderMode
|
|
1118
|
+
} = matchedRoute;
|
|
1119
|
+
if (renderMode !== RenderMode.Prerender) {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
const {
|
|
1123
|
+
method
|
|
1124
|
+
} = request;
|
|
1125
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
const assetPath = this.buildServerAssetPathFromRequest(request);
|
|
1129
|
+
const {
|
|
1130
|
+
manifest: {
|
|
1131
|
+
locale
|
|
1132
|
+
},
|
|
1133
|
+
assets
|
|
1134
|
+
} = this;
|
|
1135
|
+
if (!assets.hasServerAsset(assetPath)) {
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
const {
|
|
1139
|
+
text,
|
|
1140
|
+
hash,
|
|
1141
|
+
size
|
|
1142
|
+
} = assets.getServerAsset(assetPath);
|
|
1143
|
+
const etag = `"${hash}"`;
|
|
1144
|
+
return request.headers.get('if-none-match') === etag ? new Response(undefined, {
|
|
1145
|
+
status: 304,
|
|
1146
|
+
statusText: 'Not Modified'
|
|
1147
|
+
}) : new Response(await text(), {
|
|
1148
|
+
headers: {
|
|
1149
|
+
'Content-Length': size.toString(),
|
|
1150
|
+
'ETag': etag,
|
|
1151
|
+
'Content-Type': 'text/html;charset=UTF-8',
|
|
1152
|
+
...(locale !== undefined ? {
|
|
1153
|
+
'Content-Language': locale
|
|
1154
|
+
} : {}),
|
|
1155
|
+
...headers
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
async handleRendering(request, matchedRoute, requestContext) {
|
|
1160
|
+
const {
|
|
1161
|
+
renderMode,
|
|
1162
|
+
headers,
|
|
1163
|
+
status,
|
|
1164
|
+
preload
|
|
1165
|
+
} = matchedRoute;
|
|
1166
|
+
if (!this.allowStaticRouteRender && renderMode === RenderMode.Prerender) {
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
const url = new URL(request.url);
|
|
1170
|
+
const platformProviders = [];
|
|
1171
|
+
const {
|
|
1172
|
+
manifest: {
|
|
1173
|
+
bootstrap,
|
|
1174
|
+
locale
|
|
1175
|
+
},
|
|
1176
|
+
assets
|
|
1177
|
+
} = this;
|
|
1178
|
+
const responseInit = {
|
|
1179
|
+
status,
|
|
1180
|
+
headers: new Headers({
|
|
1181
|
+
'Content-Type': 'text/html;charset=UTF-8',
|
|
1182
|
+
...(locale !== undefined ? {
|
|
1183
|
+
'Content-Language': locale
|
|
1184
|
+
} : {}),
|
|
1185
|
+
...headers
|
|
1186
|
+
})
|
|
1187
|
+
};
|
|
1188
|
+
if (renderMode === RenderMode.Server) {
|
|
1189
|
+
platformProviders.push({
|
|
1190
|
+
provide: REQUEST,
|
|
1191
|
+
useValue: request
|
|
1192
|
+
}, {
|
|
1193
|
+
provide: REQUEST_CONTEXT,
|
|
1194
|
+
useValue: requestContext
|
|
1195
|
+
}, {
|
|
1196
|
+
provide: RESPONSE_INIT,
|
|
1197
|
+
useValue: responseInit
|
|
1198
|
+
});
|
|
1199
|
+
} else if (renderMode === RenderMode.Client) {
|
|
1200
|
+
let html = await this.assets.getServerAsset('index.csr.html').text();
|
|
1201
|
+
html = await this.runTransformsOnHtml(html, url, preload);
|
|
1202
|
+
return new Response(html, responseInit);
|
|
1203
|
+
}
|
|
1204
|
+
if (locale !== undefined) {
|
|
1205
|
+
platformProviders.push({
|
|
1206
|
+
provide: LOCALE_ID,
|
|
1207
|
+
useValue: locale
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
this.boostrap ??= await bootstrap();
|
|
1211
|
+
let html = await assets.getIndexServerHtml().text();
|
|
1212
|
+
html = await this.runTransformsOnHtml(html, url, preload);
|
|
1213
|
+
const result = await renderAngular(html, this.boostrap, url, platformProviders, SERVER_CONTEXT_VALUE[renderMode]);
|
|
1214
|
+
if (result.hasNavigationError) {
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
if (result.redirectTo) {
|
|
1218
|
+
return createRedirectResponse(result.redirectTo, status);
|
|
1219
|
+
}
|
|
1220
|
+
if (renderMode === RenderMode.Prerender) {
|
|
1221
|
+
const renderedHtml = await result.content();
|
|
1222
|
+
const finalHtml = await this.inlineCriticalCss(renderedHtml, url);
|
|
1223
|
+
return new Response(finalHtml, responseInit);
|
|
1224
|
+
}
|
|
1225
|
+
const stream = new ReadableStream({
|
|
1226
|
+
start: async controller => {
|
|
1227
|
+
const renderedHtml = await result.content();
|
|
1228
|
+
const finalHtml = await this.inlineCriticalCssWithCache(renderedHtml, url);
|
|
1229
|
+
controller.enqueue(finalHtml);
|
|
1230
|
+
controller.close();
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
return new Response(stream, responseInit);
|
|
1234
|
+
}
|
|
1235
|
+
async inlineCriticalCss(html, url) {
|
|
1236
|
+
const {
|
|
1237
|
+
inlineCriticalCssProcessor
|
|
1238
|
+
} = this;
|
|
1239
|
+
if (!inlineCriticalCssProcessor) {
|
|
1240
|
+
return html;
|
|
2206
1241
|
}
|
|
1242
|
+
try {
|
|
1243
|
+
return await inlineCriticalCssProcessor.process(html);
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
console.error(`An error occurred while inlining critical CSS for: ${url}.`, error);
|
|
1246
|
+
return html;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
async inlineCriticalCssWithCache(html, url) {
|
|
1250
|
+
const {
|
|
1251
|
+
inlineCriticalCssProcessor,
|
|
1252
|
+
criticalCssLRUCache,
|
|
1253
|
+
textDecoder
|
|
1254
|
+
} = this;
|
|
1255
|
+
if (!inlineCriticalCssProcessor) {
|
|
1256
|
+
return textDecoder.encode(html);
|
|
1257
|
+
}
|
|
1258
|
+
const cacheKey = url.toString();
|
|
1259
|
+
const cached = criticalCssLRUCache.get(cacheKey);
|
|
1260
|
+
const shaOfContentPreInlinedCss = await sha256(html);
|
|
1261
|
+
if (cached?.shaOfContentPreInlinedCss === shaOfContentPreInlinedCss) {
|
|
1262
|
+
return cached.contentWithCriticialCSS;
|
|
1263
|
+
}
|
|
1264
|
+
const processedHtml = await this.inlineCriticalCss(html, url);
|
|
1265
|
+
const finalHtml = textDecoder.encode(processedHtml);
|
|
1266
|
+
criticalCssLRUCache.put(cacheKey, {
|
|
1267
|
+
shaOfContentPreInlinedCss,
|
|
1268
|
+
contentWithCriticialCSS: finalHtml
|
|
1269
|
+
});
|
|
1270
|
+
return finalHtml;
|
|
1271
|
+
}
|
|
1272
|
+
buildServerAssetPathFromRequest(request) {
|
|
1273
|
+
let {
|
|
1274
|
+
pathname: assetPath
|
|
1275
|
+
} = new URL(request.url);
|
|
1276
|
+
if (!assetPath.endsWith('/index.html')) {
|
|
1277
|
+
assetPath = joinUrlParts(assetPath, 'index.html');
|
|
1278
|
+
}
|
|
1279
|
+
const {
|
|
1280
|
+
baseHref
|
|
1281
|
+
} = this.manifest;
|
|
1282
|
+
if (baseHref.length > 1 && assetPath.startsWith(baseHref)) {
|
|
1283
|
+
assetPath = assetPath.slice(baseHref.length);
|
|
1284
|
+
}
|
|
1285
|
+
return stripLeadingSlash(assetPath);
|
|
1286
|
+
}
|
|
1287
|
+
async runTransformsOnHtml(html, url, preload) {
|
|
1288
|
+
if (this.hooks.has('html:transform:pre')) {
|
|
1289
|
+
html = await this.hooks.run('html:transform:pre', {
|
|
1290
|
+
html,
|
|
1291
|
+
url
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
if (preload?.length) {
|
|
1295
|
+
html = appendPreloadHintsToHtml(html, preload);
|
|
1296
|
+
}
|
|
1297
|
+
return html;
|
|
1298
|
+
}
|
|
2207
1299
|
}
|
|
2208
1300
|
let angularServerApp;
|
|
2209
|
-
/**
|
|
2210
|
-
* Retrieves or creates an instance of `AngularServerApp`.
|
|
2211
|
-
* - If an instance of `AngularServerApp` already exists, it will return the existing one.
|
|
2212
|
-
* - If no instance exists, it will create a new one with the provided options.
|
|
2213
|
-
*
|
|
2214
|
-
* @param options Optional configuration options for the server application.
|
|
2215
|
-
*
|
|
2216
|
-
* @returns The existing or newly created instance of `AngularServerApp`.
|
|
2217
|
-
*/
|
|
2218
1301
|
function getOrCreateAngularServerApp(options) {
|
|
2219
|
-
|
|
1302
|
+
return angularServerApp ??= new AngularServerApp(options);
|
|
2220
1303
|
}
|
|
2221
|
-
/**
|
|
2222
|
-
* Destroys the existing `AngularServerApp` instance, releasing associated resources and resetting the
|
|
2223
|
-
* reference to `undefined`.
|
|
2224
|
-
*
|
|
2225
|
-
* This function is primarily used to enable the recreation of the `AngularServerApp` instance,
|
|
2226
|
-
* typically when server configuration or application state needs to be refreshed.
|
|
2227
|
-
*/
|
|
2228
1304
|
function destroyAngularServerApp() {
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
_resetCompiledComponents();
|
|
2234
|
-
}
|
|
2235
|
-
angularServerApp = undefined;
|
|
1305
|
+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
1306
|
+
_resetCompiledComponents();
|
|
1307
|
+
}
|
|
1308
|
+
angularServerApp = undefined;
|
|
2236
1309
|
}
|
|
2237
|
-
/**
|
|
2238
|
-
* Appends module preload hints to an HTML string for specified JavaScript resources.
|
|
2239
|
-
* This function enhances the HTML by injecting `<link rel="modulepreload">` elements
|
|
2240
|
-
* for each provided resource, allowing browsers to preload the specified JavaScript
|
|
2241
|
-
* modules for better performance.
|
|
2242
|
-
*
|
|
2243
|
-
* @param html - The original HTML string to which preload hints will be added.
|
|
2244
|
-
* @param preload - An array of URLs representing the JavaScript resources to preload.
|
|
2245
|
-
* @returns The modified HTML string with the preload hints injected before the closing `</body>` tag.
|
|
2246
|
-
* If `</body>` is not found, the links are not added.
|
|
2247
|
-
*/
|
|
2248
1310
|
function appendPreloadHintsToHtml(html, preload) {
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
// Placing them earlier can cause the browser to prioritize downloading these modules
|
|
2255
|
-
// over other critical page resources like images, CSS, and fonts.
|
|
2256
|
-
return [
|
|
2257
|
-
html.slice(0, bodyCloseIdx),
|
|
2258
|
-
...preload.map((val) => `<link rel="modulepreload" href="${val}">`),
|
|
2259
|
-
html.slice(bodyCloseIdx),
|
|
2260
|
-
].join('\n');
|
|
1311
|
+
const bodyCloseIdx = html.lastIndexOf('</body>');
|
|
1312
|
+
if (bodyCloseIdx === -1) {
|
|
1313
|
+
return html;
|
|
1314
|
+
}
|
|
1315
|
+
return [html.slice(0, bodyCloseIdx), ...preload.map(val => `<link rel="modulepreload" href="${val}">`), html.slice(bodyCloseIdx)].join('\n');
|
|
2261
1316
|
}
|
|
2262
|
-
/**
|
|
2263
|
-
* Creates an HTTP redirect response with a specified location and status code.
|
|
2264
|
-
*
|
|
2265
|
-
* @param location - The URL to which the response should redirect.
|
|
2266
|
-
* @param status - The HTTP status code for the redirection. Defaults to 302 (Found).
|
|
2267
|
-
* See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status
|
|
2268
|
-
* @returns A `Response` object representing the HTTP redirect.
|
|
2269
|
-
*/
|
|
2270
1317
|
function createRedirectResponse(location, status = 302) {
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
1318
|
+
return new Response(null, {
|
|
1319
|
+
status,
|
|
1320
|
+
headers: {
|
|
1321
|
+
'Location': location
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
2277
1324
|
}
|
|
2278
1325
|
|
|
2279
|
-
/**
|
|
2280
|
-
* Extracts a potential locale ID from a given URL based on the specified base path.
|
|
2281
|
-
*
|
|
2282
|
-
* This function parses the URL to locate a potential locale identifier that immediately
|
|
2283
|
-
* follows the base path segment in the URL's pathname. If the URL does not contain a valid
|
|
2284
|
-
* locale ID, an empty string is returned.
|
|
2285
|
-
*
|
|
2286
|
-
* @param url - The full URL from which to extract the locale ID.
|
|
2287
|
-
* @param basePath - The base path used as the reference point for extracting the locale ID.
|
|
2288
|
-
* @returns The extracted locale ID if present, or an empty string if no valid locale ID is found.
|
|
2289
|
-
*
|
|
2290
|
-
* @example
|
|
2291
|
-
* ```js
|
|
2292
|
-
* const url = new URL('https://example.com/base/en/page');
|
|
2293
|
-
* const basePath = '/base';
|
|
2294
|
-
* const localeId = getPotentialLocaleIdFromUrl(url, basePath);
|
|
2295
|
-
* console.log(localeId); // Output: 'en'
|
|
2296
|
-
* ```
|
|
2297
|
-
*/
|
|
2298
1326
|
function getPotentialLocaleIdFromUrl(url, basePath) {
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
return pathname.slice(start, end);
|
|
1327
|
+
const {
|
|
1328
|
+
pathname
|
|
1329
|
+
} = url;
|
|
1330
|
+
let start = basePath.length;
|
|
1331
|
+
if (pathname[start] === '/') {
|
|
1332
|
+
start++;
|
|
1333
|
+
}
|
|
1334
|
+
let end = pathname.indexOf('/', start);
|
|
1335
|
+
if (end === -1) {
|
|
1336
|
+
end = pathname.length;
|
|
1337
|
+
}
|
|
1338
|
+
return pathname.slice(start, end);
|
|
2312
1339
|
}
|
|
2313
|
-
/**
|
|
2314
|
-
* Parses the `Accept-Language` header and returns a list of locale preferences with their respective quality values.
|
|
2315
|
-
*
|
|
2316
|
-
* The `Accept-Language` header is typically a comma-separated list of locales, with optional quality values
|
|
2317
|
-
* in the form of `q=<value>`. If no quality value is specified, a default quality of `1` is assumed.
|
|
2318
|
-
* Special case: if the header is `*`, it returns the default locale with a quality of `1`.
|
|
2319
|
-
*
|
|
2320
|
-
* @param header - The value of the `Accept-Language` header, typically a comma-separated list of locales
|
|
2321
|
-
* with optional quality values (e.g., `en-US;q=0.8,fr-FR;q=0.9`). If the header is `*`,
|
|
2322
|
-
* it represents a wildcard for any language, returning the default locale.
|
|
2323
|
-
*
|
|
2324
|
-
* @returns A `ReadonlyMap` where the key is the locale (e.g., `en-US`, `fr-FR`), and the value is
|
|
2325
|
-
* the associated quality value (a number between 0 and 1). If no quality value is provided,
|
|
2326
|
-
* a default of `1` is used.
|
|
2327
|
-
*
|
|
2328
|
-
* @example
|
|
2329
|
-
* ```js
|
|
2330
|
-
* parseLanguageHeader('en-US;q=0.8,fr-FR;q=0.9')
|
|
2331
|
-
* // returns new Map([['en-US', 0.8], ['fr-FR', 0.9]])
|
|
2332
|
-
|
|
2333
|
-
* parseLanguageHeader('*')
|
|
2334
|
-
* // returns new Map([['*', 1]])
|
|
2335
|
-
* ```
|
|
2336
|
-
*/
|
|
2337
1340
|
function parseLanguageHeader(header) {
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
})
|
|
2351
|
-
.sort(([_localeA, qualityA], [_localeB, qualityB]) => qualityB - qualityA);
|
|
2352
|
-
return new Map(parsedValues);
|
|
1341
|
+
if (header === '*') {
|
|
1342
|
+
return new Map([['*', 1]]);
|
|
1343
|
+
}
|
|
1344
|
+
const parsedValues = header.split(',').map(item => {
|
|
1345
|
+
const [locale, qualityValue] = item.split(';', 2).map(v => v.trim());
|
|
1346
|
+
let quality = qualityValue?.startsWith('q=') ? parseFloat(qualityValue.slice(2)) : undefined;
|
|
1347
|
+
if (typeof quality !== 'number' || isNaN(quality) || quality < 0 || quality > 1) {
|
|
1348
|
+
quality = 1;
|
|
1349
|
+
}
|
|
1350
|
+
return [locale, quality];
|
|
1351
|
+
}).sort(([_localeA, qualityA], [_localeB, qualityB]) => qualityB - qualityA);
|
|
1352
|
+
return new Map(parsedValues);
|
|
2353
1353
|
}
|
|
2354
|
-
/**
|
|
2355
|
-
* Gets the preferred locale based on the highest quality value from the provided `Accept-Language` header
|
|
2356
|
-
* and the set of available locales.
|
|
2357
|
-
*
|
|
2358
|
-
* This function adheres to the HTTP `Accept-Language` header specification as defined in
|
|
2359
|
-
* [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5), including:
|
|
2360
|
-
* - Case-insensitive matching of language tags.
|
|
2361
|
-
* - Quality value handling (e.g., `q=1`, `q=0.8`). If no quality value is provided, it defaults to `q=1`.
|
|
2362
|
-
* - Prefix matching (e.g., `en` matching `en-US` or `en-GB`).
|
|
2363
|
-
*
|
|
2364
|
-
* @param header - The `Accept-Language` header string to parse and evaluate. It may contain multiple
|
|
2365
|
-
* locales with optional quality values, for example: `'en-US;q=0.8,fr-FR;q=0.9'`.
|
|
2366
|
-
* @param supportedLocales - An array of supported locales (e.g., `['en-US', 'fr-FR']`),
|
|
2367
|
-
* representing the locales available in the application.
|
|
2368
|
-
* @returns The best matching locale from the supported languages, or `undefined` if no match is found.
|
|
2369
|
-
*
|
|
2370
|
-
* @example
|
|
2371
|
-
* ```js
|
|
2372
|
-
* getPreferredLocale('en-US;q=0.8,fr-FR;q=0.9', ['en-US', 'fr-FR', 'de-DE'])
|
|
2373
|
-
* // returns 'fr-FR'
|
|
2374
|
-
*
|
|
2375
|
-
* getPreferredLocale('en;q=0.9,fr-FR;q=0.8', ['en-US', 'fr-FR', 'de-DE'])
|
|
2376
|
-
* // returns 'en-US'
|
|
2377
|
-
*
|
|
2378
|
-
* getPreferredLocale('es-ES;q=0.7', ['en-US', 'fr-FR', 'de-DE'])
|
|
2379
|
-
* // returns undefined
|
|
2380
|
-
* ```
|
|
2381
|
-
*/
|
|
2382
1354
|
function getPreferredLocale(header, supportedLocales) {
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
const normalizedLocale = normalizeLocale(locale);
|
|
2405
|
-
if (quality === 0) {
|
|
2406
|
-
qualityZeroNormalizedLocales.add(normalizedLocale);
|
|
2407
|
-
continue; // Skip locales with quality value of 0.
|
|
2408
|
-
}
|
|
2409
|
-
// Exact match found.
|
|
2410
|
-
if (normalizedSupportedLocales.has(normalizedLocale)) {
|
|
2411
|
-
return normalizedSupportedLocales.get(normalizedLocale);
|
|
2412
|
-
}
|
|
2413
|
-
// If an exact match is not found, try prefix matching (e.g., "en" matches "en-US").
|
|
2414
|
-
// Store the first prefix match encountered, as it has the highest quality value.
|
|
2415
|
-
if (bestMatch !== undefined) {
|
|
2416
|
-
continue;
|
|
2417
|
-
}
|
|
2418
|
-
const [languagePrefix] = normalizedLocale.split('-', 1);
|
|
2419
|
-
for (const supportedLocale of normalizedSupportedLocales.keys()) {
|
|
2420
|
-
if (supportedLocale.startsWith(languagePrefix)) {
|
|
2421
|
-
bestMatch = normalizedSupportedLocales.get(supportedLocale);
|
|
2422
|
-
break; // No need to continue searching for this locale.
|
|
2423
|
-
}
|
|
2424
|
-
}
|
|
1355
|
+
if (supportedLocales.length < 2) {
|
|
1356
|
+
return supportedLocales[0];
|
|
1357
|
+
}
|
|
1358
|
+
const parsedLocales = parseLanguageHeader(header);
|
|
1359
|
+
if (parsedLocales.size === 0 || parsedLocales.size === 1 && parsedLocales.has('*')) {
|
|
1360
|
+
return supportedLocales[0];
|
|
1361
|
+
}
|
|
1362
|
+
const normalizedSupportedLocales = new Map();
|
|
1363
|
+
for (const locale of supportedLocales) {
|
|
1364
|
+
normalizedSupportedLocales.set(normalizeLocale(locale), locale);
|
|
1365
|
+
}
|
|
1366
|
+
let bestMatch;
|
|
1367
|
+
const qualityZeroNormalizedLocales = new Set();
|
|
1368
|
+
for (const [locale, quality] of parsedLocales) {
|
|
1369
|
+
const normalizedLocale = normalizeLocale(locale);
|
|
1370
|
+
if (quality === 0) {
|
|
1371
|
+
qualityZeroNormalizedLocales.add(normalizedLocale);
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
if (normalizedSupportedLocales.has(normalizedLocale)) {
|
|
1375
|
+
return normalizedSupportedLocales.get(normalizedLocale);
|
|
2425
1376
|
}
|
|
2426
1377
|
if (bestMatch !== undefined) {
|
|
2427
|
-
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
for (const
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
const [languagePrefix] = normalizedLocale.split('-', 1);
|
|
1381
|
+
for (const supportedLocale of normalizedSupportedLocales.keys()) {
|
|
1382
|
+
if (supportedLocale.startsWith(languagePrefix)) {
|
|
1383
|
+
bestMatch = normalizedSupportedLocales.get(supportedLocale);
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (bestMatch !== undefined) {
|
|
1389
|
+
return bestMatch;
|
|
1390
|
+
}
|
|
1391
|
+
for (const [normalizedLocale, locale] of normalizedSupportedLocales) {
|
|
1392
|
+
if (!qualityZeroNormalizedLocales.has(normalizedLocale)) {
|
|
1393
|
+
return locale;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
2435
1396
|
}
|
|
2436
|
-
/**
|
|
2437
|
-
* Normalizes a locale string by converting it to lowercase.
|
|
2438
|
-
*
|
|
2439
|
-
* @param locale - The locale string to normalize.
|
|
2440
|
-
* @returns The normalized locale string in lowercase.
|
|
2441
|
-
*
|
|
2442
|
-
* @example
|
|
2443
|
-
* ```ts
|
|
2444
|
-
* const normalized = normalizeLocale('EN-US');
|
|
2445
|
-
* console.log(normalized); // Output: "en-us"
|
|
2446
|
-
* ```
|
|
2447
|
-
*/
|
|
2448
1397
|
function normalizeLocale(locale) {
|
|
2449
|
-
|
|
1398
|
+
return locale.toLowerCase();
|
|
2450
1399
|
}
|
|
2451
1400
|
|
|
2452
|
-
/**
|
|
2453
|
-
* Angular server application engine.
|
|
2454
|
-
* Manages Angular server applications (including localized ones), handles rendering requests,
|
|
2455
|
-
* and optionally transforms index HTML before rendering.
|
|
2456
|
-
*
|
|
2457
|
-
* @remarks This class should be instantiated once and used as a singleton across the server-side
|
|
2458
|
-
* application to ensure consistent handling of rendering requests and resource management.
|
|
2459
|
-
*/
|
|
2460
1401
|
class AngularAppEngine {
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
*
|
|
2498
|
-
* @remarks A request to `https://www.example.com/page/index.html` will serve or render the Angular route
|
|
2499
|
-
* corresponding to `https://www.example.com/page`.
|
|
2500
|
-
*/
|
|
2501
|
-
async handle(request, requestContext) {
|
|
2502
|
-
const serverApp = await this.getAngularServerAppForRequest(request);
|
|
2503
|
-
if (serverApp) {
|
|
2504
|
-
return serverApp.handle(request, requestContext);
|
|
2505
|
-
}
|
|
2506
|
-
if (this.supportedLocales.length > 1) {
|
|
2507
|
-
// Redirect to the preferred language if i18n is enabled.
|
|
2508
|
-
return this.redirectBasedOnAcceptLanguage(request);
|
|
2509
|
-
}
|
|
2510
|
-
return null;
|
|
2511
|
-
}
|
|
2512
|
-
/**
|
|
2513
|
-
* Handles requests for the base path when i18n is enabled.
|
|
2514
|
-
* Redirects the user to a locale-specific path based on the `Accept-Language` header.
|
|
2515
|
-
*
|
|
2516
|
-
* @param request The incoming request.
|
|
2517
|
-
* @returns A `Response` object with a 302 redirect, or `null` if i18n is not enabled
|
|
2518
|
-
* or the request is not for the base path.
|
|
2519
|
-
*/
|
|
2520
|
-
redirectBasedOnAcceptLanguage(request) {
|
|
2521
|
-
const { basePath, supportedLocales } = this.manifest;
|
|
2522
|
-
// If the request is not for the base path, it's not our responsibility to handle it.
|
|
2523
|
-
const { pathname } = new URL(request.url);
|
|
2524
|
-
if (pathname !== basePath) {
|
|
2525
|
-
return null;
|
|
2526
|
-
}
|
|
2527
|
-
// For requests to the base path (typically '/'), attempt to extract the preferred locale
|
|
2528
|
-
// from the 'Accept-Language' header.
|
|
2529
|
-
const preferredLocale = getPreferredLocale(request.headers.get('Accept-Language') || '*', this.supportedLocales);
|
|
2530
|
-
if (preferredLocale) {
|
|
2531
|
-
const subPath = supportedLocales[preferredLocale];
|
|
2532
|
-
if (subPath !== undefined) {
|
|
2533
|
-
return new Response(null, {
|
|
2534
|
-
status: 302, // Use a 302 redirect as language preference may change.
|
|
2535
|
-
headers: {
|
|
2536
|
-
'Location': joinUrlParts(pathname, subPath),
|
|
2537
|
-
'Vary': 'Accept-Language',
|
|
2538
|
-
},
|
|
2539
|
-
});
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
return null;
|
|
2543
|
-
}
|
|
2544
|
-
/**
|
|
2545
|
-
* Retrieves the Angular server application instance for a given request.
|
|
2546
|
-
*
|
|
2547
|
-
* This method checks if the request URL corresponds to an Angular application entry point.
|
|
2548
|
-
* If so, it initializes or retrieves an instance of the Angular server application for that entry point.
|
|
2549
|
-
* Requests that resemble file requests (except for `/index.html`) are skipped.
|
|
2550
|
-
*
|
|
2551
|
-
* @param request - The incoming HTTP request object.
|
|
2552
|
-
* @returns A promise that resolves to an `AngularServerApp` instance if a valid entry point is found,
|
|
2553
|
-
* or `null` if no entry point matches the request URL.
|
|
2554
|
-
*/
|
|
2555
|
-
async getAngularServerAppForRequest(request) {
|
|
2556
|
-
// Skip if the request looks like a file but not `/index.html`.
|
|
2557
|
-
const url = new URL(request.url);
|
|
2558
|
-
const entryPoint = await this.getEntryPointExportsForUrl(url);
|
|
2559
|
-
if (!entryPoint) {
|
|
2560
|
-
return null;
|
|
2561
|
-
}
|
|
2562
|
-
// Note: Using `instanceof` is not feasible here because `AngularServerApp` will
|
|
2563
|
-
// be located in separate bundles, making `instanceof` checks unreliable.
|
|
2564
|
-
const ɵgetOrCreateAngularServerApp = entryPoint.ɵgetOrCreateAngularServerApp;
|
|
2565
|
-
const serverApp = ɵgetOrCreateAngularServerApp({
|
|
2566
|
-
allowStaticRouteRender: AngularAppEngine.ɵallowStaticRouteRender,
|
|
2567
|
-
hooks: AngularAppEngine.ɵhooks,
|
|
1402
|
+
static ɵallowStaticRouteRender = false;
|
|
1403
|
+
static ɵhooks = /* #__PURE__*/new Hooks();
|
|
1404
|
+
manifest = getAngularAppEngineManifest();
|
|
1405
|
+
supportedLocales = Object.keys(this.manifest.supportedLocales);
|
|
1406
|
+
entryPointsCache = new Map();
|
|
1407
|
+
async handle(request, requestContext) {
|
|
1408
|
+
const serverApp = await this.getAngularServerAppForRequest(request);
|
|
1409
|
+
if (serverApp) {
|
|
1410
|
+
return serverApp.handle(request, requestContext);
|
|
1411
|
+
}
|
|
1412
|
+
if (this.supportedLocales.length > 1) {
|
|
1413
|
+
return this.redirectBasedOnAcceptLanguage(request);
|
|
1414
|
+
}
|
|
1415
|
+
return null;
|
|
1416
|
+
}
|
|
1417
|
+
redirectBasedOnAcceptLanguage(request) {
|
|
1418
|
+
const {
|
|
1419
|
+
basePath,
|
|
1420
|
+
supportedLocales
|
|
1421
|
+
} = this.manifest;
|
|
1422
|
+
const {
|
|
1423
|
+
pathname
|
|
1424
|
+
} = new URL(request.url);
|
|
1425
|
+
if (pathname !== basePath) {
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
const preferredLocale = getPreferredLocale(request.headers.get('Accept-Language') || '*', this.supportedLocales);
|
|
1429
|
+
if (preferredLocale) {
|
|
1430
|
+
const subPath = supportedLocales[preferredLocale];
|
|
1431
|
+
if (subPath !== undefined) {
|
|
1432
|
+
return new Response(null, {
|
|
1433
|
+
status: 302,
|
|
1434
|
+
headers: {
|
|
1435
|
+
'Location': joinUrlParts(pathname, subPath),
|
|
1436
|
+
'Vary': 'Accept-Language'
|
|
1437
|
+
}
|
|
2568
1438
|
});
|
|
2569
|
-
|
|
2570
|
-
}
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1443
|
+
async getAngularServerAppForRequest(request) {
|
|
1444
|
+
const url = new URL(request.url);
|
|
1445
|
+
const entryPoint = await this.getEntryPointExportsForUrl(url);
|
|
1446
|
+
if (!entryPoint) {
|
|
1447
|
+
return null;
|
|
1448
|
+
}
|
|
1449
|
+
const ɵgetOrCreateAngularServerApp = entryPoint.ɵgetOrCreateAngularServerApp;
|
|
1450
|
+
const serverApp = ɵgetOrCreateAngularServerApp({
|
|
1451
|
+
allowStaticRouteRender: AngularAppEngine.ɵallowStaticRouteRender,
|
|
1452
|
+
hooks: AngularAppEngine.ɵhooks
|
|
1453
|
+
});
|
|
1454
|
+
return serverApp;
|
|
1455
|
+
}
|
|
1456
|
+
getEntryPointExports(potentialLocale) {
|
|
1457
|
+
const cachedEntryPoint = this.entryPointsCache.get(potentialLocale);
|
|
1458
|
+
if (cachedEntryPoint) {
|
|
1459
|
+
return cachedEntryPoint;
|
|
1460
|
+
}
|
|
1461
|
+
const {
|
|
1462
|
+
entryPoints
|
|
1463
|
+
} = this.manifest;
|
|
1464
|
+
const entryPoint = entryPoints[potentialLocale];
|
|
1465
|
+
if (!entryPoint) {
|
|
1466
|
+
return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
const entryPointExports = entryPoint();
|
|
1469
|
+
this.entryPointsCache.set(potentialLocale, entryPointExports);
|
|
1470
|
+
return entryPointExports;
|
|
1471
|
+
}
|
|
1472
|
+
getEntryPointExportsForUrl(url) {
|
|
1473
|
+
const {
|
|
1474
|
+
basePath
|
|
1475
|
+
} = this.manifest;
|
|
1476
|
+
if (this.supportedLocales.length === 1) {
|
|
1477
|
+
return this.getEntryPointExports('');
|
|
1478
|
+
}
|
|
1479
|
+
const potentialLocale = getPotentialLocaleIdFromUrl(url, basePath);
|
|
1480
|
+
return this.getEntryPointExports(potentialLocale) ?? this.getEntryPointExports('');
|
|
1481
|
+
}
|
|
2610
1482
|
}
|
|
2611
1483
|
|
|
2612
|
-
/**
|
|
2613
|
-
* Annotates a request handler function with metadata, marking it as a special
|
|
2614
|
-
* handler.
|
|
2615
|
-
*
|
|
2616
|
-
* @param handler - The request handler function to be annotated.
|
|
2617
|
-
* @returns The same handler function passed in, with metadata attached.
|
|
2618
|
-
*
|
|
2619
|
-
* @example
|
|
2620
|
-
* Example usage in a Hono application:
|
|
2621
|
-
* ```ts
|
|
2622
|
-
* const app = new Hono();
|
|
2623
|
-
* export default createRequestHandler(app.fetch);
|
|
2624
|
-
* ```
|
|
2625
|
-
*
|
|
2626
|
-
* @example
|
|
2627
|
-
* Example usage in a H3 application:
|
|
2628
|
-
* ```ts
|
|
2629
|
-
* const app = createApp();
|
|
2630
|
-
* const handler = toWebHandler(app);
|
|
2631
|
-
* export default createRequestHandler(handler);
|
|
2632
|
-
* ```
|
|
2633
|
-
*/
|
|
2634
1484
|
function createRequestHandler(handler) {
|
|
2635
|
-
|
|
2636
|
-
|
|
1485
|
+
handler['__ng_request_handler__'] = true;
|
|
1486
|
+
return handler;
|
|
2637
1487
|
}
|
|
2638
1488
|
|
|
2639
1489
|
export { AngularAppEngine, PrerenderFallback, RenderMode, createRequestHandler, provideServerRendering, withAppShell, withRoutes, InlineCriticalCssProcessor as ɵInlineCriticalCssProcessor, destroyAngularServerApp as ɵdestroyAngularServerApp, extractRoutesAndCreateRouteTree as ɵextractRoutesAndCreateRouteTree, getOrCreateAngularServerApp as ɵgetOrCreateAngularServerApp, getRoutesFromAngularRouterConfig as ɵgetRoutesFromAngularRouterConfig, setAngularAppEngineManifest as ɵsetAngularAppEngineManifest, setAngularAppManifest as ɵsetAngularAppManifest };
|