Skip to content

Commit 298dd63

Browse files
committed
refactor(@angular/ssr): remove RenderMode.AppShell in favor of new configuration option
This commit removes the `RenderMode.AppShell` option. Instead, a new configuration parameter, `{ appShellRoute: 'shell' }`, is introduced to the `provideServerRoutesConfig` method. ```ts provideServerRoutesConfig(serverRoutes, { appShellRoute: 'shell' }) ```
1 parent b0965a9 commit 298dd63

File tree

14 files changed

+164
-142
lines changed

14 files changed

+164
-142
lines changed

goldens/public-api/angular/ssr/index.api.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@ export enum PrerenderFallback {
2424
}
2525

2626
// @public
27-
export function provideServerRoutesConfig(routes: ServerRoute[]): EnvironmentProviders;
27+
export function provideServerRoutesConfig(routes: ServerRoute[], options?: ServerRoutesConfigOptions): EnvironmentProviders;
2828

2929
// @public
3030
export enum RenderMode {
31-
AppShell = 0,
3231
Client = 2,
3332
Prerender = 3,
3433
Server = 1
@@ -38,12 +37,7 @@ export enum RenderMode {
3837
export type RequestHandlerFunction = (request: Request) => Promise<Response | null> | null | Response;
3938

4039
// @public
41-
export type ServerRoute = ServerRouteAppShell | ServerRouteClient | ServerRoutePrerender | ServerRoutePrerenderWithParams | ServerRouteServer;
42-
43-
// @public
44-
export interface ServerRouteAppShell extends Omit<ServerRouteCommon, 'headers' | 'status'> {
45-
renderMode: RenderMode.AppShell;
46-
}
40+
export type ServerRoute = ServerRouteClient | ServerRoutePrerender | ServerRoutePrerenderWithParams | ServerRouteServer;
4741

4842
// @public
4943
export interface ServerRouteClient extends ServerRouteCommon {
@@ -69,6 +63,11 @@ export interface ServerRoutePrerenderWithParams extends Omit<ServerRoutePrerende
6963
getPrerenderParams: () => Promise<Record<string, string>[]>;
7064
}
7165

66+
// @public
67+
export interface ServerRoutesConfigOptions {
68+
appShellRoute?: string;
69+
}
70+
7271
// @public
7372
export interface ServerRouteServer extends ServerRouteCommon {
7473
renderMode: RenderMode.Server;

packages/angular/build/src/builders/application/tests/options/app-shell_spec.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,6 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
117117
harness.expectFile('dist/browser/main.js').toExist();
118118
const indexFileContent = harness.expectFile('dist/browser/index.html').content;
119119
indexFileContent.toContain('app-shell works!');
120-
// TODO(alanagius): enable once integration of routes in complete.
121-
// indexFileContent.toContain('ng-server-context="app-shell"');
122120
});
123121

124122
it('critical CSS is inlined', async () => {

packages/angular/build/src/utils/server-rendering/models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type WritableSerializableRouteTreeNode = Writeable<SerializableRouteTreeN
2323

2424
export interface RoutersExtractorWorkerResult {
2525
serializedRouteTree: SerializableRouteTreeNode;
26+
appShellRoute?: string;
2627
errors: string[];
2728
}
2829

@@ -33,7 +34,6 @@ export interface RoutersExtractorWorkerResult {
3334
* It maps `RenderMode` enum values to their corresponding numeric identifiers.
3435
*/
3536
export const RouteRenderMode: Record<keyof typeof RenderMode, RenderMode> = {
36-
AppShell: 0,
3737
Server: 1,
3838
Client: 2,
3939
Prerender: 3,

packages/angular/build/src/utils/server-rendering/prerender.ts

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -97,24 +97,26 @@ export async function prerenderPages(
9797
}
9898

9999
// Get routes to prerender
100-
const { errors: extractionErrors, serializedRouteTree: serializableRouteTreeNode } =
101-
await getAllRoutes(
102-
workspaceRoot,
103-
baseHref,
104-
outputFilesForWorker,
105-
assetsReversed,
106-
appShellOptions,
107-
prerenderOptions,
108-
sourcemap,
109-
outputMode,
110-
).catch((err) => {
111-
return {
112-
errors: [
113-
`An error occurred while extracting routes.\n\n${err.stack ?? err.message ?? err}`,
114-
],
115-
serializedRouteTree: [],
116-
};
117-
});
100+
const {
101+
errors: extractionErrors,
102+
serializedRouteTree: serializableRouteTreeNode,
103+
appShellRoute,
104+
} = await getAllRoutes(
105+
workspaceRoot,
106+
baseHref,
107+
outputFilesForWorker,
108+
assetsReversed,
109+
appShellOptions,
110+
prerenderOptions,
111+
sourcemap,
112+
outputMode,
113+
).catch((err) => {
114+
return {
115+
errors: [`An error occurred while extracting routes.\n\n${err.stack ?? err.message ?? err}`],
116+
serializedRouteTree: [],
117+
appShellRoute: undefined,
118+
};
119+
});
118120

119121
errors.push(...extractionErrors);
120122

@@ -133,7 +135,6 @@ export async function prerenderPages(
133135
switch (metadata.renderMode) {
134136
case undefined: /* Legacy building mode */
135137
case RouteRenderMode.Prerender:
136-
case RouteRenderMode.AppShell:
137138
serializableRouteTreeNodeForPrerender.push(metadata);
138139
break;
139140
case RouteRenderMode.Server:
@@ -166,6 +167,7 @@ export async function prerenderPages(
166167
assetsReversed,
167168
appShellOptions,
168169
outputMode,
170+
appShellRoute ?? appShellOptions?.route,
169171
);
170172

171173
errors.push(...renderingErrors);
@@ -188,6 +190,7 @@ async function renderPages(
188190
assetFilesForWorker: Record<string, string>,
189191
appShellOptions: AppShellOptions | undefined,
190192
outputMode: OutputMode | undefined,
193+
appShellRoute: string | undefined,
191194
): Promise<{
192195
output: PrerenderOutput;
193196
errors: string[];
@@ -215,7 +218,7 @@ async function renderPages(
215218

216219
try {
217220
const renderingPromises: Promise<void>[] = [];
218-
const appShellRoute = appShellOptions && addLeadingSlash(appShellOptions.route);
221+
const appShellRouteWithLeadingSlash = appShellRoute && addLeadingSlash(appShellRoute);
219222
const baseHrefWithLeadingSlash = addLeadingSlash(baseHref);
220223

221224
for (const { route, redirectTo, renderMode } of serializableRouteTreeNode) {
@@ -232,16 +235,14 @@ async function renderPages(
232235
continue;
233236
}
234237

235-
const isAppShellRoute =
236-
renderMode === RouteRenderMode.AppShell ||
237-
// Legacy handling
238-
(renderMode === undefined && appShellRoute === routeWithoutBaseHref);
239-
240-
const render: Promise<string | null> = renderWorker.run({ url: route, isAppShellRoute });
238+
const render: Promise<string | null> = renderWorker.run({ url: route });
241239
const renderResult: Promise<void> = render
242240
.then((content) => {
243241
if (content !== null) {
244-
output[outPath] = { content, appShellRoute: isAppShellRoute };
242+
output[outPath] = {
243+
content,
244+
appShellRoute: appShellRouteWithLeadingSlash === routeWithoutBaseHref,
245+
};
245246
}
246247
})
247248
.catch((err) => {
@@ -274,14 +275,21 @@ async function getAllRoutes(
274275
prerenderOptions: PrerenderOptions | undefined,
275276
sourcemap: boolean,
276277
outputMode: OutputMode | undefined,
277-
): Promise<{ serializedRouteTree: SerializableRouteTreeNode; errors: string[] }> {
278+
): Promise<{
279+
serializedRouteTree: SerializableRouteTreeNode;
280+
appShellRoute?: string;
281+
errors: string[];
282+
}> {
278283
const { routesFile, discoverRoutes } = prerenderOptions ?? {};
279284
const routes: WritableSerializableRouteTreeNode = [];
285+
let appShellRoute: string | undefined;
280286

281287
if (appShellOptions) {
288+
appShellRoute = urlJoin(baseHref, appShellOptions.route);
289+
282290
routes.push({
283-
renderMode: RouteRenderMode.AppShell,
284-
route: urlJoin(baseHref, appShellOptions.route),
291+
renderMode: RouteRenderMode.Prerender,
292+
route: appShellRoute,
285293
});
286294
}
287295

@@ -296,7 +304,7 @@ async function getAllRoutes(
296304
}
297305

298306
if (!discoverRoutes) {
299-
return { errors: [], serializedRouteTree: routes };
307+
return { errors: [], appShellRoute, serializedRouteTree: routes };
300308
}
301309

302310
const workerExecArgv = [IMPORT_EXEC_ARGV];
@@ -319,12 +327,11 @@ async function getAllRoutes(
319327
});
320328

321329
try {
322-
const { serializedRouteTree, errors }: RoutersExtractorWorkerResult = await renderWorker.run(
323-
{},
324-
);
330+
const { serializedRouteTree, appShellRoute, errors }: RoutersExtractorWorkerResult =
331+
await renderWorker.run({});
325332

326333
if (!routes.length) {
327-
return { errors, serializedRouteTree };
334+
return { errors, appShellRoute, serializedRouteTree };
328335
}
329336

330337
// Merge the routing trees

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ async function extractRoutes(): Promise<RoutersExtractorWorkerResult> {
3333
const { ɵextractRoutesAndCreateRouteTree: extractRoutesAndCreateRouteTree } =
3434
await loadEsmModuleFromMemory('./main.server.mjs');
3535

36-
const { routeTree, errors } = await extractRoutesAndCreateRouteTree(
36+
const { routeTree, appShellRoute, errors } = await extractRoutesAndCreateRouteTree(
3737
serverURL,
3838
undefined /** manifest */,
3939
true /** invokeGetPrerenderParams */,
@@ -42,6 +42,7 @@ async function extractRoutes(): Promise<RoutersExtractorWorkerResult> {
4242

4343
return {
4444
errors,
45+
appShellRoute,
4546
serializedRouteTree: routeTree.toObject(),
4647
};
4748
}

packages/angular/ssr/public_api.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import { provideZoneChangeDetection } from '@angular/core';
10+
911
export * from './private_export';
1012

1113
export { AngularAppEngine } from './src/app-engine';
@@ -14,12 +16,14 @@ export { createRequestHandler, type RequestHandlerFunction } from './src/handler
1416
export {
1517
type PrerenderFallback,
1618
type ServerRoute,
19+
type ServerRoutesConfigOptions,
1720
provideServerRoutesConfig,
1821
RenderMode,
19-
type ServerRouteAppShell,
2022
type ServerRouteClient,
2123
type ServerRoutePrerender,
2224
type ServerRoutePrerenderWithParams,
2325
type ServerRouteServer,
2426
type ServerRouteCommon,
2527
} from './src/routes/route-config';
28+
29+
provideZoneChangeDetection();

packages/angular/ssr/src/app.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,11 @@ const MAX_INLINE_CSS_CACHE_ENTRIES = 50;
3535
*
3636
* - `RenderMode.Prerender` maps to `'ssg'` (Static Site Generation).
3737
* - `RenderMode.Server` maps to `'ssr'` (Server-Side Rendering).
38-
* - `RenderMode.AppShell` maps to `'app-shell'` (pre-rendered application shell).
3938
* - `RenderMode.Client` maps to an empty string `''` (Client-Side Rendering, no server context needed).
4039
*/
4140
const SERVER_CONTEXT_VALUE: Record<RenderMode, string> = {
4241
[RenderMode.Prerender]: 'ssg',
4342
[RenderMode.Server]: 'ssr',
44-
[RenderMode.AppShell]: 'app-shell',
4543
[RenderMode.Client]: '',
4644
};
4745

@@ -240,10 +238,7 @@ export class AngularServerApp {
240238
}
241239

242240
const { renderMode, headers } = matchedRoute;
243-
if (
244-
!this.allowStaticRouteRender &&
245-
(renderMode === RenderMode.Prerender || renderMode === RenderMode.AppShell)
246-
) {
241+
if (!this.allowStaticRouteRender && renderMode === RenderMode.Prerender) {
247242
return null;
248243
}
249244

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

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ import { Console } from '../console';
2222
import { AngularAppManifest, getAngularAppManifest } from '../manifest';
2323
import { AngularBootstrap, isNgModule } from '../utils/ng';
2424
import { joinUrlParts, stripLeadingSlash } from '../utils/url';
25-
import { PrerenderFallback, RenderMode, SERVER_ROUTES_CONFIG, ServerRoute } from './route-config';
25+
import {
26+
PrerenderFallback,
27+
RenderMode,
28+
SERVER_ROUTES_CONFIG,
29+
ServerRoute,
30+
ServerRoutesConfig,
31+
} from './route-config';
2632
import { RouteTree, RouteTreeNodeMetadata } from './route-tree';
2733

2834
/**
@@ -80,6 +86,11 @@ interface AngularRouterConfigResult {
8086
* A list of errors encountered during the route extraction process.
8187
*/
8288
errors: string[];
89+
90+
/**
91+
* The specified route for the app-shell, if configured.
92+
*/
93+
appShellRoute?: string;
8394
}
8495

8596
/**
@@ -317,22 +328,30 @@ function resolveRedirectTo(routePath: string, redirectTo: string): string {
317328
/**
318329
* Builds a server configuration route tree from the given server routes configuration.
319330
*
320-
* @param serverRoutesConfig - The array of server routes to be used for configuration.
331+
* @param serverRoutesConfig - The server routes to be used for configuration.
321332
322333
* @returns An object containing:
323334
* - `serverConfigRouteTree`: A populated `RouteTree` instance, which organizes the server routes
324335
* along with their additional metadata.
325336
* - `errors`: An array of strings that list any errors encountered during the route tree construction
326337
* process, such as invalid paths.
327338
*/
328-
function buildServerConfigRouteTree(serverRoutesConfig: ServerRoute[]): {
339+
function buildServerConfigRouteTree({ routes, appShellRoute }: ServerRoutesConfig): {
329340
errors: string[];
330341
serverConfigRouteTree: RouteTree<ServerConfigRouteTreeAdditionalMetadata>;
331342
} {
343+
const serverRoutes: ServerRoute[] = [...routes];
344+
if (appShellRoute !== undefined) {
345+
serverRoutes.unshift({
346+
path: appShellRoute,
347+
renderMode: RenderMode.Prerender,
348+
});
349+
}
350+
332351
const serverConfigRouteTree = new RouteTree<ServerConfigRouteTreeAdditionalMetadata>();
333352
const errors: string[] = [];
334353

335-
for (const { path, ...metadata } of serverRoutesConfig) {
354+
for (const { path, ...metadata } of serverRoutes) {
336355
if (path[0] === '/') {
337356
errors.push(`Invalid '${path}' route configuration: the path cannot start with a slash.`);
338357

@@ -442,18 +461,6 @@ export async function getRoutesFromAngularRouterConfig(
442461
if ('error' in result) {
443462
errors.push(result.error);
444463
} else {
445-
if (result.renderMode === RenderMode.AppShell) {
446-
if (seenAppShellRoute !== undefined) {
447-
errors.push(
448-
`Error: Both '${seenAppShellRoute}' and '${stripLeadingSlash(result.route)}' routes have ` +
449-
`their 'renderMode' set to 'AppShell'. AppShell renderMode should only be assigned to one route. ` +
450-
`Please review your route configurations to ensure that only one route is set to 'RenderMode.AppShell'.`,
451-
);
452-
}
453-
454-
seenAppShellRoute = stripLeadingSlash(result.route);
455-
}
456-
457464
routesResults.push(result);
458465
}
459466
}
@@ -485,6 +492,7 @@ export async function getRoutesFromAngularRouterConfig(
485492
baseHref,
486493
routes: routesResults,
487494
errors,
495+
appShellRoute: serverRoutesConfig?.appShellRoute,
488496
};
489497
} finally {
490498
platformRef.destroy();
@@ -508,18 +516,19 @@ export async function getRoutesFromAngularRouterConfig(
508516
*
509517
* @returns A promise that resolves to an object containing:
510518
* - `routeTree`: A populated `RouteTree` containing all extracted routes from the Angular application.
519+
* - `appShellRoute`: The specified route for the app-shell, if configured.
511520
* - `errors`: An array of strings representing any errors encountered during the route extraction process.
512521
*/
513522
export async function extractRoutesAndCreateRouteTree(
514523
url: URL,
515524
manifest: AngularAppManifest = getAngularAppManifest(),
516525
invokeGetPrerenderParams = false,
517526
includePrerenderFallbackRoutes = true,
518-
): Promise<{ routeTree: RouteTree; errors: string[] }> {
527+
): Promise<{ routeTree: RouteTree; appShellRoute?: string; errors: string[] }> {
519528
const routeTree = new RouteTree();
520529
const document = await new ServerAssets(manifest).getIndexServerHtml().text();
521530
const bootstrap = await manifest.bootstrap();
522-
const { baseHref, routes, errors } = await getRoutesFromAngularRouterConfig(
531+
const { baseHref, appShellRoute, routes, errors } = await getRoutesFromAngularRouterConfig(
523532
bootstrap,
524533
document,
525534
url,
@@ -537,6 +546,7 @@ export async function extractRoutesAndCreateRouteTree(
537546
}
538547

539548
return {
549+
appShellRoute,
540550
routeTree,
541551
errors,
542552
};

0 commit comments

Comments
 (0)