Skip to content

Commit f630726

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

File tree

9 files changed

+283
-29
lines changed

9 files changed

+283
-29
lines changed

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

+4-12
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

+2-2
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
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 assert from 'node:assert';
10+
import { createServer } from 'node:http';
11+
import { loadEsmModule } from '../load-esm';
12+
import { loadEsmModuleFromMemory } from './load-esm-from-memory';
13+
import { isSsrNodeRequestHandler, isSsrRequestHandler } from './utils';
14+
15+
export const DEFAULT_URL = new URL('http://ng-localhost/');
16+
17+
/**
18+
* Launches a server that handles local requests.
19+
*
20+
* @returns A promise that resolves to the URL of the running server.
21+
*/
22+
export async function launchServer(): Promise<URL> {
23+
const { default: handler } = await loadEsmModuleFromMemory('./server.mjs');
24+
const { createWebRequestFromNodeRequest, writeResponseToNodeResponse } =
25+
await loadEsmModule<typeof import('@angular/ssr/node')>('@angular/ssr/node');
26+
27+
if (!isSsrNodeRequestHandler(handler) && !isSsrRequestHandler(handler)) {
28+
return DEFAULT_URL;
29+
}
30+
31+
const server = createServer((req, res) => {
32+
(async () => {
33+
// handle request
34+
if (isSsrNodeRequestHandler(handler)) {
35+
await handler(req, res, (e) => {
36+
throw e;
37+
});
38+
} else {
39+
const webRes = await handler(createWebRequestFromNodeRequest(req));
40+
if (webRes) {
41+
await writeResponseToNodeResponse(webRes, res);
42+
} else {
43+
res.statusCode = 501;
44+
res.end('Not Implemented.');
45+
}
46+
}
47+
})().catch((e) => {
48+
res.statusCode = 500;
49+
res.end('Internal Server Error.');
50+
// eslint-disable-next-line no-console
51+
console.error(e);
52+
});
53+
});
54+
55+
server.unref();
56+
57+
await new Promise<void>((resolve) => server.listen(0, 'localhost', resolve));
58+
59+
const serverAddress = server.address();
60+
assert(serverAddress, 'Server address should be defined.');
61+
assert(typeof serverAddress !== 'string', 'Server address should not be a string.');
62+
63+
return new URL(`http://localhost:${serverAddress.port}/`);
64+
}

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

+12-1
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

+9-3
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export async function prerenderPages(
165165
outputFilesForWorker,
166166
assetsReversed,
167167
appShellOptions,
168+
outputMode,
168169
);
169170

170171
errors.push(...renderingErrors);
@@ -186,6 +187,7 @@ async function renderPages(
186187
outputFilesForWorker: Record<string, string>,
187188
assetFilesForWorker: Record<string, string>,
188189
appShellOptions: AppShellOptions | undefined,
190+
outputMode: OutputMode | undefined,
189191
): Promise<{
190192
output: PrerenderOutput;
191193
errors: string[];
@@ -210,6 +212,8 @@ async function renderPages(
210212
workspaceRoot,
211213
outputFiles: outputFilesForWorker,
212214
assetFiles: assetFilesForWorker,
215+
outputMode,
216+
hasSsrEntry: !!outputFilesForWorker['/server.mjs'],
213217
} as RenderWorkerData,
214218
execArgv: workerExecArgv,
215219
});
@@ -314,14 +318,16 @@ async function getAllRoutes(
314318
workspaceRoot,
315319
outputFiles: outputFilesForWorker,
316320
assetFiles: assetFilesForWorker,
321+
outputMode,
322+
hasSsrEntry: !!outputFilesForWorker['/server.mjs'],
317323
} as RoutesExtractorWorkerData,
318324
execArgv: workerExecArgv,
319325
});
320326

321327
try {
322-
const { serializedRouteTree, errors }: RoutersExtractorWorkerResult = await renderWorker.run({
323-
outputMode,
324-
});
328+
const { serializedRouteTree, errors }: RoutersExtractorWorkerResult = await renderWorker.run(
329+
{},
330+
);
325331

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

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

+22-3
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,33 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import { workerData } from 'worker_threads';
10+
import type { OutputMode } from '../../builders/application/schema';
911
import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loader-hooks';
1012
import { patchFetchToLoadInMemoryAssets } from './fetch-patch';
13+
import { DEFAULT_URL, launchServer } from './launch-server';
1114
import { loadEsmModuleFromMemory } from './load-esm-from-memory';
1215

1316
export interface RenderWorkerData extends ESMInMemoryFileLoaderWorkerData {
1417
assetFiles: Record</** Destination */ string, /** Source */ string>;
18+
outputMode: OutputMode | undefined;
19+
hasSsrEntry: boolean;
1520
}
1621

1722
export interface RenderOptions {
1823
url: string;
1924
}
2025

26+
/**
27+
* This is passed as workerData when setting up the worker via the `piscina` package.
28+
*/
29+
const { outputMode, hasSsrEntry } = workerData as {
30+
outputMode: OutputMode | undefined;
31+
hasSsrEntry: boolean;
32+
};
33+
34+
let serverURL = DEFAULT_URL;
35+
2136
/**
2237
* Renders each route in routes and writes them to <outputPath>/<route>/index.html.
2338
*/
@@ -26,15 +41,19 @@ async function renderPage({ url }: RenderOptions): Promise<string | null> {
2641
await loadEsmModuleFromMemory('./main.server.mjs');
2742
const angularServerApp = getOrCreateAngularServerApp();
2843
const response = await angularServerApp.renderStatic(
29-
new URL(url, 'http://local-angular-prerender'),
44+
new URL(url, serverURL),
3045
AbortSignal.timeout(30_000),
3146
);
3247

3348
return response ? response.text() : null;
3449
}
3550

36-
function initialize() {
37-
patchFetchToLoadInMemoryAssets();
51+
async function initialize() {
52+
if (outputMode !== undefined && hasSsrEntry) {
53+
serverURL = await launchServer();
54+
}
55+
56+
patchFetchToLoadInMemoryAssets(serverURL);
3857

3958
return renderPage;
4059
}

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

+23-8
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,35 @@
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 { DEFAULT_URL, 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, hasSsrEntry } = workerData as {
25+
outputMode: OutputMode | undefined;
26+
hasSsrEntry: boolean;
27+
};
28+
29+
let serverURL = DEFAULT_URL;
30+
1831
/** Renders an application based on a provided options. */
19-
async function extractRoutes({
20-
outputMode,
21-
}: ExtractRoutesOptions): Promise<RoutersExtractorWorkerResult> {
32+
async function extractRoutes(): Promise<RoutersExtractorWorkerResult> {
2233
const { ɵextractRoutesAndCreateRouteTree: extractRoutesAndCreateRouteTree } =
2334
await loadEsmModuleFromMemory('./main.server.mjs');
2435

2536
const { routeTree, errors } = await extractRoutesAndCreateRouteTree(
26-
new URL('http://local-angular-prerender/'),
37+
serverURL,
2738
undefined /** manifest */,
2839
true /** invokeGetPrerenderParams */,
2940
outputMode === OutputMode.Server /** includePrerenderFallbackRoutes */,
@@ -35,8 +46,12 @@ async function extractRoutes({
3546
};
3647
}
3748

38-
function initialize() {
39-
patchFetchToLoadInMemoryAssets();
49+
async function initialize() {
50+
if (outputMode !== undefined && hasSsrEntry) {
51+
serverURL = await launchServer();
52+
}
53+
54+
patchFetchToLoadInMemoryAssets(serverURL);
4055

4156
return extractRoutes;
4257
}
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)