diff --git a/README.md b/README.md index a403ea19e..eb787f3fd 100644 --- a/README.md +++ b/README.md @@ -70,12 +70,34 @@ This property allows a user to pass the list of HTTP request methods accepted by ### headers -Type: `Object` +Type: `Object|Function` Default: `undefined` This property allows a user to pass custom HTTP headers on each request. eg. `{ "X-Custom-Header": "yes" }` +or + +```js +webpackDevMiddleware(compiler, { + headers: () => { + return { + 'Last-Modified': new Date(), + }; + }, +}); +``` + +or + +```js +webpackDevMiddleware(compiler, { + headers: (req, res, context) => { + res.setHeader('Last-Modified', new Date()); + }, +}); +``` + ### index Type: `Boolean|String` diff --git a/src/middleware.js b/src/middleware.js index e22393db1..8f6aab6f1 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -42,7 +42,12 @@ export default function wrapper(context) { async function processRequest() { const filename = getFilenameFromUrl(context, req.url); - const { headers } = context.options; + let { headers } = context.options; + + if (typeof headers === 'function') { + headers = headers(req, res, context); + } + let content; if (!filename) { diff --git a/src/options.json b/src/options.json index 0969a0154..a6c605696 100644 --- a/src/options.json +++ b/src/options.json @@ -25,7 +25,14 @@ } }, "headers": { - "type": "object" + "anyOf": [ + { + "type": "object" + }, + { + "instanceof": "Function" + } + ] }, "publicPath": { "description": "The `publicPath` specifies the public URL address of the output files when referenced in a browser.", diff --git a/test/__snapshots__/validation-options.test.js.snap.webpack4 b/test/__snapshots__/validation-options.test.js.snap.webpack4 index 3b46c1351..4cb0a3ea8 100644 --- a/test/__snapshots__/validation-options.test.js.snap.webpack4 +++ b/test/__snapshots__/validation-options.test.js.snap.webpack4 @@ -1,9 +1,23 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +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 + Details: + * 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 an object: - object { … }" + - options.headers should be one of these: + object { … } | function + Details: + * 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`] = ` diff --git a/test/__snapshots__/validation-options.test.js.snap.webpack5 b/test/__snapshots__/validation-options.test.js.snap.webpack5 index 3b46c1351..4cb0a3ea8 100644 --- a/test/__snapshots__/validation-options.test.js.snap.webpack5 +++ b/test/__snapshots__/validation-options.test.js.snap.webpack5 @@ -1,9 +1,23 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +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 + Details: + * 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 an object: - object { … }" + - options.headers should be one of these: + object { … } | function + Details: + * 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`] = ` diff --git a/test/middleware.test.js b/test/middleware.test.js index 0f2657f55..99b000ad3 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -2392,45 +2392,137 @@ describe.each([ }); describe('headers option', () => { - beforeEach((done) => { - const compiler = getCompiler(webpackConfig); + describe('works with object', () => { + beforeEach((done) => { + const compiler = getCompiler(webpackConfig); - instance = middleware(compiler, { - headers: { 'X-nonsense-1': 'yes', 'X-nonsense-2': 'no' }, + instance = middleware(compiler, { + headers: { 'X-nonsense-1': 'yes', 'X-nonsense-2': 'no' }, + }); + + app = framework(); + app.use(instance); + + listen = listenShorthand(done); }); - app = framework(); - app.use(instance); + afterEach(close); - listen = listenShorthand(done); + it('should return the "200" code for the "GET" request to the bundle file and return headers', (done) => { + request(app) + .get('/bundle.js') + .expect('X-nonsense-1', 'yes') + .expect('X-nonsense-2', 'no') + .expect(200, done); + }); + + it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { + 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-nonsense-1']).toBeUndefined(); + expect(res.headers['X-nonsense-2']).toBeUndefined(); + }); }); + describe('works with function', () => { + beforeEach((done) => { + const compiler = getCompiler(webpackConfig); - afterEach(close); + instance = middleware(compiler, { + headers: () => { + return { 'X-nonsense-1': 'yes', 'X-nonsense-2': 'no' }; + }, + }); - it('should return the "200" code for the "GET" request to the bundle file and return headers', (done) => { - request(app) - .get('/bundle.js') - .expect('X-nonsense-1', 'yes') - .expect('X-nonsense-2', 'no') - .expect(200, done); + app = framework(); + app.use(instance); + + listen = listenShorthand(done); + }); + + afterEach(close); + + it('should return the "200" code for the "GET" request to the bundle file and return headers', (done) => { + request(app) + .get('/bundle.js') + .expect('X-nonsense-1', 'yes') + .expect('X-nonsense-2', 'no') + .expect(200, done); + }); + + it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { + 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-nonsense-1']).toBeUndefined(); + expect(res.headers['X-nonsense-2']).toBeUndefined(); + }); }); + describe('works with headers function with params', () => { + beforeEach((done) => { + const compiler = getCompiler(webpackConfig); - it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { - app.use('/file.jpg', (req, res) => { - // Express API - if (res.send) { - res.send('welcome'); - } - // Connect API - else { - res.end('welcome'); - } + instance = middleware(compiler, { + // eslint-disable-next-line no-unused-vars + headers: (req, res, context) => { + res.setHeader('X-nonsense-1', 'yes'); + res.setHeader('X-nonsense-2', 'no'); + }, + }); + + app = framework(); + app.use(instance); + + listen = listenShorthand(done); }); - const res = await request(app).get('/file.jpg'); - expect(res.statusCode).toEqual(200); - expect(res.headers['X-nonsense-1']).toBeUndefined(); - expect(res.headers['X-nonsense-2']).toBeUndefined(); + afterEach(close); + + it('should return the "200" code for the "GET" request to the bundle file and return headers', (done) => { + request(app) + .get('/bundle.js') + .expect('X-nonsense-1', 'yes') + .expect('X-nonsense-2', 'no') + .expect(200, done); + }); + + it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { + 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-nonsense-1']).toBeUndefined(); + expect(res.headers['X-nonsense-2']).toBeUndefined(); + }); }); }); diff --git a/test/validation-options.test.js b/test/validation-options.test.js index 300d6ba80..9607aa557 100644 --- a/test/validation-options.test.js +++ b/test/validation-options.test.js @@ -28,8 +28,8 @@ describe('validation', () => { failure: [{}, true], }, headers: { - success: [{ 'X-Custom-Header': 'yes' }], - failure: [true], + success: [{ 'X-Custom-Header': 'yes' }, () => {}], + failure: [true, 1], }, publicPath: { success: ['/foo', '', 'auto', () => '/public/path'],