Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions goldens/public-api/angular/ssr/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,12 @@
```ts

// @public
export interface AngularServerAppManager {
export class AngularAppEngine {
getHeaders(request: Request): Readonly<Map<string, string>>;
render(request: Request, requestContext?: unknown): Promise<Response | null>;
static ɵhooks: Hooks;
}

// @public
export function destroyAngularAppEngine(): void;

// @public
export function getOrCreateAngularAppEngine(): AngularServerAppManager;

// (No @packageDocumentation comment for this package)

```
6 changes: 6 additions & 0 deletions goldens/public-api/angular/ssr/node/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import type { ServerResponse } from 'node:http';
import { StaticProvider } from '@angular/core';
import { Type } from '@angular/core';

// @public
export class AngularNodeAppEngine {
getHeaders(request: IncomingMessage): Readonly<Map<string, string>>;
render(request: IncomingMessage, requestContext?: unknown): Promise<Response | null>;
}

// @public
export class CommonEngine {
constructor(options?: CommonEngineOptions | undefined);
Expand Down
2 changes: 2 additions & 0 deletions packages/angular/ssr/node/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ export {
type CommonEngineOptions,
} from './src/common-engine/common-engine';

export { AngularNodeAppEngine } from './src/app-engine';

export { writeResponseToNodeResponse } from './src/response';
export { createWebRequestFromNodeRequest } from './src/request';
72 changes: 72 additions & 0 deletions packages/angular/ssr/node/src/app-engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { AngularAppEngine } from '@angular/ssr';
import type { IncomingMessage } from 'node:http';
import { createWebRequestFromNodeRequest } from './request';

/**
* Angular server application engine.
* Manages Angular server applications (including localized ones), handles rendering requests,
* and optionally transforms index HTML before rendering.
*
* @note This class should be instantiated once and used as a singleton across the server-side
* application to ensure consistent handling of rendering requests and resource management.
*
* @developerPreview
*/
export class AngularNodeAppEngine {
private readonly angularAppEngine = new AngularAppEngine();

/**
* Renders an HTTP response based on the incoming request using the Angular server application.
*
* The method processes the incoming request, determines the appropriate route, and prepares the
* rendering context to generate a response. If the request URL corresponds to a static file (excluding `/index.html`),
* the method returns `null`.
*
* Example: A request to `https://www.example.com/page/index.html` will render the Angular route
* associated with `https://www.example.com/page`.
*
* @param request - The incoming HTTP request object to be rendered.
* @param requestContext - Optional additional context for the request, such as metadata or custom settings.
* @returns A promise that resolves to a `Response` object, or `null` if the request URL is for a static file
* (e.g., `./logo.png`) rather than an application route.
*/
render(request: IncomingMessage, requestContext?: unknown): Promise<Response | null> {
return this.angularAppEngine.render(createWebRequestFromNodeRequest(request), requestContext);
}

/**
* Retrieves HTTP headers for a request associated with statically generated (SSG) pages,
* based on the URL pathname.
*
* @param request - The incoming request object.
* @returns A `Map` containing the HTTP headers as key-value pairs.
* @note This function should be used exclusively for retrieving headers of SSG pages.
* @example
* ```typescript
* const angularAppEngine = new AngularNodeAppEngine();
*
* app.use(express.static('dist/browser', {
* setHeaders: (res, path) => {
* // Retrieve headers for the current request
* const headers = angularAppEngine.getHeaders(res.req);
*
* // Apply the retrieved headers to the response
* for (const { key, value } of headers) {
* res.setHeader(key, value);
* }
* }
}));
* ```
*/
getHeaders(request: IncomingMessage): Readonly<Map<string, string>> {
return this.angularAppEngine.getHeaders(createWebRequestFromNodeRequest(request));
}
}
2 changes: 0 additions & 2 deletions packages/angular/ssr/private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,4 @@ export {
setAngularAppEngineManifest as ɵsetAngularAppEngineManifest,
} from './src/manifest';

export { AngularAppEngine as ɵAngularAppEngine } from './src/app-engine';

export { InlineCriticalCssProcessor as ɵInlineCriticalCssProcessor } from './src/utils/inline-critical-css';
6 changes: 1 addition & 5 deletions packages/angular/ssr/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,4 @@

export * from './private_export';

export {
type AngularServerAppManager,
getOrCreateAngularAppEngine,
destroyAngularAppEngine,
} from './src/app-engine';
export { AngularAppEngine } from './src/app-engine';
75 changes: 23 additions & 52 deletions packages/angular/ssr/src/app-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,19 @@ import type { AngularServerApp } from './app';
import { Hooks } from './hooks';
import { getPotentialLocaleIdFromUrl } from './i18n';
import { EntryPointExports, getAngularAppEngineManifest } from './manifest';

/**
* Angular server application engine.
* Manages Angular server applications (including localized ones) and handles rendering requests.

* @developerPreview
*/
export interface AngularServerAppManager {
/**
* Renders a response for the given HTTP request using the server application.
*
* This method processes the request, determines the appropriate route and rendering context,
* and returns an HTTP response.
*
* If the request URL appears to be for a file (excluding `/index.html`), the method returns `null`.
* A request to `https://www.example.com/page/index.html` will render the Angular route
* corresponding to `https://www.example.com/page`.
*
* @param request - The incoming HTTP request object to be rendered.
* @param requestContext - Optional additional context for the request, such as metadata.
* @returns A promise that resolves to a Response object, or `null` if the request URL represents a file (e.g., `./logo.png`)
* rather than an application route.
*/
render(request: Request, requestContext?: unknown): Promise<Response | null>;
}
import { stripIndexHtmlFromURL } from './utils/url';

/**
* Angular server application engine.
* Manages Angular server applications (including localized ones), handles rendering requests,
* and optionally transforms index HTML before rendering.
*
* @note This class should be instantiated once and used as a singleton across the server-side
* application to ensure consistent handling of rendering requests and resource management.
*
* @developerPreview
*/
export class AngularAppEngine implements AngularServerAppManager {
export class AngularAppEngine {
/**
* Hooks for extending or modifying the behavior of the server application.
* These hooks are used by the Angular CLI when running the development server and
Expand Down Expand Up @@ -120,33 +101,23 @@ export class AngularAppEngine implements AngularServerAppManager {

return entryPoints.get(potentialLocale);
}
}

let angularAppEngine: AngularAppEngine | undefined;
/**
* Retrieves HTTP headers for a request associated with statically generated (SSG) pages,
* based on the URL pathname.
*
* @param request - The incoming request object.
* @returns A `Map` containing the HTTP headers as key-value pairs.
* @note This function should be used exclusively for retrieving headers of SSG pages.
*/
getHeaders(request: Request): Readonly<Map<string, string>> {
if (this.manifest.staticPathsHeaders.size === 0) {
return new Map();
}

/**
* Retrieves an existing `AngularAppEngine` instance or creates a new one if none exists.
*
* This method ensures that only a single instance of `AngularAppEngine` is created and reused across
* the application lifecycle, providing efficient resource management. If the instance does not exist,
* it will be instantiated upon the first call.
*
* @developerPreview
* @returns The existing or newly created instance of `AngularAppEngine`.
*/
export function getOrCreateAngularAppEngine(): AngularServerAppManager {
return (angularAppEngine ??= new AngularAppEngine());
}
const { pathname } = stripIndexHtmlFromURL(new URL(request.url));
const headers = this.manifest.staticPathsHeaders.get(pathname);

/**
* Destroys the current `AngularAppEngine` instance, releasing any associated resources.
*
* This method resets the reference to the `AngularAppEngine` instance to `undefined`, allowing
* a new instance to be created on the next call to `getOrCreateAngularAppEngine()`. It is typically
* used when reinitializing the server environment or refreshing the application state is necessary.
*
* @developerPreview
*/
export function destroyAngularAppEngine(): void {
angularAppEngine = undefined;
return new Map(headers);
}
}
12 changes: 12 additions & 0 deletions packages/angular/ssr/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ export interface AngularAppEngineManifest {
* This is used to determine the root path of the application.
*/
readonly basePath: string;

/**
* A map that associates static paths with their corresponding HTTP headers.
* Each entry in the map consists of:
* - `key`: The static path as a string.
* - `value`: An array of tuples, where each tuple contains:
* - `headerName`: The name of the HTTP header.
* - `headerValue`: The value of the HTTP header.
*/
readonly staticPathsHeaders: Readonly<
Map<string, readonly [headerName: string, headerValue: string][]>
>;
}

/**
Expand Down
146 changes: 146 additions & 0 deletions packages/angular/ssr/src/routes/route-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { EnvironmentProviders, InjectionToken, makeEnvironmentProviders } from '@angular/core';

/**
* Different rendering modes for server routes.
* @developerPreview
*/
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. */
SSR,

/** Client-Side Rendering (CSR) mode, where content is rendered on the client side in the browser. */
CSR,

/** Static Site Generation (SSG) mode, where content is pre-rendered at build time and served as static files. */
SSG,
}

/**
* Fallback strategies for Static Site Generation (SSG) routes.
* @developerPreview
*/
export enum SSGFallback {
/** Use Server-Side Rendering (SSR) as the fallback for this route. */
SSR,

/** Use Client-Side Rendering (CSR) as the fallback for this route. */
CSR,

/** No fallback; Angular will not handle the response if the path is not pre-rendered. */
None,
}

/**
* Common interface for server routes, providing shared properties.
*/
export interface ServerRouteCommon {
/** The path associated with this route. */
path: string;

/** Optional additional headers to include in the response for this route. */
headers?: Record<string, string>;

/** Optional status code to return for this route. */
status?: number;
}

/**
* A server route that uses AppShell rendering mode.
*/
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.
*/
export interface ServerRouteCSR extends ServerRouteCommon {
/** Specifies that the route uses Client-Side Rendering (CSR) mode. */
renderMode: RenderMode.CSR;
}

/**
* A server route that uses Static Site Generation (SSG) mode.
*/
export interface ServerRouteSSG extends Omit<ServerRouteCommon, 'status'> {
/** Specifies that the route uses Static Site Generation (SSG) mode. */
renderMode: RenderMode.SSG;

/**
* Optional fallback strategy to use if the SSG path is not pre-rendered.
* Defaults to `SSGFallback.SSR` if not provided.
*/
fallback?: SSGFallback;
/**
* A function that returns a Promise resolving to an array of objects, each representing a route path with URL parameters.
* This function runs in the injector context, allowing access to Angular services and dependencies.
*
* @returns A Promise resolving to an array where each element is an object with string keys (representing URL parameter names)
* and string values (representing the corresponding values for those parameters in the route path).
*
* @example
* ```typescript
* export const serverRouteConfig: ServerRoutes[] = [
* {
* path: '/product/:id',
* remderMode: RenderMode.SSG,
* async getPrerenderPaths() {
* const productService = inject(ProductService);
* const ids = await productService.getIds(); // Assuming this returns ['1', '2', '3']
*
* return ids.map(id => ({ id })); // Generates paths like: [{ id: '1' }, { id: '2' }, { id: '3' }]
* },
* },
* ];
* ```
*/
getPrerenderPaths?: () => Promise<Record<string, string>[]>;
}

/**
* A server route that uses Server-Side Rendering (SSR) mode.
*/
export interface ServerRouteSSR extends ServerRouteCommon {
/** Specifies that the route uses Server-Side Rendering (SSR) mode. */
renderMode: RenderMode.SSR;
}

/**
* Server route configuration.
* @developerPreview
*/
export type ServerRoute = ServerRouteAppShell | ServerRouteCSR | ServerRouteSSG | ServerRouteSSR;

/**
* Token for providing the server routes configuration.
* @internal
*/
export const SERVER_ROUTES_CONFIG = new InjectionToken<ServerRoute[]>('SERVER_ROUTES_CONFIG');

/**
* Configures the necessary providers for server routes configuration.
*
* @param routes - An array of server routes to be provided.
* @returns An `EnvironmentProviders` object that contains the server routes configuration.
* @developerPreview
*/
export function provideServerRoutesConfig(routes: ServerRoute[]): EnvironmentProviders {
return makeEnvironmentProviders([
{
provide: SERVER_ROUTES_CONFIG,
useValue: routes,
},
]);
}
Loading