Skip to content

Commit 01df293

Browse files
committed
feat(@angular/ssr): add server routing configuration API
This commit introduces a new server routing configuration API, as discussed in RFC angular/angular#56785. The new API provides several enhancements: ## Add headers and status code to individual pages: ```ts const serverRoutes: ServerRoute[] = [ { path: '/error', renderMode: RenderMode.SSR, status: 404, headers: { 'Cache-Control': 'no-cache' } } ]; ``` ## Provide values for prerendering pages with dynamic routes: ```ts const serverRoutes: ServerRoute[] = [ { path: '/product/:id', renderMode: RenderMode.SSG, async getPrerenderPaths() { const dataService = inject(ProductService); const ids = await dataService.getIds(); // Assuming this returns ['1', '2', '3'] return ids.map(id => ({ id })); // Generates paths like: [{ id: '1' }, { id: '2' }, { id: '3' }] } } ]; ``` ## Specify fallback behavior for SSG routes with parameterized paths: ```ts const serverRoutes: ServerRoute[] = [ { path: '/product/:id', renderMode: RenderMode.SSG, fallback: SSGFallback.SSR, // Can be SSR, CSR, or None async getPrerenderPaths() { } } ]; ``` ## Control rendering mode for individual routes: ```ts const serverRoutes: ServerRoute[] = [ { path: '/product/:id', renderMode: RenderMode.SSR, }, { path: '/error', renderMode: RenderMode.CSR, }, { path: '/**', renderMode: RenderMode.SSG, }, ]; ``` These additions aim to provide greater flexibility and control over server-side rendering configurations and prerendering behaviors.
1 parent 7815301 commit 01df293

File tree

14 files changed

+846
-163
lines changed

14 files changed

+846
-163
lines changed

goldens/public-api/angular/ssr/index.api.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
55
```ts
66

7+
import { EnvironmentProviders } from '@angular/core';
8+
79
// @public
810
export interface AngularServerAppManager {
911
render(request: Request, requestContext?: unknown): Promise<Response | null>;
@@ -15,6 +17,56 @@ export function destroyAngularAppEngine(): void;
1517
// @public
1618
export function getOrCreateAngularAppEngine(): AngularServerAppManager;
1719

20+
// @public
21+
export function provideServerRoutesConfig(routes: ServerRoute[]): EnvironmentProviders;
22+
23+
// @public
24+
export enum RenderMode {
25+
AppShell = 0,
26+
CSR = 2,
27+
SSG = 3,
28+
SSR = 1
29+
}
30+
31+
// @public
32+
export type ServerRoute = ServerRouteAppShell | ServerRouteCSR | ServerRouteSSG | ServerRouteSSR;
33+
34+
// @public
35+
export interface ServerRouteAppShell extends Omit<ServerRouteCommon, 'headers' | 'status'> {
36+
renderMode: RenderMode.AppShell;
37+
}
38+
39+
// @public
40+
export interface ServerRouteCommon {
41+
headers?: Record<string, string>;
42+
path: string;
43+
status?: number;
44+
}
45+
46+
// @public
47+
export interface ServerRouteCSR extends ServerRouteCommon {
48+
renderMode: RenderMode.CSR;
49+
}
50+
51+
// @public
52+
export interface ServerRouteSSG extends Omit<ServerRouteCommon, 'status'> {
53+
fallback?: SSGFallback;
54+
getPrenderPaths?: () => Promise<Record<string, string>[]>;
55+
renderMode: RenderMode.SSG;
56+
}
57+
58+
// @public
59+
export interface ServerRouteSSR extends ServerRouteCommon {
60+
renderMode: RenderMode.SSR;
61+
}
62+
63+
// @public
64+
export enum SSGFallback {
65+
CSR = 1,
66+
None = 2,
67+
SSR = 0
68+
}
69+
1870
// (No @packageDocumentation comment for this package)
1971

2072
```

packages/angular/ssr/private_export.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export {
1212
extractRoutesAndCreateRouteTree as ɵextractRoutesAndCreateRouteTree,
1313
} from './src/routes/ng-routes';
1414
export {
15-
ServerRenderContext as ɵServerRenderContext,
1615
getOrCreateAngularServerApp as ɵgetOrCreateAngularServerApp,
1716
destroyAngularServerApp as ɵdestroyAngularServerApp,
1817
} from './src/app';

