Skip to content

Commit 6fe6e24

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 6fe6e24

File tree

17 files changed

+880
-164
lines changed

17 files changed

+880
-164
lines changed

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

Lines changed: 23 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,27 @@ 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 enum SSGFallback {
36+
CSR = 1,
37+
None = 2,
38+
SSR = 0
39+
}
40+
1841
// (No @packageDocumentation comment for this package)
1942

2043
```
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
## API Report File for "@angular/devkit-repo"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
7+
// @public
8+
export interface ServerRouteAppShell extends Omit<ServerRouteCommon, 'headers' | 'status'> {
9+
renderMode: RenderMode.AppShell;
10+
}
11+
12+
// @public
13+
export interface ServerRouteCommon {
14+
headers?: Record<string, string>;
15+
path: string;
16+
status?: number;
17+
}
18+
19+
// @public
20+
export interface ServerRouteCSR extends ServerRouteCommon {
21+
renderMode: RenderMode.CSR;
22+
}
23+
24+
// @public
25+
export interface ServerRouteSSG extends Omit<ServerRouteCommon, 'status'> {
26+
fallback?: SSGFallback;
27+
getPrerenderPaths?: () => Promise<Record<string, string>[]>;
28+
renderMode: RenderMode.SSG;
29+
}
30+
31+
// @public
32+
export interface ServerRouteSSR extends ServerRouteCommon {
33+
renderMode: RenderMode.SSR;
34+
}
35+
36+
// (No @packageDocumentation comment for this package)
37+
38+
```

packages/angular/ssr/BUILD.bazel

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("@npm//@angular/build-tooling/bazel/api-golden:index.bzl", "api_golden_test_npm_package")
1+
load("@npm//@angular/build-tooling/bazel/api-golden:index.bzl", "api_golden_test", "api_golden_test_npm_package")
22
load("@rules_pkg//:pkg.bzl", "pkg_tar")
33
load("//tools:defaults.bzl", "ng_package", "ts_library")
44

@@ -67,3 +67,13 @@ api_golden_test_npm_package(
6767
golden_dir = "angular_cli/goldens/public-api/angular/ssr",
6868
npm_package = "angular_cli/packages/angular/ssr/npm_package",
6969
)
70+
71+
api_golden_test(
72+
name = "ssr_transitive_api",
73+
data = [
74+
":ssr",
75+
"//goldens:public-api",
76+
],
77+
entry_point = "angular_cli/packages/angular/ssr/public_api_transitive.d.ts",
78+
golden = "angular_cli/goldens/public-api/angular/ssr/index_transitive.api.md",
79+
)

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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,10 @@ export {
1313
getOrCreateAngularAppEngine,
1414
destroyAngularAppEngine,
1515
} from './src/app-engine';
16+
17+
export {
18+
type SSGFallback,
19+
type RenderMode,
20+
type ServerRoute,
21+
provideServerRoutesConfig,
22+
} from './src/routes/route-config';
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
// This file exports symbols that are not part of the public API but are
10+
// dependencies of public API symbols. Including them here ensures they
11+
// are tracked in the API golden file, preventing accidental breaking changes.
12+
13+
export {
14+
type ServerRouteAppShell,
15+
type ServerRouteCSR,
16+
type ServerRouteSSG,
17+
type ServerRouteSSR,
18+
type ServerRouteCommon,
19+
} 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)