From 9911437186a28b2249adfb282524361d7afe6c25 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Thu, 16 Sep 2021 17:41:34 +0530 Subject: [PATCH] feat: allow array for `headers` option (#3847) --- examples/headers/array/README.md | 36 +++++ examples/headers/array/app.js | 6 + examples/headers/array/webpack.config.js | 22 +++ lib/Server.js | 15 +- lib/options.json | 24 +++ .../validate-options.test.js.snap.webpack4 | 19 ++- .../validate-options.test.js.snap.webpack5 | 19 ++- .../headers.test.js.snap.webpack4 | 20 +++ .../headers.test.js.snap.webpack5 | 20 +++ test/e2e/headers.test.js | 144 ++++++++++++++++++ test/validate-options.test.js | 4 +- 11 files changed, 320 insertions(+), 9 deletions(-) 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", + }, + ], + }, +}); diff --git a/lib/Server.js b/lib/Server.js index 5d1c6835d5..8193582d8f 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -1845,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 (!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(); diff --git a/lib/options.json b/lib/options.json index b1c6215516..f1c01e5d91 100644 --- a/lib/options.json +++ b/lib/options.json @@ -369,8 +369,32 @@ "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" + } + }, + "cli": { + "exclude": true + } + }, "Headers": { "anyOf": [ + { + "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/e2e/__snapshots__/headers.test.js.snap.webpack4 b/test/e2e/__snapshots__/headers.test.js.snap.webpack4 index 808f4404ac..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 []`; @@ -19,6 +29,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..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 []`; @@ -19,6 +29,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..8c1b216cdb 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; @@ -186,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; 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],