77 */
88
99import remapping , { SourceMapInput } from '@ampproject/remapping' ;
10- import { lookup as lookupMimeType } from 'mrmime' ;
1110import assert from 'node:assert' ;
1211import { readFile } from 'node:fs/promises' ;
13- import { ServerResponse } from 'node:http' ;
14- import { dirname , extname , join , relative } from 'node:path' ;
12+ import { dirname , join , relative } from 'node:path' ;
1513import type { Connect , Plugin } from 'vite' ;
16- import { renderPage } from '../../utils/server-rendering/render-page' ;
14+ import {
15+ angularHtmlFallbackMiddleware ,
16+ createAngularAssetsMiddleware ,
17+ createAngularIndexHtmlMiddleware ,
18+ createAngularSSRMiddleware ,
19+ } from './middlewares' ;
20+ import { AngularMemoryOutputFiles } from './utils' ;
1721
1822export interface AngularMemoryPluginOptions {
1923 workspaceRoot : string ;
2024 virtualProjectRoot : string ;
21- outputFiles : Map < string , { contents : Uint8Array ; servable : boolean } > ;
25+ outputFiles : AngularMemoryOutputFiles ;
2226 assets : Map < string , string > ;
2327 ssr : boolean ;
2428 external ?: string [ ] ;
2529 extensionMiddleware ?: Connect . NextHandleFunction [ ] ;
26- extraHeaders ?: Record < string , string > ;
2730 indexHtmlTransformer ?: ( content : string ) => Promise < string > ;
2831 normalizePath : ( path : string ) => string ;
2932}
3033
31- // eslint-disable-next-line max-lines-per-function
3234export function createAngularMemoryPlugin ( options : AngularMemoryPluginOptions ) : Plugin {
3335 const {
3436 workspaceRoot,
@@ -38,7 +40,6 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
3840 external,
3941 ssr,
4042 extensionMiddleware,
41- extraHeaders,
4243 indexHtmlTransformer,
4344 normalizePath,
4445 } = options ;
@@ -112,84 +113,7 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
112113 } ;
113114
114115 // Assets and resources get handled first
115- server . middlewares . use ( function angularAssetsMiddleware ( req , res , next ) {
116- if ( req . url === undefined || res . writableEnded ) {
117- return ;
118- }
119-
120- // Parse the incoming request.
121- // The base of the URL is unused but required to parse the URL.
122- const pathname = pathnameWithoutBasePath ( req . url , server . config . base ) ;
123- const extension = extname ( pathname ) ;
124- const pathnameHasTrailingSlash = pathname [ pathname . length - 1 ] === '/' ;
125-
126- // Rewrite all build assets to a vite raw fs URL
127- const assetSourcePath = assets . get ( pathname ) ;
128- if ( assetSourcePath !== undefined ) {
129- // Workaround to disable Vite transformer middleware.
130- // See: https://github.com/vitejs/vite/blob/746a1daab0395f98f0afbdee8f364cb6cf2f3b3f/packages/vite/src/node/server/middlewares/transform.ts#L201 and
131- // https://github.com/vitejs/vite/blob/746a1daab0395f98f0afbdee8f364cb6cf2f3b3f/packages/vite/src/node/server/transformRequest.ts#L204-L206
132- req . headers . accept = 'text/html' ;
133-
134- // The encoding needs to match what happens in the vite static middleware.
135- // ref: https://github.com/vitejs/vite/blob/d4f13bd81468961c8c926438e815ab6b1c82735e/packages/vite/src/node/server/middlewares/static.ts#L163
136- req . url = `${ server . config . base } @fs/${ encodeURI ( assetSourcePath ) } ` ;
137- next ( ) ;
138-
139- return ;
140- }
141-
142- // HTML fallbacking
143- // This matches what happens in the vite html fallback middleware.
144- // ref: https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/middlewares/htmlFallback.ts#L9
145- const htmlAssetSourcePath = pathnameHasTrailingSlash
146- ? // Trailing slash check for `index.html`.
147- assets . get ( pathname + 'index.html' )
148- : // Non-trailing slash check for fallback `.html`
149- assets . get ( pathname + '.html' ) ;
150-
151- if ( htmlAssetSourcePath ) {
152- req . url = `${ server . config . base } @fs/${ encodeURI ( htmlAssetSourcePath ) } ` ;
153- next ( ) ;
154-
155- return ;
156- }
157-
158- // Resource files are handled directly.
159- // Global stylesheets (CSS files) are currently considered resources to workaround
160- // dev server sourcemap issues with stylesheets.
161- if ( extension !== '.js' && extension !== '.html' ) {
162- const outputFile = outputFiles . get ( pathname ) ;
163- if ( outputFile ?. servable ) {
164- const mimeType = lookupMimeType ( extension ) ;
165- if ( mimeType ) {
166- res . setHeader ( 'Content-Type' , mimeType ) ;
167- }
168- res . setHeader ( 'Cache-Control' , 'no-cache' ) ;
169- if ( extraHeaders ) {
170- Object . entries ( extraHeaders ) . forEach ( ( [ name , value ] ) => res . setHeader ( name , value ) ) ;
171- }
172- res . end ( outputFile . contents ) ;
173-
174- return ;
175- }
176- }
177-
178- // If the path has no trailing slash and it matches a servable directory redirect to the same path with slash.
179- // This matches the default express static behaviour.
180- // See: https://github.com/expressjs/serve-static/blob/89fc94567fae632718a2157206c52654680e9d01/index.js#L182
181- if ( ! pathnameHasTrailingSlash ) {
182- for ( const assetPath of assets . keys ( ) ) {
183- if ( pathname === assetPath . substring ( 0 , assetPath . lastIndexOf ( '/' ) ) ) {
184- redirect ( res , req . url + '/' ) ;
185-
186- return ;
187- }
188- }
189- }
190-
191- next ( ) ;
192- } ) ;
116+ server . middlewares . use ( createAngularAssetsMiddleware ( server , assets , outputFiles ) ) ;
193117
194118 if ( extensionMiddleware ?. length ) {
195119 extensionMiddleware . forEach ( ( middleware ) => server . middlewares . use ( middleware ) ) ;
@@ -200,111 +124,16 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
200124 return ( ) => {
201125 server . middlewares . use ( angularHtmlFallbackMiddleware ) ;
202126
203- function angularSSRMiddleware (
204- req : Connect . IncomingMessage ,
205- res : ServerResponse ,
206- next : Connect . NextFunction ,
207- ) {
208- const url = req . originalUrl ;
209- if (
210- ! req . url ||
211- // Skip if path is not defined.
212- ! url ||
213- // Skip if path is like a file.
214- // NOTE: We use a mime type lookup to mitigate against matching requests like: /browse/pl.0ef59752c0cd457dbf1391f08cbd936f
215- lookupMimeTypeFromRequest ( url )
216- ) {
217- next ( ) ;
218-
219- return ;
220- }
221-
222- const rawHtml = outputFiles . get ( '/index.server.html' ) ?. contents ;
223- if ( ! rawHtml ) {
224- next ( ) ;
225-
226- return ;
227- }
228-
229- transformIndexHtmlAndAddHeaders ( req . url , rawHtml , res , next , async ( html ) => {
230- const resolvedUrls = server . resolvedUrls ;
231- const baseUrl = resolvedUrls ?. local [ 0 ] ?? resolvedUrls ?. network [ 0 ] ;
232-
233- const { content } = await renderPage ( {
234- document : html ,
235- route : new URL ( req . originalUrl ?? '/' , baseUrl ) . toString ( ) ,
236- serverContext : 'ssr' ,
237- loadBundle : ( uri : string ) =>
238- // eslint-disable-next-line @typescript-eslint/no-explicit-any
239- server . ssrLoadModule ( uri . slice ( 1 ) ) as any ,
240- // Files here are only needed for critical CSS inlining.
241- outputFiles : { } ,
242- // TODO: add support for critical css inlining.
243- inlineCriticalCss : false ,
244- } ) ;
245-
246- return indexHtmlTransformer && content ? await indexHtmlTransformer ( content ) : content ;
247- } ) ;
248- }
249-
250127 if ( ssr ) {
251- server . middlewares . use ( angularSSRMiddleware ) ;
128+ server . middlewares . use (
129+ createAngularSSRMiddleware ( server , outputFiles , indexHtmlTransformer ) ,
130+ ) ;
252131 }
253132
254- server . middlewares . use ( function angularIndexMiddleware ( req , res , next ) {
255- if ( ! req . url ) {
256- next ( ) ;
257-
258- return ;
259- }
260-
261- // Parse the incoming request.
262- // The base of the URL is unused but required to parse the URL.
263- const pathname = pathnameWithoutBasePath ( req . url , server . config . base ) ;
264-
265- if ( pathname === '/' || pathname === `/index.html` ) {
266- const rawHtml = outputFiles . get ( '/index.html' ) ?. contents ;
267- if ( rawHtml ) {
268- transformIndexHtmlAndAddHeaders ( req . url , rawHtml , res , next , indexHtmlTransformer ) ;
269-
270- return ;
271- }
272- }
273-
274- next ( ) ;
275- } ) ;
133+ server . middlewares . use (
134+ createAngularIndexHtmlMiddleware ( server , outputFiles , indexHtmlTransformer ) ,
135+ ) ;
276136 } ;
277-
278- function transformIndexHtmlAndAddHeaders (
279- url : string ,
280- rawHtml : Uint8Array ,
281- res : ServerResponse < import ( 'http' ) . IncomingMessage > ,
282- next : Connect . NextFunction ,
283- additionalTransformer ?: ( html : string ) => Promise < string | undefined > ,
284- ) {
285- server
286- . transformIndexHtml ( url , Buffer . from ( rawHtml ) . toString ( 'utf-8' ) )
287- . then ( async ( processedHtml ) => {
288- if ( additionalTransformer ) {
289- const content = await additionalTransformer ( processedHtml ) ;
290- if ( ! content ) {
291- next ( ) ;
292-
293- return ;
294- }
295-
296- processedHtml = content ;
297- }
298-
299- res . setHeader ( 'Content-Type' , 'text/html' ) ;
300- res . setHeader ( 'Cache-Control' , 'no-cache' ) ;
301- if ( extraHeaders ) {
302- Object . entries ( extraHeaders ) . forEach ( ( [ name , value ] ) => res . setHeader ( name , value ) ) ;
303- }
304- res . end ( processedHtml ) ;
305- } )
306- . catch ( ( error ) => next ( error ) ) ;
307- }
308137 } ,
309138 } ;
310139}
@@ -334,61 +163,3 @@ async function loadViteClientCode(file: string): Promise<string> {
334163
335164 return updatedContents ;
336165}
337-
338- function pathnameWithoutBasePath ( url : string , basePath : string ) : string {
339- const parsedUrl = new URL ( url , 'http://localhost' ) ;
340- const pathname = decodeURIComponent ( parsedUrl . pathname ) ;
341-
342- // slice(basePath.length - 1) to retain the trailing slash
343- return basePath !== '/' && pathname . startsWith ( basePath )
344- ? pathname . slice ( basePath . length - 1 )
345- : pathname ;
346- }
347-
348- function angularHtmlFallbackMiddleware (
349- req : Connect . IncomingMessage ,
350- res : ServerResponse ,
351- next : Connect . NextFunction ,
352- ) : void {
353- // Similar to how it is handled in vite
354- // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/middlewares/htmlFallback.ts#L15C19-L15C45
355- if (
356- ( req . method === 'GET' || req . method === 'HEAD' ) &&
357- ( ! req . url || ! lookupMimeTypeFromRequest ( req . url ) ) &&
358- ( ! req . headers . accept ||
359- req . headers . accept . includes ( 'text/html' ) ||
360- req . headers . accept . includes ( 'text/*' ) ||
361- req . headers . accept . includes ( '*/*' ) )
362- ) {
363- req . url = '/index.html' ;
364- }
365-
366- next ( ) ;
367- }
368-
369- function lookupMimeTypeFromRequest ( url : string ) : string | undefined {
370- const extension = extname ( url . split ( '?' ) [ 0 ] ) ;
371-
372- if ( extension === '.ico' ) {
373- return 'image/x-icon' ;
374- }
375-
376- return extension && lookupMimeType ( extension ) ;
377- }
378-
379- function redirect ( res : ServerResponse , location : string ) : void {
380- res . statusCode = 301 ;
381- res . setHeader ( 'Content-Type' , 'text/html' ) ;
382- res . setHeader ( 'Location' , location ) ;
383- res . end ( `
384- <!DOCTYPE html>
385- <html lang="en">
386- <head>
387- <meta charset="utf-8">
388- <title>Redirecting</title>
389- </head>
390- <body>
391- <pre>Redirecting to <a href="${ location } ">${ location } </a></pre>
392- </body>
393- </html>` ) ;
394- }
0 commit comments