Skip to content

Commit f7529c3

Browse files
feat: allow to configure the Cache-Control header (#1923)
1 parent a516834 commit f7529c3

File tree

6 files changed

+300
-14
lines changed

6 files changed

+300
-14
lines changed

README.md

+30-14
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,22 @@ See [below](#other-servers) for an example of use with fastify.
6060

6161
## Options
6262

63-
| Name | Type | Default | Description |
64-
| :---------------------------------------------: | :---------------------------: | :-------------------------------------------: | :------------------------------------------------------------------------------------------------------------------- |
65-
| **[`methods`](#methods)** | `Array` | `[ 'GET', 'HEAD' ]` | Allows to pass the list of HTTP request methods accepted by the middleware |
66-
| **[`headers`](#headers)** | `Array\|Object\|Function` | `undefined` | Allows to pass custom HTTP headers on each request. |
67-
| **[`index`](#index)** | `Boolean\|String` | `index.html` | If `false` (but not `undefined`), the server will not respond to requests to the root URL. |
68-
| **[`mimeTypes`](#mimetypes)** | `Object` | `undefined` | Allows to register custom mime types or extension mappings. |
69-
| **[`mimeTypeDefault`](#mimetypedefault)** | `String` | `undefined` | Allows to register a default mime type when we can't determine the content type. |
70-
| **[`etag`](#tag)** | `boolean\| "weak"\| "strong"` | `undefined` | Enable or disable etag generation. |
71-
| **[`publicPath`](#publicpath)** | `String` | `output.publicPath` (from a configuration) | The public path that the middleware is bound to. |
72-
| **[`stats`](#stats)** | `Boolean\|String\|Object` | `stats` (from a configuration) | Stats options object or preset name. |
73-
| **[`serverSideRender`](#serversiderender)** | `Boolean` | `undefined` | Instructs the module to enable or disable the server-side rendering mode. |
74-
| **[`writeToDisk`](#writetodisk)** | `Boolean\|Function` | `false` | Instructs the module to write files to the configured location on disk as specified in your `webpack` configuration. |
75-
| **[`outputFileSystem`](#outputfilesystem)** | `Object` | [`memfs`](https://github.com/streamich/memfs) | Set the default file system which will be used by webpack as primary destination of generated files. |
76-
| **[`modifyResponseData`](#modifyresponsedata)** | `Function` | `undefined` | Allows to set up a callback to change the response data. |
63+
| Name | Type | Default | Description |
64+
| :---------------------------------------------: | :-------------------------------: | :-------------------------------------------: | :------------------------------------------------------------------------------------------------------------------- |
65+
| **[`methods`](#methods)** | `Array` | `[ 'GET', 'HEAD' ]` | Allows to pass the list of HTTP request methods accepted by the middleware |
66+
| **[`headers`](#headers)** | `Array\|Object\|Function` | `undefined` | Allows to pass custom HTTP headers on each request. |
67+
| **[`index`](#index)** | `boolean\|string` | `index.html` | If `false` (but not `undefined`), the server will not respond to requests to the root URL. |
68+
| **[`mimeTypes`](#mimetypes)** | `Object` | `undefined` | Allows to register custom mime types or extension mappings. |
69+
| **[`mimeTypeDefault`](#mimetypedefault)** | `string` | `undefined` | Allows to register a default mime type when we can't determine the content type. |
70+
| **[`etag`](#tag)** | `boolean\| "weak"\| "strong"` | `undefined` | Enable or disable etag generation. |
71+
| **[`lastModified`](#lastmodified)** | `boolean` | `undefined` | Enable or disable `Last-Modified` header. Uses the file system's last modified value. |
72+
| **[`cacheControl`](#cachecontrol)** | `boolean\|number\|string\|Object` | `undefined` | Enable or disable `Last-Modified` header. Uses the file system's last modified value. |
73+
| **[`publicPath`](#publicpath)** | `string` | `undefined` | The public path that the middleware is bound to. |
74+
| **[`stats`](#stats)** | `boolean\|string\|Object` | `stats` (from a configuration) | Stats options object or preset name. |
75+
| **[`serverSideRender`](#serversiderender)** | `boolean` | `undefined` | Instructs the module to enable or disable the server-side rendering mode. |
76+
| **[`writeToDisk`](#writetodisk)** | `boolean\|Function` | `false` | Instructs the module to write files to the configured location on disk as specified in your `webpack` configuration. |
77+
| **[`outputFileSystem`](#outputfilesystem)** | `Object` | [`memfs`](https://github.com/streamich/memfs) | Set the default file system which will be used by webpack as primary destination of generated files. |
78+
| **[`modifyResponseData`](#modifyresponsedata)** | `Function` | `undefined` | Allows to set up a callback to change the response data. |
7779

7880
The middleware accepts an `options` Object. The following is a property reference for the Object.
7981

@@ -186,6 +188,20 @@ Default: `undefined`
186188

187189
Enable or disable `Last-Modified` header. Uses the file system's last modified value.
188190

191+
### cacheControl
192+
193+
Type: `Boolean | Number | String | { maxAge?: number, immutable?: boolean }`
194+
Default: `undefined`
195+
196+
Depending on the setting, the following headers will be generated:
197+
198+
- `Boolean` - `Cache-Control: public, max-age=31536000000`
199+
- `Number` - `Cache-Control: public, max-age=YOUR_NUMBER`
200+
- `String` - `Cache-Control: YOUR_STRING`
201+
- `{ maxAge?: number, immutable?: boolean }` - `Cache-Control: public, max-age=YOUR_MAX_AGE_or_31536000000`, also `, immutable` can be added if you set the `immutable` option to `true`
202+
203+
Enable or disable setting `Cache-Control` response header.
204+
189205
### publicPath
190206

191207
Type: `String`

src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ const noop = () => {};
118118
* @property {ModifyResponseData<RequestInternal, ResponseInternal>} [modifyResponseData]
119119
* @property {"weak" | "strong"} [etag]
120120
* @property {boolean} [lastModified]
121+
* @property {boolean | number | string | { maxAge: number, immutable: boolean }} [cacheControl]
121122
*/
122123

123124
/**

src/middleware.js

+37
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ const parseRangeHeaders = memorize(
118118
},
119119
);
120120

121+
const MAX_MAX_AGE = 31536000000;
122+
121123
/**
122124
* @template {IncomingMessage} Request
123125
* @template {ServerResponse} Response
@@ -549,6 +551,41 @@ function wrapper(context) {
549551
setResponseHeader(res, "Accept-Ranges", "bytes");
550552
}
551553

554+
if (
555+
context.options.cacheControl &&
556+
!getResponseHeader(res, "Cache-Control")
557+
) {
558+
const { cacheControl } = context.options;
559+
560+
let cacheControlValue;
561+
562+
if (typeof cacheControl === "boolean") {
563+
cacheControlValue = "public, max-age=31536000";
564+
} else if (typeof cacheControl === "number") {
565+
const maxAge = Math.floor(
566+
Math.min(Math.max(0, cacheControl), MAX_MAX_AGE) / 1000,
567+
);
568+
569+
cacheControlValue = `public, max-age=${maxAge}`;
570+
} else if (typeof cacheControl === "string") {
571+
cacheControlValue = cacheControl;
572+
} else {
573+
const maxAge = cacheControl.maxAge
574+
? Math.floor(
575+
Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE) / 1000,
576+
)
577+
: MAX_MAX_AGE;
578+
579+
cacheControlValue = `public, max-age=${maxAge}`;
580+
581+
if (cacheControl.immutable) {
582+
cacheControlValue += ", immutable";
583+
}
584+
}
585+
586+
setResponseHeader(res, "Cache-Control", cacheControlValue);
587+
}
588+
552589
if (
553590
context.options.lastModified &&
554591
!getResponseHeader(res, "Last-Modified")

src/options.json

+28
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,34 @@
139139
"description": "Enable or disable `Last-Modified` header. Uses the file system's last modified value.",
140140
"link": "https://github.com/webpack/webpack-dev-middleware#lastmodified",
141141
"type": "boolean"
142+
},
143+
"cacheControl": {
144+
"description": "Enable or disable setting `Cache-Control` response header.",
145+
"link": "https://github.com/webpack/webpack-dev-middleware#cachecontrol",
146+
"anyOf": [
147+
{
148+
"type": "boolean"
149+
},
150+
{
151+
"type": "number"
152+
},
153+
{
154+
"type": "string",
155+
"minLength": 1
156+
},
157+
{
158+
"type": "object",
159+
"properties": {
160+
"maxAge": {
161+
"type": "number"
162+
},
163+
"immutable": {
164+
"type": "boolean"
165+
}
166+
},
167+
"additionalProperties": false
168+
}
169+
]
142170
}
143171
},
144172
"additionalProperties": false

test/middleware.test.js

+194
Original file line numberDiff line numberDiff line change
@@ -5511,5 +5511,199 @@ describe.each([
55115511
});
55125512
});
55135513
});
5514+
5515+
describe.only("cacheControl", () => {
5516+
describe("should work and don't generate `Cache-Control` header by default", () => {
5517+
beforeEach(async () => {
5518+
const compiler = getCompiler(webpackConfig);
5519+
5520+
[server, req, instance] = await frameworkFactory(
5521+
name,
5522+
framework,
5523+
compiler,
5524+
);
5525+
});
5526+
5527+
afterEach(async () => {
5528+
await close(server, instance);
5529+
});
5530+
5531+
it('should return the "200" code for the "GET" request to the bundle file and don\'t generate `Cache-Control` header', async () => {
5532+
const response = await req.get(`/bundle.js`);
5533+
5534+
expect(response.statusCode).toEqual(200);
5535+
expect(response.headers["cache-control"]).toBeUndefined();
5536+
});
5537+
});
5538+
5539+
describe("should work and generate `Cache-Control` header when it is `true`", () => {
5540+
beforeEach(async () => {
5541+
const compiler = getCompiler(webpackConfig);
5542+
5543+
[server, req, instance] = await frameworkFactory(
5544+
name,
5545+
framework,
5546+
compiler,
5547+
{ cacheControl: true },
5548+
);
5549+
});
5550+
5551+
afterEach(async () => {
5552+
await close(server, instance);
5553+
});
5554+
5555+
it('should return the "200" code for the "GET" request to the bundle file and don\'t generate `Cache-Control` header', async () => {
5556+
const response = await req.get(`/bundle.js`);
5557+
5558+
expect(response.statusCode).toEqual(200);
5559+
expect(response.headers["cache-control"]).toBeDefined();
5560+
expect(response.headers["cache-control"]).toBe(
5561+
"public, max-age=31536000",
5562+
);
5563+
});
5564+
});
5565+
5566+
describe("should work and generate `Cache-Control` header when it is a number", () => {
5567+
beforeEach(async () => {
5568+
const compiler = getCompiler(webpackConfig);
5569+
5570+
[server, req, instance] = await frameworkFactory(
5571+
name,
5572+
framework,
5573+
compiler,
5574+
{ cacheControl: 100000 },
5575+
);
5576+
});
5577+
5578+
afterEach(async () => {
5579+
await close(server, instance);
5580+
});
5581+
5582+
it('should return the "200" code for the "GET" request to the bundle file and don\'t generate `Cache-Control` header', async () => {
5583+
const response = await req.get(`/bundle.js`);
5584+
5585+
expect(response.statusCode).toEqual(200);
5586+
expect(response.headers["cache-control"]).toBeDefined();
5587+
expect(response.headers["cache-control"]).toBe("public, max-age=100");
5588+
});
5589+
});
5590+
5591+
describe("should work and generate `Cache-Control` header when it is a string", () => {
5592+
beforeEach(async () => {
5593+
const compiler = getCompiler(webpackConfig);
5594+
5595+
[server, req, instance] = await frameworkFactory(
5596+
name,
5597+
framework,
5598+
compiler,
5599+
{ cacheControl: "max-age=123456" },
5600+
);
5601+
});
5602+
5603+
afterEach(async () => {
5604+
await close(server, instance);
5605+
});
5606+
5607+
it('should return the "200" code for the "GET" request to the bundle file and don\'t generate `Cache-Control` header', async () => {
5608+
const response = await req.get(`/bundle.js`);
5609+
5610+
expect(response.statusCode).toEqual(200);
5611+
expect(response.headers["cache-control"]).toBeDefined();
5612+
expect(response.headers["cache-control"]).toBe("max-age=123456");
5613+
});
5614+
});
5615+
5616+
describe("should work and generate `Cache-Control` header when it is an object with max-age and immutable", () => {
5617+
beforeEach(async () => {
5618+
const compiler = getCompiler(webpackConfig);
5619+
5620+
[server, req, instance] = await frameworkFactory(
5621+
name,
5622+
framework,
5623+
compiler,
5624+
{
5625+
cacheControl: {
5626+
maxAge: 100000,
5627+
immutable: true,
5628+
},
5629+
},
5630+
);
5631+
});
5632+
5633+
afterEach(async () => {
5634+
await close(server, instance);
5635+
});
5636+
5637+
it('should return the "200" code for the "GET" request to the bundle file and don\'t generate `Cache-Control` header', async () => {
5638+
const response = await req.get(`/bundle.js`);
5639+
5640+
expect(response.statusCode).toEqual(200);
5641+
expect(response.headers["cache-control"]).toBeDefined();
5642+
expect(response.headers["cache-control"]).toBe(
5643+
"public, max-age=100, immutable",
5644+
);
5645+
});
5646+
});
5647+
5648+
describe("should work and generate `Cache-Control` header when it is an object without max-age, but with immutable", () => {
5649+
beforeEach(async () => {
5650+
const compiler = getCompiler(webpackConfig);
5651+
5652+
[server, req, instance] = await frameworkFactory(
5653+
name,
5654+
framework,
5655+
compiler,
5656+
{
5657+
cacheControl: {
5658+
immutable: true,
5659+
},
5660+
},
5661+
);
5662+
});
5663+
5664+
afterEach(async () => {
5665+
await close(server, instance);
5666+
});
5667+
5668+
it('should return the "200" code for the "GET" request to the bundle file and don\'t generate `Cache-Control` header', async () => {
5669+
const response = await req.get(`/bundle.js`);
5670+
5671+
expect(response.statusCode).toEqual(200);
5672+
expect(response.headers["cache-control"]).toBeDefined();
5673+
expect(response.headers["cache-control"]).toBe(
5674+
"public, max-age=31536000000, immutable",
5675+
);
5676+
});
5677+
});
5678+
5679+
describe("should work and generate `Cache-Control` header when it is an object with max-age, but without immutable", () => {
5680+
beforeEach(async () => {
5681+
const compiler = getCompiler(webpackConfig);
5682+
5683+
[server, req, instance] = await frameworkFactory(
5684+
name,
5685+
framework,
5686+
compiler,
5687+
{
5688+
cacheControl: {
5689+
maxAge: 100000,
5690+
},
5691+
},
5692+
);
5693+
});
5694+
5695+
afterEach(async () => {
5696+
await close(server, instance);
5697+
});
5698+
5699+
it('should return the "200" code for the "GET" request to the bundle file and don\'t generate `Cache-Control` header', async () => {
5700+
const response = await req.get(`/bundle.js`);
5701+
5702+
expect(response.statusCode).toEqual(200);
5703+
expect(response.headers["cache-control"]).toBeDefined();
5704+
expect(response.headers["cache-control"]).toBe("public, max-age=100");
5705+
});
5706+
});
5707+
});
55145708
});
55155709
});

types/index.d.ts

+10
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export = wdm;
9090
* @property {ModifyResponseData<RequestInternal, ResponseInternal>} [modifyResponseData]
9191
* @property {"weak" | "strong"} [etag]
9292
* @property {boolean} [lastModified]
93+
* @property {boolean | number | string | { maxAge: number, immutable: boolean }} [cacheControl]
9394
*/
9495
/**
9596
* @template {IncomingMessage} [RequestInternal=IncomingMessage]
@@ -354,6 +355,15 @@ type Options<
354355
| undefined;
355356
etag?: "strong" | "weak" | undefined;
356357
lastModified?: boolean | undefined;
358+
cacheControl?:
359+
| string
360+
| number
361+
| boolean
362+
| {
363+
maxAge: number;
364+
immutable: boolean;
365+
}
366+
| undefined;
357367
};
358368
type Middleware<
359369
RequestInternal extends IncomingMessage = import("http").IncomingMessage,

0 commit comments

Comments
 (0)