Skip to content

Commit c68b966

Browse files
committed
feat(@angular/build): utilize server.ts during prerendering to enable access to local API routes
The server.ts file is now utilized during prerendering, allowing access to locally defined API routes for improved data fetching and rendering.
1 parent 9030fa2 commit c68b966

File tree

9 files changed

+274
-29
lines changed

9 files changed

+274
-29
lines changed

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

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88

99
import type {
1010
AngularAppEngine as SSRAngularAppEngine,
11-
createRequestHandler,
1211
ɵgetOrCreateAngularServerApp as getOrCreateAngularServerApp,
1312
} from '@angular/ssr';
14-
import type { createNodeRequestHandler } from '@angular/ssr/node';
1513
import type { ServerResponse } from 'node:http';
1614
import type { Connect, ViteDevServer } from 'vite';
1715
import { loadEsmModule } from '../../../utils/load-esm';
16+
import {
17+
isSsrNodeRequestHandler,
18+
isSsrRequestHandler,
19+
} from '../../../utils/server-rendering/utils';
1820

1921
export function createAngularSsrInternalMiddleware(
2022
server: ViteDevServer,
@@ -136,13 +138,3 @@ export async function createAngularSsrExternalMiddleware(
136138
})().catch(next);
137139
};
138140
}
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/utils/server-rendering/fetch-patch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const { assetFiles } = workerData as {
2121
const assetsCache: Map<string, { headers: undefined | Record<string, string>; content: Buffer }> =
2222
new Map();
2323

24-
export function patchFetchToLoadInMemoryAssets(): void {
24+
export function patchFetchToLoadInMemoryAssets(baseURL: URL): void {
2525
const originalFetch = globalThis.fetch;
2626
const patchedFetch: typeof fetch = async (input, init) => {
2727
let url: URL;
@@ -38,7 +38,7 @@ export function patchFetchToLoadInMemoryAssets(): void {
3838
const { hostname } = url;
3939
const pathname = decodeURIComponent(url.pathname);
4040

41-
if (hostname !== 'local-angular-prerender' || !assetFiles[pathname]) {
41+
if (hostname !== baseURL.hostname || !assetFiles[pathname]) {
4242
// Only handle relative requests or files that are in assets.
4343
return originalFetch(input, init);
4444
}
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 { createServer } from 'http';
10+
import assert from 'node:assert';
11+
import { workerData } from 'worker_threads';
12+
import { OutputMode } from '../../builders/application/schema';
13+
import { loadEsmModule } from '../load-esm';
14+
import { loadEsmModuleFromMemory } from './load-esm-from-memory';
15+
import { isSsrNodeRequestHandler, isSsrRequestHandler } from './utils';
16+
17+
const DEFAULT_URL = new URL('http://ng-localhost/');
18+
19+
/**
20+
* This is passed as workerData when setting up the worker via the `piscina` package.
21+
*/
22+
const { outputMode } = workerData as {
23+
outputMode: OutputMode | undefined;
24+
};
25+
26+
export async function launchServer(): Promise<URL> {
27+
if (!outputMode) {
28+
// Legacy mode
29+
return DEFAULT_URL;
30+
}
31+
32+
let handler: unknown;
33+
try {
34+
const serverEntry = await loadEsmModuleFromMemory('./server.mjs');
35+
handler = serverEntry.default;
36+
} catch {
37+
return DEFAULT_URL;
38+
}
39+
40+
const { createWebRequestFromNodeRequest, writeResponseToNodeResponse } =
41+
await loadEsmModule<typeof import('@angular/ssr/node')>('@angular/ssr/node');
42+
43+
if (!isSsrNodeRequestHandler(handler) && !isSsrRequestHandler(handler)) {
44+
return DEFAULT_URL;
45+
}
46+
47+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
48+
const server = createServer(async (req, res) => {
49+
// handle request
50+
if (isSsrNodeRequestHandler(handler)) {
51+
// eslint-disable-next-line no-console
52+
await handler(req, res, (e) => {
53+
throw e;
54+
});
55+
} else {
56+
const webRes = await handler(createWebRequestFromNodeRequest(req));
57+
if (webRes) {
58+
await writeResponseToNodeResponse(webRes, res);
59+
}
60+
}
61+
});
62+
63+
server.unref();
64+
65+
return new Promise<URL>((resolve) => {
66+
server.listen(0, () => {
67+
const serverAddress = server.address();
68+
assert(serverAddress, 'Server address should be defined.');
69+
assert(typeof serverAddress !== 'string', 'Server address should not be a string.');
70+
71+
resolve(new URL(`http://localhost:${serverAddress.port}/`));
72+
});
73+
});
74+
}

packages/angular/build/src/utils/server-rendering/load-esm-from-memory.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,20 @@ interface MainServerBundleExports {
2020
ɵgetOrCreateAngularServerApp: typeof ɵgetOrCreateAngularServerApp;
2121
}
2222

23+
/**
24+
* Represents the exports available from the server bundle.
25+
*/
26+
interface ServerBundleExports {
27+
default: unknown;
28+
}
29+
2330
export function loadEsmModuleFromMemory(
2431
path: './main.server.mjs',
25-
): Promise<MainServerBundleExports> {
32+
): Promise<MainServerBundleExports>;
33+
export function loadEsmModuleFromMemory(path: './server.mjs'): Promise<ServerBundleExports>;
34+
export function loadEsmModuleFromMemory(
35+
path: './main.server.mjs' | './server.mjs',
36+
): Promise<MainServerBundleExports | ServerBundleExports> {
2637
return loadEsmModule(new URL(path, 'memory://')).catch((e) => {
2738
assertIsError(e);
2839

packages/angular/build/src/utils/server-rendering/prerender.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export async function prerenderPages(
160160
outputFilesForWorker,
161161
assetsReversed,
162162
appShellOptions,
163+
outputMode,
163164
);
164165

165166
errors.push(...renderingErrors);
@@ -181,6 +182,7 @@ async function renderPages(
181182
outputFilesForWorker: Record<string, string>,
182183
assetFilesForWorker: Record<string, string>,
183184
appShellOptions: AppShellOptions | undefined,
185+
outputMode: OutputMode | undefined,
184186
): Promise<{
185187
output: PrerenderOutput;
186188
errors: string[];
@@ -205,6 +207,7 @@ async function renderPages(
205207
workspaceRoot,
206208
outputFiles: outputFilesForWorker,
207209
assetFiles: assetFilesForWorker,
210+
outputMode,
208211
} as RenderWorkerData,
209212
execArgv: workerExecArgv,
210213
});
@@ -309,14 +312,15 @@ async function getAllRoutes(
309312
workspaceRoot,
310313
outputFiles: outputFilesForWorker,
311314
assetFiles: assetFilesForWorker,
315+
outputMode,
312316
} as RoutesExtractorWorkerData,
313317
execArgv: workerExecArgv,
314318
});
315319

316320
try {
317-
const { serializedRouteTree, errors }: RoutersExtractorWorkerResult = await renderWorker.run({
318-
outputMode,
319-
});
321+
const { serializedRouteTree, errors }: RoutersExtractorWorkerResult = await renderWorker.run(
322+
{},
323+
);
320324

321325
return { errors, serializedRouteTree: [...routes, ...serializedRouteTree] };
322326
} catch (err) {

packages/angular/build/src/utils/server-rendering/render-worker.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,23 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import type { OutputMode } from '../../builders/application/schema';
910
import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loader-hooks';
1011
import { patchFetchToLoadInMemoryAssets } from './fetch-patch';
12+
import { launchServer } from './launch-server';
1113
import { loadEsmModuleFromMemory } from './load-esm-from-memory';
1214

1315
export interface RenderWorkerData extends ESMInMemoryFileLoaderWorkerData {
1416
assetFiles: Record</** Destination */ string, /** Source */ string>;
17+
outputMode: OutputMode | undefined;
1518
}
1619

1720
export interface RenderOptions {
1821
url: string;
1922
}
2023

24+
let serverURL: URL;
25+
2126
/**
2227
* Renders each route in routes and writes them to <outputPath>/<route>/index.html.
2328
*/
@@ -26,15 +31,16 @@ async function renderPage({ url }: RenderOptions): Promise<string | null> {
2631
await loadEsmModuleFromMemory('./main.server.mjs');
2732
const angularServerApp = getOrCreateAngularServerApp();
2833
const response = await angularServerApp.renderStatic(
29-
new URL(url, 'http://local-angular-prerender'),
34+
new URL(url, serverURL),
3035
AbortSignal.timeout(30_000),
3136
);
3237

3338
return response ? response.text() : null;
3439
}
3540

36-
function initialize() {
37-
patchFetchToLoadInMemoryAssets();
41+
async function initialize() {
42+
serverURL = await launchServer();
43+
patchFetchToLoadInMemoryAssets(serverURL);
3844

3945
return renderPage;
4046
}

packages/angular/build/src/utils/server-rendering/routes-extractor-worker.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,34 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import { workerData } from 'worker_threads';
910
import { OutputMode } from '../../builders/application/schema';
11+
import { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loader-hooks';
1012
import { patchFetchToLoadInMemoryAssets } from './fetch-patch';
13+
import { launchServer } from './launch-server';
1114
import { loadEsmModuleFromMemory } from './load-esm-from-memory';
1215
import { RoutersExtractorWorkerResult } from './models';
1316

14-
export interface ExtractRoutesOptions {
15-
outputMode?: OutputMode;
17+
export interface ExtractRoutesWorkerData extends ESMInMemoryFileLoaderWorkerData {
18+
outputMode: OutputMode | undefined;
1619
}
1720

21+
/**
22+
* This is passed as workerData when setting up the worker via the `piscina` package.
23+
*/
24+
const { outputMode } = workerData as {
25+
outputMode: OutputMode | undefined;
26+
};
27+
28+
let serverURL: URL;
29+
1830
/** Renders an application based on a provided options. */
19-
async function extractRoutes({
20-
outputMode,
21-
}: ExtractRoutesOptions): Promise<RoutersExtractorWorkerResult> {
31+
async function extractRoutes(): Promise<RoutersExtractorWorkerResult> {
2232
const { ɵextractRoutesAndCreateRouteTree: extractRoutesAndCreateRouteTree } =
2333
await loadEsmModuleFromMemory('./main.server.mjs');
2434

2535
const { routeTree, errors } = await extractRoutesAndCreateRouteTree(
26-
new URL('http://local-angular-prerender/'),
36+
serverURL,
2737
undefined /** manifest */,
2838
true /** invokeGetPrerenderParams */,
2939
outputMode === OutputMode.Server /** includePrerenderFallbackRoutes */,
@@ -35,8 +45,9 @@ async function extractRoutes({
3545
};
3646
}
3747

38-
function initialize() {
39-
patchFetchToLoadInMemoryAssets();
48+
async function initialize() {
49+
serverURL = await launchServer();
50+
patchFetchToLoadInMemoryAssets(serverURL);
4051

4152
return extractRoutes;
4253
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 { createRequestHandler } from '@angular/ssr';
10+
import type { createNodeRequestHandler } from '@angular/ssr/node';
11+
12+
export function isSsrNodeRequestHandler(
13+
value: unknown,
14+
): value is ReturnType<typeof createNodeRequestHandler> {
15+
return typeof value === 'function' && '__ng_node_request_handler__' in value;
16+
}
17+
export function isSsrRequestHandler(
18+
value: unknown,
19+
): value is ReturnType<typeof createRequestHandler> {
20+
return typeof value === 'function' && '__ng_request_handler__' in value;
21+
}

0 commit comments

Comments
 (0)