@@ -13,12 +13,14 @@ import { lookup as lookupMimeType } from 'mrmime';
13
13
import assert from 'node:assert' ;
14
14
import { BinaryLike , createHash } from 'node:crypto' ;
15
15
import { readFile } from 'node:fs/promises' ;
16
+ import { ServerResponse } from 'node:http' ;
16
17
import type { AddressInfo } from 'node:net' ;
17
18
import path from 'node:path' ;
18
- import { InlineConfig , ViteDevServer , createServer , normalizePath } from 'vite' ;
19
+ import { Connect , InlineConfig , ViteDevServer , createServer , normalizePath } from 'vite' ;
19
20
import { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer' ;
21
+ import { RenderOptions , renderPage } from '../../utils/server-rendering/render-page' ;
20
22
import { buildEsbuildBrowser } from '../browser-esbuild' ;
21
- import type { Schema as BrowserBuilderOptions } from '../browser-esbuild/schema' ;
23
+ import { Schema as BrowserBuilderOptions } from '../browser-esbuild/schema' ;
22
24
import { loadProxyConfiguration } from './load-proxy-config' ;
23
25
import type { NormalizedDevServerOptions } from './options' ;
24
26
import type { DevServerBuilderOutput } from './webpack-server' ;
@@ -107,7 +109,9 @@ export async function* serveWithVite(
107
109
assetFiles ,
108
110
browserOptions . preserveSymlinks ,
109
111
browserOptions . externalDependencies ,
112
+ ! ! browserOptions . ssr ,
110
113
) ;
114
+
111
115
server = await createServer ( serverConfiguration ) ;
112
116
113
117
await server . listen ( ) ;
@@ -191,6 +195,7 @@ export async function setupServer(
191
195
assets : Map < string , string > ,
192
196
preserveSymlinks : boolean | undefined ,
193
197
prebundleExclude : string [ ] | undefined ,
198
+ ssr : boolean ,
194
199
) : Promise < InlineConfig > {
195
200
const proxy = await loadProxyConfiguration (
196
201
serverOptions . workspaceRoot ,
@@ -227,6 +232,10 @@ export async function setupServer(
227
232
ignored : [ '**/*' ] ,
228
233
} ,
229
234
} ,
235
+ ssr : {
236
+ // Exclude any provided dependencies (currently build defined externals)
237
+ external : prebundleExclude ,
238
+ } ,
230
239
plugins : [
231
240
{
232
241
name : 'vite:angular-memory' ,
@@ -271,14 +280,7 @@ export async function setupServer(
271
280
272
281
// Parse the incoming request.
273
282
// 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 ) ;
282
284
const extension = path . extname ( pathname ) ;
283
285
284
286
// Rewrite all build assets to a vite raw fs URL
@@ -317,7 +319,63 @@ export async function setupServer(
317
319
318
320
// Returning a function, installs middleware after the main transform middleware but
319
321
// 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
+
321
379
server . middlewares . use ( function angularIndexMiddleware ( req , res , next ) {
322
380
if ( ! req . url ) {
323
381
next ( ) ;
@@ -327,14 +385,8 @@ export async function setupServer(
327
385
328
386
// Parse the incoming request.
329
387
// 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
+
338
390
if ( pathname === '/' || pathname === `/index.html` ) {
339
391
const rawHtml = outputFiles . get ( '/index.html' ) ?. contents ;
340
392
if ( rawHtml ) {
@@ -358,6 +410,7 @@ export async function setupServer(
358
410
359
411
next ( ) ;
360
412
} ) ;
413
+ } ;
361
414
} ,
362
415
} ,
363
416
] ,
@@ -413,3 +466,16 @@ export async function setupServer(
413
466
414
467
return configuration ;
415
468
}
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
+ }
0 commit comments