7
7
*/
8
8
9
9
import remapping , { SourceMapInput } from '@ampproject/remapping' ;
10
- import { lookup as lookupMimeType } from 'mrmime' ;
11
10
import assert from 'node:assert' ;
12
11
import { 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' ;
15
13
import 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' ;
17
21
18
22
export interface AngularMemoryPluginOptions {
19
23
workspaceRoot : string ;
20
24
virtualProjectRoot : string ;
21
- outputFiles : Map < string , { contents : Uint8Array ; servable : boolean } > ;
25
+ outputFiles : AngularMemoryOutputFiles ;
22
26
assets : Map < string , string > ;
23
27
ssr : boolean ;
24
28
external ?: string [ ] ;
25
29
extensionMiddleware ?: Connect . NextHandleFunction [ ] ;
26
- extraHeaders ?: Record < string , string > ;
27
30
indexHtmlTransformer ?: ( content : string ) => Promise < string > ;
28
31
normalizePath : ( path : string ) => string ;
29
32
}
30
33
31
- // eslint-disable-next-line max-lines-per-function
32
34
export function createAngularMemoryPlugin ( options : AngularMemoryPluginOptions ) : Plugin {
33
35
const {
34
36
workspaceRoot,
@@ -38,7 +40,6 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
38
40
external,
39
41
ssr,
40
42
extensionMiddleware,
41
- extraHeaders,
42
43
indexHtmlTransformer,
43
44
normalizePath,
44
45
} = options ;
@@ -112,84 +113,7 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
112
113
} ;
113
114
114
115
// 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 ) ) ;
193
117
194
118
if ( extensionMiddleware ?. length ) {
195
119
extensionMiddleware . forEach ( ( middleware ) => server . middlewares . use ( middleware ) ) ;
@@ -200,111 +124,16 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
200
124
return ( ) => {
201
125
server . middlewares . use ( angularHtmlFallbackMiddleware ) ;
202
126
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
-
250
127
if ( ssr ) {
251
- server . middlewares . use ( angularSSRMiddleware ) ;
128
+ server . middlewares . use (
129
+ createAngularSSRMiddleware ( server , outputFiles , indexHtmlTransformer ) ,
130
+ ) ;
252
131
}
253
132
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
+ ) ;
276
136
} ;
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
- }
308
137
} ,
309
138
} ;
310
139
}
@@ -334,61 +163,3 @@ async function loadViteClientCode(file: string): Promise<string> {
334
163
335
164
return updatedContents ;
336
165
}
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