Skip to content

Commit d66aaa3

Browse files
committedSep 12, 2024·
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: ```ts const serverRoutes: ServerRoute[] = [ { path: '/error', renderMode: RenderMode.Server, status: 404, headers: { 'Cache-Control': 'no-cache' } } ]; ``` ```ts const serverRoutes: ServerRoute[] = [ { path: '/product/:id', renderMode: RenderMode.Prerender, 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' }] } } ]; ``` ```ts const serverRoutes: ServerRoute[] = [ { path: '/product/:id', renderMode: RenderMode.Prerender, fallback: PrerenderFallback.Server, // Can be Server, Client, or None async getPrerenderPaths() { } } ]; ``` ```ts const serverRoutes: ServerRoute[] = [ { path: '/product/:id', renderMode: RenderMode.Server, }, { path: '/error', renderMode: RenderMode.Client, }, { path: '/**', renderMode: RenderMode.Prerender, }, ]; ``` These additions aim to provide greater flexibility and control over server-side rendering configurations and prerendering behaviors.
1 parent 793f6a0 commit d66aaa3

27 files changed

+868
-225
lines changed
 

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

+24-1
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,36 @@
44
55
```ts
66

7+
import { EnvironmentProviders } from '@angular/core';
8+
79
// @public
810
export class AngularAppEngine {
9-
getHeaders(request: Request): ReadonlyMap<string, string>;
11+
getPrerenderHeaders(request: Request): ReadonlyMap<string, string>;
1012
render(request: Request, requestContext?: unknown): Promise<Response | null>;
1113
static ɵhooks: Hooks;
1214
}
1315

16+
// @public
17+
export enum PrerenderFallback {
18+
Client = 1,
19+
None = 2,
20+
Server = 0
21+
}
22+
23+
// @public
24+
export function provideServerRoutesConfig(routes: ServerRoute[]): EnvironmentProviders;
25+
26+
// @public
27+
export enum RenderMode {
28+
AppShell = 0,
29+
Client = 2,
30+
Prerender = 3,
31+
Server = 1
32+
}
33+
34+
// @public
35+
export type ServerRoute = ServerRouteAppShell | ServerRouteClient | ServerRoutePrerender | ServerRoutePrerenderWithParams | ServerRouteServer;
36+
1437
// (No @packageDocumentation comment for this package)
1538

1639
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 ServerRouteClient extends ServerRouteCommon {
14+
renderMode: RenderMode.Client;
15+
}
16+
17+
// @public
18+
export interface ServerRouteCommon {
19+
headers?: Record<string, string>;
20+
path: string;
21+
status?: number;
22+
}
23+
24+
// @public
25+
export interface ServerRoutePrerender extends Omit<ServerRouteCommon, 'status'> {
26+
fallback?: never;
27+
renderMode: RenderMode.Prerender;
28+
}
29+
30+
// @public
31+
export interface ServerRoutePrerenderWithParams extends Omit<ServerRoutePrerender, 'fallback'> {
32+
fallback?: PrerenderFallback;
33+
getPrerenderParams: () => Promise<Record<string, string>[]>;
34+
}
35+
36+
// @public
37+
export interface ServerRouteServer extends ServerRouteCommon {
38+
renderMode: RenderMode.Server;
39+
}
40+
41+
// (No @packageDocumentation comment for this package)
42+
43+
```

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Type } from '@angular/core';
1212

1313
// @public
1414
export class AngularNodeAppEngine {
15-
getHeaders(request: IncomingMessage): ReadonlyMap<string, string>;
15+
getPrerenderHeaders(request: IncomingMessage): ReadonlyMap<string, string>;
1616
render(request: IncomingMessage, requestContext?: unknown): Promise<Response | null>;
1717
}
1818

‎packages/angular/build/src/builders/application/tests/options/app-shell_spec.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
116116
harness.expectFile('dist/browser/main.js').toExist();
117117
const indexFileContent = harness.expectFile('dist/browser/index.html').content;
118118
indexFileContent.toContain('app-shell works!');
119-
indexFileContent.toContain('ng-server-context="app-shell"');
119+
// TODO(alanagius): enable once integration of routes in complete.
120+
// indexFileContent.toContain('ng-server-context="app-shell"');
120121
});
121122

