Skip to content

Commit 280ebbd

Browse files
committedNov 7, 2024·
fix(@angular/ssr): support for HTTP/2 request/response handling
This commit introduces support for handling HTTP/2 requests and responses in the `@angular/ssr` package. Closes #28807 (cherry picked from commit ffbe9b9)
1 parent 59500d0 commit 280ebbd

File tree

13 files changed

+383
-47
lines changed

13 files changed

+383
-47
lines changed
 

Diff for: ‎goldens/public-api/angular/ssr/node/index.api.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
```ts
66

77
import { ApplicationRef } from '@angular/core';
8+
import type { Http2ServerRequest } from 'node:http2';
9+
import type { Http2ServerResponse } from 'node:http2';
810
import type { IncomingMessage } from 'node:http';
911
import type { ServerResponse } from 'node:http';
1012
import { StaticProvider } from '@angular/core';
1113
import { Type } from '@angular/core';
1214

1315
// @public
1416
export class AngularNodeAppEngine {
15-
handle(request: IncomingMessage, requestContext?: unknown): Promise<Response | null>;
17+
handle(request: IncomingMessage | Http2ServerRequest, requestContext?: unknown): Promise<Response | null>;
1618
}
1719

1820
// @public
@@ -46,7 +48,7 @@ export interface CommonEngineRenderOptions {
4648
export function createNodeRequestHandler<T extends NodeRequestHandlerFunction>(handler: T): T;
4749

4850
// @public
49-
export function createWebRequestFromNodeRequest(nodeRequest: IncomingMessage): Request;
51+
export function createWebRequestFromNodeRequest(nodeRequest: IncomingMessage | Http2ServerRequest): Request;
5052

5153
// @public
5254
export function isMainModule(url: string): boolean;
@@ -55,7 +57,7 @@ export function isMainModule(url: string): boolean;
5557
export type NodeRequestHandlerFunction = (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => Promise<void> | void;
5658

5759
// @public
58-
export function writeResponseToNodeResponse(source: Response, destination: ServerResponse): Promise<void>;
60+
export function writeResponseToNodeResponse(source: Response, destination: ServerResponse | Http2ServerResponse<Http2ServerRequest>): Promise<void>;
5961

6062
// (No @packageDocumentation comment for this package)
6163

Diff for: ‎packages/angular/ssr/node/src/app-engine.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import { AngularAppEngine } from '@angular/ssr';
1010
import type { IncomingMessage } from 'node:http';
11+
import type { Http2ServerRequest } from 'node:http2';
1112
import { createWebRequestFromNodeRequest } from './request';
1213

1314
/**
@@ -27,14 +28,20 @@ export class AngularNodeAppEngine {
2728
* Handles an incoming HTTP request by serving prerendered content, performing server-side rendering,
2829
* or delivering a static file for client-side rendered routes based on the `RenderMode` setting.
2930
*
30-
* @param request - The HTTP request to handle.
31+
* This method adapts Node.js's `IncomingMessage` or `Http2ServerRequest`
32+
* to a format compatible with the `AngularAppEngine` and delegates the handling logic to it.
33+
*
34+
* @param request - The incoming HTTP request (`IncomingMessage` or `Http2ServerRequest`).
3135
* @param requestContext - Optional context for rendering, such as metadata associated with the request.
3236
* @returns A promise that resolves to the resulting HTTP response object, or `null` if no matching Angular route is found.
3337
*
3438
* @remarks A request to `https://www.example.com/page/index.html` will serve or render the Angular route
3539
* corresponding to `https://www.example.com/page`.
3640
*/
37-
async handle(request: IncomingMessage, requestContext?: unknown): Promise<Response | null> {
41+
async handle(
42+
request: IncomingMessage | Http2ServerRequest,
43+
requestContext?: unknown,
44+
): Promise<Response | null> {
3845
const webRequest = createWebRequestFromNodeRequest(request);
3946

4047
return this.angularAppEngine.handle(webRequest, requestContext);

Diff for: ‎packages/angular/ssr/node/src/request.ts

+25-5
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,31 @@
77
*/
88

99
import type { IncomingHttpHeaders, IncomingMessage } from 'node:http';
10+
import type { Http2ServerRequest } from 'node:http2';
1011

1112
/**
12-
* Converts a Node.js `IncomingMessage` into a Web Standard `Request`.
13+
* A set containing all the pseudo-headers defined in the HTTP/2 specification.
1314
*
14-
* @param nodeRequest - The Node.js `IncomingMessage` object to convert.
15+
* This set can be used to filter out pseudo-headers from a list of headers,
16+
* as they are not allowed to be set directly using the `Node.js` Undici API or
17+
* the web `Headers` API.
18+
*/
19+
const HTTP2_PSEUDO_HEADERS = new Set([':method', ':scheme', ':authority', ':path', ':status']);
20+
21+
/**
22+
* Converts a Node.js `IncomingMessage` or `Http2ServerRequest` into a
23+
* Web Standard `Request` object.
24+
*
25+
* This function adapts the Node.js request objects to a format that can
26+
* be used by web platform APIs.
27+
*
28+
* @param nodeRequest - The Node.js request object (`IncomingMessage` or `Http2ServerRequest`) to convert.
1529
* @returns A Web Standard `Request` object.
1630
* @developerPreview
1731
*/
18-
export function createWebRequestFromNodeRequest(nodeRequest: IncomingMessage): Request {
32+
export function createWebRequestFromNodeRequest(
33+
nodeRequest: IncomingMessage | Http2ServerRequest,
34+
): Request {
1935
const { headers, method = 'GET' } = nodeRequest;
2036
const withBody = method !== 'GET' && method !== 'HEAD';
2137

@@ -37,6 +53,10 @@ function createRequestHeaders(nodeHeaders: IncomingHttpHeaders): Headers {
3753
const headers = new Headers();
3854

3955
for (const [name, value] of Object.entries(nodeHeaders)) {
56+
if (HTTP2_PSEUDO_HEADERS.has(name)) {
57+
continue;
58+
}
59+
4060
if (typeof value === 'string') {
4161
headers.append(name, value);
4262
} else if (Array.isArray(value)) {
@@ -52,10 +72,10 @@ function createRequestHeaders(nodeHeaders: IncomingHttpHeaders): Headers {
5272
/**
5373
* Creates a `URL` object from a Node.js `IncomingMessage`, taking into account the protocol, host, and port.
5474
*
55-
* @param nodeRequest - The Node.js `IncomingMessage` object to extract URL information from.
75+
* @param nodeRequest - The Node.js `IncomingMessage` or `Http2ServerRequest` object to extract URL information from.
5676
* @returns A `URL` object representing the request URL.
5777
*/
58-
function createRequestUrl(nodeRequest: IncomingMessage): URL {
78+
function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerRequest): URL {
5979
const {
6080
headers,
6181
socket,

Diff for: ‎packages/angular/ssr/node/src/response.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,23 @@
77
*/
88

99
import type { ServerResponse } from 'node:http';
10+
import type { Http2ServerRequest, Http2ServerResponse } from 'node:http2';
1011

1112
/**
12-
* Streams a web-standard `Response` into a Node.js `ServerResponse`.
13+
* Streams a web-standard `Response` into a Node.js `ServerResponse`
14+
* or `Http2ServerResponse`.
15+
*
16+
* This function adapts the web `Response` object to write its content
17+
* to a Node.js response object, handling both HTTP/1.1 and HTTP/2.
1318
*
1419
* @param source - The web-standard `Response` object to stream from.
15-
* @param destination - The Node.js `ServerResponse` object to stream into.
20+
* @param destination - The Node.js response object (`ServerResponse` or `Http2ServerResponse`) to stream into.
1621
* @returns A promise that resolves once the streaming operation is complete.
1722
* @developerPreview
1823
*/
1924
export async function writeResponseToNodeResponse(
2025
source: Response,
21-
destination: ServerResponse,
26+
destination: ServerResponse | Http2ServerResponse<Http2ServerRequest>,
2227
): Promise<void> {
2328
const { status, headers, body } = source;
2429
destination.statusCode = status;
@@ -66,7 +71,7 @@ export async function writeResponseToNodeResponse(
6671
break;
6772
}
6873

69-
destination.write(value);
74+
(destination as ServerResponse).write(value);
7075
}
7176
} catch {
7277
destination.end('Internal server error.');

Diff for: ‎packages/angular/ssr/node/test/request_spec.ts renamed to ‎packages/angular/ssr/node/test/request_http1_spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { IncomingMessage, Server, ServerResponse, createServer, request } from '
1010
import { AddressInfo } from 'node:net';
1111
import { createWebRequestFromNodeRequest } from '../src/request';
1212

13-
describe('createWebRequestFromNodeRequest', () => {
13+
describe('createWebRequestFromNodeRequest (HTTP/1.1)', () => {
1414
let server: Server;
1515
let port: number;
1616

Diff for: ‎packages/angular/ssr/node/test/request_http2_spec.ts

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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 {
10+
ClientHttp2Session,
11+
Http2Server,
12+
Http2ServerRequest,
13+
Http2ServerResponse,
14+
connect,
15+
createServer,
16+
} from 'node:http2';
17+
import { AddressInfo } from 'node:net';
18+
import { createWebRequestFromNodeRequest } from '../src/request';
19+
20+
describe('createWebRequestFromNodeRequest (HTTP/2)', () => {
21+
let server: Http2Server;
22+
let port: number;
23+
let client: ClientHttp2Session;
24+
25+
function extractNodeRequest(makeRequest: () => void): Promise<Http2ServerRequest> {
26+
const nodeRequest = getNodeRequest();
27+
makeRequest();
28+
29+
return nodeRequest;
30+
}
31+
32+
async function getNodeRequest(): Promise<Http2ServerRequest> {
33+
const { req, res } = await new Promise<{
34+
req: Http2ServerRequest;
35+
res: Http2ServerResponse<Http2ServerRequest>;
36+
}>((resolve) => {
37+
server.once('request', (req, res) => resolve({ req, res }));
38+
});
39+
40+
res.end();
41+
42+
return req;
43+
}
44+
45+
beforeAll((done) => {
46+
server = createServer();
47+
server.listen(0, () => {
48+
port = (server.address() as AddressInfo).port;
49+
done();
50+
client = connect(`http://localhost:${port}`);
51+
});
52+
});
53+
54+
afterAll((done) => {
55+
client.close();
56+
server.close(done);
57+
});
58+
59+
describe('GET Handling', () => {
60+
it('should correctly handle a basic GET request', async () => {
61+
const nodeRequest = await extractNodeRequest(() => {
62+
client
63+
.request({
64+
':path': '/basic-get',
65+
':method': 'GET',
66+
})
67+
.end();
68+
});
69+
70+
const webRequest = createWebRequestFromNodeRequest(nodeRequest);
71+
expect(webRequest.method).toBe('GET');
72+
expect(webRequest.url).toBe(`http://localhost:${port}/basic-get`);
73+
});
74+
75+
it('should correctly handle GET request with query parameters', async () => {
76+
const nodeRequest = await extractNodeRequest(() => {
77+
client
78+
.request({
79+
':scheme': 'http',
80+
':path': '/search?query=hello&page=2',
81+
':method': 'POST',
82+
})
83+
.end();
84+
});
85+
86+
const webRequest = createWebRequestFromNodeRequest(nodeRequest);
87+
expect(webRequest.method).toBe('POST');
88+
expect(webRequest.url).toBe(`http://localhost:${port}/search?query=hello&page=2`);
89+
});
90+
91+
it('should correctly handle GET request with custom headers', async () => {
92+
const nodeRequest = await extractNodeRequest(() => {
93+
client
94+
.request({
95+
':path': '/with-headers',
96+
':method': 'GET',
97+
'X-Custom-Header1': 'value1',
98+
'X-Custom-Header2': 'value2',
99+
})
100+
.end();
101+
});
102+
103+
const webRequest = createWebRequestFromNodeRequest(nodeRequest);
104+
expect(webRequest.method).toBe('GET');
105+
expect(webRequest.url).toBe(`http://localhost:${port}/with-headers`);
106+
expect(webRequest.headers.get('x-custom-header1')).toBe('value1');
107+
expect(webRequest.headers.get('x-custom-header2')).toBe('value2');
108+
});
109+
});
110+
111+
describe('POST Handling', () => {
112+
it('should handle POST request with JSON body and correct response', async () => {
113+
const postData = JSON.stringify({ message: 'Hello from POST' });
114+
const nodeRequest = await extractNodeRequest(() => {
115+
const clientRequest = client.request({
116+
':path': '/post-json',
117+
':method': 'POST',
118+
'Content-Type': 'application/json',
119+
'Content-Length': Buffer.byteLength(postData),
120+
});
121+
clientRequest.write(postData);
122+
clientRequest.end();
123+
});
124+
125+
const webRequest = createWebRequestFromNodeRequest(nodeRequest);
126+
expect(webRequest.method).toBe('POST');
127+
expect(webRequest.url).toBe(`http://localhost:${port}/post-json`);
128+
expect(webRequest.headers.get('content-type')).toBe('application/json');
129+
expect(await webRequest.json()).toEqual({ message: 'Hello from POST' });
130+
});
131+
132+
it('should handle POST request with empty text body', async () => {
133+
const postData = '';
134+
const nodeRequest = await extractNodeRequest(() => {
135+
const clientRequest = client.request({
136+
':path': '/post-text',
137+
':method': 'POST',
138+
'Content-Type': 'text/plain',
139+
'Content-Length': Buffer.byteLength(postData),
140+
});
141+
clientRequest.write(postData);
142+
clientRequest.end();
143+
});
144+
145+
const webRequest = createWebRequestFromNodeRequest(nodeRequest);
146+
expect(webRequest.method).toBe('POST');
147+
expect(webRequest.url).toBe(`http://localhost:${port}/post-text`);
148+
expect(webRequest.headers.get('content-type')).toBe('text/plain');
149+
expect(await webRequest.text()).toBe('');
150+
});
151+
});
152+
});

Diff for: ‎packages/angular/ssr/node/test/response_spec.ts renamed to ‎packages/angular/ssr/node/test/response_http1_spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { IncomingMessage, Server, createServer, request as requestCb } from 'nod
1010
import { AddressInfo } from 'node:net';
1111
import { writeResponseToNodeResponse } from '../src/response';
1212

13-
describe('writeResponseToNodeResponse', () => {
13+
describe('writeResponseToNodeResponse (HTTP/1.1)', () => {
1414
let server: Server;
1515

1616
function simulateResponse(
+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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 {
10+
Http2Server,
11+
IncomingHttpHeaders,
12+
IncomingHttpStatusHeader,
13+
connect,
14+
createServer,
15+
} from 'node:http2';
16+
import { AddressInfo } from 'node:net';
17+
import { writeResponseToNodeResponse } from '../src/response';
18+
19+
describe('writeResponseToNodeResponse (HTTP/2)', () => {
20+
let server: Http2Server;
21+
function simulateResponse(res: Response): Promise<{
22+
body: string | null;
23+
headers: IncomingHttpHeaders & IncomingHttpStatusHeader;
24+
statusCode: number | undefined;
25+
}> {
26+
server.once('request', (req, nodeResponse) => {
27+
void writeResponseToNodeResponse(res, nodeResponse);
28+
});
29+
30+
const { port } = server.address() as AddressInfo;
31+
const client = connect(`http://localhost:${port}`);
32+
33+
return new Promise<{
34+
body: string | null;
35+
headers: IncomingHttpHeaders & IncomingHttpStatusHeader;
36+
statusCode: number | undefined;
37+
}>((resolve, reject) => {
38+
const req = client.request({
39+
':path': '/',
40+
});
41+
42+
req.once('response', (headers) => {
43+
let body: string | null = null;
44+
45+
req
46+
.on('data', (chunk) => {
47+
body ??= '';
48+
body += chunk;
49+
})
50+
.on('end', () => resolve({ headers, statusCode: headers[':status'], body }))
51+
.on('error', reject);
52+
});
53+
}).finally(() => {
54+
client.close();
55+
});
56+
}
57+
58+
beforeAll((done) => {
59+
server = createServer();
60+
server.listen(0, done);
61+
});
62+
63+
afterAll((done) => {
64+
server.close(done);
65+
});
66+
67+
it('should write status, headers, and body to Node.js response', async () => {
68+
const { headers, statusCode, body } = await simulateResponse(
69+
new Response('Hello, world!', {
70+
status: 201,
71+
headers: {
72+
'Content-Type': 'text/plain',
73+
'X-Custom-Header': 'custom-value',
74+
},
75+
}),
76+
);
77+
78+
expect(statusCode).toBe(201);
79+
expect(headers['content-type']).toBe('text/plain');
80+
expect(headers['x-custom-header']).toBe('custom-value');
81+
expect(body).toBe('Hello, world!');
82+
});
83+
84+
it('should handle empty body', async () => {
85+
const { statusCode, body } = await simulateResponse(
86+
new Response(null, {
87+
status: 204,
88+
}),
89+
);
90+
91+
expect(statusCode).toBe(204);
92+
expect(body).toBeNull();
93+
});
94+
95+
it('should handle JSON content types', async () => {
96+
const jsonData = JSON.stringify({ message: 'Hello JSON' });
97+
const { statusCode, body } = await simulateResponse(
98+
new Response(jsonData, {
99+
headers: { 'Content-Type': 'application/json' },
100+
}),
101+
);
102+
103+
expect(statusCode).toBe(200);
104+
expect(body).toBe(jsonData);
105+
});
106+
107+
it('should set cookies on the ServerResponse', async () => {
108+
const cookieValue: string[] = [
109+
'myCookie=myValue; Path=/; HttpOnly',
110+
'anotherCookie=anotherValue; Path=/test',
111+
];
112+
113+
const headers = new Headers();
114+
cookieValue.forEach((v) => headers.append('set-cookie', v));
115+
const { headers: resHeaders } = await simulateResponse(new Response(null, { headers }));
116+
117+
expect(resHeaders['set-cookie']).toEqual(cookieValue);
118+
});
119+
});

Diff for: ‎tests/legacy-cli/e2e/tests/vite/ssr-entry-express.ts

+1-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { setTimeout } from 'node:timers/promises';
33
import { replaceInFile, writeMultipleFiles } from '../../utils/fs';
44
import { ng, silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process';
55
import { installWorkspacePackages, uninstallPackage } from '../../utils/packages';
6-
import { ngServe, updateJsonFile, useSha } from '../../utils/project';
6+
import { ngServe, useSha } from '../../utils/project';
77
import { getGlobalVariable } from '../../utils/env';
88

99
export default async function () {
@@ -18,13 +18,6 @@ export default async function () {
1818
await useSha();
1919
await installWorkspacePackages();
2020

21-
// Update angular.json
22-
await updateJsonFile('angular.json', (workspaceJson) => {
23-
const appArchitect = workspaceJson.projects['test-project'].architect;
24-
const options = appArchitect.build.options;
25-
options.outputMode = 'server';
26-
});
27-
2821
await writeMultipleFiles({
2922
// Replace the template of app.component.html as it makes it harder to debug
3023
'src/app/app.component.html': '<router-outlet />',

Diff for: ‎tests/legacy-cli/e2e/tests/vite/ssr-entry-fastify.ts

+1-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { setTimeout } from 'node:timers/promises';
33
import { replaceInFile, writeMultipleFiles } from '../../utils/fs';
44
import { ng, silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process';
55
import { installPackage, installWorkspacePackages, uninstallPackage } from '../../utils/packages';
6-
import { ngServe, updateJsonFile, useSha } from '../../utils/project';
6+
import { ngServe, useSha } from '../../utils/project';
77
import { getGlobalVariable } from '../../utils/env';
88

99
export default async function () {
@@ -19,13 +19,6 @@ export default async function () {
1919
await installWorkspacePackages();
2020
await installPackage('fastify@5');
2121

22-
// Update angular.json
23-
await updateJsonFile('angular.json', (workspaceJson) => {
24-
const appArchitect = workspaceJson.projects['test-project'].architect;
25-
const options = appArchitect.build.options;
26-
options.outputMode = 'server';
27-
});
28-
2922
await writeMultipleFiles({
3023
// Replace the template of app.component.html as it makes it harder to debug
3124
'src/app/app.component.html': '<router-outlet />',

Diff for: ‎tests/legacy-cli/e2e/tests/vite/ssr-entry-h3.ts

+1-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { setTimeout } from 'node:timers/promises';
33
import { replaceInFile, writeMultipleFiles } from '../../utils/fs';
44
import { ng, silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process';
55
import { installPackage, installWorkspacePackages, uninstallPackage } from '../../utils/packages';
6-
import { ngServe, updateJsonFile, useSha } from '../../utils/project';
6+
import { ngServe, useSha } from '../../utils/project';
77
import { getGlobalVariable } from '../../utils/env';
88

99
export default async function () {
@@ -19,13 +19,6 @@ export default async function () {
1919
await installWorkspacePackages();
2020
await installPackage('h3@1');
2121

22-
// Update angular.json
23-
await updateJsonFile('angular.json', (workspaceJson) => {
24-
const appArchitect = workspaceJson.projects['test-project'].architect;
25-
const options = appArchitect.build.options;
26-
options.outputMode = 'server';
27-
});
28-
2922
await writeMultipleFiles({
3023
// Replace the template of app.component.html as it makes it harder to debug
3124
'src/app/app.component.html': '<router-outlet />',

Diff for: ‎tests/legacy-cli/e2e/tests/vite/ssr-entry-hono.ts

-7
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,6 @@ export default async function () {
1919
await installWorkspacePackages();
2020
await installPackage('hono@4');
2121

22-
// Update angular.json
23-
await updateJsonFile('angular.json', (workspaceJson) => {
24-
const appArchitect = workspaceJson.projects['test-project'].architect;
25-
const options = appArchitect.build.options;
26-
options.outputMode = 'server';
27-
});
28-
2922
await writeMultipleFiles({
3023
// Replace the template of app.component.html as it makes it harder to debug
3124
'src/app/app.component.html': '<router-outlet />',

Diff for: ‎tests/legacy-cli/e2e/tests/vite/ssr-with-ssl.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import assert from 'node:assert';
2+
import { writeMultipleFiles } from '../../utils/fs';
3+
import { ng, silentNg } from '../../utils/process';
4+
import { installWorkspacePackages, uninstallPackage } from '../../utils/packages';
5+
import { ngServe, useSha } from '../../utils/project';
6+
import { getGlobalVariable } from '../../utils/env';
7+
8+
export default async function () {
9+
assert(
10+
getGlobalVariable('argv')['esbuild'],
11+
'This test should not be called in the Webpack suite.',
12+
);
13+
14+
// Forcibly remove in case another test doesn't clean itself up.
15+
await uninstallPackage('@angular/ssr');
16+
await ng('add', '@angular/ssr', '--server-routing', '--skip-confirmation', '--skip-install');
17+
await useSha();
18+
await installWorkspacePackages();
19+
20+
await writeMultipleFiles({
21+
// Replace the template of app.component.html as it makes it harder to debug
22+
'src/app/app.component.html': '<router-outlet />',
23+
'src/app/app.routes.ts': `
24+
import { Routes } from '@angular/router';
25+
import { HomeComponent } from './home/home.component';
26+
27+
export const routes: Routes = [
28+
{ path: 'home', component: HomeComponent }
29+
];
30+
`,
31+
'src/app/app.routes.server.ts': `
32+
import { RenderMode, ServerRoute } from '@angular/ssr';
33+
34+
export const serverRoutes: ServerRoute[] = [
35+
{ path: '**', renderMode: RenderMode.Server }
36+
];
37+
`,
38+
});
39+
40+
await silentNg('generate', 'component', 'home');
41+
42+
const port = await ngServe('--ssl');
43+
44+
// Verify the server is running and the API response is correct.
45+
await validateResponse('/main.js', /bootstrapApplication/);
46+
await validateResponse('/home', /home works/);
47+
48+
async function validateResponse(pathname: string, match: RegExp): Promise<void> {
49+
try {
50+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
51+
const response = await fetch(new URL(pathname, `https://localhost:${port}`));
52+
const text = await response.text();
53+
assert.match(text, match);
54+
assert.equal(response.status, 200);
55+
} finally {
56+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)
Please sign in to comment.