Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -582,12 +586,20 @@ function wrapper(context) {

if (typeof cacheControl === "boolean") {
cacheControlValue = "public, max-age=31536000";

if (hasCacheImmutable) {
cacheControlValue += ", immutable";
}
Comment thread
bjohansebas marked this conversation as resolved.
Outdated
} 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 {
Expand All @@ -600,7 +612,7 @@ function wrapper(context) {

cacheControlValue = `public, max-age=${maxAge}`;

if (cacheControl.immutable) {
if (cacheControl.immutable !== false && hasCacheImmutable) {
cacheControlValue += ", immutable";
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we refactor this place? Convert boolean/number/string to object value to avoid logic duplication?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}
Expand Down
299 changes: 293 additions & 6 deletions test/middleware.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
});
});
Expand All @@ -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",
);
});
});

Expand Down Expand Up @@ -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",
);
});
});

Expand Down Expand Up @@ -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",
);
});
});
Expand All @@ -6129,7 +6133,6 @@ describe.each([
name,
framework,
compiler,
{ cacheImmutable: true },
Copy link
Copy Markdown
Member

@alexander-akait alexander-akait Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bjohansebas Why it was changed? This is a serous breaking change, by default we should cache only immutable (we store it in asset info) , i.e. files witch have contenthash or other hashed, we need to revert it ASAP

Please avoid merge without approve

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was changed to check how it behaves when that option doesn’t exist, and that’s why I added tests below to verify how it behaves when cacheImmutable is explicitly set to false

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only breaking change is that now, if an asset is immutable, it will add that header by default. Otherwise, it behaves as before. In the first commit there was an error, but after reviewing the code I realized it was wrong, which is why I added those two new commits that fixed the issue (0f5c4f6, d66f6b8) where the header was being added even when the asset was not immutable yet.

);
});

Expand Down Expand Up @@ -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",
);
});

Expand All @@ -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",
);
});
});
});
});
});