122123
it('critical CSS is inlined', async () => {

‎packages/angular/build/src/tools/esbuild/application-code-bundle.ts

-1
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,6 @@ export function createServerMainCodeBundleOptions(
325325

326326
// Add @angular/ssr exports
327327
`export {
328-
ɵServerRenderContext,
329328
ɵdestroyAngularServerApp,
330329
ɵextractRoutesAndCreateRouteTree,
331330
ɵgetOrCreateAngularServerApp,

‎packages/angular/build/src/utils/server-rendering/load-esm-from-memory.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@
77
*/
88

99
import type { ApplicationRef, Type } from '@angular/core';
10-
import type {
11-
ɵServerRenderContext,
12-
ɵextractRoutesAndCreateRouteTree,
13-
ɵgetOrCreateAngularServerApp,
14-
} from '@angular/ssr';
10+
import type { ɵextractRoutesAndCreateRouteTree, ɵgetOrCreateAngularServerApp } from '@angular/ssr';
1511
import { assertIsError } from '../error';
1612
import { loadEsmModule } from '../load-esm';
1713

@@ -20,7 +16,6 @@ import { loadEsmModule } from '../load-esm';
2016
*/
2117
interface MainServerBundleExports {
2218
default: (() => Promise<ApplicationRef>) | Type<unknown>;
23-
ɵServerRenderContext: typeof ɵServerRenderContext;
2419
ɵextractRoutesAndCreateRouteTree: typeof ɵextractRoutesAndCreateRouteTree;
2520
ɵgetOrCreateAngularServerApp: typeof ɵgetOrCreateAngularServerApp;
2621
}

‎packages/angular/build/src/utils/server-rendering/prerender.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,13 @@ async function renderPages(
210210
route.slice(baseHrefWithLeadingSlash.length - 1),
211211
);
212212

213-
const isAppShellRoute = appShellRoute === routeWithoutBaseHref;
214-
const render: Promise<string | null> = renderWorker.run({ url: route, isAppShellRoute });
213+
const render: Promise<string | null> = renderWorker.run({ url: route });
215214
const renderResult: Promise<void> = render
216215
.then((content) => {
217216
if (content !== null) {
218217
const outPath = posix.join(removeLeadingSlash(routeWithoutBaseHref), 'index.html');
218+
const isAppShellRoute = appShellRoute === routeWithoutBaseHref;
219+
219220
output[outPath] = { content, appShellRoute: isAppShellRoute };
220221
}
221222
})

‎packages/angular/build/src/utils/server-rendering/render-worker.ts

+6-12
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,18 @@ export interface RenderWorkerData extends ESMInMemoryFileLoaderWorkerData {
1616

1717
export interface RenderOptions {
1818
url: string;
19-
isAppShellRoute: boolean;
2019
}
2120

2221
/**
2322
* Renders each route in routes and writes them to <outputPath>/<route>/index.html.
2423
*/
25-
async function renderPage({ url, isAppShellRoute }: RenderOptions): Promise<string | null> {
26-
const {
27-
ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp,
28-
ɵServerRenderContext: ServerRenderContext,
29-
} = await loadEsmModuleFromMemory('./main.server.mjs');
24+
async function renderPage({ url }: RenderOptions): Promise<string | null> {
25+
const { ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp } =
26+
await loadEsmModuleFromMemory('./main.server.mjs');
3027
const angularServerApp = getOrCreateAngularServerApp();
31-
const response = await angularServerApp.render(
32-
new Request(new URL(url, 'http://local-angular-prerender'), {
33-
signal: AbortSignal.timeout(30_000),
34-
}),
35-
undefined,
36-
isAppShellRoute ? ServerRenderContext.AppShell : ServerRenderContext.SSG,
28+
const response = await angularServerApp.renderStatic(
29+
new URL(url, 'http://local-angular-prerender'),
30+
AbortSignal.timeout(30_000),
3731
);
3832

3933
return response ? response.text() : null;

‎packages/angular/build/src/utils/server-rendering/routes-extractor-worker.ts

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ async function extractRoutes(): Promise<RoutersExtractorWorkerResult> {
2626

2727
const routeTree = await extractRoutesAndCreateRouteTree(
2828
new URL('http://local-angular-prerender/'),
29+
/** manifest */ undefined,
30+
/** invokeGetPrerenderParams */ true,
2931
);
3032

3133
return routeTree.toObject();

‎packages/angular/ssr/BUILD.bazel

+11-1
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/node/src/app-engine.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class AngularNodeAppEngine {
5656
* app.use(express.static('dist/browser', {
5757
* setHeaders: (res, path) => {
5858
* // Retrieve headers for the current request
59-
* const headers = angularAppEngine.getHeaders(res.req);
59+
* const headers = angularAppEngine.getPrerenderHeaders(res.req);
6060
*
6161
* // Apply the retrieved headers to the response
6262
* for (const { key, value } of headers) {
@@ -66,7 +66,7 @@ export class AngularNodeAppEngine {
6666
}));
6767
* ```
6868
*/
69-
getHeaders(request: IncomingMessage): ReadonlyMap<string, string> {
70-
return this.angularAppEngine.getHeaders(createWebRequestFromNodeRequest(request));
69+
getPrerenderHeaders(request: IncomingMessage): ReadonlyMap<string, string> {
70+
return this.angularAppEngine.getPrerenderHeaders(createWebRequestFromNodeRequest(request));
7171
}
7272
}

‎packages/angular/ssr/private_export.ts

-1
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

+7
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@
99
export * from './private_export';
1010

1111
export { AngularAppEngine } from './src/app-engine';
12+
13+
export {
14+
type PrerenderFallback,
15+
type RenderMode,
16+
type ServerRoute,
17+
provideServerRoutesConfig,
18+
} from './src/routes/route-config';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 type {
14+
ServerRouteAppShell,
15+
ServerRouteClient,
16+
ServerRoutePrerender,
17+
ServerRoutePrerenderWithParams,
18+
ServerRouteServer,
19+
ServerRouteCommon,
20+
} from './src/routes/route-config';

‎packages/angular/ssr/src/app-engine.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export class AngularAppEngine {
110110
* @returns A `Map` containing the HTTP headers as key-value pairs.
111111
* @note This function should be used exclusively for retrieving headers of SSG pages.
112112
*/
113-
getHeaders(request: Request): ReadonlyMap<string, string> {
113+
getPrerenderHeaders(request: Request): ReadonlyMap<string, string> {
114114
if (this.manifest.staticPathsHeaders.size === 0) {
115115
return new Map();
116116
}

0 commit comments

Comments
 (0)