From e9780abd5c99f5337ae73c987a580418743eeb17 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Wed, 15 Sep 2021 14:00:13 +0530 Subject: [PATCH 1/8] feat: allow array for `headers` option --- lib/Server.js | 15 ++++++++++++++- lib/options.json | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/Server.js b/lib/Server.js index 5d1c6835d5..0e42499cd7 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -490,7 +490,20 @@ class Server { options.devMiddleware = {}; } - // No need to normalize `headers` + // normalize `headers` + + if ( + typeof options.headers !== "undefined" && + Array.isArray(options.headers) + ) { + const allHeaders = {}; + + options.headers.forEach((header) => { + allHeaders[header.key] = header.value; + }); + + options.headers = allHeaders; + } if (typeof options.historyApiFallback === "undefined") { options.historyApiFallback = false; diff --git a/lib/options.json b/lib/options.json index b1c6215516..cef66d0149 100644 --- a/lib/options.json +++ b/lib/options.json @@ -369,8 +369,28 @@ "description": "Allows to configure the server's listening socket for TLS (by default, dev server will be served over HTTP).", "link": "https://webpack.js.org/configuration/dev-server/#devserverhttps" }, + "HeaderObject": { + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "description": "key of header.", + "type": "string" + }, + "value": { + "description": "value of header.", + "type": "string" + } + } + }, "Headers": { "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/definitions/HeaderObject" + } + }, { "type": "object" }, From 4d935faf9a60b326bdfea243e2b234f961e80a56 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Wed, 15 Sep 2021 14:04:41 +0530 Subject: [PATCH 2/8] test: add validation tests --- lib/options.json | 3 ++- .../validate-options.test.js.snap.webpack4 | 19 +++++++++++++++++-- .../validate-options.test.js.snap.webpack5 | 19 +++++++++++++++++-- test/validate-options.test.js | 4 ++-- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/lib/options.json b/lib/options.json index cef66d0149..639ed5c391 100644 --- a/lib/options.json +++ b/lib/options.json @@ -389,7 +389,8 @@ "type": "array", "items": { "$ref": "#/definitions/HeaderObject" - } + }, + "minItems": 1 }, { "type": "object" diff --git a/test/__snapshots__/validate-options.test.js.snap.webpack4 b/test/__snapshots__/validate-options.test.js.snap.webpack4 index dd1a5f8e30..79c47d8241 100644 --- a/test/__snapshots__/validate-options.test.js.snap.webpack4 +++ b/test/__snapshots__/validate-options.test.js.snap.webpack4 @@ -223,13 +223,26 @@ exports[`options validate should throw an error on the "devMiddleware" option wi -> Read more at https://webpack.js.org/configuration/dev-server/#devserverdevmiddleware" `; +exports[`options validate should throw an error on the "headers" option with '[]' value 1`] = ` +"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. + - options.headers should be a non-empty array." +`; + +exports[`options validate should throw an error on the "headers" option with '[{"foo":"bar"}]' value 1`] = ` +"ValidationError: Invalid options object. Dev Server 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[`options validate should throw an error on the "headers" option with '1' value 1`] = ` "ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. - options.headers should be one of these: - object { … } | function + [object { key?, value? }, ...] (should not have fewer than 1 item) | object { … } | function -> Allows to set custom headers on response. -> Read more at https://webpack.js.org/configuration/dev-server/#devserverheaders 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." @@ -238,10 +251,12 @@ exports[`options validate should throw an error on the "headers" option with '1' exports[`options validate should throw an error on the "headers" option with 'false' value 1`] = ` "ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. - options.headers should be one of these: - object { … } | function + [object { key?, value? }, ...] (should not have fewer than 1 item) | object { … } | function -> Allows to set custom headers on response. -> Read more at https://webpack.js.org/configuration/dev-server/#devserverheaders 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." diff --git a/test/__snapshots__/validate-options.test.js.snap.webpack5 b/test/__snapshots__/validate-options.test.js.snap.webpack5 index dd1a5f8e30..79c47d8241 100644 --- a/test/__snapshots__/validate-options.test.js.snap.webpack5 +++ b/test/__snapshots__/validate-options.test.js.snap.webpack5 @@ -223,13 +223,26 @@ exports[`options validate should throw an error on the "devMiddleware" option wi -> Read more at https://webpack.js.org/configuration/dev-server/#devserverdevmiddleware" `; +exports[`options validate should throw an error on the "headers" option with '[]' value 1`] = ` +"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. + - options.headers should be a non-empty array." +`; + +exports[`options validate should throw an error on the "headers" option with '[{"foo":"bar"}]' value 1`] = ` +"ValidationError: Invalid options object. Dev Server 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[`options validate should throw an error on the "headers" option with '1' value 1`] = ` "ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. - options.headers should be one of these: - object { … } | function + [object { key?, value? }, ...] (should not have fewer than 1 item) | object { … } | function -> Allows to set custom headers on response. -> Read more at https://webpack.js.org/configuration/dev-server/#devserverheaders 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." @@ -238,10 +251,12 @@ exports[`options validate should throw an error on the "headers" option with '1' exports[`options validate should throw an error on the "headers" option with 'false' value 1`] = ` "ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. - options.headers should be one of these: - object { … } | function + [object { key?, value? }, ...] (should not have fewer than 1 item) | object { … } | function -> Allows to set custom headers on response. -> Read more at https://webpack.js.org/configuration/dev-server/#devserverheaders 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." diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 829ed40e84..7bc528b944 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -169,8 +169,8 @@ const tests = { failure: [true, false, 123, [], [""]], }, headers: { - success: [{}, { foo: "bar" }, () => {}], - failure: [false, 1], + success: [{}, { foo: "bar" }, () => {}, [{ key: "foo", value: "bar" }]], + failure: [false, 1, [], [{ foo: "bar" }]], }, historyApiFallback: { success: [{}, true], From 9cd2cd6ca7fc6cedad0947ae5ce8a9ef9aef5e4e Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Wed, 15 Sep 2021 14:08:07 +0530 Subject: [PATCH 3/8] test: add e2e tests --- .../headers.test.js.snap.webpack4 | 10 +++ .../headers.test.js.snap.webpack5 | 10 +++ test/e2e/headers.test.js | 72 +++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/test/e2e/__snapshots__/headers.test.js.snap.webpack4 b/test/e2e/__snapshots__/headers.test.js.snap.webpack4 index 808f4404ac..c9edf9f641 100644 --- a/test/e2e/__snapshots__/headers.test.js.snap.webpack4 +++ b/test/e2e/__snapshots__/headers.test.js.snap.webpack4 @@ -19,6 +19,16 @@ exports[`headers option as a string should handle GET request with headers: resp exports[`headers option as a string should handle GET request with headers: response status 1`] = `200`; +exports[`headers option as an array of objects should handle GET request with headers: console messages 1`] = `Array []`; + +exports[`headers option as an array of objects should handle GET request with headers: page errors 1`] = `Array []`; + +exports[`headers option as an array of objects should handle GET request with headers: response headers x-bar 1`] = `"value2"`; + +exports[`headers option as an array of objects should handle GET request with headers: response headers x-foo 1`] = `"value1"`; + +exports[`headers option as an array of objects should handle GET request with headers: response status 1`] = `200`; + exports[`headers option as an array should handle GET request with headers as an array: console messages 1`] = `Array []`; exports[`headers option as an array should handle GET request with headers as an array: page errors 1`] = `Array []`; diff --git a/test/e2e/__snapshots__/headers.test.js.snap.webpack5 b/test/e2e/__snapshots__/headers.test.js.snap.webpack5 index 808f4404ac..c9edf9f641 100644 --- a/test/e2e/__snapshots__/headers.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/headers.test.js.snap.webpack5 @@ -19,6 +19,16 @@ exports[`headers option as a string should handle GET request with headers: resp exports[`headers option as a string should handle GET request with headers: response status 1`] = `200`; +exports[`headers option as an array of objects should handle GET request with headers: console messages 1`] = `Array []`; + +exports[`headers option as an array of objects should handle GET request with headers: page errors 1`] = `Array []`; + +exports[`headers option as an array of objects should handle GET request with headers: response headers x-bar 1`] = `"value2"`; + +exports[`headers option as an array of objects should handle GET request with headers: response headers x-foo 1`] = `"value1"`; + +exports[`headers option as an array of objects should handle GET request with headers: response status 1`] = `200`; + exports[`headers option as an array should handle GET request with headers as an array: console messages 1`] = `Array []`; exports[`headers option as an array should handle GET request with headers as an array: page errors 1`] = `Array []`; diff --git a/test/e2e/headers.test.js b/test/e2e/headers.test.js index a921f7f4dd..780c20d88d 100644 --- a/test/e2e/headers.test.js +++ b/test/e2e/headers.test.js @@ -66,6 +66,78 @@ describe("headers option", () => { }); }); + describe("as an array of objects", () => { + let compiler; + let server; + let page; + let browser; + let pageErrors; + let consoleMessages; + + beforeEach(async () => { + compiler = webpack(config); + + server = new Server( + { + headers: [ + { + key: "X-Foo", + value: "value1", + }, + { + key: "X-Bar", + value: "value2", + }, + ], + port, + }, + compiler + ); + + await server.start(); + + ({ page, browser } = await runBrowser()); + + pageErrors = []; + consoleMessages = []; + }); + + afterEach(async () => { + await browser.close(); + await server.stop(); + }); + + it("should handle GET request with headers", async () => { + page + .on("console", (message) => { + consoleMessages.push(message); + }) + .on("pageerror", (error) => { + pageErrors.push(error); + }); + + const response = await page.goto(`http://127.0.0.1:${port}/main.js`, { + waitUntil: "networkidle0", + }); + + expect(response.headers()["x-foo"]).toMatchSnapshot( + "response headers x-foo" + ); + + expect(response.headers()["x-bar"]).toMatchSnapshot( + "response headers x-bar" + ); + + expect(response.status()).toMatchSnapshot("response status"); + + expect(consoleMessages.map((message) => message.text())).toMatchSnapshot( + "console messages" + ); + + expect(pageErrors).toMatchSnapshot("page errors"); + }); + }); + describe("as an array", () => { let compiler; let server; From b9400371fa2fb1056355d2551d25ec8f3d7f4436 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Wed, 15 Sep 2021 14:13:45 +0530 Subject: [PATCH 4/8] docs: add example --- examples/headers/array/README.md | 36 ++++++++++++++++++++++++ examples/headers/array/app.js | 6 ++++ examples/headers/array/webpack.config.js | 22 +++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 examples/headers/array/README.md create mode 100644 examples/headers/array/app.js create mode 100644 examples/headers/array/webpack.config.js diff --git a/examples/headers/array/README.md b/examples/headers/array/README.md new file mode 100644 index 0000000000..3ef60579b1 --- /dev/null +++ b/examples/headers/array/README.md @@ -0,0 +1,36 @@ +# headers option as an object + +Adds headers to all responses. + +**webpack.config.js** + +```js +module.exports = { + // ... + devServer: { + headers: [ + { + key: "X-Foo", + value: "value1", + }, + { + key: "X-Bar", + value: "value2", + }, + ], + }, +}; +``` + +To run this example use the following command: + +```console +npx webpack serve --open +``` + +## What should happen + +1. The script should open `http://localhost:8080/`. +2. You should see the text on the page itself change to read `Success!`. +3. Open the console in your browser's devtools and select the _Network_ tab. +4. Find `main.js`. The response headers should contain `X-Foo: value1` and `X-Bar: value2`. diff --git a/examples/headers/array/app.js b/examples/headers/array/app.js new file mode 100644 index 0000000000..51cf4a396b --- /dev/null +++ b/examples/headers/array/app.js @@ -0,0 +1,6 @@ +"use strict"; + +const target = document.querySelector("#target"); + +target.classList.add("pass"); +target.innerHTML = "Success!"; diff --git a/examples/headers/array/webpack.config.js b/examples/headers/array/webpack.config.js new file mode 100644 index 0000000000..e2ad9d34a0 --- /dev/null +++ b/examples/headers/array/webpack.config.js @@ -0,0 +1,22 @@ +"use strict"; + +// our setup function adds behind-the-scenes bits to the config that all of our +// examples need +const { setup } = require("../../util"); + +module.exports = setup({ + context: __dirname, + entry: "./app.js", + devServer: { + headers: [ + { + key: "X-Foo", + value: "value1", + }, + { + key: "X-Bar", + value: "value2", + }, + ], + }, +}); From fde859a5e8ec29d0951e4e1908a8b2cedbb6683b Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Wed, 15 Sep 2021 14:33:10 +0530 Subject: [PATCH 5/8] refactor: schema --- lib/Server.js | 1 - lib/options.json | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Server.js b/lib/Server.js index 0e42499cd7..eef76a092d 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -491,7 +491,6 @@ class Server { } // normalize `headers` - if ( typeof options.headers !== "undefined" && Array.isArray(options.headers) diff --git a/lib/options.json b/lib/options.json index 639ed5c391..f1c01e5d91 100644 --- a/lib/options.json +++ b/lib/options.json @@ -381,6 +381,9 @@ "description": "value of header.", "type": "string" } + }, + "cli": { + "exclude": true } }, "Headers": { From dbb6de428c963a860228bbbe69e0edfa245b90cc Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Wed, 15 Sep 2021 19:05:22 +0530 Subject: [PATCH 6/8] test: more --- lib/Server.js | 26 ++++--- .../headers.test.js.snap.webpack4 | 10 +++ .../headers.test.js.snap.webpack5 | 10 +++ test/e2e/headers.test.js | 72 +++++++++++++++++++ 4 files changed, 110 insertions(+), 8 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index eef76a092d..7688da5dfc 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -491,17 +491,27 @@ class Server { } // normalize `headers` - if ( - typeof options.headers !== "undefined" && - Array.isArray(options.headers) - ) { + if (typeof options.headers !== "undefined") { const allHeaders = {}; - options.headers.forEach((header) => { - allHeaders[header.key] = header.value; - }); + if (Array.isArray(options.headers)) { + options.headers.forEach((header) => { + allHeaders[header.key] = header.value; + }); + options.headers = allHeaders; + } - options.headers = allHeaders; + if (typeof options.headers === "function") { + const returnedHeaders = options.headers(); + + if (Array.isArray(returnedHeaders)) { + returnedHeaders.forEach((header) => { + allHeaders[header.key] = header.value; + }); + + options.headers = allHeaders; + } + } } if (typeof options.historyApiFallback === "undefined") { diff --git a/test/e2e/__snapshots__/headers.test.js.snap.webpack4 b/test/e2e/__snapshots__/headers.test.js.snap.webpack4 index c9edf9f641..2deab3832d 100644 --- a/test/e2e/__snapshots__/headers.test.js.snap.webpack4 +++ b/test/e2e/__snapshots__/headers.test.js.snap.webpack4 @@ -1,5 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`headers option as a function returning an array should handle GET request with headers: console messages 1`] = `Array []`; + +exports[`headers option as a function returning an array should handle GET request with headers: page errors 1`] = `Array []`; + +exports[`headers option as a function returning an array should handle GET request with headers: response headers x-bar 1`] = `"value2"`; + +exports[`headers option as a function returning an array should handle GET request with headers: response headers x-foo 1`] = `"value1"`; + +exports[`headers option as a function returning an array should handle GET request with headers: response status 1`] = `200`; + exports[`headers option as a function should handle GET request with headers as a function: console messages 1`] = `Array []`; exports[`headers option as a function should handle GET request with headers as a function: page errors 1`] = `Array []`; diff --git a/test/e2e/__snapshots__/headers.test.js.snap.webpack5 b/test/e2e/__snapshots__/headers.test.js.snap.webpack5 index c9edf9f641..2deab3832d 100644 --- a/test/e2e/__snapshots__/headers.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/headers.test.js.snap.webpack5 @@ -1,5 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`headers option as a function returning an array should handle GET request with headers: console messages 1`] = `Array []`; + +exports[`headers option as a function returning an array should handle GET request with headers: page errors 1`] = `Array []`; + +exports[`headers option as a function returning an array should handle GET request with headers: response headers x-bar 1`] = `"value2"`; + +exports[`headers option as a function returning an array should handle GET request with headers: response headers x-foo 1`] = `"value1"`; + +exports[`headers option as a function returning an array should handle GET request with headers: response status 1`] = `200`; + exports[`headers option as a function should handle GET request with headers as a function: console messages 1`] = `Array []`; exports[`headers option as a function should handle GET request with headers as a function: page errors 1`] = `Array []`; diff --git a/test/e2e/headers.test.js b/test/e2e/headers.test.js index 780c20d88d..8c1b216cdb 100644 --- a/test/e2e/headers.test.js +++ b/test/e2e/headers.test.js @@ -258,6 +258,78 @@ describe("headers option", () => { }); }); + describe("as a function returning an array", () => { + let compiler; + let server; + let page; + let browser; + let pageErrors; + let consoleMessages; + + beforeEach(async () => { + compiler = webpack(config); + + server = new Server( + { + headers: () => [ + { + key: "X-Foo", + value: "value1", + }, + { + key: "X-Bar", + value: "value2", + }, + ], + port, + }, + compiler + ); + + await server.start(); + + ({ page, browser } = await runBrowser()); + + pageErrors = []; + consoleMessages = []; + }); + + afterEach(async () => { + await browser.close(); + await server.stop(); + }); + + it("should handle GET request with headers", async () => { + page + .on("console", (message) => { + consoleMessages.push(message); + }) + .on("pageerror", (error) => { + pageErrors.push(error); + }); + + const response = await page.goto(`http://127.0.0.1:${port}/main.js`, { + waitUntil: "networkidle0", + }); + + expect(response.headers()["x-foo"]).toMatchSnapshot( + "response headers x-foo" + ); + + expect(response.headers()["x-bar"]).toMatchSnapshot( + "response headers x-bar" + ); + + expect(response.status()).toMatchSnapshot("response status"); + + expect(consoleMessages.map((message) => message.text())).toMatchSnapshot( + "console messages" + ); + + expect(pageErrors).toMatchSnapshot("page errors"); + }); + }); + describe("dev middleware headers take precedence for dev middleware output files", () => { let compiler; let server; From cde531941217ea5bce479f4d52e12d8efbae2508 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Thu, 16 Sep 2021 08:36:31 +0530 Subject: [PATCH 7/8] refactor: code --- lib/Server.js | 39 +++++++++++++-------------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index 7688da5dfc..6e407a55da 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -490,29 +490,7 @@ class Server { options.devMiddleware = {}; } - // normalize `headers` - if (typeof options.headers !== "undefined") { - const allHeaders = {}; - - if (Array.isArray(options.headers)) { - options.headers.forEach((header) => { - allHeaders[header.key] = header.value; - }); - options.headers = allHeaders; - } - - if (typeof options.headers === "function") { - const returnedHeaders = options.headers(); - - if (Array.isArray(returnedHeaders)) { - returnedHeaders.forEach((header) => { - allHeaders[header.key] = header.value; - }); - - options.headers = allHeaders; - } - } - } + // No need to normalize `headers` if (typeof options.historyApiFallback === "undefined") { options.historyApiFallback = false; @@ -1867,10 +1845,19 @@ class Server { headers = headers(req, res, this.middleware.context); } - // eslint-disable-next-line guard-for-in - for (const name in headers) { - res.setHeader(name, headers[name]); + const allHeaders = []; + + if (typeof headers === "object" && !Array.isArray(headers)) { + // eslint-disable-next-line guard-for-in + for (const name in headers) { + allHeaders.push({ key: name, value: headers[name] }); + } + headers = allHeaders; } + + headers.forEach((header) => { + res.setHeader(header.key, header.value); + }); } next(); From f2a2d9c3f3cb3d6b249e5bee0ac2c0d4761f17da Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Thu, 16 Sep 2021 16:53:55 +0530 Subject: [PATCH 8/8] refactor: code Co-authored-by: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> --- lib/Server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Server.js b/lib/Server.js index 6e407a55da..8193582d8f 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -1847,7 +1847,7 @@ class Server { const allHeaders = []; - if (typeof headers === "object" && !Array.isArray(headers)) { + if (!Array.isArray(headers)) { // eslint-disable-next-line guard-for-in for (const name in headers) { allHeaders.push({ key: name, value: headers[name] });