@@ -13,12 +13,14 @@ import { lookup as lookupMimeType } from 'mrmime';
1313import assert from 'node:assert' ;
1414import { BinaryLike , createHash } from 'node:crypto' ;
1515import { readFile } from 'node:fs/promises' ;
16+ import { ServerResponse } from 'node:http' ;
1617import type { AddressInfo } from 'node:net' ;
1718import path from 'node:path' ;
18- import { InlineConfig , ViteDevServer , createServer , normalizePath } from 'vite' ;
19+ import { Connect , InlineConfig , ViteDevServer , createServer , normalizePath } from 'vite' ;
1920import { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer' ;
21+ import { RenderOptions , renderPage } from '../../utils/server-rendering/render-page' ;
2022import { buildEsbuildBrowser } from '../browser-esbuild' ;
21- import type { Schema as BrowserBuilderOptions } from '../browser-esbuild/schema' ;
23+ import { Schema as BrowserBuilderOptions } from '../browser-esbuild/schema' ;
2224import { loadProxyConfiguration } from './load-proxy-config' ;
2325import type { NormalizedDevServerOptions } from './options' ;
2426import 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+ }
0 commit comments