mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-16 18:43:42 +08:00
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:
parent
553d3d7f6e
commit
b2e2be052f
@ -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;
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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,
|
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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 },
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -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 }]);
|
||||||
|
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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',
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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',
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user