Skip to content

Commit ea4b99b

Browse files
committed
refactor(@angular/ssr): add option to exclude fallback SSG routes from extraction
This option allows validation during the build process to ensure that, when the output mode is set to static, no routes requiring server-side rendering are included.
1 parent f346ee8 commit ea4b99b

File tree

2 files changed

+114
-17
lines changed

2 files changed

+114
-17
lines changed

packages/angular/ssr/src/routes/ng-routes.ts

+33-17
Original file line numberDiff line numberDiff line change
@@ -93,21 +93,25 @@ interface AngularRouterConfigResult {
9393
* @param options - The configuration options for traversing routes.
9494
* @returns An async iterable iterator yielding either route tree node metadata or an error object with an error message.
9595
*/
96-
async function* traverseRoutesConfig({
97-
routes,
98-
compiler,
99-
parentInjector,
100-
parentRoute,
101-
serverConfigRouteTree,
102-
invokeGetPrerenderParams,
103-
}: {
96+
async function* traverseRoutesConfig(options: {
10497
routes: Route[];
10598
compiler: Compiler;
10699
parentInjector: Injector;
107100
parentRoute: string;
108101
serverConfigRouteTree: RouteTree<ServerConfigRouteTreeAdditionalMetadata> | undefined;
109102
invokeGetPrerenderParams: boolean;
103+
includePrerenderFallbackRoutes: boolean;
110104
}): AsyncIterableIterator<RouteTreeNodeMetadata | { error: string }> {
105+
const {
106+
routes,
107+
compiler,
108+
parentInjector,
109+
parentRoute,
110+
serverConfigRouteTree,
111+
invokeGetPrerenderParams,
112+
includePrerenderFallbackRoutes,
113+
} = options;
114+
111115
for (const route of routes) {
112116
try {
113117
const { path = '', redirectTo, loadChildren, children } = route;
@@ -147,20 +151,22 @@ async function* traverseRoutesConfig({
147151
yield { ...metadata, redirectTo: redirectToResolved };
148152
} else if (metadata.renderMode === RenderMode.Prerender) {
149153
// Handle SSG routes
150-
yield* handleSSGRoute(metadata, parentInjector, invokeGetPrerenderParams);
154+
yield* handleSSGRoute(
155+
metadata,
156+
parentInjector,
157+
invokeGetPrerenderParams,
158+
includePrerenderFallbackRoutes,
159+
);
151160
} else {
152161
yield metadata;
153162
}
154163

155164
// Recursively process child routes
156165
if (children?.length) {
157166
yield* traverseRoutesConfig({
167+
...options,
158168
routes: children,
159-
compiler,
160-
parentInjector,
161169
parentRoute: currentRoutePath,
162-
serverConfigRouteTree,
163-
invokeGetPrerenderParams,
164170
});
165171
}
166172

@@ -175,12 +181,10 @@ async function* traverseRoutesConfig({
175181
if (loadedChildRoutes) {
176182
const { routes: childRoutes, injector = parentInjector } = loadedChildRoutes;
177183
yield* traverseRoutesConfig({
184+
...options,
178185
routes: childRoutes,
179-
compiler,
180186
parentInjector: injector,
181187
parentRoute: currentRoutePath,
182-
serverConfigRouteTree,
183-
invokeGetPrerenderParams,
184188
});
185189
}
186190
}
@@ -197,12 +201,14 @@ async function* traverseRoutesConfig({
197201
* @param metadata - The metadata associated with the route tree node.
198202
* @param parentInjector - The dependency injection container for the parent route.
199203
* @param invokeGetPrerenderParams - A flag indicating whether to invoke the `getPrerenderParams` function.
204+
* @param includePrerenderFallbackRoutes - A flag indicating whether to include fallback routes in the result.
200205
* @returns An async iterable iterator that yields route tree node metadata for each SSG path or errors.
201206
*/
202207
async function* handleSSGRoute(
203208
metadata: ServerConfigRouteTreeNodeMetadata,
204209
parentInjector: Injector,
205210
invokeGetPrerenderParams: boolean,
211+
includePrerenderFallbackRoutes: boolean,
206212
): AsyncIterableIterator<RouteTreeNodeMetadata | { error: string }> {
207213
if (metadata.renderMode !== RenderMode.Prerender) {
208214
throw new Error(
@@ -267,7 +273,10 @@ async function* handleSSGRoute(
267273
}
268274

269275
// Handle fallback render modes
270-
if (fallback !== PrerenderFallback.None || !invokeGetPrerenderParams) {
276+
if (
277+
includePrerenderFallbackRoutes &&
278+
(fallback !== PrerenderFallback.None || !invokeGetPrerenderParams)
279+
) {
271280
yield {
272281
...meta,
273282
route: currentRoutePath,
@@ -345,13 +354,16 @@ function buildServerConfigRouteTree(serverRoutesConfig: ServerRoute[]): {
345354
* for ensuring that API requests for relative paths succeed, which is essential for accurate route extraction.
346355
* @param invokeGetPrerenderParams - A boolean flag indicating whether to invoke `getPrerenderParams` for parameterized SSG routes
347356
* to handle prerendering paths. Defaults to `false`.
357+
* @param includePrerenderFallbackRoutes - A flag indicating whether to include fallback routes in the result. Defaults to `true`.
358+
*
348359
* @returns A promise that resolves to an object of type `AngularRouterConfigResult` or errors.
349360
*/
350361
export async function getRoutesFromAngularRouterConfig(
351362
bootstrap: AngularBootstrap,
352363
document: string,
353364
url: URL,
354365
invokeGetPrerenderParams = false,
366+
includePrerenderFallbackRoutes = true,
355367
): Promise<AngularRouterConfigResult> {
356368
const { protocol, host } = url;
357369

@@ -418,6 +430,7 @@ export async function getRoutesFromAngularRouterConfig(
418430
parentRoute: '',
419431
serverConfigRouteTree,
420432
invokeGetPrerenderParams,
433+
includePrerenderFallbackRoutes,
421434
});
422435

423436
for await (const result of traverseRoutes) {
@@ -454,6 +467,7 @@ export async function getRoutesFromAngularRouterConfig(
454467
* If not provided, the default manifest is retrieved using `getAngularAppManifest()`.
455468
* @param invokeGetPrerenderParams - A boolean flag indicating whether to invoke `getPrerenderParams` for parameterized SSG routes
456469
* to handle prerendering paths. Defaults to `false`.
470+
* @param includePrerenderFallbackRoutes - A flag indicating whether to include fallback routes in the result. Defaults to `true`.
457471
*
458472
* @returns A promise that resolves to an object containing:
459473
* - `routeTree`: A populated `RouteTree` containing all extracted routes from the Angular application.
@@ -463,6 +477,7 @@ export async function extractRoutesAndCreateRouteTree(
463477
url: URL,
464478
manifest: AngularAppManifest = getAngularAppManifest(),
465479
invokeGetPrerenderParams = false,
480+
includePrerenderFallbackRoutes = true,
466481
): Promise<{ routeTree: RouteTree; errors: string[] }> {
467482
const routeTree = new RouteTree();
468483
const document = await new ServerAssets(manifest).getIndexServerHtml();
@@ -472,6 +487,7 @@ export async function extractRoutesAndCreateRouteTree(
472487
document,
473488
url,
474489
invokeGetPrerenderParams,
490+
includePrerenderFallbackRoutes,
475491
);
476492

477493
for (const { route, ...metadata } of routes) {

packages/angular/ssr/test/routes/ng-routes_spec.ts

+81
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,85 @@ describe('extractRoutesAndCreateRouteTree', () => {
213213
{ route: '/user/:id/role/:role', renderMode: RenderMode.Server },
214214
]);
215215
});
216+
217+
it('should not include fallback routes for SSG when `includePrerenderFallbackRoutes` is false', async () => {
218+
setAngularAppTestingManifest(
219+
[
220+
{ path: 'home', component: DummyComponent },
221+
{ path: 'user/:id/role/:role', component: DummyComponent },
222+
],
223+
[
224+
{
225+
path: 'user/:id/role/:role',
226+
fallback: PrerenderFallback.Client,
227+
renderMode: RenderMode.Prerender,
228+
async getPrerenderParams() {
229+
return [
230+
{ id: 'joe', role: 'admin' },
231+
{ id: 'jane', role: 'writer' },
232+
];
233+
},
234+
},
235+
{ path: '**', renderMode: RenderMode.Server },
236+
],
237+
);
238+
239+
const { routeTree, errors } = await extractRoutesAndCreateRouteTree(
240+
url,
241+
/** manifest */ undefined,
242+
/** invokeGetPrerenderParams */ true,
243+
/** includePrerenderFallbackRoutes */ false,
244+
);
245+
246+
expect(errors).toHaveSize(0);
247+
expect(routeTree.toObject()).toEqual([
248+
{ route: '/home', renderMode: RenderMode.Server },
249+
{ route: '/user/joe/role/admin', renderMode: RenderMode.Prerender },
250+
{
251+
route: '/user/jane/role/writer',
252+
renderMode: RenderMode.Prerender,
253+
},
254+
]);
255+
});
256+
257+
it('should include fallback routes for SSG when `includePrerenderFallbackRoutes` is true', async () => {
258+
setAngularAppTestingManifest(
259+
[
260+
{ path: 'home', component: DummyComponent },
261+
{ path: 'user/:id/role/:role', component: DummyComponent },
262+
],
263+
[
264+
{
265+
path: 'user/:id/role/:role',
266+
renderMode: RenderMode.Prerender,
267+
fallback: PrerenderFallback.Client,
268+
async getPrerenderParams() {
269+
return [
270+
{ id: 'joe', role: 'admin' },
271+
{ id: 'jane', role: 'writer' },
272+
];
273+
},
274+
},
275+
{ path: '**', renderMode: RenderMode.Server },
276+
],
277+
);
278+
279+
const { routeTree, errors } = await extractRoutesAndCreateRouteTree(
280+
url,
281+
/** manifest */ undefined,
282+
/** invokeGetPrerenderParams */ true,
283+
/** includePrerenderFallbackRoutes */ true,
284+
);
285+
286+
expect(errors).toHaveSize(0);
287+
expect(routeTree.toObject()).toEqual([
288+
{ route: '/home', renderMode: RenderMode.Server },
289+
{ route: '/user/joe/role/admin', renderMode: RenderMode.Prerender },
290+
{
291+
route: '/user/jane/role/writer',
292+
renderMode: RenderMode.Prerender,
293+
},
294+
{ route: '/user/:id/role/:role', renderMode: RenderMode.Client },
295+
]);
296+
});
216297
});

0 commit comments

Comments
 (0)