packages/angular/ssr/public_api.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,15 @@ export {
1313
getOrCreateAngularAppEngine,
1414
destroyAngularAppEngine,
1515
} from './src/app-engine';
16+
17+
export {
18+
type SSGFallback,
19+
type RenderMode,
20+
type ServerRoute,
21+
type ServerRouteAppShell,
22+
type ServerRouteCSR,
23+
type ServerRouteSSG,
24+
type ServerRouteSSR,
25+
type ServerRouteCommon,
26+
provideServerRoutesConfig,
27+
} from './src/routes/route-config';

packages/angular/ssr/src/app.ts

Lines changed: 66 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,34 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { StaticProvider, ɵConsole, ɵresetCompiledComponents } from '@angular/core';
10-
import { ɵSERVER_CONTEXT as SERVER_CONTEXT } from '@angular/platform-server';
9+
import { StaticProvider, ɵresetCompiledComponents } from '@angular/core';
1110
import { ServerAssets } from './assets';
12-
import { Console } from './console';
1311
import { Hooks } from './hooks';
1412
import { getAngularAppManifest } from './manifest';
13+
import { RenderMode } from './routes/route-config';
1514
import { ServerRouter } from './routes/router';
1615
import { REQUEST, REQUEST_CONTEXT, RESPONSE_INIT } from './tokens';
1716
import { InlineCriticalCssProcessor } from './utils/inline-critical-css';
1817
import { renderAngular } from './utils/ng';
1918

2019
/**
21-
* Enum representing the different contexts in which server rendering can occur.
20+
* A mapping of `RenderMode` enum values to corresponding string representations.
21+
*
22+
* This record is used to map each `RenderMode` to a specific string value that represents
23+
* the server context. The string values are used internally to differentiate
24+
* between various rendering strategies when processing routes.
25+
*
26+
* - `RenderMode.SSG` maps to `'ssg'` (Static Site Generation).
27+
* - `RenderMode.SSR` maps to `'ssr'` (Server-Side Rendering).
28+
* - `RenderMode.AppShell` maps to `'app-shell'` (pre-rendered application shell).
29+
* - `RenderMode.CSR` maps to an empty string `''` (Client-Side Rendering, no server context needed).
2230
*/
23-
export enum ServerRenderContext {
24-
SSR = 'ssr',
25-
SSG = 'ssg',
26-
AppShell = 'app-shell',
27-
}
31+
const SERVER_CONTEXT_VALUE: Record<RenderMode, string> = {
32+
[RenderMode.SSG]: 'ssg',
33+
[RenderMode.SSR]: 'ssr',
34+
[RenderMode.AppShell]: 'app-shell',
35+
[RenderMode.CSR]: '',
36+
};
2837

