Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added the cacheImmutable option to cache immutable assets (as… #1924

Merged
merged 2 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ See [below](#other-servers) for an example of use with fastify.
| **[`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. |
| **[`cacheControl`](#cachecontrol)** | `boolean\|number\|string\|Object` | `undefined` | Enable or disable setting `Cache-Control` response header. |
| **[`cacheImmutable`](#cacheimmutable)** | `boolean\` | `undefined` | Enable or disable setting `Cache-Control: public, max-age=31536000, immutable` response header for immutable assets. |
| **[`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. |
Expand Down Expand Up @@ -202,6 +203,15 @@ Depending on the setting, the following headers will be generated:

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

### cacheImmutable

Type: `Boolean`
Default: `undefined`

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`).
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.
Take preference over the `cacheControl` option if the asset was defined as immutable.

### publicPath

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

/**
Expand Down
78 changes: 43 additions & 35 deletions src/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -551,39 +551,43 @@ 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 (!getResponseHeader(res, "Cache-Control")) {
// TODO enable the `cacheImmutable` by default for the next major release
const cacheControl =
context.options.cacheImmutable && 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,
);

if (cacheControl.immutable) {
cacheControlValue += ", immutable";
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 / 1000;

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

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

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

if (
Expand All @@ -604,7 +608,7 @@ function wrapper(context) {

/** @type {undefined | Buffer | ReadStream} */
let bufferOrStream;
/** @type {number} */
/** @type {number | undefined} */
let byteLength;

const rangeHeader = getRangeHeader();
Expand Down Expand Up @@ -781,13 +785,17 @@ function wrapper(context) {
req,
res,
bufferOrStream,
// @ts-ignore
byteLength,
/** @type {number} */
(byteLength),
));
}

// @ts-ignore
setResponseHeader(res, "Content-Length", byteLength);
setResponseHeader(
res,
"Content-Length",
/** @type {number} */
(byteLength),
);

if (method === "HEAD") {
if (!isPartialContent) {
Expand Down
5 changes: 5 additions & 0 deletions src/options.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@
"additionalProperties": false
}
]
},
"cacheImmutable": {
"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`).",
"link": "https://github.com/webpack/webpack-dev-middleware#cacheimmutable",
"type": "boolean"
}
},
"additionalProperties": false
Expand Down
9 changes: 8 additions & 1 deletion src/utils/getFilenameFromUrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
* @typedef {Object} Extra
* @property {import("fs").Stats=} stats
* @property {number=} errorCode
* @property {boolean=} immutable
*/

/**
Expand Down Expand Up @@ -65,7 +66,7 @@ function getFilenameFromUrl(context, url, extra = {}) {
return;
}

for (const { publicPath, outputPath } of paths) {
for (const { publicPath, outputPath, assetsInfo } of paths) {
/** @type {string | undefined} */
let filename;
/** @type {URL} */
Expand Down Expand Up @@ -122,6 +123,12 @@ function getFilenameFromUrl(context, url, extra = {}) {
if (extra.stats.isFile()) {
foundFilename = filename;

const assetInfo = assetsInfo.get(
pathname.slice(publicPathObject.pathname.length),
);

extra.immutable = assetInfo ? assetInfo.immutable : false;

break;
} else if (
extra.stats.isDirectory() &&
Expand Down
6 changes: 5 additions & 1 deletion src/utils/getPaths.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ function getPaths(context) {
? compilation.getPath(compilation.outputOptions.publicPath)
: "";

publicPaths.push({ outputPath, publicPath });
publicPaths.push({
outputPath,
publicPath,
assetsInfo: compilation.assetsInfo,
});
}

return publicPaths;
Expand Down
20 changes: 20 additions & 0 deletions test/__snapshots__/validation-options.test.js.snap.webpack5
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`validation should throw an error on the "cacheControl" option with "{"unknown":true,"maxAge":10000}" value 1`] = `
"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema.
- options.cacheControl has an unknown property 'unknown'. These properties are valid:
object { maxAge?, immutable? }"
`;

exports[`validation should throw an error on the "cacheImmutable" option with "0" value 1`] = `
"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema.
- options.cacheImmutable should be a boolean.
-> 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\`).
-> Read more at https://github.com/webpack/webpack-dev-middleware#cacheimmutable"
`;

exports[`validation should throw an error on the "cacheImmutable" option with "foo" value 1`] = `
"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema.
- options.cacheImmutable should be a boolean.
-> 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\`).
-> Read more at https://github.com/webpack/webpack-dev-middleware#cacheimmutable"
`;

exports[`validation should throw an error on the "etag" option with "0" value 1`] = `
"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema.
- options.etag should be one of these:
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/immutable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
new URL("./svg.svg", import.meta.url);
17 changes: 17 additions & 0 deletions test/fixtures/webpack.immutable.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

const path = require('path');

module.exports = {
mode: 'development',
context: path.resolve(__dirname),
entry: './immutable.js',
output: {
publicPath: "/static/",
path: path.resolve(__dirname, '../outputs/basic'),
},
infrastructureLogging: {
level: 'none'
},
stats: 'normal'
};
Loading
Loading