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' })
```
This commit is contained in:
Alan Agius 2024-11-08 18:27:37 +00:00 committed by Alan Agius
parent 553d3d7f6e
commit b2e2be052f
14 changed files with 174 additions and 146 deletions

View File

@ -24,26 +24,20 @@ export enum PrerenderFallback {
} }
// @public // @public
export function provideServerRoutesConfig(routes: ServerRoute[]): EnvironmentProviders; export function provideServerRoutesConfig(routes: ServerRoute[], options?: ServerRoutesConfigOptions): EnvironmentProviders;
// @public // @public
export enum RenderMode { export enum RenderMode {
AppShell = 0, Client = 1,
Client = 2, Prerender = 2,
Prerender = 3, Server = 0
Server = 1
} }
// @public // @public
export type RequestHandlerFunction = (request: Request) => Promise<Response | null> | null | Response; export type RequestHandlerFunction = (request: Request) => Promise<Response | null> | null | Response;
// @public // @public
export type ServerRoute = ServerRouteAppShell | ServerRouteClient | ServerRoutePrerender | ServerRoutePrerenderWithParams | ServerRouteServer; export type ServerRoute = ServerRouteClient | ServerRoutePrerender | ServerRoutePrerenderWithParams | ServerRouteServer;
// @public
export interface ServerRouteAppShell extends Omit<ServerRouteCommon, 'headers' | 'status'> {
renderMode: RenderMode.AppShell;
}
// @public // @public
export interface ServerRouteClient extends ServerRouteCommon { export interface ServerRouteClient extends ServerRouteCommon {
@ -69,6 +63,11 @@ export interface ServerRoutePrerenderWithParams extends Omit<ServerRoutePrerende
getPrerenderParams: () => Promise<Record<string, string>[]>; getPrerenderParams: () => Promise<Record<string, string>[]>;
} }
// @public
export interface ServerRoutesConfigOptions {
appShellRoute?: string;
}
// @public // @public
export interface ServerRouteServer extends ServerRouteCommon { export interface ServerRouteServer extends ServerRouteCommon {
renderMode: RenderMode.Server; renderMode: RenderMode.Server;

View File

@ -117,8 +117,6 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
harness.expectFile('dist/browser/main.js').toExist(); harness.expectFile('dist/browser/main.js').toExist();
const indexFileContent = harness.expectFile('dist/browser/index.html').content; const indexFileContent = harness.expectFile('dist/browser/index.html').content;
indexFileContent.toContain('app-shell works!'); indexFileContent.toContain('app-shell works!');
// TODO(alanagius): enable once integration of routes in complete.
// indexFileContent.toContain('ng-server-context="app-shell"');
}); });
it('critical CSS is inlined', async () => { it('critical CSS is inlined', async () => {

View File

@ -23,6 +23,7 @@ export type WritableSerializableRouteTreeNode = Writeable<SerializableRouteTreeN
export interface RoutersExtractorWorkerResult { export interface RoutersExtractorWorkerResult {
serializedRouteTree: SerializableRouteTreeNode; serializedRouteTree: SerializableRouteTreeNode;
appShellRoute?: string;
errors: string[]; errors: string[];
} }
@ -33,8 +34,7 @@ export interface RoutersExtractorWorkerResult {
* It maps `RenderMode` enum values to their corresponding numeric identifiers. * It maps `RenderMode` enum values to their corresponding numeric identifiers.
*/ */
export const RouteRenderMode: Record<keyof typeof RenderMode, RenderMode> = { export const RouteRenderMode: Record<keyof typeof RenderMode, RenderMode> = {
AppShell: 0, Server: 0,
Server: 1, Client: 1,
Client: 2, Prerender: 2,
Prerender: 3,
}; };

View File

@ -97,24 +97,26 @@ export async function prerenderPages(
} }
// Get routes to prerender // Get routes to prerender
const { errors: extractionErrors, serializedRouteTree: serializableRouteTreeNode } = const {
await getAllRoutes( errors: extractionErrors,
workspaceRoot, serializedRouteTree: serializableRouteTreeNode,
baseHref, appShellRoute,
outputFilesForWorker, } = await getAllRoutes(
assetsReversed, workspaceRoot,
appShellOptions, baseHref,
prerenderOptions, outputFilesForWorker,
sourcemap, assetsReversed,
outputMode, appShellOptions,
).catch((err) => { prerenderOptions,
return { sourcemap,
errors: [ outputMode,
`An error occurred while extracting routes.\n\n${err.stack ?? err.message ?? err}`, ).catch((err) => {
], return {
serializedRouteTree: [], errors: [`An error occurred while extracting routes.\n\n${err.stack ?? err.message ?? err}`],
}; serializedRouteTree: [],
}); appShellRoute: undefined,
};
});
errors.push(...extractionErrors); errors.push(...extractionErrors);
@ -133,7 +135,6 @@ export async function prerenderPages(
switch (metadata.renderMode) { switch (metadata.renderMode) {
case undefined: /* Legacy building mode */ case undefined: /* Legacy building mode */
case RouteRenderMode.Prerender: case RouteRenderMode.Prerender:
case RouteRenderMode.AppShell:
serializableRouteTreeNodeForPrerender.push(metadata); serializableRouteTreeNodeForPrerender.push(metadata);
break; break;
case RouteRenderMode.Server: case RouteRenderMode.Server:
@ -166,6 +167,7 @@ export async function prerenderPages(
assetsReversed, assetsReversed,
appShellOptions, appShellOptions,
outputMode, outputMode,
appShellRoute ?? appShellOptions?.route,
); );
errors.push(...renderingErrors); errors.push(...renderingErrors);
@ -188,6 +190,7 @@ async function renderPages(
assetFilesForWorker: Record<string, string>, assetFilesForWorker: Record<string, string>,
appShellOptions: AppShellOptions | undefined, appShellOptions: AppShellOptions | undefined,
outputMode: OutputMode | undefined, outputMode: OutputMode | undefined,
appShellRoute: string | undefined,
): Promise<{ ): Promise<{
output: PrerenderOutput; output: PrerenderOutput;
errors: string[]; errors: string[];
@ -215,7 +218,7 @@ async function renderPages(
try { try {
const renderingPromises: Promise<void>[] = []; const renderingPromises: Promise<void>[] = [];
const appShellRoute = appShellOptions && addLeadingSlash(appShellOptions.route); const appShellRouteWithLeadingSlash = appShellRoute && addLeadingSlash(appShellRoute);
const baseHrefWithLeadingSlash = addLeadingSlash(baseHref); const baseHrefWithLeadingSlash = addLeadingSlash(baseHref);
for (const { route, redirectTo, renderMode } of serializableRouteTreeNode) { for (const { route, redirectTo, renderMode } of serializableRouteTreeNode) {
@ -232,16 +235,14 @@ async function renderPages(
continue; continue;
} }
const isAppShellRoute = const render: Promise<string | null> = renderWorker.run({ url: route });
renderMode === RouteRenderMode.AppShell ||
// Legacy handling
(renderMode === undefined && appShellRoute === routeWithoutBaseHref);
const render: Promise<string | null> = renderWorker.run({ url: route, isAppShellRoute });
const renderResult: Promise<void> = render const renderResult: Promise<void> = render
.then((content) => { .then((content) => {
if (content !== null) { if (content !== null) {
output[outPath] = { content, appShellRoute: isAppShellRoute }; output[outPath] = {
content,
appShellRoute: appShellRouteWithLeadingSlash === routeWithoutBaseHref,
};
} }
}) })
.catch((err) => { .catch((err) => {
@ -274,14 +275,21 @@ async function getAllRoutes(
prerenderOptions: PrerenderOptions | undefined, prerenderOptions: PrerenderOptions | undefined,
sourcemap: boolean, sourcemap: boolean,
outputMode: OutputMode | undefined, outputMode: OutputMode | undefined,
): Promise<{ serializedRouteTree: SerializableRouteTreeNode; errors: string[] }> { ): Promise<{
serializedRouteTree: SerializableRouteTreeNode;
appShellRoute?: string;
errors: string[];
}> {
const { routesFile, discoverRoutes } = prerenderOptions ?? {}; const { routesFile, discoverRoutes } = prerenderOptions ?? {};
const routes: WritableSerializableRouteTreeNode = []; const routes: WritableSerializableRouteTreeNode = [];
let appShellRoute: string | undefined;
if (appShellOptions) { if (appShellOptions) {
appShellRoute = urlJoin(baseHref, appShellOptions.route);
routes.push({ routes.push({
renderMode: RouteRenderMode.AppShell, renderMode: RouteRenderMode.Prerender,
route: urlJoin(baseHref, appShellOptions.route), route: appShellRoute,
}); });
} }
@ -296,7 +304,7 @@ async function getAllRoutes(
} }
if (!discoverRoutes) { if (!discoverRoutes) {
return { errors: [], serializedRouteTree: routes }; return { errors: [], appShellRoute, serializedRouteTree: routes };
} }
const workerExecArgv = [IMPORT_EXEC_ARGV]; const workerExecArgv = [IMPORT_EXEC_ARGV];
@ -319,12 +327,11 @@ async function getAllRoutes(
}); });
try { try {
const { serializedRouteTree, errors }: RoutersExtractorWorkerResult = await renderWorker.run( const { serializedRouteTree, appShellRoute, errors }: RoutersExtractorWorkerResult =
{}, await renderWorker.run({});
);
if (!routes.length) { if (!routes.length) {
return { errors, serializedRouteTree }; return { errors, appShellRoute, serializedRouteTree };
} }
// Merge the routing trees // Merge the routing trees

View File

@ -33,7 +33,7 @@ async function extractRoutes(): Promise<RoutersExtractorWorkerResult> {
const { ɵextractRoutesAndCreateRouteTree: extractRoutesAndCreateRouteTree } = const { ɵextractRoutesAndCreateRouteTree: extractRoutesAndCreateRouteTree } =
await loadEsmModuleFromMemory('./main.server.mjs'); await loadEsmModuleFromMemory('./main.server.mjs');
const { routeTree, errors } = await extractRoutesAndCreateRouteTree( const { routeTree, appShellRoute, errors } = await extractRoutesAndCreateRouteTree(
serverURL, serverURL,
undefined /** manifest */, undefined /** manifest */,
true /** invokeGetPrerenderParams */, true /** invokeGetPrerenderParams */,
@ -42,6 +42,7 @@ async function extractRoutes(): Promise<RoutersExtractorWorkerResult> {
return { return {
errors, errors,
appShellRoute,
serializedRouteTree: routeTree.toObject(), serializedRouteTree: routeTree.toObject(),
}; };
} }

View File

@ -14,9 +14,9 @@ export { createRequestHandler, type RequestHandlerFunction } from './src/handler
export { export {
type PrerenderFallback, type PrerenderFallback,
type ServerRoute, type ServerRoute,
type ServerRoutesConfigOptions,
provideServerRoutesConfig, provideServerRoutesConfig,
RenderMode, RenderMode,
type ServerRouteAppShell,
type ServerRouteClient, type ServerRouteClient,
type ServerRoutePrerender, type ServerRoutePrerender,
type ServerRoutePrerenderWithParams, type ServerRoutePrerenderWithParams,

View File

@ -35,13 +35,11 @@ const MAX_INLINE_CSS_CACHE_ENTRIES = 50;
* *
* - `RenderMode.Prerender` maps to `'ssg'` (Static Site Generation). * - `RenderMode.Prerender` maps to `'ssg'` (Static Site Generation).
* - `RenderMode.Server` maps to `'ssr'` (Server-Side Rendering). * - `RenderMode.Server` maps to `'ssr'` (Server-Side Rendering).
* - `RenderMode.AppShell` maps to `'app-shell'` (pre-rendered application shell).
* - `RenderMode.Client` maps to an empty string `''` (Client-Side Rendering, no server context needed). * - `RenderMode.Client` maps to an empty string `''` (Client-Side Rendering, no server context needed).
*/ */
const SERVER_CONTEXT_VALUE: Record<RenderMode, string> = { const SERVER_CONTEXT_VALUE: Record<RenderMode, string> = {
[RenderMode.Prerender]: 'ssg', [RenderMode.Prerender]: 'ssg',
[RenderMode.Server]: 'ssr', [RenderMode.Server]: 'ssr',
[RenderMode.AppShell]: 'app-shell',
[RenderMode.Client]: '', [RenderMode.Client]: '',
}; };
@ -237,11 +235,18 @@ export class AngularServerApp {
matchedRoute: RouteTreeNodeMetadata, matchedRoute: RouteTreeNodeMetadata,
requestContext?: unknown, requestContext?: unknown,
): Promise<Response | null> { ): Promise<Response | null> {
const { renderMode, headers, status } = matchedRoute; const { redirectTo, status } = matchedRoute;
if (
!this.allowStaticRouteRender && if (redirectTo !== undefined) {
(renderMode === RenderMode.Prerender || renderMode === RenderMode.AppShell) // Note: The status code is validated during route extraction.
) { // 302 Found is used by default for redirections
// See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return Response.redirect(new URL(redirectTo, new URL(request.url)), (status as any) ?? 302);
}
const { renderMode, headers } = matchedRoute;
if (!this.allowStaticRouteRender && renderMode === RenderMode.Prerender) {
return null; return null;
} }

View File

@ -22,7 +22,13 @@ import { Console } from '../console';
import { AngularAppManifest, getAngularAppManifest } from '../manifest'; import { AngularAppManifest, getAngularAppManifest } from '../manifest';
import { AngularBootstrap, isNgModule } from '../utils/ng'; import { AngularBootstrap, isNgModule } from '../utils/ng';
import { joinUrlParts, stripLeadingSlash } from '../utils/url'; import { joinUrlParts, stripLeadingSlash } from '../utils/url';
import { PrerenderFallback, RenderMode, SERVER_ROUTES_CONFIG, ServerRoute } from './route-config'; import {
PrerenderFallback,
RenderMode,
SERVER_ROUTES_CONFIG,
ServerRoute,
ServerRoutesConfig,
} from './route-config';
import { RouteTree, RouteTreeNodeMetadata } from './route-tree'; import { RouteTree, RouteTreeNodeMetadata } from './route-tree';
/** /**
@ -80,6 +86,11 @@ interface AngularRouterConfigResult {
* A list of errors encountered during the route extraction process. * A list of errors encountered during the route extraction process.
*/ */
errors: string[]; errors: string[];
/**
* The specified route for the app-shell, if configured.
*/
appShellRoute?: string;
} }
/** /**
@ -317,7 +328,7 @@ function resolveRedirectTo(routePath: string, redirectTo: string): string {
/** /**
* Builds a server configuration route tree from the given server routes configuration. * Builds a server configuration route tree from the given server routes configuration.
* *
* @param serverRoutesConfig - The array of server routes to be used for configuration. * @param serverRoutesConfig - The server routes to be used for configuration.
* @returns An object containing: * @returns An object containing:
* - `serverConfigRouteTree`: A populated `RouteTree` instance, which organizes the server routes * - `serverConfigRouteTree`: A populated `RouteTree` instance, which organizes the server routes
@ -325,14 +336,22 @@ function resolveRedirectTo(routePath: string, redirectTo: string): string {
* - `errors`: An array of strings that list any errors encountered during the route tree construction * - `errors`: An array of strings that list any errors encountered during the route tree construction
* process, such as invalid paths. * process, such as invalid paths.
*/ */
function buildServerConfigRouteTree(serverRoutesConfig: ServerRoute[]): { function buildServerConfigRouteTree({ routes, appShellRoute }: ServerRoutesConfig): {
errors: string[]; errors: string[];
serverConfigRouteTree: RouteTree<ServerConfigRouteTreeAdditionalMetadata>; serverConfigRouteTree: RouteTree<ServerConfigRouteTreeAdditionalMetadata>;
} { } {
const serverRoutes: ServerRoute[] = [...routes];
if (appShellRoute !== undefined) {
serverRoutes.unshift({
path: appShellRoute,
renderMode: RenderMode.Prerender,
});
}
const serverConfigRouteTree = new RouteTree<ServerConfigRouteTreeAdditionalMetadata>(); const serverConfigRouteTree = new RouteTree<ServerConfigRouteTreeAdditionalMetadata>();
const errors: string[] = []; const errors: string[] = [];
for (const { path, ...metadata } of serverRoutesConfig) { for (const { path, ...metadata } of serverRoutes) {
if (path[0] === '/') { if (path[0] === '/') {
errors.push(`Invalid '${path}' route configuration: the path cannot start with a slash.`); errors.push(`Invalid '${path}' route configuration: the path cannot start with a slash.`);
@ -442,18 +461,6 @@ export async function getRoutesFromAngularRouterConfig(
if ('error' in result) { if ('error' in result) {
errors.push(result.error); errors.push(result.error);
} else { } else {
if (result.renderMode === RenderMode.AppShell) {
if (seenAppShellRoute !== undefined) {
errors.push(
`Error: Both '${seenAppShellRoute}' and '${stripLeadingSlash(result.route)}' routes have ` +
`their 'renderMode' set to 'AppShell'. AppShell renderMode should only be assigned to one route. ` +
`Please review your route configurations to ensure that only one route is set to 'RenderMode.AppShell'.`,
);
}
seenAppShellRoute = stripLeadingSlash(result.route);
}
routesResults.push(result); routesResults.push(result);
} }
} }
@ -485,6 +492,7 @@ export async function getRoutesFromAngularRouterConfig(
baseHref, baseHref,
routes: routesResults, routes: routesResults,
errors, errors,
appShellRoute: serverRoutesConfig?.appShellRoute,
}; };
} finally { } finally {
platformRef.destroy(); platformRef.destroy();
@ -508,6 +516,7 @@ export async function getRoutesFromAngularRouterConfig(
* *
* @returns A promise that resolves to an object containing: * @returns A promise that resolves to an object containing:
* - `routeTree`: A populated `RouteTree` containing all extracted routes from the Angular application. * - `routeTree`: A populated `RouteTree` containing all extracted routes from the Angular application.
* - `appShellRoute`: The specified route for the app-shell, if configured.
* - `errors`: An array of strings representing any errors encountered during the route extraction process. * - `errors`: An array of strings representing any errors encountered during the route extraction process.
*/ */
export async function extractRoutesAndCreateRouteTree( export async function extractRoutesAndCreateRouteTree(
@ -515,11 +524,11 @@ export async function extractRoutesAndCreateRouteTree(
manifest: AngularAppManifest = getAngularAppManifest(), manifest: AngularAppManifest = getAngularAppManifest(),
invokeGetPrerenderParams = false, invokeGetPrerenderParams = false,
includePrerenderFallbackRoutes = true, includePrerenderFallbackRoutes = true,
): Promise<{ routeTree: RouteTree; errors: string[] }> { ): Promise<{ routeTree: RouteTree; appShellRoute?: string; errors: string[] }> {
const routeTree = new RouteTree(); const routeTree = new RouteTree();
const document = await new ServerAssets(manifest).getIndexServerHtml().text(); const document = await new ServerAssets(manifest).getIndexServerHtml().text();
const bootstrap = await manifest.bootstrap(); const bootstrap = await manifest.bootstrap();
const { baseHref, routes, errors } = await getRoutesFromAngularRouterConfig( const { baseHref, appShellRoute, routes, errors } = await getRoutesFromAngularRouterConfig(
bootstrap, bootstrap,
document, document,
url, url,
@ -537,6 +546,7 @@ export async function extractRoutesAndCreateRouteTree(
} }
return { return {
appShellRoute,
routeTree, routeTree,
errors, errors,
}; };

View File

@ -15,9 +15,6 @@ import { EnvironmentProviders, InjectionToken, makeEnvironmentProviders } from '
* @developerPreview * @developerPreview
*/ */
export enum RenderMode { export enum RenderMode {
/** AppShell rendering mode, typically used for pre-rendered shells of the application. */
AppShell,
/** Server-Side Rendering (SSR) mode, where content is rendered on the server for each request. */ /** Server-Side Rendering (SSR) mode, where content is rendered on the server for each request. */
Server, Server,
@ -69,16 +66,6 @@ export interface ServerRouteCommon {
status?: number; status?: number;
} }
/**
* A server route that uses AppShell rendering mode.
* @see {@link RenderMode}
* @developerPreview
*/
export interface ServerRouteAppShell extends Omit<ServerRouteCommon, 'headers' | 'status'> {
/** Specifies that the route uses AppShell rendering mode. */
renderMode: RenderMode.AppShell;
}
/** /**
* A server route that uses Client-Side Rendering (CSR) mode. * A server route that uses Client-Side Rendering (CSR) mode.
* @see {@link RenderMode} * @see {@link RenderMode}
@ -165,27 +152,67 @@ export interface ServerRouteServer extends ServerRouteCommon {
* @developerPreview * @developerPreview
*/ */
export type ServerRoute = export type ServerRoute =
| ServerRouteAppShell
| ServerRouteClient | ServerRouteClient
| ServerRoutePrerender | ServerRoutePrerender
| ServerRoutePrerenderWithParams | ServerRoutePrerenderWithParams
| ServerRouteServer; | ServerRouteServer;
/**
* Configuration options for server routes.
*
* This interface defines the optional settings available for configuring server routes
* in the server-side environment, such as specifying a path to the app shell route.
*
* @see {@link provideServerRoutesConfig}
* @developerPreview
*/
export interface ServerRoutesConfigOptions {
/**
* Defines the route to be used as the app shell, which serves as the main entry
* point for the application. This route is often used to enable server-side rendering
* of the application shell for requests that do not match any specific server route.
*
* @see {@link https://angular.dev/ecosystem/service-workers/app-shell | App shell pattern on Angular.dev}
*/
appShellRoute?: string;
}
/**
* Configuration value for server routes configuration.
* @internal
*/
export interface ServerRoutesConfig extends ServerRoutesConfigOptions {
routes: ServerRoute[];
}
/** /**
* Token for providing the server routes configuration. * Token for providing the server routes configuration.
* @internal * @internal
*/ */
export const SERVER_ROUTES_CONFIG = new InjectionToken<ServerRoute[]>('SERVER_ROUTES_CONFIG'); export const SERVER_ROUTES_CONFIG = new InjectionToken<ServerRoutesConfig>('SERVER_ROUTES_CONFIG');
/** /**
* Configures the necessary providers for server routes configuration. /**
* Sets up the necessary providers for configuring server routes.
* This function accepts an array of server routes and optional configuration
* options, returning an `EnvironmentProviders` object that encapsulates
* the server routes and configuration settings.
* *
* @param routes - An array of server routes to be provided. * @param routes - An array of server routes to be provided.
* @param options - (Optional) An object containing additional configuration options for server routes.
* @returns An `EnvironmentProviders` instance with the server routes configuration.
*
* @returns An `EnvironmentProviders` object that contains the server routes configuration. * @returns An `EnvironmentProviders` object that contains the server routes configuration.
*
* @see {@link ServerRoute} * @see {@link ServerRoute}
* @see {@link ServerRoutesConfigOptions}
* @developerPreview * @developerPreview
*/ */
export function provideServerRoutesConfig(routes: ServerRoute[]): EnvironmentProviders { export function provideServerRoutesConfig(
routes: ServerRoute[],
options?: ServerRoutesConfigOptions,
): EnvironmentProviders {
if (typeof ngServerMode === 'undefined' || !ngServerMode) { if (typeof ngServerMode === 'undefined' || !ngServerMode) {
throw new Error( throw new Error(
`The 'provideServerRoutesConfig' function should not be invoked within the browser portion of the application.`, `The 'provideServerRoutesConfig' function should not be invoked within the browser portion of the application.`,
@ -195,7 +222,7 @@ export function provideServerRoutesConfig(routes: ServerRoute[]): EnvironmentPro
return makeEnvironmentProviders([ return makeEnvironmentProviders([
{ {
provide: SERVER_ROUTES_CONFIG, provide: SERVER_ROUTES_CONFIG,
useValue: routes, useValue: { routes, ...options },
}, },
]); ]);
} }

View File

@ -340,28 +340,6 @@ describe('extractRoutesAndCreateRouteTree', () => {
); );
}); });
it(`should error when 'RenderMode.AppShell' is used on more than one route`, async () => {
setAngularAppTestingManifest(
[
{ path: 'home', component: DummyComponent },
{ path: 'shell', component: DummyComponent },
],
[{ path: '**', renderMode: RenderMode.AppShell }],
);
const { errors } = await extractRoutesAndCreateRouteTree(
url,
/** manifest */ undefined,
/** invokeGetPrerenderParams */ false,
/** includePrerenderFallbackRoutes */ false,
);
expect(errors).toHaveSize(1);
expect(errors[0]).toContain(
`Both 'home' and 'shell' routes have their 'renderMode' set to 'AppShell'.`,
);
});
it('should apply RenderMode matching the wildcard when no Angular routes are defined', async () => { it('should apply RenderMode matching the wildcard when no Angular routes are defined', async () => {
setAngularAppTestingManifest([], [{ path: '**', renderMode: RenderMode.Server }]); setAngularAppTestingManifest([], [{ path: '**', renderMode: RenderMode.Server }]);

View File

@ -126,8 +126,6 @@ describe('AppShell Builder', () => {
const fileName = 'dist/index.html'; const fileName = 'dist/index.html';
const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName)));
expect(content).toMatch('Welcome to app'); expect(content).toMatch('Welcome to app');
// TODO(alanagius): enable once integration of routes in complete.
// expect(content).toMatch('ng-server-context="app-shell"');
}); });
it('works with route', async () => { it('works with route', async () => {

View File

@ -265,7 +265,7 @@ function addStandaloneServerRoute(options: AppShellOptions): Rule {
throw new SchematicsException(`Cannot find "${configFilePath}".`); throw new SchematicsException(`Cannot find "${configFilePath}".`);
} }
const recorder = host.beginUpdate(configFilePath); let recorder = host.beginUpdate(configFilePath);
let configSourceFile = getSourceFile(host, configFilePath); let configSourceFile = getSourceFile(host, configFilePath);
if (!isImported(configSourceFile, 'ROUTES', '@angular/router')) { if (!isImported(configSourceFile, 'ROUTES', '@angular/router')) {
const routesChange = insertImport( const routesChange = insertImport(
@ -306,6 +306,24 @@ function addStandaloneServerRoute(options: AppShellOptions): Rule {
recorder.insertRight(providersLiteral.getStart(), `[\n${updatedProvidersString.join(',\n')}]`); recorder.insertRight(providersLiteral.getStart(), `[\n${updatedProvidersString.join(',\n')}]`);
if (options.serverRouting) {
host.commitUpdate(recorder);
configSourceFile = getSourceFile(host, configFilePath);
const functionCall = findNodes(configSourceFile, ts.isCallExpression).find(
(n) =>
ts.isIdentifier(n.expression) && n.expression.getText() === 'provideServerRoutesConfig',
);
if (!functionCall) {
throw new SchematicsException(
`Cannot find the "provideServerRoutesConfig" function call in "${configFilePath}".`,
);
}
recorder = host.beginUpdate(configFilePath);
recorder.insertLeft(functionCall.end - 1, `, { appShellRoute: '${APP_SHELL_ROUTE}' }`);
}
// Add AppShellComponent import // Add AppShellComponent import
const appShellImportChange = insertImport( const appShellImportChange = insertImport(
configSourceFile, configSourceFile,
@ -376,9 +394,7 @@ export default function (options: AppShellOptions): Rule {
...(isStandalone ...(isStandalone
? [addStandaloneServerRoute(options)] ? [addStandaloneServerRoute(options)]
: [addRouterModule(browserEntryPoint), addServerRoutes(options)]), : [addRouterModule(browserEntryPoint), addServerRoutes(options)]),
options.serverRouting options.serverRouting ? noop() : addAppShellConfigToWorkspace(options),
? addServerRoutingConfig(options)
: addAppShellConfigToWorkspace(options),
schematic('component', { schematic('component', {
name: 'app-shell', name: 'app-shell',
module: 'app.module.server.ts', module: 'app.module.server.ts',

View File

@ -200,17 +200,12 @@ describe('App Shell Schematic', () => {
expect(content).toMatch(/app-shell\.component/); expect(content).toMatch(/app-shell\.component/);
}); });
it('should update the server routing configuration', async () => { it(`should update the 'provideServerRoutesConfig' call to include 'appShellRoute`, async () => {
const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree); const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree);
const content = tree.readContent('/projects/bar/src/app/app.routes.server.ts'); const content = tree.readContent('/projects/bar/src/app/app.config.server.ts');
expect(tags.oneLine`${content}`).toContain(tags.oneLine`{ expect(tags.oneLine`${content}`).toContain(
path: 'shell', tags.oneLine`provideServerRoutesConfig(serverRoutes, { appShellRoute: 'shell' })`,
renderMode: RenderMode.AppShell );
},
{
path: '**',
renderMode: RenderMode.Prerender
}`);
}); });
it('should define a server route', async () => { it('should define a server route', async () => {

View File

@ -29,14 +29,9 @@ export default async function () {
import { CsrComponent } from './csr/csr.component'; import { CsrComponent } from './csr/csr.component';
import { SsrComponent } from './ssr/ssr.component'; import { SsrComponent } from './ssr/ssr.component';
import { SsgComponent } from './ssg/ssg.component'; import { SsgComponent } from './ssg/ssg.component';
import { AppShellComponent } from './app-shell/app-shell.component';
import { SsgWithParamsComponent } from './ssg-with-params/ssg-with-params.component'; import { SsgWithParamsComponent } from './ssg-with-params/ssg-with-params.component';
export const routes: Routes = [ export const routes: Routes = [
{
path: 'app-shell',
component: AppShellComponent
},
{ {
path: '', path: '',
component: HomeComponent, component: HomeComponent,
@ -88,10 +83,6 @@ export default async function () {
renderMode: RenderMode.Client, renderMode: RenderMode.Client,
headers: { 'x-custom': 'csr' }, headers: { 'x-custom': 'csr' },
}, },
{
path: 'app-shell',
renderMode: RenderMode.AppShell,
},
{ {
path: '**', path: '**',
renderMode: RenderMode.Prerender, renderMode: RenderMode.Prerender,
@ -102,12 +93,15 @@ export default async function () {
); );
// Generate components for the above routes // Generate components for the above routes
const componentNames: string[] = ['home', 'ssg', 'ssg-with-params', 'csr', 'ssr', 'app-shell']; const componentNames: string[] = ['home', 'ssg', 'ssg-with-params', 'csr', 'ssr'];
for (const componentName of componentNames) { for (const componentName of componentNames) {
await silentNg('generate', 'component', componentName); await silentNg('generate', 'component', componentName);
} }
// Generate app-shell
await ng('g', 'app-shell');
await noSilentNg('build', '--output-mode=server'); await noSilentNg('build', '--output-mode=server');
const expects: Record<string, string> = { const expects: Record<string, string> = {
@ -155,7 +149,7 @@ export default async function () {
}, },
'/csr': { '/csr': {
content: 'app-shell works', content: 'app-shell works',
serverContext: 'ng-server-context="app-shell"', serverContext: 'ng-server-context="ssg"',
headers: { headers: {
'x-custom': 'csr', 'x-custom': 'csr',
}, },