@@ -22,7 +22,7 @@ const PNG = 'image/png'
2222const JPEG = 'image/jpeg'
2323const GIF = 'image/gif'
2424const SVG = 'image/svg+xml'
25- const CACHE_VERSION = 2
25+ const CACHE_VERSION = 3
2626const MODERN_TYPES = [ /* AVIF, */ WEBP ]
2727const ANIMATABLE_TYPES = [ WEBP , PNG , GIF ]
2828const VECTOR_TYPES = [ SVG ]
@@ -35,7 +35,8 @@ export async function imageOptimizer(
3535 res : ServerResponse ,
3636 parsedUrl : UrlWithParsedQuery ,
3737 nextConfig : NextConfig ,
38- distDir : string
38+ distDir : string ,
39+ isDev = false
3940) {
4041 const imageData : ImageConfig = nextConfig . images || imageConfigDefault
4142 const { deviceSizes = [ ] , imageSizes = [ ] , domains = [ ] , loader } = imageData
@@ -158,24 +159,24 @@ export async function imageOptimizer(
158159 if ( await fileExists ( hashDir , 'directory' ) ) {
159160 const files = await promises . readdir ( hashDir )
160161 for ( let file of files ) {
161- const [ prefix , etag , extension ] = file . split ( '.' )
162- const expireAt = Number ( prefix )
162+ const [ maxAgeStr , expireAtSt , etag , extension ] = file . split ( '.' )
163+ const maxAge = Number ( maxAgeStr )
164+ const expireAt = Number ( expireAtSt )
163165 const contentType = getContentType ( extension )
164166 const fsPath = join ( hashDir , file )
165167 if ( now < expireAt ) {
166- res . setHeader (
167- 'Cache-Control' ,
168- isStatic
169- ? 'public, max-age=315360000, immutable'
170- : 'public, max-age=0, must-revalidate'
168+ const result = setResponseHeaders (
169+ req ,
170+ res ,
171+ etag ,
172+ maxAge ,
173+ contentType ,
174+ isStatic ,
175+ isDev
171176 )
172- if ( sendEtagResponse ( req , res , etag ) ) {
173- return { finished : true }
177+ if ( ! result . finished ) {
178+ createReadStream ( fsPath ) . pipe ( res )
174179 }
175- if ( contentType ) {
176- res . setHeader ( 'Content-Type' , contentType )
177- }
178- createReadStream ( fsPath ) . pipe ( res )
179180 return { finished : true }
180181 } else {
181182 await promises . unlink ( fsPath )
@@ -271,8 +272,22 @@ export async function imageOptimizer(
271272 const animate =
272273 ANIMATABLE_TYPES . includes ( upstreamType ) && isAnimated ( upstreamBuffer )
273274 if ( vector || animate ) {
274- await writeToCacheDir ( hashDir , upstreamType , expireAt , upstreamBuffer )
275- sendResponse ( req , res , upstreamType , upstreamBuffer , isStatic )
275+ await writeToCacheDir (
276+ hashDir ,
277+ upstreamType ,
278+ maxAge ,
279+ expireAt ,
280+ upstreamBuffer
281+ )
282+ sendResponse (
283+ req ,
284+ res ,
285+ maxAge ,
286+ upstreamType ,
287+ upstreamBuffer ,
288+ isStatic ,
289+ isDev
290+ )
276291 return { finished : true }
277292 }
278293
@@ -342,13 +357,35 @@ export async function imageOptimizer(
342357 }
343358
344359 if ( optimizedBuffer ) {
345- await writeToCacheDir ( hashDir , contentType , expireAt , optimizedBuffer )
346- sendResponse ( req , res , contentType , optimizedBuffer , isStatic )
360+ await writeToCacheDir (
361+ hashDir ,
362+ contentType ,
363+ maxAge ,
364+ expireAt ,
365+ optimizedBuffer
366+ )
367+ sendResponse (
368+ req ,
369+ res ,
370+ maxAge ,
371+ contentType ,
372+ optimizedBuffer ,
373+ isStatic ,
374+ isDev
375+ )
347376 } else {
348377 throw new Error ( 'Unable to optimize buffer' )
349378 }
350379 } catch ( error ) {
351- sendResponse ( req , res , upstreamType , upstreamBuffer , isStatic )
380+ sendResponse (
381+ req ,
382+ res ,
383+ maxAge ,
384+ upstreamType ,
385+ upstreamBuffer ,
386+ isStatic ,
387+ isDev
388+ )
352389 }
353390
354391 return { finished : true }
@@ -362,37 +399,64 @@ export async function imageOptimizer(
362399async function writeToCacheDir (
363400 dir : string ,
364401 contentType : string ,
402+ maxAge : number ,
365403 expireAt : number ,
366404 buffer : Buffer
367405) {
368406 await promises . mkdir ( dir , { recursive : true } )
369407 const extension = getExtension ( contentType )
370408 const etag = getHash ( [ buffer ] )
371- const filename = join ( dir , `${ expireAt } .${ etag } .${ extension } ` )
409+ const filename = join ( dir , `${ maxAge } . ${ expireAt } .${ etag } .${ extension } ` )
372410 await promises . writeFile ( filename , buffer )
373411}
374412
375- function sendResponse (
413+ function setResponseHeaders (
376414 req : IncomingMessage ,
377415 res : ServerResponse ,
416+ etag : string ,
417+ maxAge : number ,
378418 contentType : string | null ,
379- buffer : Buffer ,
380- isStatic : boolean
419+ isStatic : boolean ,
420+ isDev : boolean
381421) {
382- const etag = getHash ( [ buffer ] )
383422 res . setHeader (
384423 'Cache-Control' ,
385424 isStatic
386425 ? 'public, max-age=315360000, immutable'
387- : ' public, max-age=0 , must-revalidate'
426+ : ` public, max-age=${ isDev ? 0 : maxAge } , must-revalidate`
388427 )
389428 if ( sendEtagResponse ( req , res , etag ) ) {
390- return
429+ // already called res.end() so we're finished
430+ return { finished : true }
391431 }
392432 if ( contentType ) {
393433 res . setHeader ( 'Content-Type' , contentType )
394434 }
395- res . end ( buffer )
435+ return { finished : false }
436+ }
437+
438+ function sendResponse (
439+ req : IncomingMessage ,
440+ res : ServerResponse ,
441+ maxAge : number ,
442+ contentType : string | null ,
443+ buffer : Buffer ,
444+ isStatic : boolean ,
445+ isDev : boolean
446+ ) {
447+ const etag = getHash ( [ buffer ] )
448+ const result = setResponseHeaders (
449+ req ,
450+ res ,
451+ etag ,
452+ maxAge ,
453+ contentType ,
454+ isStatic ,
455+ isDev
456+ )
457+ if ( ! result . finished ) {
458+ res . end ( buffer )
459+ }
396460}
397461
398462function getSupportedMimeType ( options : string [ ] , accept = '' ) : string {
0 commit comments