Skip to content

Commit 94396da

Browse files
committed
fixup! fix(@angular/ssr): enable serving of prerendered pages in the App Engine
1 parent b1c6fd6 commit 94396da

File tree

7 files changed

+113
-64
lines changed

7 files changed

+113
-64
lines changed

packages/angular/build/src/builders/application/execute-post-bundle.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,6 @@ export async function executePostBundleSteps(
183183
}
184184
case RouteRenderMode.Server:
185185
case RouteRenderMode.Client:
186-
case RouteRenderMode.AppShell:
187186
serializableRouteTreeNodeForManifest.push(metadata);
188187

189188
break;

packages/angular/build/src/utils/server-rendering/manifest.ts

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

9+
import { extname } from 'node:path';
910
import {
10-
INDEX_HTML_CSR,
11-
INDEX_HTML_SERVER,
1211
NormalizedApplicationBuildOptions,
1312
getLocaleBaseHref,
1413
} from '../../builders/application/options';
@@ -135,7 +134,8 @@ export function generateAngularServerAppManifest(
135134
): string {
136135
const serverAssetsContent: string[] = [];
137136
for (const file of [...additionalHtmlOutputFiles.values(), ...outputFiles]) {
138-
if (file.path.endsWith('.html') || (inlineCriticalCss && file.path.endsWith('.css'))) {
137+
const extension = extname(file.path);
138+
if (extension === '.html' || (inlineCriticalCss && extension === '.css')) {
139139
serverAssetsContent.push(`['${file.path}', async () => \`${escapeUnsafeChars(file.text)}\`]`);
140140
}
141141
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,6 @@ export class AngularNodeAppEngine {
7171
async process(request: IncomingMessage, requestContext?: unknown): Promise<Response | null> {
7272
const webRequest = createWebRequestFromNodeRequest(request);
7373

74-
return (
75-
(await this.angularAppEngine.serve(webRequest)) ??
76-
(await this.angularAppEngine.render(webRequest, requestContext))
77-
);
74+
return this.angularAppEngine.process(webRequest, requestContext);
7875
}
7976
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,8 @@ export class AngularAppEngine {
103103
*/
104104
async process(request: Request, requestContext?: unknown): Promise<Response | null> {
105105
const serverApp = await this.getAngularServerAppForRequest(request);
106-
if (!serverApp) {
107-
return null;
108-
}
109106

110-
return (await serverApp.serve(request)) ?? (await serverApp.render(request, requestContext));
107+
return serverApp ? serverApp.process(request, requestContext) : null;
111108
}
112109

113110
/**

packages/angular/ssr/src/app.ts

Lines changed: 100 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ServerAssets } from './assets';
1212
import { Hooks } from './hooks';
1313
import { getAngularAppManifest } from './manifest';
1414
import { RenderMode } from './routes/route-config';
15+
import { RouteTreeNodeMetadata } from './routes/route-tree';
1516
import { ServerRouter } from './routes/router';
1617
import { sha256 } from './utils/crypto';
1718
import { InlineCriticalCssProcessor } from './utils/inline-critical-css';
@@ -21,9 +22,9 @@ import { joinUrlParts, stripIndexHtmlFromURL, stripLeadingSlash } from './utils/
2122

2223
/**
2324
* The default maximum age in seconds.
24-
* Represents the total number of seconds in a 30-day period.
25+
* Represents the total number of seconds in a 365-day period.
2526
*/
26-
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60;
27+
const DEFAULT_MAX_AGE = 365 * 24 * 60 * 60;
2728

2829
/**
2930
* Maximum number of critical CSS entries the cache can store.
@@ -107,10 +108,7 @@ export class AngularServerApp {
107108
* @returns A promise that resolves to the HTTP response object resulting from the rendering, or null if no match is found.
108109
*/
109110
render(request: Request, requestContext?: unknown): Promise<Response | null> {
110-
return Promise.race([
111-
this.createAbortPromise(request),
112-
this.handleRendering(request, /** isSsrMode */ true, requestContext),
113-
]);
111+
return this.handleAbortableRendering(request, /** isSsrMode */ true, undefined, requestContext);
114112
}
115113

116114
/**
@@ -125,10 +123,7 @@ export class AngularServerApp {
125123
renderStatic(url: URL, signal?: AbortSignal): Promise<Response | null> {
126124
const request = new Request(url, { signal });
127125

128-
return Promise.race([
129-
this.createAbortPromise(request),
130-
this.handleRendering(request, /** isSsrMode */ false),
131-
]);
126+
return this.handleAbortableRendering(request, /** isSsrMode */ false);
132127
}
133128

134129
/**
@@ -143,10 +138,62 @@ export class AngularServerApp {
143138
* if available, or `null` if the request does not match a prerendered route or asset.
144139
*/
145140
async serve(request: Request): Promise<Response | null> {
146-
const url = stripIndexHtmlFromURL(new URL(request.url));
141+
return this.handleServe(request);
142+
}
143+
144+
/**
145+
* Handles incoming HTTP requests by serving prerendered content or rendering the page.
146+
*
147+
* This method first attempts to serve a prerendered page. If the prerendered page is not available,
148+
* it falls back to rendering the requested page using server-side rendering. The function returns
149+
* a promise that resolves to the appropriate HTTP response.
150+
*
151+
* @param request - The incoming HTTP request to be processed.
152+
* @param requestContext - Optional additional context for rendering, such as request metadata.
153+
* @returns A promise that resolves to the HTTP response object resulting from the request handling,
154+
* or null if no matching content is found.
155+
*/
156+
async process(request: Request, requestContext?: unknown): Promise<Response | null> {
157+
const url = new URL(request.url);
147158
this.router ??= await ServerRouter.from(this.manifest, url);
148159

149-
const matchedRoute = this.router.match(new URL(request.url));
160+
const matchedRoute = this.router.match(url);
161+
if (!matchedRoute) {
162+
// Not a known Angular route.
163+
return null;
164+
}
165+
166+
return (
167+
(await this.handleServe(request, matchedRoute)) ??
168+
this.handleAbortableRendering(request, /** isSsrMode */ true, matchedRoute, requestContext)
169+
);
170+
}
171+
172+
/**
173+
* Retrieves the matched route for the incoming request based on the request URL.
174+
*
175+
* @param request - The incoming HTTP request to match against routes.
176+
* @returns A promise that resolves to the matched route metadata or `undefined` if no route matches.
177+
*/
178+
private async getMatchedRoute(request: Request): Promise<RouteTreeNodeMetadata | undefined> {
179+
this.router ??= await ServerRouter.from(this.manifest, new URL(request.url));
180+
181+
return this.router.match(new URL(request.url));
182+
}
183+
184+
/**
185+
* Handles serving a prerendered static asset if available for the matched route.
186+
*
187+
* @param request - The incoming HTTP request for serving a static page.
188+
* @param matchedRoute - Optional parameter representing the metadata of the matched route for rendering.
189+
* If not provided, the method attempts to find a matching route based on the request URL.
190+
* @returns A promise that resolves to a `Response` object if the prerendered page is found, or `null`.
191+
*/
192+
private async handleServe(
193+
request: Request,
194+
matchedRoute?: RouteTreeNodeMetadata,
195+
): Promise<Response | null> {
196+
matchedRoute ??= await this.getMatchedRoute(request);
150197
if (!matchedRoute) {
151198
return null;
152199
}
@@ -156,7 +203,8 @@ export class AngularServerApp {
156203
return null;
157204
}
158205

159-
const assetPath = stripLeadingSlash(joinUrlParts(url.pathname, 'index.html'));
206+
const { pathname } = stripIndexHtmlFromURL(new URL(request.url));
207+
const assetPath = stripLeadingSlash(joinUrlParts(pathname, 'index.html'));
160208
if (!this.assets.hasServerAsset(assetPath)) {
161209
return null;
162210
}
@@ -176,41 +224,43 @@ export class AngularServerApp {
176224
}
177225

178226
/**
179-
* Handles incoming HTTP requests by serving prerendered content or rendering the page.
180-
*
181-
* This method first attempts to serve a prerendered page. If the prerendered page is not available,
182-
* it falls back to rendering the requested page using server-side rendering. The function returns
183-
* a promise that resolves to the appropriate HTTP response.
227+
* Handles the server-side rendering process for the given HTTP request, allowing for abortion
228+
* of the rendering if the request is aborted. This method matches the request URL to a route
229+
* and performs rendering if a matching route is found.
184230
*
185-
* @param request - The incoming HTTP request to be processed.
231+
* @param request - The incoming HTTP request to be processed. It includes a signal to monitor
232+
* for abortion events.
233+
* @param isSsrMode - A boolean indicating whether the rendering is performed in server-side
234+
* rendering (SSR) mode.
235+
* @param matchedRoute - Optional parameter representing the metadata of the matched route for
236+
* rendering. If not provided, the method attempts to find a matching route based on the request URL.
186237
* @param requestContext - Optional additional context for rendering, such as request metadata.
187-
* @returns A promise that resolves to the HTTP response object resulting from the request handling,
188-
* or null if no matching content is found.
189-
*/
190-
async process(request: Request, requestContext?: unknown): Promise<Response | null> {
191-
return (await this.serve(request)) ?? (await this.render(request, requestContext));
192-
}
193-
194-
/**
195-
* Creates a promise that rejects when the request is aborted.
196238
*
197-
* @param request - The HTTP request to monitor for abortion.
198-
* @returns A promise that never resolves but rejects with an `AbortError` if the request is aborted.
239+
* @returns A promise that resolves to the rendered response, or null if no matching route is found.
240+
* If the request is aborted, the promise will reject with an `AbortError`.
199241
*/
200-
private createAbortPromise(request: Request): Promise<never> {
201-
return new Promise<never>((_, reject) => {
202-
request.signal.addEventListener(
203-
'abort',
204-
() => {
205-
const abortError = new Error(
206-
`Request for: ${request.url} was aborted.\n${request.signal.reason}`,
207-
);
208-
abortError.name = 'AbortError';
209-
reject(abortError);
210-
},
211-
{ once: true },
212-
);
213-
});
242+
private async handleAbortableRendering(
243+
request: Request,
244+
isSsrMode: boolean,
245+
matchedRoute?: RouteTreeNodeMetadata,
246+
requestContext?: unknown,
247+
): Promise<Response | null> {
248+
return Promise.race([
249+
new Promise<never>((_, reject) => {
250+
request.signal.addEventListener(
251+
'abort',
252+
() => {
253+
const abortError = new Error(
254+
`Request for: ${request.url} was aborted.\n${request.signal.reason}`,
255+
);
256+
abortError.name = 'AbortError';
257+
reject(abortError);
258+
},
259+
{ once: true },
260+
);
261+
}),
262+
this.handleRendering(request, isSsrMode, matchedRoute, requestContext),
263+
]);
214264
}
215265

216266
/**
@@ -219,25 +269,26 @@ export class AngularServerApp {
219269
*
220270
* @param request - The incoming HTTP request to be processed.
221271
* @param isSsrMode - A boolean indicating whether the rendering is performed in server-side rendering (SSR) mode.
272+
* @param matchedRoute - Optional parameter representing the metadata of the matched route for rendering.
273+
* If not provided, the method attempts to find a matching route based on the request URL.
222274
* @param requestContext - Optional additional context for rendering, such as request metadata.
223275
*
224276
* @returns A promise that resolves to the rendered response, or null if no matching route is found.
225277
*/
226278
private async handleRendering(
227279
request: Request,
228280
isSsrMode: boolean,
281+
matchedRoute?: RouteTreeNodeMetadata,
229282
requestContext?: unknown,
230283
): Promise<Response | null> {
231-
const url = new URL(request.url);
232-
this.router ??= await ServerRouter.from(this.manifest, url);
233-
234-
const matchedRoute = this.router.match(url);
284+
matchedRoute ??= await this.getMatchedRoute(request);
235285
if (!matchedRoute) {
236-
// Not a known Angular route.
237286
return null;
238287
}
239288

240289
const { redirectTo, status } = matchedRoute;
290+
const url = new URL(request.url);
291+
241292
if (redirectTo !== undefined) {
242293
// Note: The status code is validated during route extraction.
243294
// 302 Found is used by default for redirections

packages/angular/ssr/src/manifest.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
import type { SerializableRouteTreeNode } from './routes/route-tree';
1010
import { AngularBootstrap } from './utils/ng';
1111

12+
/**
13+
* A function that returns a promise resolving to the file contents of the asset.
14+
*/
15+
export type ServerAsset = () => Promise<string>;
16+
1217
/**
1318
* Represents the exports of an Angular server application entry point.
1419
*/
@@ -55,7 +60,7 @@ export interface AngularAppManifest {
5560
* - `key`: The path of the asset.
5661
* - `value`: A function returning a promise that resolves to the file contents of the asset.
5762
*/
58-
readonly assets: ReadonlyMap<string, () => Promise<string>>;
63+
readonly assets: ReadonlyMap<string, ServerAsset>;
5964

6065
/**
6166
* The bootstrap mechanism for the server application.

packages/angular/ssr/test/testing-utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Component, provideExperimentalZonelessChangeDetection } from '@angular/
1010
import { bootstrapApplication } from '@angular/platform-browser';
1111
import { provideServerRendering } from '@angular/platform-server';
1212
import { RouterOutlet, Routes, provideRouter } from '@angular/router';
13-
import { AngularAppManifest, setAngularAppManifest } from '../src/manifest';
13+
import { AngularAppManifest, ServerAsset, setAngularAppManifest } from '../src/manifest';
1414
import { ServerRoute, provideServerRoutesConfig } from '../src/routes/route-config';
1515

1616
/**
@@ -27,7 +27,7 @@ export function setAngularAppTestingManifest(
2727
routes: Routes,
2828
serverRoutes: ServerRoute[],
2929
baseHref = '',
30-
additionalServerAssets: Record<string, () => Promise<string>> = {},
30+
additionalServerAssets: Record<string, ServerAsset> = {},
3131
): void {
3232
setAngularAppManifest({
3333
inlineCriticalCss: false,

0 commit comments

Comments
 (0)