Skip to content

Commit 065dcc5

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

File tree

5 files changed

+118
-58
lines changed

5 files changed

+118
-58
lines changed

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: 108 additions & 47 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';
@@ -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,72 @@ 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+
if (matchedRoute.renderMode === RenderMode.Prerender) {
167+
const response = await this.handleServe(request, matchedRoute);
168+
if (response) {
169+
// During development, prerendered pages may not exist, hence fallback to render on the fly.
170+
return response;
171+
}
172+
}
173+
174+
return this.handleAbortableRendering(
175+
request,
176+
/** isSsrMode */ true,
177+
matchedRoute,
178+
requestContext,
179+
);
180+
}
181+
182+
/**
183+
* Retrieves the matched route for the incoming request based on the request URL.
184+
*
185+
* @param request - The incoming HTTP request to match against routes.
186+
* @returns A promise that resolves to the matched route metadata or `undefined` if no route matches.
187+
*/
188+
private async getMatchedRoute(request: Request): Promise<RouteTreeNodeMetadata | undefined> {
189+
this.router ??= await ServerRouter.from(this.manifest, new URL(request.url));
190+
191+
return this.router.match(new URL(request.url));
192+
}
193+
194+
/**
195+
* Handles serving a prerendered static asset if available for the matched route.
196+
*
197+
* @param request - The incoming HTTP request for serving a static page.
198+
* @param matchedRoute - Optional parameter representing the metadata of the matched route for rendering.
199+
* If not provided, the method attempts to find a matching route based on the request URL.
200+
* @returns A promise that resolves to a `Response` object if the prerendered page is found, or `null`.
201+
*/
202+
private async handleServe(
203+
request: Request,
204+
matchedRoute?: RouteTreeNodeMetadata,
205+
): Promise<Response | null> {
206+
matchedRoute ??= await this.getMatchedRoute(request);
150207
if (!matchedRoute) {
151208
return null;
152209
}
@@ -156,7 +213,8 @@ export class AngularServerApp {
156213
return null;
157214
}
158215

159-
const assetPath = stripLeadingSlash(joinUrlParts(url.pathname, 'index.html'));
216+
const { pathname } = stripIndexHtmlFromURL(new URL(request.url));
217+
const assetPath = stripLeadingSlash(joinUrlParts(pathname, 'index.html'));
160218
if (!this.assets.hasServerAsset(assetPath)) {
161219
return null;
162220
}
@@ -176,41 +234,43 @@ export class AngularServerApp {
176234
}
177235

178236
/**
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.
237+
* Handles the server-side rendering process for the given HTTP request, allowing for abortion
238+
* of the rendering if the request is aborted. This method matches the request URL to a route
239+
* and performs rendering if a matching route is found.
184240
*
185-
* @param request - The incoming HTTP request to be processed.
241+
* @param request - The incoming HTTP request to be processed. It includes a signal to monitor
242+
* for abortion events.
243+
* @param isSsrMode - A boolean indicating whether the rendering is performed in server-side
244+
* rendering (SSR) mode.
245+
* @param matchedRoute - Optional parameter representing the metadata of the matched route for
246+
* rendering. If not provided, the method attempts to find a matching route based on the request URL.
186247
* @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.
196248
*
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.
249+
* @returns A promise that resolves to the rendered response, or null if no matching route is found.
250+
* If the request is aborted, the promise will reject with an `AbortError`.
199251
*/
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-
});
252+
private async handleAbortableRendering(
253+
request: Request,
254+
isSsrMode: boolean,
255+
matchedRoute?: RouteTreeNodeMetadata,
256+
requestContext?: unknown,
257+
): Promise<Response | null> {
258+
return Promise.race([
259+
new Promise<never>((_, reject) => {
260+
request.signal.addEventListener(
261+
'abort',
262+
() => {
263+
const abortError = new Error(
264+
`Request for: ${request.url} was aborted.\n${request.signal.reason}`,
265+
);
266+
abortError.name = 'AbortError';
267+
reject(abortError);
268+
},
269+
{ once: true },
270+
);
271+
}),
272+
this.handleRendering(request, isSsrMode, matchedRoute, requestContext),
273+
]);
214274
}
215275

216276
/**
@@ -219,25 +279,26 @@ export class AngularServerApp {
219279
*
220280
* @param request - The incoming HTTP request to be processed.
221281
* @param isSsrMode - A boolean indicating whether the rendering is performed in server-side rendering (SSR) mode.
282+
* @param matchedRoute - Optional parameter representing the metadata of the matched route for rendering.
283+
* If not provided, the method attempts to find a matching route based on the request URL.
222284
* @param requestContext - Optional additional context for rendering, such as request metadata.
223285
*
224286
* @returns A promise that resolves to the rendered response, or null if no matching route is found.
225287
*/
226288
private async handleRendering(
227289
request: Request,
228290
isSsrMode: boolean,
291+
matchedRoute?: RouteTreeNodeMetadata,
229292
requestContext?: unknown,
230293
): 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);
294+
matchedRoute ??= await this.getMatchedRoute(request);
235295
if (!matchedRoute) {
236-
// Not a known Angular route.
237296
return null;
238297
}
239298

240299
const { redirectTo, status } = matchedRoute;
300+
const url = new URL(request.url);
301+
241302
if (redirectTo !== undefined) {
242303
// Note: The status code is validated during route extraction.
243304
// 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)