Skip to content

Commit 3ed259c

Browse files
committed
feat(@angular/ssr): add defineNodeNextHandler utility
Introduced the `defineNodeNextHandler` utility 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 defineNodeNextHandler(app); ``` **Hono** ```ts export default defineNodeNextHandler(async (req, res, next) => { try { const webRes = await app.fetch(createWebRequestFromNodeRequest(req)); if (webRes) { await writeResponseToNodeResponse(webRes, res); } else { next(); } } catch (error) { next(error); } }); ``` **Fastify** ```ts export default defineNodeNextHandler(async (req, res) => { await app.ready(); app.server.emit('request', req, res); }); ```
1 parent 6acfaf1 commit 3ed259c

File tree

7 files changed

+101
-11
lines changed

7 files changed

+101
-11
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export interface CommonEngineRenderOptions {
4646
// @public
4747
export function createWebRequestFromNodeRequest(nodeRequest: IncomingMessage): Request;
4848

49+
// @public
50+
export function defineNodeNextHandler<T extends NextHandleFunction>(handler: T): T;
51+
4952
// @public
5053
export function isMainModule(url: string): boolean;
5154

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ export function createAngularSsrExternalMiddleware(
104104
indexHtmlTransformer,
105105
);
106106

107-
return angularSsrInternalMiddleware(req, res, next);
107+
angularSsrInternalMiddleware(req, res, next);
108+
109+
return;
108110
}
109111

110112
if (cachedAngularAppEngine !== AngularAppEngine) {
@@ -118,7 +120,8 @@ export function createAngularSsrExternalMiddleware(
118120
}
119121

120122
// Forward the request to the middleware in server.ts
121-
return (handler as unknown as Connect.NextHandleFunction)(req, res, next);
123+
// eslint-disable-next-line @typescript-eslint/await-thenable
124+
await (handler as Function as Connect.NextHandleFunction)(req, res, next);
122125
})().catch(next);
123126
};
124127
}

packages/angular/ssr/node/public_api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export {
1414

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

17+
export { defineNodeNextHandler } from './src/handler';
1718
export { writeResponseToNodeResponse } from './src/response';
1819
export { createWebRequestFromNodeRequest } from './src/request';
1920
export { isMainModule } from './src/module';
Lines changed: 74 additions & 0 deletions
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 NextHandleFunction = (
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 defineNodeNextHandler(app);
38+
* ```
39+
*
40+
* @example
41+
* Usage in a Hono application:
42+
* ```ts
43+
* const app = new Hono();
44+
* export default defineNodeNextHandler(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 defineNodeNextHandler(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 defineNodeNextHandler<T extends NextHandleFunction>(handler: T): T {
71+
(handler as T & { __ng_node_next_handler__?: boolean })['__ng_node_next_handler__'] = true;
72+
73+
return handler;
74+
}

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

Lines changed: 6 additions & 3 deletions
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';
@@ -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(100);
141144
}

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

Lines changed: 6 additions & 3 deletions
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';
@@ -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(100);
141144
}

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

Lines changed: 6 additions & 3 deletions
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';
@@ -116,7 +117,7 @@ export default async function () {
116117
await validateResponse('/api/test', /bar/);
117118
await validateResponse('/home', /yay home works/);
118119

119-
async function validateResponse(pathname: string, match: RegExp) {
120+
async function validateResponse(pathname: string, match: RegExp): Promise<void> {
120121
const response = await fetch(new URL(pathname, `http://localhost:${port}`));
121122
const text = await response.text();
122123
assert.match(text, match);
@@ -128,9 +129,11 @@ async function modifyFileAndWaitUntilUpdated(
128129
filePath: string,
129130
searchValue: string,
130131
replaceValue: string,
131-
) {
132+
): Promise<void> {
132133
await Promise.all([
133-
waitForAnyProcessOutputToMatch(/Application bundle generation complete./),
134+
waitForAnyProcessOutputToMatch(/Page reload sent to client/),
134135
replaceInFile(filePath, searchValue, replaceValue),
135136
]);
137+
138+
await setTimeout(100);
136139
}

0 commit comments

Comments
 (0)