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
3 changes: 1 addition & 2 deletions goldens/public-api/angular/ssr/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import { EnvironmentProviders } from '@angular/core';

// @public
export class AngularAppEngine {
getPrerenderHeaders(request: Request): ReadonlyMap<string, string>;
render(request: Request, requestContext?: unknown): Promise<Response | null>;
handle(request: Request, requestContext?: unknown): Promise<Response | null>;
static ɵhooks: Hooks;
}

Expand Down
3 changes: 1 addition & 2 deletions goldens/public-api/angular/ssr/node/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import { Type } from '@angular/core';

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

// @public
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ export async function executePostBundleSteps(
switch (metadata.renderMode) {
case RouteRenderMode.Prerender:
case /* Legacy building mode */ undefined: {
if (!metadata.redirectTo || outputMode === OutputMode.Static) {
if (!metadata.redirectTo) {
serializableRouteTreeNodeForManifest.push(metadata);
prerenderedRoutes[metadata.route] = { headers: metadata.headers };
}
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function createAngularSsrInternalMiddleware(
const webReq = new Request(createWebRequestFromNodeRequest(req), {
signal: AbortSignal.timeout(30_000),
});
const webRes = await angularServerApp.render(webReq);
const webRes = await angularServerApp.handle(webReq);
if (!webRes) {
return next();
}
Expand Down
10 changes: 3 additions & 7 deletions packages/angular/build/src/utils/server-rendering/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { extname } from 'node:path';
import {
INDEX_HTML_CSR,
INDEX_HTML_SERVER,
NormalizedApplicationBuildOptions,
getLocaleBaseHref,
} from '../../builders/application/options';
Expand Down Expand Up @@ -135,11 +134,8 @@ export function generateAngularServerAppManifest(
): string {
const serverAssetsContent: string[] = [];
for (const file of [...additionalHtmlOutputFiles.values(), ...outputFiles]) {
if (
file.path === INDEX_HTML_SERVER ||
file.path === INDEX_HTML_CSR ||
(inlineCriticalCss && file.path.endsWith('.css'))
) {
const extension = extname(file.path);
if (extension === '.html' || (inlineCriticalCss && extension === '.css')) {
serverAssetsContent.push(`['${file.path}', async () => \`${escapeUnsafeChars(file.text)}\`]`);
}
}
Expand Down
50 changes: 10 additions & 40 deletions packages/angular/ssr/node/src/app-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,49 +24,19 @@ export class AngularNodeAppEngine {
private readonly angularAppEngine = new AngularAppEngine();

/**
* Renders an HTTP response based on the incoming request using the Angular server application.
* Handles an incoming HTTP request by serving prerendered content, performing server-side rendering,
* or delivering a static file for client-side rendered routes based on the `RenderMode` setting.
*
* 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`.
* @param request - The HTTP request to handle.
* @param requestContext - Optional context for rendering, such as metadata associated with the request.
* @returns A promise that resolves to the resulting HTTP response object, or `null` if no matching Angular route is found.
*
* 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.
* @note A request to `https://www.example.com/page/index.html` will serve or render the Angular route
* corresponding to `https://www.example.com/page`.
*/
render(request: IncomingMessage, requestContext?: unknown): Promise<Response | null> {
return this.angularAppEngine.render(createWebRequestFromNodeRequest(request), requestContext);
}
async handle(request: IncomingMessage, requestContext?: unknown): Promise<Response | null> {
const webRequest = createWebRequestFromNodeRequest(request);

/**
* 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.getPrerenderHeaders(res.req);
*
* // Apply the retrieved headers to the response
* for (const [key, value] of headers) {
* res.setHeader(key, value);
* }
* }
}));
* ```
*/
getPrerenderHeaders(request: IncomingMessage): ReadonlyMap<string, string> {
return this.angularAppEngine.getPrerenderHeaders(createWebRequestFromNodeRequest(request));
return this.angularAppEngine.handle(webRequest, requestContext);
}
}
9 changes: 7 additions & 2 deletions packages/angular/ssr/node/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ function createRequestHeaders(nodeHeaders: IncomingHttpHeaders): Headers {
* @returns A `URL` object representing the request URL.
*/
function createRequestUrl(nodeRequest: IncomingMessage): URL {
const { headers, socket, url = '' } = nodeRequest;
const {
headers,
socket,
url = '',
originalUrl,
} = nodeRequest as IncomingMessage & { originalUrl?: string };
const protocol =
headers['x-forwarded-proto'] ?? ('encrypted' in socket && socket.encrypted ? 'https' : 'http');
const hostname = headers['x-forwarded-host'] ?? headers.host ?? headers[':authority'];
Expand All @@ -71,5 +76,5 @@ function createRequestUrl(nodeRequest: IncomingMessage): URL {
hostnameWithPort += `:${port}`;
}

return new URL(url, `${protocol}://${hostnameWithPort}`);
return new URL(originalUrl ?? url, `${protocol}://${hostnameWithPort}`);
}
54 changes: 24 additions & 30 deletions packages/angular/ssr/src/app-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,34 @@ export class AngularAppEngine {
private readonly entryPointsCache = new Map<string, Promise<EntryPointExports>>();

/**
* Renders a response for the given HTTP request using the server application.
* Handles an incoming HTTP request by serving prerendered content, performing server-side rendering,
* or delivering a static file for client-side rendered routes based on the `RenderMode` setting.
*
* This method processes the request, determines the appropriate route and rendering context,
* and returns an HTTP response.
* @param request - The HTTP request to handle.
* @param requestContext - Optional context for rendering, such as metadata associated with the request.
* @returns A promise that resolves to the resulting HTTP response object, or `null` if no matching Angular route is found.
*
* 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
* @note A request to `https://www.example.com/page/index.html` will serve or render the Angular route
* corresponding to `https://www.example.com/page`.
*/
async handle(request: Request, requestContext?: unknown): Promise<Response | null> {
const serverApp = await this.getAngularServerAppForRequest(request);

return serverApp ? serverApp.handle(request, requestContext) : null;
}

/**
* Retrieves the Angular server application instance for a given request.
*
* This method checks if the request URL corresponds to an Angular application entry point.
* If so, it initializes or retrieves an instance of the Angular server application for that entry point.
* Requests that resemble file requests (except for `/index.html`) are skipped.
*
* @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.
* @param request - The incoming HTTP request object.
* @returns A promise that resolves to an `AngularServerApp` instance if a valid entry point is found,
* or `null` if no entry point matches the request URL.
*/
async render(request: Request, requestContext?: unknown): Promise<Response | null> {
private async getAngularServerAppForRequest(request: Request): Promise<AngularServerApp | null> {
// Skip if the request looks like a file but not `/index.html`.
const url = new URL(request.url);
const entryPoint = await this.getEntryPointExportsForUrl(url);
Expand All @@ -82,26 +95,7 @@ export class AngularAppEngine {
const serverApp = getOrCreateAngularServerApp() as AngularServerApp;
serverApp.hooks = this.hooks;

return serverApp.render(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.
*/
getPrerenderHeaders(request: Request): ReadonlyMap<string, string> {
if (this.manifest.staticPathsHeaders.size === 0) {
return new Map();
}

const { pathname } = stripIndexHtmlFromURL(new URL(request.url));
const headers = this.manifest.staticPathsHeaders.get(stripTrailingSlash(pathname));

return new Map(headers);
return serverApp;
}

/**
Expand Down
Loading