Skip to content

Commit 50df631

Browse files
committed
fix(@angular/ssr): improve handling of route mismatches between Angular server routes and Angular router
This commit resolves an issue where routes defined in the Angular server routing configuration did not match those in the Angular router. Previously, discrepancies between these routes went unnoticed by users. With this update, appropriate error messages are now displayed when mismatches occur, enhancing the developer experience and facilitating easier troubleshooting.
1 parent 8f038de commit 50df631

File tree

3 files changed

+93
-25
lines changed

3 files changed

+93
-25
lines changed

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

+29-6
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { ServerAssets } from '../assets';
2626
import { Console } from '../console';
2727
import { AngularAppManifest, getAngularAppManifest } from '../manifest';
2828
import { AngularBootstrap, isNgModule } from '../utils/ng';
29-
import { joinUrlParts } from '../utils/url';
29+
import { joinUrlParts, stripLeadingSlash } from '../utils/url';
3030
import { PrerenderFallback, RenderMode, SERVER_ROUTES_CONFIG, ServerRoute } from './route-config';
3131
import { RouteTree, RouteTreeNodeMetadata } from './route-tree';
3232

@@ -43,7 +43,10 @@ const VALID_REDIRECT_RESPONSE_CODES = new Set([301, 302, 303, 307, 308]);
4343
/**
4444
* Additional metadata for a server configuration route tree.
4545
*/
46-
type ServerConfigRouteTreeAdditionalMetadata = Partial<ServerRoute>;
46+
type ServerConfigRouteTreeAdditionalMetadata = Partial<ServerRoute> & {
47+
/** Indicates if the route has been matched with the Angular router routes. */
48+
matched?: boolean;
49+
};
4750

