diff --git a/README.md b/README.md index 2cbb9b3a5..7233b4cea 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ This property allows a user to pass the list of HTTP request methods accepted by ### headers -Type: `Object|Function` +Type: `Array|Object|Function` Default: `undefined` This property allows a user to pass custom HTTP headers on each request. @@ -98,6 +98,42 @@ webpackDevMiddleware(compiler, { }); ``` +or + +```js +webpackDevMiddleware(compiler, { + headers: [ + { + key: "X-custom-header" + value: "foo" + }, + { + key: "Y-custom-header", + value: "bar" + } + ] + }, +}); +``` + +or + +```js +webpackDevMiddleware(compiler, { + headers: () => [ + { + key: "X-custom-header" + value: "foo" + }, + { + key: "Y-custom-header", + value: "bar" + } + ] + }, +}); +``` + ### index Type: `Boolean|String` diff --git a/src/middleware.js b/src/middleware.js index c09f0239e..6de2dafeb 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -87,14 +87,20 @@ export default function wrapper(context) { headers = headers(req, res, context); } - if (headers) { - const names = Object.keys(headers); + const allHeaders = []; - for (const name of names) { - setHeaderForResponse(res, name, headers[name]); + 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) => { + setHeaderForResponse(res, header.key, header.value); + }); + if (!getHeaderFromResponse(res, "Content-Type")) { // content-type name(like application/javascript; charset=utf-8) or false const contentType = mime.contentType(path.extname(filename)); diff --git a/src/options.json b/src/options.json index 6f18b4869..9cee60776 100644 --- a/src/options.json +++ b/src/options.json @@ -29,6 +29,24 @@ }, "headers": { "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "description": "key of header.", + "type": "string" + }, + "value": { + "description": "value of header.", + "type": "string" + } + } + }, + "minItems": 1 + }, { "type": "object" }, diff --git a/test/__snapshots__/validation-options.test.js.snap.webpack4 b/test/__snapshots__/validation-options.test.js.snap.webpack4 index 6002911f6..00e92ef14 100644 --- a/test/__snapshots__/validation-options.test.js.snap.webpack4 +++ b/test/__snapshots__/validation-options.test.js.snap.webpack4 @@ -1,12 +1,25 @@ // 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 { … } | function + [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." @@ -15,10 +28,12 @@ exports[`validation should throw an error on the "headers" option with "1" value 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 { … } | function + [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." diff --git a/test/__snapshots__/validation-options.test.js.snap.webpack5 b/test/__snapshots__/validation-options.test.js.snap.webpack5 index 6002911f6..00e92ef14 100644 --- a/test/__snapshots__/validation-options.test.js.snap.webpack5 +++ b/test/__snapshots__/validation-options.test.js.snap.webpack5 @@ -1,12 +1,25 @@ // 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 { … } | function + [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." @@ -15,10 +28,12 @@ exports[`validation should throw an error on the "headers" option with "1" value 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 { … } | function + [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." diff --git a/test/middleware.test.js b/test/middleware.test.js index 8be9f00e3..8808cc6bb 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -1012,6 +1012,7 @@ describe.each([ instance = middleware(compiler); app = framework(); + // eslint-disable-next-line no-shadow app.use((req, res, next) => { // Express API if (res.set) { @@ -2133,6 +2134,7 @@ describe.each([ app = framework(); app.use(instance); + // eslint-disable-next-line no-shadow app.use("/file.jpg", (req, res) => { // Express API if (res.send) { @@ -2762,6 +2764,7 @@ describe.each([ }); it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { + // eslint-disable-next-line no-shadow app.use("/file.jpg", (req, res) => { // Express API if (res.send) { @@ -2779,6 +2782,62 @@ describe.each([ expect(res.headers["X-nonsense-2"]).toBeUndefined(); }); }); + + describe("works with array of objects", () => { + beforeEach((done) => { + const compiler = getCompiler(webpackConfig); + + instance = middleware(compiler, { + headers: [ + { + key: "X-Foo", + value: "value1", + }, + { + key: "X-Bar", + value: "value2", + }, + ], + }); + + app = framework(); + app.use(instance); + + listen = listenShorthand(done); + + req = request(app); + }); + + afterEach(close); + + it('should return the "200" code for the "GET" request to the bundle file and return headers', async () => { + const response = await req.get(`/bundle.js`); + + expect(response.statusCode).toEqual(200); + expect(response.headers["x-foo"]).toEqual("value1"); + expect(response.headers["x-bar"]).toEqual("value2"); + }); + + it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { + // eslint-disable-next-line no-shadow + app.use("/file.jpg", (req, res) => { + // Express API + if (res.send) { + res.send("welcome"); + } + // Connect API + else { + res.end("welcome"); + } + }); + + const res = await request(app).get("/file.jpg"); + expect(res.statusCode).toEqual(200); + expect(res.headers["x-foo"]).toBeUndefined(); + expect(res.headers["x-bar"]).toBeUndefined(); + }); + }); + describe("works with function", () => { beforeEach((done) => { const compiler = getCompiler(webpackConfig); @@ -2808,6 +2867,7 @@ describe.each([ }); it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { + // eslint-disable-next-line no-shadow app.use("/file.jpg", (req, res) => { // Express API if (res.send) { @@ -2826,12 +2886,67 @@ describe.each([ }); }); + describe("works with function returning an array", () => { + beforeEach((done) => { + const compiler = getCompiler(webpackConfig); + + instance = middleware(compiler, { + headers: () => [ + { + key: "X-Foo", + value: "value1", + }, + { + key: "X-Bar", + value: "value2", + }, + ], + }); + + app = framework(); + app.use(instance); + + listen = listenShorthand(done); + + req = request(app); + }); + + afterEach(close); + + it('should return the "200" code for the "GET" request to the bundle file and return headers', async () => { + const response = await req.get(`/bundle.js`); + + expect(response.statusCode).toEqual(200); + expect(response.headers["x-foo"]).toEqual("value1"); + expect(response.headers["x-bar"]).toEqual("value2"); + }); + + it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { + // eslint-disable-next-line no-shadow + app.use("/file.jpg", (req, res) => { + // Express API + if (res.send) { + res.send("welcome"); + } + // Connect API + else { + res.end("welcome"); + } + }); + + const res = await req.get("/file.jpg"); + expect(res.statusCode).toEqual(200); + expect(res.headers["x-foo"]).toBeUndefined(); + expect(res.headers["x-bar"]).toBeUndefined(); + }); + }); + describe("works with headers function with params", () => { beforeEach((done) => { const compiler = getCompiler(webpackConfig); instance = middleware(compiler, { - // eslint-disable-next-line no-unused-vars + // eslint-disable-next-line no-unused-vars, no-shadow headers: (req, res, context) => { res.setHeader("X-nonsense-1", "yes"); res.setHeader("X-nonsense-2", "no"); @@ -2857,6 +2972,7 @@ describe.each([ }); it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { + // eslint-disable-next-line no-shadow app.use("/file.jpg", (req, res) => { // Express API if (res.send) { @@ -2934,6 +3050,7 @@ describe.each([ app = framework(); app.use(instance); + // eslint-disable-next-line no-shadow app.use((req, res) => { // eslint-disable-next-line prefer-destructuring locals = res.locals; diff --git a/test/validation-options.test.js b/test/validation-options.test.js index be7164773..b1e8e49ee 100644 --- a/test/validation-options.test.js +++ b/test/validation-options.test.js @@ -28,8 +28,12 @@ describe("validation", () => { failure: [{}, true], }, headers: { - success: [{ "X-Custom-Header": "yes" }, () => {}], - failure: [true, 1], + success: [ + { "X-Custom-Header": "yes" }, + () => {}, + [{ key: "foo", value: "bar" }], + ], + failure: [true, 1, [], [{ foo: "bar" }]], }, publicPath: { success: ["/foo", "", "auto", () => "/public/path"],