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