Skip to content

Commit 099cec7

Browse files
alan-agius4clydin
authored andcommitted
feat(@angular-devkit/build-angular): add support for serving SSR with dev-server when using the application builder
This commit introduces experimental support for serving the application in SSR mode with the Vite based dev-server when using the application builder.
1 parent bdee1ac commit 099cec7

File tree

2 files changed

+86
-20
lines changed

2 files changed

+86
-20
lines changed

packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import { lookup as lookupMimeType } from 'mrmime';
1313
import assert from 'node:assert';
1414
import { BinaryLike, createHash } from 'node:crypto';
1515
import { readFile } from 'node:fs/promises';
16+
import { ServerResponse } from 'node:http';
1617
import type { AddressInfo } from 'node:net';
1718
import path from 'node:path';
18-
import { InlineConfig, ViteDevServer, createServer, normalizePath } from 'vite';
19+
import { Connect, InlineConfig, ViteDevServer, createServer, normalizePath } from 'vite';
1920
import { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer';
21+
import { RenderOptions, renderPage } from '../../utils/server-rendering/render-page';
2022
import { buildEsbuildBrowser } from '../browser-esbuild';
21-
import type { Schema as BrowserBuilderOptions } from '../browser-esbuild/schema';
23+
import { Schema as BrowserBuilderOptions } from '../browser-esbuild/schema';
2224
import { loadProxyConfiguration } from './load-proxy-config';
2325
import type { NormalizedDevServerOptions } from './options';
2426
import type { DevServerBuilderOutput } from './webpack-server';
@@ -107,7 +109,9 @@ export async function* serveWithVite(
107109
assetFiles,
108110
browserOptions.preserveSymlinks,
109111
browserOptions.externalDependencies,
112+
!!browserOptions.ssr,
110113
);
114+
111115
server = await createServer(serverConfiguration);
112116

113117
await server.listen();
@@ -191,6 +195,7 @@ export async function setupServer(
191195
assets: Map<string, string>,
192196
preserveSymlinks: boolean | undefined,
193197
prebundleExclude: string[] | undefined,
198+
ssr: boolean,
194199
): Promise<InlineConfig> {
195200
const proxy = await loadProxyConfiguration(
196201
serverOptions.workspaceRoot,
@@ -227,6 +232,10 @@ export async function setupServer(
227232
ignored: ['**/*'],
228233
},
229234
},
235+
ssr: {
236+
// Exclude any provided dependencies (currently build defined externals)
237+
external: prebundleExclude,
238+
},
230239
plugins: [
231240
{
232241
name: 'vite:angular-memory',
@@ -271,14 +280,7 @@ export async function setupServer(
271280

272281
// Parse the incoming request.
273282
// The base of the URL is unused but required to parse the URL.
274-
const parsedUrl = new URL(req.url, 'http://localhost');
275-
let pathname = decodeURIComponent(parsedUrl.pathname);
276-
if (serverOptions.servePath && pathname.startsWith(serverOptions.servePath)) {
277-
pathname = pathname.slice(serverOptions.servePath.length);
278-
if (pathname[0] !== '/') {
279-
pathname = '/' + pathname;
280-
}
281-
}
283+
const pathname = pathnameWithoutServePath(req.url, serverOptions);
282284
const extension = path.extname(pathname);
283285

284286
// Rewrite all build assets to a vite raw fs URL
@@ -317,7 +319,63 @@ export async function setupServer(
317319

318320
// Returning a function, installs middleware after the main transform middleware but
319321
// before the built-in HTML middleware
320-
return () =>
322+
return () => {
323+
function angularSSRMiddleware(
324+
req: Connect.IncomingMessage,
325+
res: ServerResponse,
326+
next: Connect.NextFunction,
327+
) {
328+
const url = req.originalUrl;
329+
if (!url) {
330+
next();
331+
332+
return;
333+
}
334+
335+
const rawHtml = outputFiles.get('/index.server.html')?.contents;
336+
if (!rawHtml) {
337+
next();
338+
339+
return;
340+
}
341+
342+
server
343+
.transformIndexHtml(url, Buffer.from(rawHtml).toString('utf-8'))
344+
.then(async (html) => {
345+
const { content } = await renderPage({
346+
document: html,
347+
route: pathnameWithoutServePath(url, serverOptions),
348+
serverContext: 'ssr',
349+
loadBundle: (path: string) =>
350+
server.ssrLoadModule(path.slice(1)) as ReturnType<
351+
NonNullable<RenderOptions['loadBundle']>
352+
>,
353+
// Files here are only needed for critical CSS inlining.
354+
outputFiles: {},
355+
// TODO: add support for critical css inlining.
356+
inlineCriticalCss: false,
357+
});
358+
359+
if (content) {
360+
res.setHeader('Content-Type', 'text/html');
361+
res.setHeader('Cache-Control', 'no-cache');
362+
if (serverOptions.headers) {
363+
Object.entries(serverOptions.headers).forEach(([name, value]) =>
364+
res.setHeader(name, value),
365+
);
366+
}
367+
res.end(content);
368+
} else {
369+
next();
370+
}
371+
})
372+
.catch((error) => next(error));
373+
}
374+
375+
if (ssr) {
376+
server.middlewares.use(angularSSRMiddleware);
377+
}
378+
321379
server.middlewares.use(function angularIndexMiddleware(req, res, next) {
322380
if (!req.url) {
323381
next();
@@ -327,14 +385,8 @@ export async function setupServer(
327385

328386
// Parse the incoming request.
329387
// The base of the URL is unused but required to parse the URL.
330-
const parsedUrl = new URL(req.url, 'http://localhost');
331-
let pathname = parsedUrl.pathname;
332-
if (serverOptions.servePath && pathname.startsWith(serverOptions.servePath)) {
333-
pathname = pathname.slice(serverOptions.servePath.length);
334-
if (pathname[0] !== '/') {
335-
pathname = '/' + pathname;
336-
}
337-
}
388+
const pathname = pathnameWithoutServePath(req.url, serverOptions);
389+
338390
if (pathname === '/' || pathname === `/index.html`) {
339391
const rawHtml = outputFiles.get('/index.html')?.contents;
340392
if (rawHtml) {
@@ -358,6 +410,7 @@ export async function setupServer(
358410

359411
next();
360412
});
413+
};
361414
},
362415
},
363416
],
@@ -413,3 +466,16 @@ export async function setupServer(
413466

414467
return configuration;
415468
}
469+
470+
function pathnameWithoutServePath(url: string, serverOptions: NormalizedDevServerOptions): string {
471+
const parsedUrl = new URL(url, 'http://localhost');
472+
let pathname = decodeURIComponent(parsedUrl.pathname);
473+
if (serverOptions.servePath && pathname.startsWith(serverOptions.servePath)) {
474+
pathname = pathname.slice(serverOptions.servePath.length);
475+
if (pathname[0] !== '/') {
476+
pathname = '/' + pathname;
477+
}
478+
}
479+
480+
return pathname;
481+
}

packages/angular_devkit/build_angular/src/utils/server-rendering/render-page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export async function renderPage({
8686

8787
if (inlineCriticalCss) {
8888
const inlineCriticalCssProcessor = new InlineCriticalCssProcessor({
89-
minify: false,
89+
minify: false, // CSS has already been minified during the build.
9090
readAsset: async (filePath) => {
9191
filePath = basename(filePath);
9292
const content = outputFiles[filePath];

0 commit comments

Comments
 (0)