From 4e0bb322402b6e9ab111471e31cd7ed2f46dd84f Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 19 Jan 2026 15:16:18 -0500 Subject: [PATCH 1/6] feat: enable cacheImmutable by default --- src/middleware.js | 18 ++- test/middleware.test.js | 299 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 308 insertions(+), 9 deletions(-) diff --git a/src/middleware.js b/src/middleware.js index ed858d6f9..2bc326fc6 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -571,9 +571,13 @@ function wrapper(context) { } if (!getResponseHeader(res, "Cache-Control")) { - // TODO enable the `cacheImmutable` by default for the next major release + const hasCacheImmutable = + context.options.cacheImmutable === undefined + ? true + : context.options.cacheImmutable; + const cacheControl = - context.options.cacheImmutable && extra.immutable + hasCacheImmutable && extra.immutable ? { immutable: true } : context.options.cacheControl; @@ -582,12 +586,20 @@ function wrapper(context) { if (typeof cacheControl === "boolean") { cacheControlValue = "public, max-age=31536000"; + + if (hasCacheImmutable) { + cacheControlValue += ", immutable"; + } } else if (typeof cacheControl === "number") { const maxAge = Math.floor( Math.min(Math.max(0, cacheControl), MAX_MAX_AGE) / 1000, ); cacheControlValue = `public, max-age=${maxAge}`; + + if (hasCacheImmutable) { + cacheControlValue += ", immutable"; + } } else if (typeof cacheControl === "string") { cacheControlValue = cacheControl; } else { @@ -600,7 +612,7 @@ function wrapper(context) { cacheControlValue = `public, max-age=${maxAge}`; - if (cacheControl.immutable) { + if (cacheControl.immutable !== false && hasCacheImmutable) { cacheControlValue += ", immutable"; } } diff --git a/test/middleware.test.js b/test/middleware.test.js index 091528f0f..ad1eb844e 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -5943,7 +5943,7 @@ describe.each([ expect(response.statusCode).toBe(200); expect(response.headers["cache-control"]).toBeDefined(); expect(response.headers["cache-control"]).toBe( - "public, max-age=31536000", + "public, max-age=31536000, immutable", ); }); }); @@ -5969,7 +5969,9 @@ describe.each([ expect(response.statusCode).toBe(200); expect(response.headers["cache-control"]).toBeDefined(); - expect(response.headers["cache-control"]).toBe("public, max-age=100"); + expect(response.headers["cache-control"]).toBe( + "public, max-age=100, immutable", + ); }); }); @@ -6086,7 +6088,9 @@ describe.each([ expect(response.statusCode).toBe(200); expect(response.headers["cache-control"]).toBeDefined(); - expect(response.headers["cache-control"]).toBe("public, max-age=100"); + expect(response.headers["cache-control"]).toBe( + "public, max-age=100, immutable", + ); }); }); @@ -6114,7 +6118,7 @@ describe.each([ expect(response.statusCode).toBe(200); expect(response.headers["cache-control"]).toBeDefined(); expect(response.headers["cache-control"]).toBe( - "public, max-age=31536000", + "public, max-age=31536000, immutable", ); }); }); @@ -6129,7 +6133,6 @@ describe.each([ name, framework, compiler, - { cacheImmutable: true }, ); }); @@ -6221,7 +6224,7 @@ describe.each([ expect(response.statusCode).toBe(200); expect(response.headers["cache-control"]).toBeDefined(); expect(response.headers["cache-control"]).toBe( - "public, max-age=1000", + "public, max-age=1000, immutable", ); }); @@ -6235,6 +6238,290 @@ describe.each([ ); }); }); + + describe("should not generate `Cache-Control` header for immutable assets when cacheImmutable is false", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheImmutable: false }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and don\'t generate `Cache-Control` header', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeUndefined(); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and don\'t generate `Cache-Control` header', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeUndefined(); + }); + }); + + describe("should use cacheControl option when cacheImmutable is false even for immutable assets", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheImmutable: false, cacheControl: 1000000 }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header from cacheControl option', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=1000", + ); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and generate `Cache-Control` header from cacheControl option (not immutable)', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=1000", + ); + }); + }); + + describe("should use cacheControl string option when cacheImmutable is false", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheImmutable: false, cacheControl: "max-age=500" }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header from cacheControl string option without immutable', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe("max-age=500"); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and generate `Cache-Control` header from cacheControl string option without immutable', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe("max-age=500"); + }); + }); + + describe("should use cacheControl object option when cacheImmutable is false", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheImmutable: false, cacheControl: { maxAge: 2000000 } }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header from cacheControl object option without immutable', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=2000", + ); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and generate `Cache-Control` header from cacheControl object option without immutable', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=2000", + ); + }); + }); + + describe("should use cacheControl object option with explicit immutable false", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheControl: { maxAge: 3000000, immutable: false } }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header without immutable when explicitly set to false', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=3000", + ); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and generate `Cache-Control` header without immutable when explicitly set to false', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=31536000, immutable", + ); + }); + }); + + describe("should use cacheControl boolean option when cacheImmutable is false", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheImmutable: false, cacheControl: true }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header from cacheControl boolean option without immutable', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=31536000", + ); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and generate `Cache-Control` header from cacheControl boolean option without immutable', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=31536000", + ); + }); + }); + + describe("should use cacheControl number option when cacheImmutable is false without immutable", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheImmutable: false, cacheControl: 5000000 }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header from cacheControl number option without immutable', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=5000", + ); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and generate `Cache-Control` header from cacheControl number option without immutable', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=5000", + ); + }); + }); }); }); }); From 4d25a229986f1613161c7df6675086e053e50507 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 24 Jan 2026 12:59:27 -0500 Subject: [PATCH 2/6] fix: thoses files shouldn't immutable --- src/middleware.js | 8 -------- test/middleware.test.js | 10 ++++------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/middleware.js b/src/middleware.js index 2bc326fc6..a0314c42f 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -586,20 +586,12 @@ function wrapper(context) { if (typeof cacheControl === "boolean") { cacheControlValue = "public, max-age=31536000"; - - if (hasCacheImmutable) { - cacheControlValue += ", immutable"; - } } else if (typeof cacheControl === "number") { const maxAge = Math.floor( Math.min(Math.max(0, cacheControl), MAX_MAX_AGE) / 1000, ); cacheControlValue = `public, max-age=${maxAge}`; - - if (hasCacheImmutable) { - cacheControlValue += ", immutable"; - } } else if (typeof cacheControl === "string") { cacheControlValue = cacheControl; } else { diff --git a/test/middleware.test.js b/test/middleware.test.js index ad1eb844e..5909f41af 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -5943,7 +5943,7 @@ describe.each([ expect(response.statusCode).toBe(200); expect(response.headers["cache-control"]).toBeDefined(); expect(response.headers["cache-control"]).toBe( - "public, max-age=31536000, immutable", + "public, max-age=31536000", ); }); }); @@ -5964,14 +5964,12 @@ describe.each([ await close(server, instance); }); - it('should return the "200" code for the "GET" request to the bundle file and don\'t generate `Cache-Control` header', async () => { + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header', async () => { const response = await req.get("/bundle.js"); expect(response.statusCode).toBe(200); expect(response.headers["cache-control"]).toBeDefined(); - expect(response.headers["cache-control"]).toBe( - "public, max-age=100, immutable", - ); + expect(response.headers["cache-control"]).toBe("public, max-age=100"); }); }); @@ -6224,7 +6222,7 @@ describe.each([ expect(response.statusCode).toBe(200); expect(response.headers["cache-control"]).toBeDefined(); expect(response.headers["cache-control"]).toBe( - "public, max-age=1000, immutable", + "public, max-age=1000", ); }); From 0f5c4f6b164768a216fb3b12d6f5b5d30a568331 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 24 Jan 2026 13:11:11 -0500 Subject: [PATCH 3/6] fix: update cache control logic becauses undefined !== false (true) --- src/middleware.js | 2 +- test/middleware.test.js | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/middleware.js b/src/middleware.js index a0314c42f..49706dc65 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -604,7 +604,7 @@ function wrapper(context) { cacheControlValue = `public, max-age=${maxAge}`; - if (cacheControl.immutable !== false && hasCacheImmutable) { + if (cacheControl.immutable && hasCacheImmutable) { cacheControlValue += ", immutable"; } } diff --git a/test/middleware.test.js b/test/middleware.test.js index 5909f41af..c2256f13d 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -6086,9 +6086,7 @@ describe.each([ expect(response.statusCode).toBe(200); expect(response.headers["cache-control"]).toBeDefined(); - expect(response.headers["cache-control"]).toBe( - "public, max-age=100, immutable", - ); + expect(response.headers["cache-control"]).toBe("public, max-age=100"); }); }); @@ -6116,7 +6114,7 @@ describe.each([ expect(response.statusCode).toBe(200); expect(response.headers["cache-control"]).toBeDefined(); expect(response.headers["cache-control"]).toBe( - "public, max-age=31536000, immutable", + "public, max-age=31536000", ); }); }); From d66f6b8109f006a242ddda95e930fd290350d5fe Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 24 Jan 2026 13:31:27 -0500 Subject: [PATCH 4/6] test: add tests for cacheControl option behavior with cacheImmutable set to false --- test/middleware.test.js | 42 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/middleware.test.js b/test/middleware.test.js index c2256f13d..1ce2d07fa 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -6393,6 +6393,48 @@ describe.each([ }); }); + describe("should use cacheControl object option (with only immutable: true) when cacheImmutable is false, and not add 'immutable' to Cache-Control header", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheImmutable: false, cacheControl: { immutable: true } }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header from cacheControl object option without immutable', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=31536000", + ); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and generate `Cache-Control` header from cacheControl object option without immutable', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=31536000", + ); + }); + }); + describe("should use cacheControl object option with explicit immutable false", () => { beforeEach(async () => { const compiler = getCompiler({ From a1d4cb99447b26b4fa5901479b888e1b4c3a08b4 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 24 Jan 2026 14:00:22 -0500 Subject: [PATCH 5/6] refactor: normalize cacheControl handling in response headers --- src/middleware.js | 48 ++++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/src/middleware.js b/src/middleware.js index 49706dc65..0ae398380 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -576,37 +576,33 @@ function wrapper(context) { ? true : context.options.cacheImmutable; - const cacheControl = - hasCacheImmutable && extra.immutable - ? { immutable: true } - : context.options.cacheControl; - - if (cacheControl) { - let cacheControlValue; - - if (typeof cacheControl === "boolean") { - cacheControlValue = "public, max-age=31536000"; - } else if (typeof cacheControl === "number") { - const maxAge = Math.floor( - Math.min(Math.max(0, cacheControl), MAX_MAX_AGE) / 1000, - ); + let { cacheControl } = context.options; + + // Normalize cacheControl to object + if (typeof cacheControl === "string") { + setResponseHeader(res, "Cache-Control", cacheControl); + } else if (hasCacheImmutable && extra.immutable) { + cacheControl = { immutable: true }; + } else if (typeof cacheControl === "boolean") { + cacheControl = { maxAge: MAX_MAX_AGE }; + } else if (typeof cacheControl === "number") { + cacheControl = { + maxAge: Math.min(Math.max(0, cacheControl), MAX_MAX_AGE), + }; + } - cacheControlValue = `public, max-age=${maxAge}`; - } else if (typeof cacheControl === "string") { - cacheControlValue = cacheControl; - } else { - const maxAge = cacheControl.maxAge + if (cacheControl && typeof cacheControl === "object") { + const maxAge = + cacheControl.maxAge !== undefined ? Math.floor( - Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE) / - 1000, + Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE), ) - : MAX_MAX_AGE / 1000; + : MAX_MAX_AGE; - cacheControlValue = `public, max-age=${maxAge}`; + let cacheControlValue = `public, max-age=${Math.floor(maxAge / 1000)}`; - if (cacheControl.immutable && hasCacheImmutable) { - cacheControlValue += ", immutable"; - } + if (cacheControl.immutable && hasCacheImmutable) { + cacheControlValue += ", immutable"; } setResponseHeader(res, "Cache-Control", cacheControlValue); From 548a48d64e698a054ca8a2cba476e81e23df5e3b Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 24 Jan 2026 14:05:16 -0500 Subject: [PATCH 6/6] refactor: simplify maxAge calculation in cacheControl handling --- src/middleware.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/middleware.js b/src/middleware.js index 0ae398380..11162482d 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -586,17 +586,13 @@ function wrapper(context) { } else if (typeof cacheControl === "boolean") { cacheControl = { maxAge: MAX_MAX_AGE }; } else if (typeof cacheControl === "number") { - cacheControl = { - maxAge: Math.min(Math.max(0, cacheControl), MAX_MAX_AGE), - }; + cacheControl = { maxAge: cacheControl }; } if (cacheControl && typeof cacheControl === "object") { const maxAge = cacheControl.maxAge !== undefined - ? Math.floor( - Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE), - ) + ? Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE) : MAX_MAX_AGE; let cacheControlValue = `public, max-age=${Math.floor(maxAge / 1000)}`;