Skip to content

Commit 92209dd

Browse files
committed
feat(@angular/ssr): add createRequestHandler and createNodeRequestHandler utilities
Introduced the `createRequestHandler` and `createNodeRequestHandler` utilities to expose middleware functions from the `server.ts` entry point for use with Vite. This provides flexibility in integrating different server frameworks, including Express, Hono, and Fastify, with Angular SSR. Examples: **Express** ```ts export default createNodeRequestHandler(app); ``` **Nest.js** ```ts const app = await NestFactory.create(AppModule); export default createNodeRequestHandler(app); ``` **Hono** ```ts const app = new Hono(); export default createRequestHandler(app.fetch); ``` **Fastify** ```ts export default createNodeRequestHandler(async (req, res) => { await app.ready(); app.server.emit('request', req, res); }); ```
1 parent bbc2901 commit 92209dd

File tree

12 files changed

+326
-38
lines changed

12 files changed

+326
-38
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export class AngularAppEngine {
1313
static ɵhooks: Hooks;
1414
}
1515

16+
// @public
17+
export function createRequestHandler(handler: RequestHandlerFunction): RequestHandlerFunction;
18+
1619
// @public
1720
export enum PrerenderFallback {
1821
Client = 1,

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

+3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export interface CommonEngineRenderOptions {
4343
url?: string;
4444
}
4545

46+
// @public
47+
export function createNodeRequestHandler<T extends RequestHandlerFunction>(handler: T): T;
48+
4649
// @public
4750
export function createWebRequestFromNodeRequest(nodeRequest: IncomingMessage): Request;
4851

packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts

+33-9
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88

99
import type {
1010
AngularAppEngine as SSRAngularAppEngine,
11+
createRequestHandler,
1112
ɵgetOrCreateAngularServerApp as getOrCreateAngularServerApp,
1213
} from '@angular/ssr';
14+
import type { createNodeRequestHandler } from '@angular/ssr/node';
1315
import type { ServerResponse } from 'node:http';
1416
import type { Connect, ViteDevServer } from 'vite';
1517
import { loadEsmModule } from '../../../utils/load-esm';
@@ -29,10 +31,6 @@ export function createAngularSsrInternalMiddleware(
2931
return next();
3032
}
3133

32-
const resolvedUrls = server.resolvedUrls;
33-
const baseUrl = resolvedUrls?.local[0] ?? resolvedUrls?.network[0];
34-
const url = new URL(req.url, baseUrl);
35-
3634
(async () => {
3735
const { writeResponseToNodeResponse, createWebRequestFromNodeRequest } =
3836
await loadEsmModule<typeof import('@angular/ssr/node')>('@angular/ssr/node');
@@ -66,16 +64,19 @@ export function createAngularSsrInternalMiddleware(
6664
};
6765
}
6866

69-
export function createAngularSsrExternalMiddleware(
67+
export async function createAngularSsrExternalMiddleware(
7068
server: ViteDevServer,
7169
indexHtmlTransformer?: (content: string) => Promise<string>,
72-
): Connect.NextHandleFunction {
70+
): Promise<Connect.NextHandleFunction> {
7371
let fallbackWarningShown = false;
7472
let cachedAngularAppEngine: typeof SSRAngularAppEngine | undefined;
7573
let angularSsrInternalMiddleware:
7674
| ReturnType<typeof createAngularSsrInternalMiddleware>
7775
| undefined;
7876

77+
const { createWebRequestFromNodeRequest, writeResponseToNodeResponse } =
78+
await loadEsmModule<typeof import('@angular/ssr/node')>('@angular/ssr/node');
79+
7980
return function angularSsrExternalMiddleware(
8081
req: Connect.IncomingMessage,
8182
res: ServerResponse,
@@ -89,7 +90,7 @@ export function createAngularSsrExternalMiddleware(
8990
AngularAppEngine: typeof SSRAngularAppEngine;
9091
};
9192

92-
if (typeof handler !== 'function' || !('__ng_node_next_handler__' in handler)) {
93+
if (!isSsrNodeRequestHandler(handler) && !isSsrRequestHandler(handler)) {
9394
if (!fallbackWarningShown) {
9495
// eslint-disable-next-line no-console
9596
console.warn(
@@ -104,7 +105,9 @@ export function createAngularSsrExternalMiddleware(
104105
indexHtmlTransformer,
105106
);
106107

107-
return angularSsrInternalMiddleware(req, res, next);
108+
angularSsrInternalMiddleware(req, res, next);
109+
110+
return;
108111
}
109112

110113
if (cachedAngularAppEngine !== AngularAppEngine) {
@@ -118,7 +121,28 @@ export function createAngularSsrExternalMiddleware(
118121
}
119122

120123
// Forward the request to the middleware in server.ts
121-
return (handler as unknown as Connect.NextHandleFunction)(req, res, next);
124+
if (isSsrNodeRequestHandler(handler)) {
125+
await handler(req, res, next);
126+
} else {
127+
const webRes = await handler(createWebRequestFromNodeRequest(req));
128+
if (!webRes) {
129+
next();
130+
131+
return;
132+
}
133+
134+
await writeResponseToNodeResponse(webRes, res);
135+
}
122136
})().catch(next);
123137
};
124138
}
139+
140+
function isSsrNodeRequestHandler(
141+
value: unknown,
142+
): value is ReturnType<typeof createNodeRequestHandler> {
143+
return typeof value === 'function' && '__ng_node_request_handler__' in value;
144+
}
145+
146+
function isSsrRequestHandler(value: unknown): value is ReturnType<typeof createRequestHandler> {
147+
return typeof value === 'function' && '__ng_request_handler__' in value;
148+
}

packages/angular/build/src/tools/vite/setup-middlewares-plugin.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,12 @@ export function createAngularSetupMiddlewaresPlugin(
7777

7878
// Returning a function, installs middleware after the main transform middleware but
7979
// before the built-in HTML middleware
80-
return () => {
80+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
81+
return async () => {
8182
if (ssrMode === ServerSsrMode.ExternalSsrMiddleware) {
82-
server.middlewares.use(createAngularSsrExternalMiddleware(server, indexHtmlTransformer));
83+
server.middlewares.use(
84+
await createAngularSsrExternalMiddleware(server, indexHtmlTransformer),
85+
);
8386

8487
return;
8588
}

packages/angular/ssr/node/public_api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export {
1414

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

17+
export { createNodeRequestHandler } from './src/handler';
1718
export { writeResponseToNodeResponse } from './src/response';
1819
export { createWebRequestFromNodeRequest } from './src/request';
1920
export { isMainModule } from './src/module';
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import type { IncomingMessage, ServerResponse } from 'node:http';
10+
11+
/**
12+
* Represents a middleware function for handling HTTP requests in a Node.js environment.
13+
*
14+
* @param req - The incoming HTTP request object.
15+
* @param res - The outgoing HTTP response object.
16+
* @param next - A callback function that signals the completion of the middleware or forwards the error if provided.
17+
*
18+
* @returns A Promise that resolves to void or simply void. The handler can be asynchronous.
19+
*/
20+
type RequestHandlerFunction = (
21+
req: IncomingMessage,
22+
res: ServerResponse,
23+
next: (err?: unknown) => void,
24+
) => Promise<void> | void;
25+
26+
/**
27+
* Attaches metadata to the handler function to mark it as a special handler for Node.js environments.
28+
*
29+
* @typeParam T - The type of the handler function.
30+
* @param handler - The handler function to be defined and annotated.
31+
* @returns The same handler function passed as an argument, with metadata attached.
32+
*
33+
* @example
34+
* Usage in an Express application:
35+
* ```ts
36+
* const app = express();
37+
* export default createNodeRequestHandler(app);
38+
* ```
39+
*
40+
* @example
41+
* Usage in a Hono application:
42+
* ```ts
43+
* const app = new Hono();
44+
* export default createNodeRequestHandler(async (req, res, next) => {
45+
* try {
46+
* const webRes = await app.fetch(createWebRequestFromNodeRequest(req));
47+
* if (webRes) {
48+
* await writeResponseToNodeResponse(webRes, res);
49+
* } else {
50+
* next();
51+
* }
52+
* } catch (error) {
53+
* next(error);
54+
* }
55+
* }));
56+
* ```
57+
*
58+
* @example
59+
* Usage in a Fastify application:
60+
* ```ts
61+
* const app = Fastify();
62+
* export default createNodeRequestHandler(async (req, res) => {
63+
* await app.ready();
64+
* app.server.emit('request', req, res);
65+
* res.send('Hello from Fastify with Node Next Handler!');
66+
* }));
67+
* ```
68+
* @developerPreview
69+
*/
70+
export function createNodeRequestHandler<T extends RequestHandlerFunction>(handler: T): T {
71+
(handler as T & { __ng_node_request_handler__?: boolean })['__ng_node_request_handler__'] = true;
72+
73+
return handler;
74+
}

packages/angular/ssr/public_api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
export * from './private_export';
1010

1111
export { AngularAppEngine } from './src/app-engine';
12+
export { createRequestHandler } from './src/handler';
1213

1314
export {
1415
type PrerenderFallback,

packages/angular/ssr/src/handler.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
/**
10+
* Function for handling HTTP requests in a web environment.
11+
*
12+
* @param request - The incoming HTTP request object.
13+
* @returns A Promise resolving to a `Response` object, `null`, or directly a `Response`,
14+
* supporting both synchronous and asynchronous handling.
15+
*/
16+
type RequestHandlerFunction = (request: Request) => Promise<Response | null> | null | Response;
17+
18+
/**
19+
* Annotates a request handler function with metadata, marking it as a special
20+
* handler.
21+
*
22+
* @param handler - The request handler function to be annotated.
23+
* @returns The same handler function passed in, with metadata attached.
24+
*
25+
* @example
26+
* Example usage in a Hono application:
27+
* ```ts
28+
* const app = new Hono();
29+
* export default createRequestHandler(app.fetch);
30+
* ```
31+
*
32+
* @example
33+
* Example usage in a H3 application:
34+
* ```ts
35+
* const app = createApp();
36+
* const handler = toWebHandler(app);
37+
* export default createRequestHandler(handler);
38+
* ```
39+
* @developerPreview
40+
*/
41+
export function createRequestHandler(handler: RequestHandlerFunction): RequestHandlerFunction {
42+
(handler as RequestHandlerFunction & { __ng_request_handler__?: boolean })[
43+
'__ng_request_handler__'
44+
] = true;
45+
46+
return handler;
47+
}

tests/legacy-cli/e2e/tests/vite/ssr-entry-express.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import assert from 'node:assert';
2+
import { setTimeout } from 'node:timers/promises';
23
import { replaceInFile, writeMultipleFiles } from '../../utils/fs';
34
import { ng, silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process';
45
import { installWorkspacePackages, uninstallPackage } from '../../utils/packages';
@@ -59,7 +60,7 @@ export default async function () {
5960
];
6061
`,
6162
'server.ts': `
62-
import { AngularNodeAppEngine, writeResponseToNodeResponse, isMainModule, defineNodeNextHandler } from '@angular/ssr/node';
63+
import { AngularNodeAppEngine, writeResponseToNodeResponse, isMainModule, createNodeRequestHandler } from '@angular/ssr/node';
6364
import express from 'express';
6465
import { fileURLToPath } from 'node:url';
6566
import { dirname, resolve } from 'node:path';
@@ -94,7 +95,7 @@ export default async function () {
9495
});
9596
}
9697
97-
export default defineNodeNextHandler(server);
98+
export default createNodeRequestHandler(server);
9899
`,
99100
});
100101

@@ -121,7 +122,7 @@ export default async function () {
121122
await validateResponse('/api/test', /bar/);
122123
await validateResponse('/home', /yay home works/);
123124

124-
async function validateResponse(pathname: string, match: RegExp) {
125+
async function validateResponse(pathname: string, match: RegExp): Promise<void> {
125126
const response = await fetch(new URL(pathname, `http://localhost:${port}`));
126127
const text = await response.text();
127128
assert.match(text, match);
@@ -133,9 +134,11 @@ async function modifyFileAndWaitUntilUpdated(
133134
filePath: string,
134135
searchValue: string,
135136
replaceValue: string,
136-
) {
137+
): Promise<void> {
137138
await Promise.all([
138-
waitForAnyProcessOutputToMatch(/Application bundle generation complete./),
139+
waitForAnyProcessOutputToMatch(/Page reload sent to client/),
139140
replaceInFile(filePath, searchValue, replaceValue),
140141
]);
142+
143+
await setTimeout(200);
141144
}

tests/legacy-cli/e2e/tests/vite/ssr-entry-fastify.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import assert from 'node:assert';
2+
import { setTimeout } from 'node:timers/promises';
23
import { replaceInFile, writeMultipleFiles } from '../../utils/fs';
34
import { ng, silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process';
45
import { installPackage, installWorkspacePackages, uninstallPackage } from '../../utils/packages';
@@ -60,7 +61,7 @@ export default async function () {
6061
];
6162
`,
6263
'server.ts': `
63-
import { AngularNodeAppEngine, writeResponseToNodeResponse, isMainModule, defineNodeNextHandler } from '@angular/ssr/node';
64+
import { AngularNodeAppEngine, writeResponseToNodeResponse, isMainModule, createNodeRequestHandler } from '@angular/ssr/node';
6465
import fastify from 'fastify';
6566
6667
export function app() {
@@ -91,7 +92,7 @@ export default async function () {
9192
});
9293
}
9394
94-
export default defineNodeNextHandler(async (req, res) => {
95+
export default createNodeRequestHandler(async (req, res) => {
9596
await server.ready();
9697
server.server.emit('request', req, res);
9798
});
@@ -121,7 +122,7 @@ export default async function () {
121122
await validateResponse('/api/test', /bar/);
122123
await validateResponse('/home', /yay home works/);
123124

124-
async function validateResponse(pathname: string, match: RegExp) {
125+
async function validateResponse(pathname: string, match: RegExp): Promise<void> {
125126
const response = await fetch(new URL(pathname, `http://localhost:${port}`));
126127
const text = await response.text();
127128
assert.match(text, match);
@@ -133,9 +134,11 @@ async function modifyFileAndWaitUntilUpdated(
133134
filePath: string,
134135
searchValue: string,
135136
replaceValue: string,
136-
) {
137+
): Promise<void> {
137138
await Promise.all([
138-
waitForAnyProcessOutputToMatch(/Application bundle generation complete./),
139+
waitForAnyProcessOutputToMatch(/Page reload sent to client/),
139140
replaceInFile(filePath, searchValue, replaceValue),
140141
]);
142+
143+
await setTimeout(200);
141144
}

0 commit comments

Comments
 (0)