Skip to content

Commit

Permalink
feat: allow to configure the Cache-Control header (#1923)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Aug 15, 2024
1 parent a516834 commit f7529c3
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 14 deletions.
44 changes: 30 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,22 @@ See [below](#other-servers) for an example of use with fastify.

## Options

| Name | Type | Default | Description |
| :---------------------------------------------: | :---------------------------: | :-------------------------------------------: | :------------------------------------------------------------------------------------------------------------------- |
| **[`methods`](#methods)** | `Array` | `[ 'GET', 'HEAD' ]` | Allows to pass the list of HTTP request methods accepted by the middleware |
| **[`headers`](#headers)** | `Array\|Object\|Function` | `undefined` | Allows to pass custom HTTP headers on each request. |
| **[`index`](#index)** | `Boolean\|String` | `index.html` | If `false` (but not `undefined`), the server will not respond to requests to the root URL. |
| **[`mimeTypes`](#mimetypes)** | `Object` | `undefined` | Allows to register custom mime types or extension mappings. |
| **[`mimeTypeDefault`](#mimetypedefault)** | `String` | `undefined` | Allows to register a default mime type when we can't determine the content type. |
| **[`etag`](#tag)** | `boolean\| "weak"\| "strong"` | `undefined` | Enable or disable etag generation. |
| **[`publicPath`](#publicpath)** | `String` | `output.publicPath` (from a configuration) | The public path that the middleware is bound to. |
| **[`stats`](#stats)** | `Boolean\|String\|Object` | `stats` (from a configuration) | Stats options object or preset name. |
| **[`serverSideRender`](#serversiderender)** | `Boolean` | `undefined` | Instructs the module to enable or disable the server-side rendering mode. |
| **[`writeToDisk`](#writetodisk)** | `Boolean\|Function` | `false` | Instructs the module to write files to the configured location on disk as specified in your `webpack` configuration. |
| **[`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. |
| **[`modifyResponseData`](#modifyresponsedata)** | `Function` | `undefined` | Allows to set up a callback to change the response data. |
| Name | Type | Default | Description |
| :---------------------------------------------: | :-------------------------------: | :-------------------------------------------: | :------------------------------------------------------------------------------------------------------------------- |
| **[`methods`](#methods)** | `Array` | `[ 'GET', 'HEAD' ]` | Allows to pass the list of HTTP request methods accepted by the middleware |
| **[`headers`](#headers)** | `Array\|Object\|Function` | `undefined` | Allows to pass custom HTTP headers on each request. |
| **[`index`](#index)** | `boolean\|string` | `index.html` | If `false` (but not `undefined`), the server will not respond to requests to the root URL. |
| **[`mimeTypes`](#mimetypes)** | `Object` | `undefined` | Allows to register custom mime types or extension mappings. |
| **[`mimeTypeDefault`](#mimetypedefault)** | `string` | `undefined` | Allows to register a default mime type when we can't determine the content type. |
| **[`etag`](#tag)** | `boolean\| "weak"\| "strong"` | `undefined` | Enable or disable etag generation. |
| **[`lastModified`](#lastmodified)** | `boolean` | `undefined` | Enable or disable `Last-Modified` header. Uses the file system's last modified value. |
| **[`cacheControl`](#cachecontrol)** | `boolean\|number\|string\|Object` | `undefined` | Enable or disable `Last-Modified` header. Uses the file system's last modified value. |
| **[`publicPath`](#publicpath)** | `string` | `undefined` | The public path that the middleware is bound to. |
| **[`stats`](#stats)** | `boolean\|string\|Object` | `stats` (from a configuration) | Stats options object or preset name. |
| **[`serverSideRender`](#serversiderender)** | `boolean` | `undefined` | Instructs the module to enable or disable the server-side rendering mode. |
| **[`writeToDisk`](#writetodisk)** | `boolean\|Function` | `false` | Instructs the module to write files to the configured location on disk as specified in your `webpack` configuration. |
| **[`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. |
| **[`modifyResponseData`](#modifyresponsedata)** | `Function` | `undefined` | Allows to set up a callback to change the response data. |

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

Expand Down Expand Up @@ -186,6 +188,20 @@ Default: `undefined`

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

### cacheControl

Type: `Boolean | Number | String | { maxAge?: number, immutable?: boolean }`
Default: `undefined`

Depending on the setting, the following headers will be generated:

- `Boolean` - `Cache-Control: public, max-age=31536000000`
- `Number` - `Cache-Control: public, max-age=YOUR_NUMBER`
- `String` - `Cache-Control: YOUR_STRING`
- `{ 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`

Enable or disable setting `Cache-Control` response header.

### publicPath

Type: `String`
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ const noop = () => {};
* @property {ModifyResponseData<RequestInternal, ResponseInternal>} [modifyResponseData]
* @property {"weak" | "strong"} [etag]
* @property {boolean} [lastModified]
* @property {boolean | number | string | { maxAge: number, immutable: boolean }} [cacheControl]
*/

/**
Expand Down
37 changes: 37 additions & 0 deletions src/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ const parseRangeHeaders = memorize(
},
);

const MAX_MAX_AGE = 31536000000;

/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
Expand Down Expand Up @@ -549,6 +551,41 @@ function wrapper(context) {
setResponseHeader(res, "Accept-Ranges", "bytes");
}

if (
context.options.cacheControl &&
!getResponseHeader(res, "Cache-Control")
) {
const { cacheControl } = context.options;

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,
);

cacheControlValue = `public, max-age=${maxAge}`;
} else if (typeof cacheControl === "string") {
cacheControlValue = cacheControl;
} else {
const maxAge = cacheControl.maxAge
? Math.floor(
Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE) / 1000,
)
: MAX_MAX_AGE;

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

if (cacheControl.immutable) {
cacheControlValue += ", immutable";
}
}

setResponseHeader(res, "Cache-Control", cacheControlValue);
}

if (
context.options.lastModified &&
!getResponseHeader(res, "Last-Modified")
Expand Down
28 changes: 28 additions & 0 deletions src/options.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,34 @@
"description": "Enable or disable `Last-Modified` header. Uses the file system's last modified value.",
"link": "https://github.com/webpack/webpack-dev-middleware#lastmodified",
"type": "boolean"
},
"cacheControl": {
"description": "Enable or disable setting `Cache-Control` response header.",
"link": "https://github.com/webpack/webpack-dev-middleware#cachecontrol",
"anyOf": [
{
"type": "boolean"
},
{
"type": "number"
},
{
"type": "string",
"minLength": 1
},
{
"type": "object",
"properties": {
"maxAge": {
"type": "number"
},
"immutable": {
"type": "boolean"
}
},
"additionalProperties": false
}
]
}
},
"additionalProperties": false
Expand Down
194 changes: 194 additions & 0 deletions test/middleware.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5511,5 +5511,199 @@ describe.each([
});
});
});

describe.only("cacheControl", () => {
describe("should work and don't generate `Cache-Control` header by default", () => {
beforeEach(async () => {
const compiler = getCompiler(webpackConfig);

[server, req, instance] = await frameworkFactory(
name,
framework,
compiler,
);
});

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(`/bundle.js`);

expect(response.statusCode).toEqual(200);
expect(response.headers["cache-control"]).toBeUndefined();
});
});

describe("should work and generate `Cache-Control` header when it is `true`", () => {
beforeEach(async () => {
const compiler = getCompiler(webpackConfig);

[server, req, instance] = await frameworkFactory(
name,
framework,
compiler,
{ cacheControl: true },
);
});

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(`/bundle.js`);

expect(response.statusCode).toEqual(200);
expect(response.headers["cache-control"]).toBeDefined();
expect(response.headers["cache-control"]).toBe(
"public, max-age=31536000",
);
});
});

describe("should work and generate `Cache-Control` header when it is a number", () => {
beforeEach(async () => {
const compiler = getCompiler(webpackConfig);

[server, req, instance] = await frameworkFactory(
name,
framework,
compiler,
{ cacheControl: 100000 },
);
});

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(`/bundle.js`);

expect(response.statusCode).toEqual(200);
expect(response.headers["cache-control"]).toBeDefined();
expect(response.headers["cache-control"]).toBe("public, max-age=100");
});
});

describe("should work and generate `Cache-Control` header when it is a string", () => {
beforeEach(async () => {
const compiler = getCompiler(webpackConfig);

[server, req, instance] = await frameworkFactory(
name,
framework,
compiler,
{ cacheControl: "max-age=123456" },
);
});

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(`/bundle.js`);

expect(response.statusCode).toEqual(200);
expect(response.headers["cache-control"]).toBeDefined();
expect(response.headers["cache-control"]).toBe("max-age=123456");
});
});

describe("should work and generate `Cache-Control` header when it is an object with max-age and immutable", () => {
beforeEach(async () => {
const compiler = getCompiler(webpackConfig);

[server, req, instance] = await frameworkFactory(
name,
framework,
compiler,
{
cacheControl: {
maxAge: 100000,
immutable: true,
},
},
);
});

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(`/bundle.js`);

expect(response.statusCode).toEqual(200);
expect(response.headers["cache-control"]).toBeDefined();
expect(response.headers["cache-control"]).toBe(
"public, max-age=100, immutable",
);
});
});

describe("should work and generate `Cache-Control` header when it is an object without max-age, but with immutable", () => {
beforeEach(async () => {
const compiler = getCompiler(webpackConfig);

[server, req, instance] = await frameworkFactory(
name,
framework,
compiler,
{
cacheControl: {
immutable: true,
},
},
);
});

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(`/bundle.js`);

expect(response.statusCode).toEqual(200);
expect(response.headers["cache-control"]).toBeDefined();
expect(response.headers["cache-control"]).toBe(
"public, max-age=31536000000, immutable",
);
});
});

describe("should work and generate `Cache-Control` header when it is an object with max-age, but without immutable", () => {
beforeEach(async () => {
const compiler = getCompiler(webpackConfig);

[server, req, instance] = await frameworkFactory(
name,
framework,
compiler,
{
cacheControl: {
maxAge: 100000,
},
},
);
});

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(`/bundle.js`);

expect(response.statusCode).toEqual(200);
expect(response.headers["cache-control"]).toBeDefined();
expect(response.headers["cache-control"]).toBe("public, max-age=100");
});
});
});
});
});
10 changes: 10 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export = wdm;
* @property {ModifyResponseData<RequestInternal, ResponseInternal>} [modifyResponseData]
* @property {"weak" | "strong"} [etag]
* @property {boolean} [lastModified]
* @property {boolean | number | string | { maxAge: number, immutable: boolean }} [cacheControl]
*/
/**
* @template {IncomingMessage} [RequestInternal=IncomingMessage]
Expand Down Expand Up @@ -354,6 +355,15 @@ type Options<
| undefined;
etag?: "strong" | "weak" | undefined;
lastModified?: boolean | undefined;
cacheControl?:
| string
| number
| boolean
| {
maxAge: number;
immutable: boolean;
}
| undefined;
};
type Middleware<
RequestInternal extends IncomingMessage = import("http").IncomingMessage,
Expand Down

0 comments on commit f7529c3

Please sign in to comment.