Skip to content

Commit 03ce24a

Browse files
committed
refactor(@angular/ssr): remove duplicate code and streamline serve functionality
This commit cleans up duplicate code left from the previous implementations of process, serve, and render. Additionally, prerender serve now exclusively handles HEAD and GET requests, aligning with updated handling requirements. The private `renderStatic` method has been removed in favor of the `handle` method for improved maintainability.
1 parent 481ccdb commit 03ce24a

File tree

9 files changed

+259
-206
lines changed

9 files changed

+259
-206
lines changed

packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export function createAngularSsrInternalMiddleware(
4747
const angularServerApp = ɵgetOrCreateAngularServerApp();
4848
// Only Add the transform hook only if it's a different instance.
4949
if (cachedAngularServerApp !== angularServerApp) {
50+
angularServerApp.allowStaticRouteRender = true;
5051
angularServerApp.hooks.on('html:transform:pre', async ({ html, url }) => {
5152
const processedHtml = await server.transformIndexHtml(url.pathname, html);
5253

@@ -96,6 +97,7 @@ export async function createAngularSsrExternalMiddleware(
9697
reqHandler?: unknown;
9798
AngularAppEngine: typeof SSRAngularAppEngine;
9899
};
100+
99101
if (!isSsrNodeRequestHandler(reqHandler) && !isSsrRequestHandler(reqHandler)) {
100102
if (!fallbackWarningShown) {
101103
// eslint-disable-next-line no-console
@@ -118,6 +120,7 @@ export async function createAngularSsrExternalMiddleware(
118120
}
119121

120122
if (cachedAngularAppEngine !== AngularAppEngine) {
123+
AngularAppEngine.ɵallowStaticRouteRender = true;
121124
AngularAppEngine.ɵhooks.on('html:transform:pre', async ({ html, url }) => {
122125
const processedHtml = await server.transformIndexHtml(url.pathname, html);
123126

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ async function renderPage({ url }: RenderOptions): Promise<string | null> {
4040
const { ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp } =
4141
await loadEsmModuleFromMemory('./main.server.mjs');
4242
const angularServerApp = getOrCreateAngularServerApp();
43-
const response = await angularServerApp.renderStatic(
44-
new URL(url, serverURL),
45-
AbortSignal.timeout(30_000),
43+
angularServerApp.allowStaticRouteRender = true;
44+
const response = await angularServerApp.handle(
45+
new Request(new URL(url, serverURL), { signal: AbortSignal.timeout(30_000) }),
4646
);
4747

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

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import type { AngularServerApp } from './app';
1010
import { Hooks } from './hooks';
1111
import { getPotentialLocaleIdFromUrl } from './i18n';
1212
import { EntryPointExports, getAngularAppEngineManifest } from './manifest';
13-
import { stripIndexHtmlFromURL, stripTrailingSlash } from './utils/url';
1413

1514
/**
1615
* Angular server application engine.
@@ -24,23 +23,23 @@ import { stripIndexHtmlFromURL, stripTrailingSlash } from './utils/url';
2423
*/
2524
export class AngularAppEngine {
2625
/**
27-
* Hooks for extending or modifying the behavior of the server application.
28-
* These hooks are used by the Angular CLI when running the development server and
29-
* provide extensibility points for the application lifecycle.
26+
* A flag to enable or disable the rendering of prerendered routes.
27+
*
28+
* Typically used during development to avoid prerendering all routes ahead of time,
29+
* allowing them to be rendered on the fly as requested.
3030
*
3131
* @private
3232
*/
33-
static ɵhooks = /* #__PURE__*/ new Hooks();
33+
static ɵallowStaticRouteRender = false;
3434

3535
/**
36-
* Provides access to the hooks for extending or modifying the server application's behavior.
37-
* This allows attaching custom functionality to various server application lifecycle events.
36+
* Hooks for extending or modifying the behavior of the server application.
37+
* These hooks are used by the Angular CLI when running the development server and
38+
* provide extensibility points for the application lifecycle.
3839
*
39-
* @internal
40+
* @private
4041
*/
41-
get hooks(): Hooks {
42-
return AngularAppEngine.ɵhooks;
43-
}
42+
static ɵhooks = /* #__PURE__*/ new Hooks();
4443

4544
/**
4645
* The manifest for the server application.
@@ -93,7 +92,8 @@ export class AngularAppEngine {
9392
// be located in separate bundles, making `instanceof` checks unreliable.
9493
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
9594
const serverApp = getOrCreateAngularServerApp() as AngularServerApp;
96-
serverApp.hooks = this.hooks;
95+
serverApp.hooks = AngularAppEngine.ɵhooks;
96+
serverApp.allowStaticRouteRender = AngularAppEngine.ɵallowStaticRouteRender;
9797

9898
return serverApp;
9999
}

packages/angular/ssr/src/app.ts

Lines changed: 78 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,21 @@ const SERVER_CONTEXT_VALUE: Record<RenderMode, string> = {
5757
* The `AngularServerApp` class handles server-side rendering and asset management for a specific locale.
5858
*/
5959
export class AngularServerApp {
60+
/**
61+
* A flag to enable or disable the rendering of prerendered routes.
62+
*
63+
* Typically used during development to avoid prerendering all routes ahead of time,
64+
* allowing them to be rendered on the fly as requested.
65+
*
66+
* @private
67+
*/
68+
allowStaticRouteRender = false;
69+
6070
/**
6171
* Hooks for extending or modifying the behavior of the server application.
6272
* This instance can be used to attach custom functionality to various events in the server application lifecycle.
73+
*
74+
s* @private
6375
*/
6476
hooks = new Hooks();
6577

@@ -97,21 +109,6 @@ export class AngularServerApp {
97109
*/
98110
private readonly criticalCssLRUCache = new LRUCache<string, string>(MAX_INLINE_CSS_CACHE_ENTRIES);
99111

100-
/**
101-
* Renders a page based on the provided URL via server-side rendering and returns the corresponding HTTP response.
102-
* The rendering process can be interrupted by an abort signal, where the first resolved promise (either from the abort
103-
* or the render process) will dictate the outcome.
104-
*
105-
* @param url - The full URL to be processed and rendered by the server.
106-
* @param signal - (Optional) An `AbortSignal` object that allows for the cancellation of the rendering process.
107-
* @returns A promise that resolves to the generated HTTP response object, or `null` if no matching route is found.
108-
*/
109-
renderStatic(url: URL, signal?: AbortSignal): Promise<Response | null> {
110-
const request = new Request(url, { signal });
111-
112-
return this.handleAbortableRendering(request, /** isSsrMode */ false);
113-
}
114-
115112
/**
116113
* Handles an incoming HTTP request by serving prerendered content, performing server-side rendering,
117114
* or delivering a static file for client-side rendered routes based on the `RenderMode` setting.
@@ -126,8 +123,8 @@ export class AngularServerApp {
126123
async handle(request: Request, requestContext?: unknown): Promise<Response | null> {
127124
const url = new URL(request.url);
128125
this.router ??= await ServerRouter.from(this.manifest, url);
129-
130126
const matchedRoute = this.router.match(url);
127+
131128
if (!matchedRoute) {
132129
// Not a known Angular route.
133130
return null;
@@ -140,131 +137,82 @@ export class AngularServerApp {
140137
}
141138
}
142139

143-
return this.handleAbortableRendering(
144-
request,
145-
/** isSsrMode */ true,
146-
matchedRoute,
147-
requestContext,
148-
);
149-
}
150-
151-
/**
152-
* Retrieves the matched route for the incoming request based on the request URL.
153-
*
154-
* @param request - The incoming HTTP request to match against routes.
155-
* @returns A promise that resolves to the matched route metadata or `undefined` if no route matches.
156-
*/
157-
private async getMatchedRoute(request: Request): Promise<RouteTreeNodeMetadata | undefined> {
158-
this.router ??= await ServerRouter.from(this.manifest, new URL(request.url));
159-
160-
return this.router.match(new URL(request.url));
140+
return Promise.race([
141+
new Promise<never>((_, reject) => {
142+
request.signal.addEventListener(
143+
'abort',
144+
() => {
145+
const abortError = new Error(
146+
`Request for: ${request.url} was aborted.\n${request.signal.reason}`,
147+
);
148+
abortError.name = 'AbortError';
149+
reject(abortError);
150+
},
151+
{ once: true },
152+
);
153+
}),
154+
this.handleRendering(request, matchedRoute, requestContext),
155+
]);
161156
}
162157

163158
/**
164159
* Handles serving a prerendered static asset if available for the matched route.
165160
*
161+
* This method only supports `GET` and `HEAD` requests.
162+
*
166163
* @param request - The incoming HTTP request for serving a static page.
167-
* @param matchedRoute - Optional parameter representing the metadata of the matched route for rendering.
164+
* @param matchedRoute - The metadata of the matched route for rendering.
168165
* If not provided, the method attempts to find a matching route based on the request URL.
169166
* @returns A promise that resolves to a `Response` object if the prerendered page is found, or `null`.
170167
*/
171168
private async handleServe(
172169
request: Request,
173-
matchedRoute?: RouteTreeNodeMetadata,
170+
matchedRoute: RouteTreeNodeMetadata,
174171
): Promise<Response | null> {
175-
matchedRoute ??= await this.getMatchedRoute(request);
176-
if (!matchedRoute) {
172+
const { headers, renderMode } = matchedRoute;
173+
if (renderMode !== RenderMode.Prerender) {
177174
return null;
178175
}
179176

180-
const { headers, renderMode } = matchedRoute;
181-
if (renderMode !== RenderMode.Prerender) {
177+
const { url, method } = request;
178+
if (method !== 'GET' && method !== 'HEAD') {
182179
return null;
183180
}
184181

185-
const { pathname } = stripIndexHtmlFromURL(new URL(request.url));
182+
const { pathname } = stripIndexHtmlFromURL(new URL(url));
186183
const assetPath = stripLeadingSlash(joinUrlParts(pathname, 'index.html'));
187184
if (!this.assets.hasServerAsset(assetPath)) {
188185
return null;
189186
}
190187

191188
// TODO(alanagius): handle etags
192-
193189
const content = await this.assets.getServerAsset(assetPath);
194190

195191
return new Response(content, {
196192
headers: {
197193
'Content-Type': 'text/html;charset=UTF-8',
198-
// 30 days in seconds
199194
'Cache-Control': `max-age=${DEFAULT_MAX_AGE}`,
200195
...headers,
201196
},
202197
});
203198
}
204199

205-
/**
206-
* Handles the server-side rendering process for the given HTTP request, allowing for abortion
207-
* of the rendering if the request is aborted. This method matches the request URL to a route
208-
* and performs rendering if a matching route is found.
209-
*
210-
* @param request - The incoming HTTP request to be processed. It includes a signal to monitor
211-
* for abortion events.
212-
* @param isSsrMode - A boolean indicating whether the rendering is performed in server-side
213-
* rendering (SSR) mode.
214-
* @param matchedRoute - Optional parameter representing the metadata of the matched route for
215-
* rendering. If not provided, the method attempts to find a matching route based on the request URL.
216-
* @param requestContext - Optional additional context for rendering, such as request metadata.
217-
*
218-
* @returns A promise that resolves to the rendered response, or null if no matching route is found.
219-
* If the request is aborted, the promise will reject with an `AbortError`.
220-
*/
221-
private async handleAbortableRendering(
222-
request: Request,
223-
isSsrMode: boolean,
224-
matchedRoute?: RouteTreeNodeMetadata,
225-
requestContext?: unknown,
226-
): Promise<Response | null> {
227-
return Promise.race([
228-
new Promise<never>((_, reject) => {
229-
request.signal.addEventListener(
230-
'abort',
231-
() => {
232-
const abortError = new Error(
233-
`Request for: ${request.url} was aborted.\n${request.signal.reason}`,
234-
);
235-
abortError.name = 'AbortError';
236-
reject(abortError);
237-
},
238-
{ once: true },
239-
);
240-
}),
241-
this.handleRendering(request, isSsrMode, matchedRoute, requestContext),
242-
]);
243-
}
244-
245200
/**
246201
* Handles the server-side rendering process for the given HTTP request.
247202
* This method matches the request URL to a route and performs rendering if a matching route is found.
248203
*
249204
* @param request - The incoming HTTP request to be processed.
250-
* @param isSsrMode - A boolean indicating whether the rendering is performed in server-side rendering (SSR) mode.
251-
* @param matchedRoute - Optional parameter representing the metadata of the matched route for rendering.
205+
* @param matchedRoute - The metadata of the matched route for rendering.
252206
* If not provided, the method attempts to find a matching route based on the request URL.
253207
* @param requestContext - Optional additional context for rendering, such as request metadata.
254208
*
255209
* @returns A promise that resolves to the rendered response, or null if no matching route is found.
256210
*/
257211
private async handleRendering(
258212
request: Request,
259-
isSsrMode: boolean,
260-
matchedRoute?: RouteTreeNodeMetadata,
213+
matchedRoute: RouteTreeNodeMetadata,
261214
requestContext?: unknown,
262215
): Promise<Response | null> {
263-
matchedRoute ??= await this.getMatchedRoute(request);
264-
if (!matchedRoute) {
265-
return null;
266-
}
267-
268216
const { redirectTo, status } = matchedRoute;
269217
const url = new URL(request.url);
270218

@@ -276,42 +224,44 @@ export class AngularServerApp {
276224
return Response.redirect(new URL(redirectTo, url), (status as any) ?? 302);
277225
}
278226

279-
const { renderMode = isSsrMode ? RenderMode.Server : RenderMode.Prerender, headers } =
280-
matchedRoute;
227+
const { renderMode, headers } = matchedRoute;
228+
if (
229+
!this.allowStaticRouteRender &&
230+
(renderMode === RenderMode.Prerender || renderMode === RenderMode.AppShell)
231+
) {
232+
return null;
233+
}
281234

282235
const platformProviders: StaticProvider[] = [];
283-
let responseInit: ResponseInit | undefined;
284-
285-
if (isSsrMode) {
286-
// Initialize the response with status and headers if available.
287-
responseInit = {
288-
status,
289-
headers: new Headers({
290-
'Content-Type': 'text/html;charset=UTF-8',
291-
...headers,
292-
}),
293-
};
294-
295-
if (renderMode === RenderMode.Server) {
296-
// Configure platform providers for request and response only for SSR.
297-
platformProviders.push(
298-
{
299-
provide: REQUEST,
300-
useValue: request,
301-
},
302-
{
303-
provide: REQUEST_CONTEXT,
304-
useValue: requestContext,
305-
},
306-
{
307-
provide: RESPONSE_INIT,
308-
useValue: responseInit,
309-
},
310-
);
311-
} else if (renderMode === RenderMode.Client) {
312-
// Serve the client-side rendered version if the route is configured for CSR.
313-
return new Response(await this.assets.getServerAsset('index.csr.html'), responseInit);
314-
}
236+
237+
// Initialize the response with status and headers if available.
238+
const responseInit = {
239+
status,
240+
headers: new Headers({
241+
'Content-Type': 'text/html;charset=UTF-8',
242+
...headers,
243+
}),
244+
};
245+
246+
if (renderMode === RenderMode.Server) {
247+
// Configure platform providers for request and response only for SSR.
248+
platformProviders.push(
249+
{
250+
provide: REQUEST,
251+
useValue: request,
252+
},
253+
{
254+
provide: REQUEST_CONTEXT,
255+
useValue: requestContext,
256+
},
257+
{
258+
provide: RESPONSE_INIT,
259+
useValue: responseInit,
260+
},
261+
);
262+
} else if (renderMode === RenderMode.Client) {
263+
// Serve the client-side rendered version if the route is configured for CSR.
264+
return new Response(await this.assets.getServerAsset('index.csr.html'), responseInit);
315265
}
316266

317267
const {
@@ -352,7 +302,7 @@ export class AngularServerApp {
352302
});
353303

354304
// TODO(alanagius): remove once Node.js version 18 is no longer supported.
355-
if (isSsrMode && typeof crypto === 'undefined') {
305+
if (renderMode === RenderMode.Server && typeof crypto === 'undefined') {
356306
// eslint-disable-next-line no-console
357307
console.error(
358308
`The global 'crypto' module is unavailable. ` +
@@ -361,7 +311,7 @@ export class AngularServerApp {
361311
);
362312
}
363313

364-
if (isSsrMode && typeof crypto !== 'undefined') {
314+
if (renderMode === RenderMode.Server && typeof crypto !== 'undefined') {
365315
// Only cache if we are running in SSR Mode.
366316
const cacheKey = await sha256(html);
367317
let htmlWithCriticalCss = this.criticalCssLRUCache.get(cacheKey);

0 commit comments

Comments
 (0)