Skip to content

Commit 5ed629d

Browse files
feat: added the cacheImmutable option to cache immutable assets (assets with a hash in file name like image.e12ab567.jpg)
1 parent f7529c3 commit 5ed629d

15 files changed

+282
-134
lines changed

README.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ See [below](#other-servers) for an example of use with fastify.
6969
| **[`mimeTypeDefault`](#mimetypedefault)** | `string` | `undefined` | Allows to register a default mime type when we can't determine the content type. |
7070
| **[`etag`](#tag)** | `boolean\| "weak"\| "strong"` | `undefined` | Enable or disable etag generation. |
7171
| **[`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. |
72+
| **[`cacheControl`](#cachecontrol)** | `boolean\|number\|string\|Object` | `undefined` | Enable or disable setting `Cache-Control` response header. |
73+
| **[`cacheImmutable`](#cacheimmutable)** | `boolean\` | `undefined` | Enable or disable setting `Cache-Control: public, max-age=31536000, immutable` response header for immutable assets. |
7374
| **[`publicPath`](#publicpath)** | `string` | `undefined` | The public path that the middleware is bound to. |
7475
| **[`stats`](#stats)** | `boolean\|string\|Object` | `stats` (from a configuration) | Stats options object or preset name. |
7576
| **[`serverSideRender`](#serversiderender)** | `boolean` | `undefined` | Instructs the module to enable or disable the server-side rendering mode. |
@@ -202,6 +203,15 @@ Depending on the setting, the following headers will be generated:
202203

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

206+
### cacheImmutable
207+
208+
Type: `Boolean`
209+
Default: `undefined`
210+
211+
Enable or disable setting `Cache-Control: public, max-age=31536000, immutable` response header for immutable assets (i.e. asset with a hash like `image.a4c12bde.jpg`).
212+
Immutable assets are assets that have their hash in the file name therefore they can be cached, because if you change their contents the file name will be changed.
213+
Take preference over the `cacheControl` option if the asset was defined as immutable.
214+
205215
### publicPath
206216

207217
Type: `String`

src/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ 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]
121+
* @property {boolean | number | string | { maxAge?: number, immutable?: boolean }} [cacheControl]
122+
* @property {boolean} [cacheImmutable]
122123
*/
123124

124125
/**

src/middleware.js

+43-35
Original file line numberDiff line numberDiff line change
@@ -551,39 +551,43 @@ function wrapper(context) {
551551
setResponseHeader(res, "Accept-Ranges", "bytes");
552552
}
553553

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}`;
554+
if (!getResponseHeader(res, "Cache-Control")) {
555+
// TODO enable the `cacheImmutable` by default for the next major release
556+
const cacheControl =
557+
context.options.cacheImmutable && extra.immutable
558+
? { immutable: true }
559+
: context.options.cacheControl;
560+
561+
if (cacheControl) {
562+
let cacheControlValue;
563+
564+
if (typeof cacheControl === "boolean") {
565+
cacheControlValue = "public, max-age=31536000";
566+
} else if (typeof cacheControl === "number") {
567+
const maxAge = Math.floor(
568+
Math.min(Math.max(0, cacheControl), MAX_MAX_AGE) / 1000,
569+
);
580570

581-
if (cacheControl.immutable) {
582-
cacheControlValue += ", immutable";
571+
cacheControlValue = `public, max-age=${maxAge}`;
572+
} else if (typeof cacheControl === "string") {
573+
cacheControlValue = cacheControl;
574+
} else {
575+
const maxAge = cacheControl.maxAge
576+
? Math.floor(
577+
Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE) /
578+
1000,
579+
)
580+
: MAX_MAX_AGE / 1000;
581+
582+
cacheControlValue = `public, max-age=${maxAge}`;
583+
584+
if (cacheControl.immutable) {
585+
cacheControlValue += ", immutable";
586+
}
583587
}
584-
}
585588

586-
setResponseHeader(res, "Cache-Control", cacheControlValue);
589+
setResponseHeader(res, "Cache-Control", cacheControlValue);
590+
}
587591
}
588592

589593
if (
@@ -604,7 +608,7 @@ function wrapper(context) {
604608

605609
/** @type {undefined | Buffer | ReadStream} */
606610
let bufferOrStream;
607-
/** @type {number} */
611+
/** @type {number | undefined} */
608612
let byteLength;
609613

610614
const rangeHeader = getRangeHeader();
@@ -781,13 +785,17 @@ function wrapper(context) {
781785
req,
782786
res,
783787
bufferOrStream,
784-
// @ts-ignore
785-
byteLength,
788+
/** @type {number} */
789+
(byteLength),
786790
));
787791
}
788792

789-
// @ts-ignore
790-
setResponseHeader(res, "Content-Length", byteLength);
793+
setResponseHeader(
794+
res,
795+
"Content-Length",
796+
/** @type {number} */
797+
(byteLength),
798+
);
791799

792800
if (method === "HEAD") {
793801
if (!isPartialContent) {

src/options.json

+5
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@
167167
"additionalProperties": false
168168
}
169169
]
170+
},
171+
"cacheImmutable": {
172+
"description": "Enable or disable setting `Cache-Control: public, max-age=31536000, immutable` response header for immutable assets (i.e. asset with a hash in file name like `image.a4c12bde.jpg`).",
173+
"link": "https://github.com/webpack/webpack-dev-middleware#cacheimmutable",
174+
"type": "boolean"
170175
}
171176
},
172177
"additionalProperties": false

src/utils/getFilenameFromUrl.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
2424
* @typedef {Object} Extra
2525
* @property {import("fs").Stats=} stats
2626
* @property {number=} errorCode
27+
* @property {boolean=} immutable
2728
*/
2829

2930
/**
@@ -65,7 +66,7 @@ function getFilenameFromUrl(context, url, extra = {}) {
6566
return;
6667
}
6768

68-
for (const { publicPath, outputPath } of paths) {
69+
for (const { publicPath, outputPath, assetsInfo } of paths) {
6970
/** @type {string | undefined} */
7071
let filename;
7172
/** @type {URL} */
@@ -122,6 +123,12 @@ function getFilenameFromUrl(context, url, extra = {}) {
122123
if (extra.stats.isFile()) {
123124
foundFilename = filename;
124125

126+
const assetInfo = assetsInfo.get(
127+
pathname.slice(publicPathObject.pathname.length),
128+
);
129+
130+
extra.immutable = assetInfo ? assetInfo.immutable : false;
131+
125132
break;
126133
} else if (
127134
extra.stats.isDirectory() &&

src/utils/getPaths.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ function getPaths(context) {
3535
? compilation.getPath(compilation.outputOptions.publicPath)
3636
: "";
3737

38-
publicPaths.push({ outputPath, publicPath });
38+
publicPaths.push({
39+
outputPath,
40+
publicPath,
41+
assetsInfo: compilation.assetsInfo,
42+
});
3943
}
4044

4145
return publicPaths;

test/__snapshots__/validation-options.test.js.snap.webpack5

+20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`validation should throw an error on the "cacheControl" option with "{"unknown":true,"maxAge":10000}" value 1`] = `
4+
"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema.
5+
- options.cacheControl has an unknown property 'unknown'. These properties are valid:
6+
object { maxAge?, immutable? }"
7+
`;
8+
9+
exports[`validation should throw an error on the "cacheImmutable" option with "0" value 1`] = `
10+
"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema.
11+
- options.cacheImmutable should be a boolean.
12+
-> Enable or disable setting \`Cache-Control: public, max-age=31536000, immutable\` response header for immutable assets (i.e. asset with a hash in file name like \`image.a4c12bde.jpg\`).
13+
-> Read more at https://github.com/webpack/webpack-dev-middleware#cacheimmutable"
14+
`;
15+
16+
exports[`validation should throw an error on the "cacheImmutable" option with "foo" value 1`] = `
17+
"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema.
18+
- options.cacheImmutable should be a boolean.
19+
-> Enable or disable setting \`Cache-Control: public, max-age=31536000, immutable\` response header for immutable assets (i.e. asset with a hash in file name like \`image.a4c12bde.jpg\`).
20+
-> Read more at https://github.com/webpack/webpack-dev-middleware#cacheimmutable"
21+
`;
22+
323
exports[`validation should throw an error on the "etag" option with "0" value 1`] = `
424
"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema.
525
- options.etag should be one of these:

test/fixtures/immutable.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
new URL("./svg.svg", import.meta.url);
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use strict';
2+
3+
const path = require('path');
4+
5+
module.exports = {
6+
mode: 'development',
7+
context: path.resolve(__dirname),
8+
entry: './immutable.js',
9+
output: {
10+
publicPath: "/static/",
11+
path: path.resolve(__dirname, '../outputs/basic'),
12+
},
13+
infrastructureLogging: {
14+
level: 'none'
15+
},
16+
stats: 'normal'
17+
};

0 commit comments

Comments
 (0)