4851
/**
4952
* Metadata for a server configuration route tree node.
@@ -124,19 +127,23 @@ async function* traverseRoutesConfig(options: {
124127
if (!matchedMetaData) {
125128
yield {
126129
error:
127-
`The '${currentRoutePath}' route does not match any route defined in the server routing configuration. ` +
130+
`The '${stripLeadingSlash(currentRoutePath)}' route does not match any route defined in the server routing configuration. ` +
128131
'Please ensure this route is added to the server routing configuration.',
129132
};
130133

131134
continue;
132135
}
136+
137+
matchedMetaData.matched = true;
133138
}
134139

135140
const metadata: ServerConfigRouteTreeNodeMetadata = {
136141
...matchedMetaData,
137142
route: currentRoutePath,
138143
};
139144

145+
delete metadata.matched;
146+
140147
// Handle redirects
141148
if (typeof redirectTo === 'string') {
142149
const redirectToResolved = resolveRedirectTo(currentRoutePath, redirectTo);
@@ -189,7 +196,9 @@ async function* traverseRoutesConfig(options: {
189196
}
190197
}
191198
} catch (error) {
192-
yield { error: `Error processing route '${route.path}': ${(error as Error).message}` };
199+
yield {
200+
error: `Error processing route '${stripLeadingSlash(route.path ?? '')}': ${(error as Error).message}`,
201+
};
193202
}
194203
}
195204
}
@@ -237,7 +246,7 @@ async function* handleSSGRoute(
237246
if (!getPrerenderParams) {
238247
yield {
239248
error:
240-
`The '${currentRoutePath}' route uses prerendering and includes parameters, but 'getPrerenderParams' is missing. ` +
249+
`The '${stripLeadingSlash(currentRoutePath)}' route uses prerendering and includes parameters, but 'getPrerenderParams' is missing. ` +
241250
`Please define 'getPrerenderParams' function for this route in your server routing configuration ` +
242251
`or specify a different 'renderMode'.`,
243252
};
@@ -253,7 +262,7 @@ async function* handleSSGRoute(
253262
const value = params[parameterName];
254263
if (typeof value !== 'string') {
255264
throw new Error(
256-
`The 'getPrerenderParams' function defined for the '${currentRoutePath}' route ` +
265+
`The 'getPrerenderParams' function defined for the '${stripLeadingSlash(currentRoutePath)}' route ` +
257266
`returned a non-string value for parameter '${parameterName}'. ` +
258267
`Please make sure the 'getPrerenderParams' function returns values for all parameters ` +
259268
'specified in this route.',
@@ -440,6 +449,20 @@ export async function getRoutesFromAngularRouterConfig(
440449
routesResults.push(result);
441450
}
442451
}
452+
453+
if (serverConfigRouteTree) {
454+
for (const { route, matched } of serverConfigRouteTree.traverse()) {
455+
if (matched || route === '**') {
456+
// Skip if matched or it's the catch-all route.
457+
continue;
458+
}
459+
460+
errors.push(
461+
`The server route '${route}' does not match any routes defined in the Angular routing configuration. ` +
462+
'Please verify and if unneeded remove this route from the server configuration.',
463+
);
464+
}
465+
}
443466
} else {
444467
routesResults.push({ route: '', renderMode: RenderMode.Prerender });
445468
}

packages/angular/ssr/src/routes/route-tree.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export class RouteTree<AdditionalMetadata extends Record<string, unknown> = {}>
207207
*
208208
* @param node - The current node to start the traversal from. Defaults to the root node of the tree.
209209
*/
210-
private *traverse(node = this.root): Generator<RouteTreeNodeMetadata & AdditionalMetadata> {
210+
*traverse(node = this.root): Generator<RouteTreeNodeMetadata & AdditionalMetadata> {
211211
if (node.metadata) {
212212
yield node.metadata;
213213
}

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

+63-18
Original file line numberDiff line numberDiff line change
@@ -66,24 +66,6 @@ describe('extractRoutesAndCreateRouteTree', () => {
6666
);
6767
});
6868

69-
it('should handle route not matching server routing configuration', async () => {
70-
setAngularAppTestingManifest(
71-
[
72-
{ path: 'home', component: DummyComponent },
73-
{ path: 'about', component: DummyComponent }, // This route is not in the server configuration
74-
],
75-
[
76-
{ path: 'home', renderMode: RenderMode.Client },
77-
// 'about' route is missing here
78-
],
79-
);
80-
81-
const { errors } = await extractRoutesAndCreateRouteTree(url);
82-
expect(errors[0]).toContain(
83-
`The '/about' route does not match any route defined in the server routing configuration.`,
84-
);
85-
});
86-
8769
describe('when `invokeGetPrerenderParams` is true', () => {
8870
it('should resolve parameterized routes for SSG and add a fallback route if fallback is Server', async () => {
8971
setAngularAppTestingManifest(
@@ -294,4 +276,67 @@ describe('extractRoutesAndCreateRouteTree', () => {
294276
{ route: '/user/:id/role/:role', renderMode: RenderMode.Client },
295277
]);
296278
});
279+
280+
it(`should not error when a catch-all route didn't match any Angular route.`, async () => {
281+
setAngularAppTestingManifest(
282+
[{ path: 'home', component: DummyComponent }],
283+
[
284+
{ path: 'home', renderMode: RenderMode.Server },
285+
{ path: '**', renderMode: RenderMode.Server },
286+
],
287+
);
288+
289+
const { errors } = await extractRoutesAndCreateRouteTree(
290+
url,
291+
/** manifest */ undefined,
292+
/** invokeGetPrerenderParams */ false,
293+
/** includePrerenderFallbackRoutes */ false,
294+
);
295+
296+
expect(errors).toHaveSize(0);
297+
});
298+
299+
it('should error when a route is not defined in the server routing configuration', async () => {
300+
setAngularAppTestingManifest(
301+
[{ path: 'home', component: DummyComponent }],
302+
[
303+
{ path: 'home', renderMode: RenderMode.Server },
304+
{ path: 'invalid', renderMode: RenderMode.Server },
305+
],
306+
);
307+
308+
const { errors } = await extractRoutesAndCreateRouteTree(
309+
url,
310+
/** manifest */ undefined,
311+
/** invokeGetPrerenderParams */ false,
312+
/** includePrerenderFallbackRoutes */ false,
313+
);
314+
315+
expect(errors).toHaveSize(1);
316+
expect(errors[0]).toContain(
317+
`The server route 'invalid' does not match any routes defined in the Angular routing configuration`,
318+
);
319+
});
320+
321+
it('should error when a server route is not defined in the Angular routing configuration', async () => {
322+
setAngularAppTestingManifest(
323+
[
324+
{ path: 'home', component: DummyComponent },
325+
{ path: 'invalid', component: DummyComponent },
326+
],
327+
[{ path: 'home', renderMode: RenderMode.Server }],
328+
);
329+
330+
const { errors } = await extractRoutesAndCreateRouteTree(
331+
url,
332+
/** manifest */ undefined,
333+
/** invokeGetPrerenderParams */ false,
334+
/** includePrerenderFallbackRoutes */ false,
335+
);
336+
337+
expect(errors).toHaveSize(1);
338+
expect(errors[0]).toContain(
339+
`The 'invalid' route does not match any route defined in the server routing configuration`,
340+
);
341+
});
297342
});

0 commit comments

Comments
 (0)