2938
/**
3039
* Represents a locale-specific Angular server application managed by the server application engine.
@@ -65,18 +74,31 @@ export class AngularServerApp {
6574
*
6675
* @param request - The incoming HTTP request to be rendered.
6776
* @param requestContext - Optional additional context for rendering, such as request metadata.
68-
* @param serverContext - The rendering context.
6977
*
7078
* @returns A promise that resolves to the HTTP response object resulting from the rendering, or null if no match is found.
7179
*/
72-
render(
73-
request: Request,
74-
requestContext?: unknown,
75-
serverContext: ServerRenderContext = ServerRenderContext.SSR,
76-
): Promise<Response | null> {
80+
render(request: Request, requestContext?: unknown): Promise<Response | null> {
7781
return Promise.race([
7882
this.createAbortPromise(request),
79-
this.handleRendering(request, requestContext, serverContext),
83+
this.handleRendering(request, /** isSsrMode */ true, requestContext),
84+
]);
85+
}
86+
87+
/**
88+
* Renders a response for the given URL using the server application.
89+
*
90+
* This method processes the request based on the provided URL and returns a response by performing server-side rendering.
91+
* The rendering process can be aborted, with the first completed promise (either the abort or the render) determining the result.
92+
*
93+
* @param url - The URL of the page url. This object contains the full URL to be processed and rendered by the server.
94+
* @returns A promise that resolves to the HTTP response object generated from the rendering, or `null` if no matching route is found.
95+
*/
96+
renderStatic(url: URL): Promise<Response | null> {
97+
const request = new Request(url);
98+
99+
return Promise.race([
100+
this.createAbortPromise(request),
101+
this.handleRendering(request, /** isSsrMode */ false),
80102
]);
81103
}
82104

@@ -107,15 +129,15 @@ export class AngularServerApp {
107129
* This method matches the request URL to a route and performs rendering if a matching route is found.
108130
*
109131
* @param request - The incoming HTTP request to be processed.
132+
* @param isSsrMode - A boolean indicating whether the rendering is performed in server-side rendering (SSR) mode.
110133
* @param requestContext - Optional additional context for rendering, such as request metadata.
111-
* @param serverContext - The rendering context. Defaults to server-side rendering (SSR).
112134
*
113135
* @returns A promise that resolves to the rendered response, or null if no matching route is found.
114136
*/
115137
private async handleRendering(
116138
request: Request,
139+
isSsrMode: boolean,
117140
requestContext?: unknown,
118-
serverContext: ServerRenderContext = ServerRenderContext.SSR,
119141
): Promise<Response | null> {
120142
const url = new URL(request.url);
121143
this.router ??= await ServerRouter.from(this.manifest, url);
@@ -126,32 +148,32 @@ export class AngularServerApp {
126148
return null;
127149
}
128150

129-
const { redirectTo } = matchedRoute;
151+
const { redirectTo, status } = matchedRoute;
130152
if (redirectTo !== undefined) {
153+
// Note: The status code is validated during route extraction.
131154
// 302 Found is used by default for redirections
132155
// See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status
133-
return Response.redirect(new URL(redirectTo, url), 302);
156+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
157+
return Response.redirect(new URL(redirectTo, url), (status as any) ?? 302);
134158
}
135159

136-
const platformProviders: StaticProvider[] = [
137-
{
138-
provide: SERVER_CONTEXT,
139-
useValue: serverContext,
140-
},
141-
{
142-
// An Angular Console Provider that does not print a set of predefined logs.
143-
provide: ɵConsole,
144-
// Using `useClass` would necessitate decorating `Console` with `@Injectable`,
145-
// which would require switching from `ts_library` to `ng_module`. This change
146-
// would also necessitate various patches of `@angular/bazel` to support ESM.
147-
useFactory: () => new Console(),
148-
},
149-
];
150-
151-
const isSsrMode = serverContext === ServerRenderContext.SSR;
152-
const responseInit: ResponseInit = {};
160+
const { renderMode = isSsrMode ? RenderMode.SSR : RenderMode.SSG, headers } = matchedRoute;
161+
162+
const platformProviders: StaticProvider[] = [];
163+
let responseInit: ResponseInit | undefined;
153164

154165
if (isSsrMode) {
166+
// Initialize the response with status and headers if available.
167+
responseInit = {
168+
status,
169+
headers: headers ? new Headers(headers) : undefined,
170+
};
171+
172+
if (renderMode === RenderMode.CSR) {
173+
// Serve the client-side rendered version if the route is configured for CSR.
174+
return new Response(await this.assets.getServerAsset('index.csr.html'), responseInit);
175+
}
176+
155177
platformProviders.push(
156178
{
157179
provide: REQUEST,
@@ -183,7 +205,13 @@ export class AngularServerApp {
183205
html = await hooks.run('html:transform:pre', { html });
184206
}
185207

186-
html = await renderAngular(html, manifest.bootstrap(), new URL(request.url), platformProviders);
208+
html = await renderAngular(
209+
html,
210+
manifest.bootstrap(),
211+
new URL(request.url),
212+
platformProviders,
213+
SERVER_CONTEXT_VALUE[renderMode],
214+
);
187215

188216
if (manifest.inlineCriticalCss) {
189217
// Optionally inline critical CSS.

0 commit comments

Comments
 (0)