diff --git a/.cspell.json b/.cspell.json index 5fd258574..f393531cc 100644 --- a/.cspell.json +++ b/.cspell.json @@ -19,7 +19,9 @@ "mycustom", "commitlint", "nosniff", - "deoptimize" + "deoptimize", + "etag", + "cachable" ], "ignorePaths": [ "CHANGELOG.md", diff --git a/README.md b/README.md index 52f751300..02de5f84d 100644 --- a/README.md +++ b/README.md @@ -60,19 +60,20 @@ 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. | -| **[`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. | +| **[`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. | The middleware accepts an `options` Object. The following is a property reference for the Object. @@ -171,6 +172,13 @@ Default: `undefined` This property allows a user to register a default mime type when we can't determine the content type. +### etag + +Type: `"weak" | "strong"` +Default: `undefined` + +Enable or disable etag generation. Boolean value use + ### publicPath Type: `String` diff --git a/package-lock.json b/package-lock.json index 3d44e2115..9aee263b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "7.1.1", "license": "MIT", "dependencies": { + "cloneable-readable": "^3.0.0", "colorette": "^2.0.10", "memfs": "^4.6.0", "mime-types": "^2.1.31", @@ -4917,7 +4918,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, "dependencies": { "event-target-shim": "^5.0.0" }, @@ -5516,7 +5516,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5659,7 +5658,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, "funding": [ { "type": "github", @@ -6034,6 +6032,29 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/cloneable-readable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-3.0.0.tgz", + "integrity": "sha512-Lkfd9IRx1nfiBr7UHNxJSl/x7DOeUfYmxzCkxYJC2tyc/9vKgV75msgLGurGQsak/NvJDHMWcshzEXRlxfvhqg==", + "dependencies": { + "readable-stream": "^4.0.0" + } + }, + "node_modules/cloneable-readable/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -9551,7 +9572,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, "engines": { "node": ">=6" } @@ -9566,7 +9586,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "engines": { "node": ">=0.8.x" } @@ -11227,7 +11246,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -15301,7 +15319,6 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, "engines": { "node": ">= 0.6.0" } @@ -15892,7 +15909,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -16560,7 +16576,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } diff --git a/src/index.js b/src/index.js index 305eaf3a4..e8fc9f736 100644 --- a/src/index.js +++ b/src/index.js @@ -117,6 +117,7 @@ const noop = () => {}; * @property {OutputFileSystem} [outputFileSystem] * @property {boolean | string} [index] * @property {ModifyResponseData} [modifyResponseData] + * @property {"weak" | "strong"} [etag] */ /** diff --git a/src/middleware.js b/src/middleware.js index 1caa4490f..84356beef 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -8,6 +8,8 @@ const getFilenameFromUrl = require("./utils/getFilenameFromUrl"); const { setStatusCode, send, pipe } = require("./utils/compatibleAPI"); const ready = require("./utils/ready"); const escapeHtml = require("./utils/escapeHtml"); +const etag = require("./utils/etag"); +const parseTokenList = require("./utils/parseTokenList"); /** @typedef {import("./index.js").NextFunction} NextFunction */ /** @typedef {import("./index.js").IncomingMessage} IncomingMessage */ @@ -27,6 +29,21 @@ function getValueContentRangeHeader(type, size, range) { return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`; } +/** + * Parse an HTTP Date into a number. + * + * @param {string} date + * @private + */ +function parseHttpDate(date) { + const timestamp = date && Date.parse(date); + + // istanbul ignore next: guard against date.js Date.parse patching + return typeof timestamp === "number" ? timestamp : NaN; +} + +const CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/; + /** * @param {import("fs").ReadStream} stream stream * @param {boolean} suppress do need suppress? @@ -174,6 +191,115 @@ function wrapper(context) { res.end(document); } + function isConditionalGET() { + return ( + req.headers["if-match"] || + req.headers["if-unmodified-since"] || + req.headers["if-none-match"] || + req.headers["if-modified-since"] + ); + } + + function isPreconditionFailure() { + const match = req.headers["if-match"]; + + if (match) { + // eslint-disable-next-line no-shadow + const etag = res.getHeader("ETag"); + + return ( + !etag || + (match !== "*" && + parseTokenList(match).every( + // eslint-disable-next-line no-shadow + (match) => + match !== etag && + match !== `W/${etag}` && + `W/${match}` !== etag, + )) + ); + } + + return false; + } + + /** + * @returns {boolean} is cachable + */ + function isCachable() { + return ( + (res.statusCode >= 200 && res.statusCode < 300) || + res.statusCode === 304 + ); + } + + /** + * @param {import("http").OutgoingHttpHeaders} resHeaders + * @returns {boolean} + */ + function isFresh(resHeaders) { + // Always return stale when Cache-Control: no-cache to support end-to-end reload requests + // https://tools.ietf.org/html/rfc2616#section-14.9.4 + const cacheControl = req.headers["cache-control"]; + + if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) { + return false; + } + + // if-none-match + const noneMatch = req.headers["if-none-match"]; + + if (noneMatch && noneMatch !== "*") { + if (!resHeaders.etag) { + return false; + } + + const matches = parseTokenList(noneMatch); + + let etagStale = true; + + for (let i = 0; i < matches.length; i++) { + const match = matches[i]; + + if ( + match === resHeaders.etag || + match === `W/${resHeaders.etag}` || + `W/${match}` === resHeaders.etag + ) { + etagStale = false; + break; + } + } + + if (etagStale) { + return false; + } + } + + // A recipient MUST ignore If-Modified-Since if the request contains an If-None-Match header field; + // the condition in If-None-Match is considered to be a more accurate replacement for the condition in If-Modified-Since, + // and the two are only combined for the sake of interoperating with older intermediaries that might not implement If-None-Match. + if (noneMatch) { + return true; + } + + // if-modified-since + const modifiedSince = req.headers["if-modified-since"]; + + if (modifiedSince) { + const lastModified = resHeaders["last-modified"]; + const modifiedStale = + !lastModified || + !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince)); + + if (modifiedStale) { + return false; + } + } + + return true; + } + async function processRequest() { // Pipe and SendFile /** @type {import("./utils/getFilenameFromUrl").Extra} */ @@ -334,6 +460,56 @@ function wrapper(context) { return; } + if (context.options.etag && !res.getHeader("ETag")) { + const value = + context.options.etag === "weak" + ? /** @type {import("fs").Stats} */ (extra.stats) + : bufferOrStream; + + const val = await etag(value); + + if (val.buffer) { + bufferOrStream = val.buffer; + } + + res.setHeader("ETag", val.hash); + } + + // Conditional GET support + if (isConditionalGET()) { + if (isPreconditionFailure()) { + sendError(412, { + modifyResponseData: context.options.modifyResponseData, + }); + + return; + } + + // For Koa + if (res.statusCode === 404) { + setStatusCode(res, 200); + } + + if ( + isCachable() && + isFresh({ + etag: /** @type {string} */ (res.getHeader("ETag")), + }) + ) { + setStatusCode(res, 304); + + // Remove content header fields + res.removeHeader("Content-Encoding"); + res.removeHeader("Content-Language"); + res.removeHeader("Content-Length"); + res.removeHeader("Content-Range"); + res.removeHeader("Content-Type"); + res.end(); + + return; + } + } + if (context.options.modifyResponseData) { ({ data: bufferOrStream, byteLength } = context.options.modifyResponseData( @@ -361,6 +537,8 @@ function wrapper(context) { /** @type {import("fs").ReadStream} */ (bufferOrStream).pipe ) === "function"; + console.log(isPipeSupports); + if (!isPipeSupports) { send(res, /** @type {Buffer} */ (bufferOrStream)); return; diff --git a/src/options.json b/src/options.json index 91086d193..357db9bf4 100644 --- a/src/options.json +++ b/src/options.json @@ -129,6 +129,11 @@ "description": "Allows to set up a callback to change the response data.", "link": "https://github.com/webpack/webpack-dev-middleware#modifyresponsedata", "instanceof": "Function" + }, + "etag": { + "description": "Enable or disable etag generation.", + "link": "https://github.com/webpack/webpack-dev-middleware#etag", + "enum": ["weak", "strong"] } }, "additionalProperties": false diff --git a/src/utils/etag.js b/src/utils/etag.js new file mode 100644 index 000000000..2aa227d8c --- /dev/null +++ b/src/utils/etag.js @@ -0,0 +1,83 @@ +const crypto = require("crypto"); + +/** @typedef {import("fs").Stats} Stats */ +/** @typedef {import("fs").ReadStream} ReadStream */ + +/** + * Generate a tag for a stat. + * + * @param {Stats} stat + * @return {{ hash: string, buffer?: Buffer }} + */ +function statTag(stat) { + const mtime = stat.mtime.getTime().toString(16); + const size = stat.size.toString(16); + + return { hash: `W/"${size}-${mtime}"` }; +} + +/** + * Generate an entity tag. + * + * @param {Buffer | ReadStream} entity + * @return {Promise<{ hash: string, buffer?: Buffer }>} + */ +async function entityTag(entity) { + const sha1 = crypto.createHash("sha1"); + + if (!Buffer.isBuffer(entity)) { + let byteLength = 0; + + /** @type {Buffer[]} */ + const buffers = []; + + await new Promise((resolve, reject) => { + entity + .on("data", (chunk) => { + sha1.update(chunk); + buffers.push(/** @type {Buffer} */ (chunk)); + byteLength += /** @type {Buffer} */ (chunk).byteLength; + }) + .on("end", () => { + resolve(sha1); + }) + .on("error", reject); + }); + + return { + buffer: Buffer.concat(buffers), + hash: `"${byteLength.toString(16)}-${sha1.digest("base64").substring(0, 27)}"`, + }; + } + + if (entity.byteLength === 0) { + // Fast-path empty + return { hash: '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"' }; + } + + // Compute hash of entity + const hash = sha1.update(entity).digest("base64").substring(0, 27); + + // Compute length of entity + const { byteLength } = entity; + + return { hash: `"${byteLength.toString(16)}-${hash}"` }; +} + +/** + * Create a simple ETag. + * + * @param {Buffer | ReadStream | Stats} entity + * @return {Promise<{ hash: string, buffer?: Buffer }>} + */ +async function etag(entity) { + const isStrong = + Buffer.isBuffer(entity) || + typeof (/** @type {ReadStream} */ (entity).pipe) === "function"; + + return isStrong + ? entityTag(/** @type {Buffer | ReadStream} */ (entity)) + : statTag(/** @type {import("fs").Stats} */ (entity)); +} + +module.exports = etag; diff --git a/src/utils/parseTokenList.js b/src/utils/parseTokenList.js new file mode 100644 index 000000000..fed6c472a --- /dev/null +++ b/src/utils/parseTokenList.js @@ -0,0 +1,43 @@ +/** + * Parse a HTTP token list. + * + * @param {string} str + * @returns {string[]} tokens + */ +function parseTokenList(str) { + let end = 0; + let start = 0; + + const list = []; + + // gather tokens + for (let i = 0, len = str.length; i < len; i++) { + switch (str.charCodeAt(i)) { + case 0x20 /* */: + if (start === end) { + end = i + 1; + start = end; + } + break; + case 0x2c /* , */: + if (start !== end) { + list.push(str.substring(start, end)); + } + end = i + 1; + start = end; + break; + default: + end = i + 1; + break; + } + } + + // final token + if (start !== end) { + list.push(str.substring(start, end)); + } + + return list; +} + +module.exports = parseTokenList; diff --git a/test/__snapshots__/logging.test.js.snap.webpack4 b/test/__snapshots__/logging.test.js.snap.webpack4 deleted file mode 100644 index e534901ff..000000000 --- a/test/__snapshots__/logging.test.js.snap.webpack4 +++ /dev/null @@ -1,584 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`logging should logging an error in "watch" method: stderr 1`] = `"Error: Watch error"`; - -exports[`logging should logging an warning: stderr 1`] = `""`; - -exports[`logging should logging an warning: stdout 1`] = ` -" -WARNING in Warning" -`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #2: stderr 1`] = `""`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #2: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Child broken: -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./broken.js] x bytes {main} [built] [failed] [1 error] - -ERROR in ./broken.js 1:3 -Module parse failed: Unexpected token (1:3) -You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders -> 1()2()3() -| -Child warning: -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./warning.js] x bytes {main} [built] - -WARNING in Warning -Child success: -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #3: stderr 1`] = `""`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #3: stderr 2`] = `""`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #3: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./broken.js] x bytes {main} [built] [failed] [1 error] - -ERROR in ./broken.js 1:3 -Module parse failed: Unexpected token (1:3) -You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders -> 1()2()3() -| -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./warning.js] x bytes {main} [built] - -WARNING in Warning -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #3: stdout 2`] = ` -"Child -Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./broken.js] x bytes {main} [built] [failed] [1 error] - -ERROR in ./broken.js 1:3 -Module parse failed: Unexpected token (1:3) -You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders -> 1()2()3() -| -Child -Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./warning.js] x bytes {main} [built] - -WARNING in Warning -Child -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted]" -`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration: stderr 1`] = `""`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./broken.js] x bytes {main} [built] [failed] [1 error] - -ERROR in ./broken.js 1:3 -Module parse failed: Unexpected token (1:3) -You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders -> 1()2()3() -| -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./warning.js] x bytes {main} [built] - -WARNING in Warning -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build and respect colors #2: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect colors #2: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build and respect colors: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect colors: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build and respect the "NO_COLOR" env: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "NO_COLOR" env: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with custom object value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with custom object value: stdout 1`] = ` -"Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted]" -`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "false" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "false" value: stdout 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "minimal" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "minimal" value: stdout 1`] = `"x modules"`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "none" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "none" value: stdout 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "true" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "true" value: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "verbose" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "verbose" value: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -chunk {main} bundle.js (xxxx) x bytes [entry] [rendered] -> ./foo.js main -[./foo.js] x bytes {main} [depth 0] [built] -single entry ./foo.js main -[./index.html] x bytes {main} [depth 1] [built] -[exports: default] -cjs require ./index.html [./foo.js] 4:0-23 -[./svg.svg] x bytes {main} [depth 1] [built] -[exports: default] -cjs require ./svg.svg [./foo.js] 3:0-20 - -LOG from xxx" -`; - -exports[`logging should logging on successfully build in multi-compiler mode: stderr 1`] = `""`; - -exports[`logging should logging on successfully build in multi-compiler mode: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built] -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./bar.js] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with object value and no colors: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with object value and no colors: stdout 1`] = ` -"Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted]" -`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with object value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with object value: stdout 1`] = ` -"Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted]" -`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "false" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "false" value: stdout 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "none" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "none" value: stdout 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "normal" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "normal" value: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "true" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "true" value: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "verbose" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "verbose" value: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -chunk {main} bundle.js (xxxx) x bytes [entry] [rendered] -> ./foo.js main -[./foo.js] x bytes {main} [depth 0] [built] -single entry ./foo.js main -[./index.html] x bytes {main} [depth 1] [built] -[exports: default] -cjs require ./index.html [./foo.js] 4:0-23 -[./svg.svg] x bytes {main} [depth 1] [built] -[exports: default] -cjs require ./svg.svg [./foo.js] 3:0-20 - -LOG from xxx" -`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the object value and colors: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the object value and colors: stdout 1`] = ` -"Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted]" -`; - -exports[`logging should logging on successfully build when the 'stats' doesn't exist: stderr 1`] = `""`; - -exports[`logging should logging on successfully build when the 'stats' doesn't exist: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build: stderr 1`] = `""`; - -exports[`logging should logging on successfully build: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with object value and colors: stderr 1`] = `""`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with object value and colors: stdout 1`] = ` -"Child -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Child -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main" -`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with object value and no colors: stderr 1`] = `""`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with object value and no colors: stdout 1`] = ` -"Child -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Child -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main" -`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "false" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "false" value: stdout 1`] = `""`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "normal" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "normal" value: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built] -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./bar.js] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "true" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "true" value: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built] -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./bar.js] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the object value: stderr 1`] = `""`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the object value: stdout 1`] = ` -"Child -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Child -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main" -`; - -exports[`logging should logging on unsuccessful build in multi-compiler: stderr 1`] = `""`; - -exports[`logging should logging on unsuccessful build in multi-compiler: stdout 1`] = ` -"Child - -ERROR in ./broken.js 1:3 -Module parse failed: Unexpected token (1:3) -You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders -> 1()2()3() -| -Child - -ERROR in ./broken.js 1:3 -Module parse failed: Unexpected token (1:3) -You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders -> 1()2()3() -|" -`; - -exports[`logging should logging on unsuccessful build: stderr 1`] = `""`; - -exports[`logging should logging on unsuccessful build: stdout 1`] = ` -" -ERROR in ./broken.js 1:3 -Module parse failed: Unexpected token (1:3) -You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders -> 1()2()3() -|" -`; - -exports[`logging should logging warnings in multi-compiler mode: stderr 1`] = `""`; - -exports[`logging should logging warnings in multi-compiler mode: stdout 1`] = ` -"Child - -WARNING in Warning -Child - -WARNING in Warning" -`; diff --git a/test/__snapshots__/validation-options.test.js.snap.webpack4 b/test/__snapshots__/validation-options.test.js.snap.webpack4 deleted file mode 100644 index 00e92ef14..000000000 --- a/test/__snapshots__/validation-options.test.js.snap.webpack4 +++ /dev/null @@ -1,145 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`validation should throw an error on the "headers" option with "[]" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.headers should be a non-empty array." -`; - -exports[`validation should throw an error on the "headers" option with "[{"foo":"bar"}]" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.headers[0] has an unknown property 'foo'. These properties are valid: - object { key?, value? }" -`; - -exports[`validation should throw an error on the "headers" option with "1" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.headers should be one of these: - [object { key?, value? }, ...] (should not have fewer than 1 item) | object { … } | function - -> Allows to pass custom HTTP headers on each request - -> Read more at https://github.com/webpack/webpack-dev-middleware#headers - Details: - * options.headers should be an array: - [object { key?, value? }, ...] (should not have fewer than 1 item) - * options.headers should be an object: - object { … } - * options.headers should be an instance of function." -`; - -exports[`validation should throw an error on the "headers" option with "true" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.headers should be one of these: - [object { key?, value? }, ...] (should not have fewer than 1 item) | object { … } | function - -> Allows to pass custom HTTP headers on each request - -> Read more at https://github.com/webpack/webpack-dev-middleware#headers - Details: - * options.headers should be an array: - [object { key?, value? }, ...] (should not have fewer than 1 item) - * options.headers should be an object: - object { … } - * options.headers should be an instance of function." -`; - -exports[`validation should throw an error on the "index" option with "{}" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.index should be one of these: - boolean | string - -> Allows to serve an index of the directory. - -> Read more at https://github.com/webpack/webpack-dev-middleware#index - Details: - * options.index should be a boolean. - * options.index should be a string." -`; - -exports[`validation should throw an error on the "index" 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.index should be one of these: - boolean | string - -> Allows to serve an index of the directory. - -> Read more at https://github.com/webpack/webpack-dev-middleware#index - Details: - * options.index should be a boolean. - * options.index should be a string." -`; - -exports[`validation should throw an error on the "methods" option with "{}" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.methods should be an array: - [string, ...] - -> Allows to pass the list of HTTP request methods accepted by the middleware. - -> Read more at https://github.com/webpack/webpack-dev-middleware#methods" -`; - -exports[`validation should throw an error on the "methods" option with "true" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.methods should be an array: - [string, ...] - -> Allows to pass the list of HTTP request methods accepted by the middleware. - -> Read more at https://github.com/webpack/webpack-dev-middleware#methods" -`; - -exports[`validation should throw an error on the "mimeTypes" 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.mimeTypes should be an object: - object { … } - -> Allows a user to register custom mime types or extension mappings. - -> Read more at https://github.com/webpack/webpack-dev-middleware#mimetypes" -`; - -exports[`validation should throw an error on the "outputFileSystem" option with "false" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.outputFileSystem should be an object: - object { … } - -> Set the default file system which will be used by webpack as primary destination of generated files. - -> Read more at https://github.com/webpack/webpack-dev-middleware#outputfilesystem" -`; - -exports[`validation should throw an error on the "publicPath" option with "false" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.publicPath should be one of these: - \\"auto\\" | string | function - -> The \`publicPath\` specifies the public URL address of the output files when referenced in a browser. - -> Read more at https://github.com/webpack/webpack-dev-middleware#publicpath - Details: - * options.publicPath should be \\"auto\\". - * options.publicPath should be a string. - * options.publicPath should be an instance of function." -`; - -exports[`validation should throw an error on the "serverSideRender" 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.serverSideRender should be a boolean. - -> Instructs the module to enable or disable the server-side rendering mode. - -> Read more at https://github.com/webpack/webpack-dev-middleware#serversiderender" -`; - -exports[`validation should throw an error on the "serverSideRender" 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.serverSideRender should be a boolean. - -> Instructs the module to enable or disable the server-side rendering mode. - -> Read more at https://github.com/webpack/webpack-dev-middleware#serversiderender" -`; - -exports[`validation should throw an error on the "stats" 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.stats should be one of these: - \\"none\\" | \\"summary\\" | \\"errors-only\\" | \\"errors-warnings\\" | \\"minimal\\" | \\"normal\\" | \\"detailed\\" | \\"verbose\\" | boolean | object { … } - -> Stats options object or preset name. - -> Read more at https://github.com/webpack/webpack-dev-middleware#stats - Details: - * options.stats should be one of these: - \\"none\\" | \\"summary\\" | \\"errors-only\\" | \\"errors-warnings\\" | \\"minimal\\" | \\"normal\\" | \\"detailed\\" | \\"verbose\\" - * options.stats should be a boolean. - * options.stats should be an object: - object { … }" -`; - -exports[`validation should throw an error on the "writeToDisk" option with "{}" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.writeToDisk should be one of these: - boolean | function - -> Allows to write generated files on disk. - -> Read more at https://github.com/webpack/webpack-dev-middleware#writetodisk - Details: - * options.writeToDisk should be a boolean. - * options.writeToDisk should be an instance of function." -`; diff --git a/test/__snapshots__/validation-options.test.js.snap.webpack5 b/test/__snapshots__/validation-options.test.js.snap.webpack5 index 3c2fc74a8..797b295bf 100644 --- a/test/__snapshots__/validation-options.test.js.snap.webpack5 +++ b/test/__snapshots__/validation-options.test.js.snap.webpack5 @@ -1,5 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +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: + "weak" | "strong" + -> Enable or disable etag generation. + -> Read more at https://github.com/webpack/webpack-dev-middleware#etag" +`; + +exports[`validation should throw an error on the "etag" 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.etag should be one of these: + "weak" | "strong" + -> Enable or disable etag generation. + -> Read more at https://github.com/webpack/webpack-dev-middleware#etag" +`; + exports[`validation should throw an error on the "headers" option with "[]" value 1`] = ` "Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - options.headers should be a non-empty array." diff --git a/test/middleware.test.js b/test/middleware.test.js index 8e076d7c8..eb068af16 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -22,7 +22,7 @@ import webpackQueryStringConfig from "./fixtures/webpack.querystring.config"; import webpackClientServerConfig from "./fixtures/webpack.client.server.config"; // Suppress unnecessary stats output -// global.console.log = jest.fn(); +global.console.log = jest.fn(); async function startServer(app) { return new Promise((resolve, reject) => { @@ -4210,5 +4210,167 @@ describe.each([ }); }); }); + + describe("etag", () => { + describe("should work and generate weak etag", () => { + beforeEach(async () => { + const compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + etag: "weak", + }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and set weak etag', async () => { + const response = await req.get(`/bundle.js`); + + expect(response.statusCode).toEqual(200); + expect(response.headers.etag).toBeDefined(); + expect(response.headers.etag.startsWith("W/")).toBe(true); + }); + + it('should return the "304" code for the "GET" request to the bundle file with etag and "if-match" header', async () => { + const response1 = await req.get(`/bundle.js`); + + expect(response1.statusCode).toEqual(200); + expect(response1.headers.etag).toBeDefined(); + expect(response1.headers.etag.startsWith("W/")).toBe(true); + + const response2 = await req + .get(`/bundle.js`) + .set("if-match", response1.headers.etag); + + expect(response2.statusCode).toEqual(304); + expect(response2.headers.etag).toBeDefined(); + expect(response2.headers.etag.startsWith("W/")).toBe(true); + + const response3 = await req.get(`/bundle.js`).set("if-match", "test"); + + expect(response3.statusCode).toEqual(412); + }); + + it('should return the "304" code for the "GET" request to the bundle file with etag "if-none-match" header', async () => { + const response1 = await req.get(`/bundle.js`); + + expect(response1.statusCode).toEqual(200); + expect(response1.headers.etag).toBeDefined(); + expect(response1.headers.etag.startsWith("W/")).toBe(true); + + const response2 = await req + .get(`/bundle.js`) + .set("if-none-match", response1.headers.etag); + + expect(response2.statusCode).toEqual(304); + expect(response2.headers.etag).toBeDefined(); + expect(response2.headers.etag.startsWith("W/")).toBe(true); + + const response3 = await req + .get(`/bundle.js`) + .set("if-none-match", "test"); + + expect(response3.statusCode).toEqual(200); + expect(response3.headers.etag).toBeDefined(); + expect(response3.headers.etag.startsWith("W/")).toBe(true); + }); + + it('should return the "412" code for the "GET" request to the bundle file with etag and wrong "if-match" header', async () => { + const response1 = await req.get(`/bundle.js`); + + expect(response1.statusCode).toEqual(200); + expect(response1.headers.etag).toBeDefined(); + expect(response1.headers.etag.startsWith("W/")).toBe(true); + + const response2 = await req.get(`/bundle.js`).set("if-match", "test"); + + expect(response2.statusCode).toEqual(412); + }); + + it('should return the "200" code for the "GET" request to the bundle file with etag and "if-match" and "cache-control: no-cache" header', async () => { + const response1 = await req.get(`/bundle.js`); + + expect(response1.statusCode).toEqual(200); + expect(response1.headers.etag).toBeDefined(); + expect(response1.headers.etag.startsWith("W/")).toBe(true); + + const response2 = await req + .get(`/bundle.js`) + .set("if-match", response1.headers.etag) + .set("Cache-Control", "no-cache"); + + expect(response2.statusCode).toEqual(200); + expect(response2.headers.etag).toBeDefined(); + expect(response2.headers.etag.startsWith("W/")).toBe(true); + }); + }); + + describe("should work and generate strong etag", () => { + beforeEach(async () => { + const compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + etag: "strong", + }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and set weak etag', async () => { + const response = await req.get(`/bundle.js`); + + expect(response.statusCode).toEqual(200); + expect(response.headers.etag).toBe( + /* cspell:disable-next-line */ + '"18c7-l/LCspQS5fbbf1kkLGOsK9FTpbg"', + ); + }); + }); + + describe("should work and generate strong etag without createReadStream", () => { + beforeEach(async () => { + const compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + etag: "strong", + }, + ); + + instance.context.outputFileSystem.createReadStream = null; + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and set weak etag', async () => { + const response = await req.get(`/bundle.js`); + + expect(response.statusCode).toEqual(200); + expect(response.headers.etag).toBe( + /* cspell:disable-next-line */ + '"18c7-l/LCspQS5fbbf1kkLGOsK9FTpbg"', + ); + }); + }); + }); }); }); diff --git a/test/validation-options.test.js b/test/validation-options.test.js index a9b0eebe0..f1d8f8328 100644 --- a/test/validation-options.test.js +++ b/test/validation-options.test.js @@ -67,6 +67,10 @@ describe("validation", () => { ], failure: [true], }, + etag: { + success: ["weak", "strong"], + failure: ["foo", 0], + }, }; function stringifyValue(value) { diff --git a/types/index.d.ts b/types/index.d.ts index ee6f4c9e0..98b3509b7 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -90,6 +90,7 @@ export = wdm; * @property {OutputFileSystem} [outputFileSystem] * @property {boolean | string} [index] * @property {ModifyResponseData} [modifyResponseData] + * @property {"weak" | "strong"} [etag] */ /** * @template {IncomingMessage} RequestInternal @@ -350,6 +351,7 @@ type Options< modifyResponseData?: | ModifyResponseData | undefined; + etag?: "strong" | "weak" | undefined; }; type Middleware< RequestInternal extends import("http").IncomingMessage, diff --git a/types/utils/etag.d.ts b/types/utils/etag.d.ts new file mode 100644 index 000000000..002599793 --- /dev/null +++ b/types/utils/etag.d.ts @@ -0,0 +1,16 @@ +export = etag; +/** + * Create a simple ETag. + * + * @param {Buffer | ReadStream | Stats} entity + * @return {Promise<{ hash: string, buffer?: Buffer }>} + */ +declare function etag(entity: Buffer | ReadStream | Stats): Promise<{ + hash: string; + buffer?: Buffer; +}>; +declare namespace etag { + export { Stats, ReadStream }; +} +type ReadStream = import("fs").ReadStream; +type Stats = import("fs").Stats; diff --git a/types/utils/parseTokenList.d.ts b/types/utils/parseTokenList.d.ts new file mode 100644 index 000000000..67b75c26e --- /dev/null +++ b/types/utils/parseTokenList.d.ts @@ -0,0 +1,8 @@ +export = parseTokenList; +/** + * Parse a HTTP token list. + * + * @param {string} str + * @returns {string[]} tokens + */ +declare function parseTokenList(str: string): string[];