@@ -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 */
5959export 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