@@ -12,6 +12,7 @@ import { ServerAssets } from './assets';
1212import { Hooks } from './hooks' ;
1313import { getAngularAppManifest } from './manifest' ;
1414import { RenderMode } from './routes/route-config' ;
15+ import { RouteTreeNodeMetadata } from './routes/route-tree' ;
1516import { ServerRouter } from './routes/router' ;
1617import { sha256 } from './utils/crypto' ;
1718import { 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
0 commit comments