diff --git a/README.md b/README.md index a4cddea06..76f9682a6 100644 --- a/README.md +++ b/README.md @@ -60,18 +60,19 @@ 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. | +| 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. | The middleware accepts an `options` Object. The following is a property reference for the Object. @@ -119,14 +120,13 @@ webpackDevMiddleware(compiler, { headers: [ { key: "X-custom-header", - value: "foo" + value: "foo", }, { key: "Y-custom-header", - value: "bar" - } - ] - }, + value: "bar", + }, + ], }); ``` @@ -137,14 +137,13 @@ webpackDevMiddleware(compiler, { headers: () => [ { key: "X-custom-header", - value: "foo" + value: "foo", }, { key: "Y-custom-header", - value: "bar" - } - ] - }, + value: "bar", + }, + ], }); ``` @@ -250,6 +249,28 @@ const compiler = webpack({ middleware(compiler, { outputFileSystem: myOutputFileSystem }); ``` +### modifyResponseData + +Allows to set up a callback to change the response data. + +```js +const webpack = require("webpack"); +const configuration = { + /* Webpack configuration */ +}; +const compiler = webpack(configuration); + +middleware(compiler, { + // Note - if you send the `Range` header you will have `ReadStream` + // Also `data` can be `string` or `Buffer` + modifyResponseData: (req, res, data, byteLength) => { + // Your logic + // Don't use `res.end()` or `res.send()` here + return { data, byteLength }; + }, +}); +``` + ## API `webpack-dev-middleware` also provides convenience methods that can be use to diff --git a/src/index.js b/src/index.js index 7252768c0..9f475cb6b 100644 --- a/src/index.js +++ b/src/index.js @@ -17,6 +17,7 @@ const noop = () => {}; /** @typedef {import("webpack").Configuration} Configuration */ /** @typedef {import("webpack").Stats} Stats */ /** @typedef {import("webpack").MultiStats} MultiStats */ +/** @typedef {import("fs").ReadStream} ReadStream */ /** * @typedef {Object} ExtendedServerResponse @@ -55,6 +56,23 @@ const noop = () => {}; * @param {Stats | MultiStats} [stats] */ +/** + * @typedef {Object} ResponseData + * @property {string | Buffer | ReadStream} data + * @property {number} byteLength + */ + +/** + * @template {IncomingMessage} RequestInternal + * @template {ServerResponse} ResponseInternal + * @callback ModifyResponseData + * @param {RequestInternal} req + * @param {ResponseInternal} res + * @param {string | Buffer | ReadStream} data + * @param {number} byteLength + * @return {ResponseData} + */ + /** * @template {IncomingMessage} RequestInternal * @template {ServerResponse} ResponseInternal @@ -89,6 +107,7 @@ const noop = () => {}; * @property {boolean} [serverSideRender] * @property {OutputFileSystem} [outputFileSystem] * @property {boolean | string} [index] + * @property {ModifyResponseData} [modifyResponseData] */ /** diff --git a/src/middleware.js b/src/middleware.js index 76c2e4eab..5f84debf2 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -202,8 +202,9 @@ function wrapper(context) { ); setHeaderForResponse(res, "Content-Type", "text/html; charset=utf-8"); - const document = createHtmlDocument(416, `Error: ${message}`); - const byteLength = Buffer.byteLength(document); + /** @type {string | Buffer | import("fs").ReadStream} */ + let document = createHtmlDocument(416, `Error: ${message}`); + let byteLength = Buffer.byteLength(document); setHeaderForResponse( res, @@ -211,6 +212,16 @@ function wrapper(context) { Buffer.byteLength(document) ); + if (context.options.modifyResponseData) { + ({ data: document, byteLength } = + context.options.modifyResponseData( + req, + res, + document, + byteLength + )); + } + send(req, res, document, byteLength); return; @@ -244,7 +255,7 @@ function wrapper(context) { const isFsSupportsStream = typeof context.outputFileSystem.createReadStream === "function"; - let bufferOtStream; + let bufferOrStream; let byteLength; try { @@ -253,7 +264,7 @@ function wrapper(context) { typeof end !== "undefined" && isFsSupportsStream ) { - bufferOtStream = + bufferOrStream = /** @type {import("fs").createReadStream} */ (context.outputFileSystem.createReadStream)(filename, { start, @@ -261,10 +272,10 @@ function wrapper(context) { }); byteLength = end - start + 1; } else { - bufferOtStream = /** @type {import("fs").readFileSync} */ ( + bufferOrStream = /** @type {import("fs").readFileSync} */ ( context.outputFileSystem.readFileSync )(filename); - ({ byteLength } = bufferOtStream); + ({ byteLength } = bufferOrStream); } } catch (_ignoreError) { await goNext(); @@ -272,7 +283,17 @@ function wrapper(context) { return; } - send(req, res, bufferOtStream, byteLength); + if (context.options.modifyResponseData) { + ({ data: bufferOrStream, byteLength } = + context.options.modifyResponseData( + req, + res, + bufferOrStream, + byteLength + )); + } + + send(req, res, bufferOrStream, byteLength); } }; } diff --git a/src/options.json b/src/options.json index 7a56c491c..91086d193 100644 --- a/src/options.json +++ b/src/options.json @@ -124,6 +124,11 @@ "minLength": 1 } ] + }, + "modifyResponseData": { + "description": "Allows to set up a callback to change the response data.", + "link": "https://github.com/webpack/webpack-dev-middleware#modifyresponsedata", + "instanceof": "Function" } }, "additionalProperties": false diff --git a/test/__snapshots__/validation-options.test.js.snap.webpack5 b/test/__snapshots__/validation-options.test.js.snap.webpack5 index fd9f23597..3c2fc74a8 100644 --- a/test/__snapshots__/validation-options.test.js.snap.webpack5 +++ b/test/__snapshots__/validation-options.test.js.snap.webpack5 @@ -77,6 +77,13 @@ exports[`validation should throw an error on the "methods" option with "true" va -> Read more at https://github.com/webpack/webpack-dev-middleware#methods" `; +exports[`validation should throw an error on the "mimeTypeDefault" 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.mimeTypeDefault should be a string. + -> Allows a user to register a default mime type when we can't determine the content type. + -> Read more at https://github.com/webpack/webpack-dev-middleware#mimetypedefault" +`; + 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: @@ -85,6 +92,13 @@ exports[`validation should throw an error on the "mimeTypes" option with "foo" v -> Read more at https://github.com/webpack/webpack-dev-middleware#mimetypes" `; +exports[`validation should throw an error on the "modifyResponseData" 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.modifyResponseData should be an instance of function. + -> Allows to set up a callback to change the response data. + -> Read more at https://github.com/webpack/webpack-dev-middleware#modifyresponsedata" +`; + 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: diff --git a/test/helpers/isWebpack5.js b/test/helpers/isWebpack5.js deleted file mode 100644 index 0e1c28e3c..000000000 --- a/test/helpers/isWebpack5.js +++ /dev/null @@ -1,3 +0,0 @@ -import webpack from "webpack"; - -export default () => webpack.version[0] === "5"; diff --git a/test/middleware.test.js b/test/middleware.test.js index da1bdf17a..45b9a9a1a 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -10,7 +10,6 @@ import del from "del"; import middleware from "../src"; import getCompiler from "./helpers/getCompiler"; -import isWebpack5 from "./helpers/isWebpack5"; import webpackConfig from "./fixtures/webpack.config"; import webpackMultiConfig from "./fixtures/webpack.array.config"; @@ -1293,12 +1292,8 @@ describe.each([ ...webpackConfig, output: { filename: "bundle.js", - publicPath: isWebpack5() - ? "/static/[fullhash]/" - : "/static/[hash]/", - path: isWebpack5() - ? path.resolve(__dirname, "./outputs/other-basic-[fullhash]") - : path.resolve(__dirname, "./outputs/other-basic-[hash]"), + publicPath: "/static/[fullhash]/", + path: path.resolve(__dirname, "./outputs/other-basic-[fullhash]"), }, }); @@ -1366,36 +1361,22 @@ describe.each([ ...webpackMultiConfig[0], output: { filename: "bundle.js", - path: isWebpack5() - ? path.resolve( - __dirname, - "./outputs/array-[fullhash]/static-one" - ) - : path.resolve( - __dirname, - "./outputs/array-[hash]/static-one" - ), - publicPath: isWebpack5() - ? "/static-one/[fullhash]/" - : "/static-one/[hash]/", + path: path.resolve( + __dirname, + "./outputs/array-[fullhash]/static-one" + ), + publicPath: "/static-one/[fullhash]/", }, }, { ...webpackMultiConfig[1], output: { filename: "bundle.js", - path: isWebpack5() - ? path.resolve( - __dirname, - "./outputs/array-[fullhash]/static-two" - ) - : path.resolve( - __dirname, - "./outputs/array-[hash]/static-two" - ), - publicPath: isWebpack5() - ? "/static-two/[fullhash]/" - : "/static-two/[hash]/", + path: path.resolve( + __dirname, + "./outputs/array-[fullhash]/static-two" + ), + publicPath: "/static-two/[fullhash]/", }, }, ]); @@ -2688,18 +2669,11 @@ describe.each([ ...{ output: { filename: "bundle.js", - publicPath: isWebpack5() - ? "/static/[fullhash]/" - : "/static/[hash]/", - path: isWebpack5() - ? path.resolve( - __dirname, - "./outputs/write-to-disk-with-hash/dist_[fullhash]" - ) - : path.resolve( - __dirname, - "./outputs/write-to-disk-with-hash/dist_[hash]" - ), + publicPath: "/static/[fullhash]/", + path: path.resolve( + __dirname, + "./outputs/write-to-disk-with-hash/dist_[fullhash]" + ), }, }, }); @@ -3590,5 +3564,63 @@ describe.each([ }); }); }); + + describe("modifyResponseData option", () => { + describe("should work", () => { + let compiler; + + beforeAll((done) => { + const outputPath = path.resolve( + __dirname, + "./outputs/modify-response-data" + ); + + compiler = getCompiler({ + ...webpackConfig, + output: { + filename: "bundle.js", + path: outputPath, + }, + }); + + instance = middleware(compiler, { + modifyResponseData: () => { + const result = Buffer.from("test"); + + return { data: result, byteLength: result.length }; + }, + }); + + app = framework(); + app.use(instance); + + listen = listenShorthand(done); + + req = request(app); + + instance.context.outputFileSystem.mkdirSync(outputPath, { + recursive: true, + }); + instance.context.outputFileSystem.writeFileSync( + path.resolve(outputPath, "file.html"), + "welcome" + ); + }); + + afterAll((done) => { + close(done); + }); + + it("should modify file", async () => { + const response = await req.get("/file.html"); + + expect(response.statusCode).toEqual(200); + expect(response.headers["content-type"]).toEqual( + "text/html; charset=utf-8" + ); + expect(response.text).toEqual("test"); + }); + }); + }); }); }); diff --git a/test/utils/getPaths.test.js b/test/utils/getPaths.test.js index 182afc620..0416b0d99 100644 --- a/test/utils/getPaths.test.js +++ b/test/utils/getPaths.test.js @@ -10,7 +10,6 @@ import listenAndCompile from "../helpers/listenAndCompile"; import webpackSimpleConfig from "../fixtures/webpack.simple.config"; import webpackPublicPathConfig from "../fixtures/webpack.public-path.config"; import webpackMultiConfig from "../fixtures/webpack.array.config"; -import isWebpack5 from "../helpers/isWebpack5"; // Suppress unnecessary stats output global.console.log = jest.fn(); @@ -23,7 +22,7 @@ describe("getPaths", () => { expected: [ { outputPath: path.resolve(__dirname, "../outputs/simple"), - publicPath: isWebpack5() ? "auto" : "", + publicPath: "auto", }, ], }, diff --git a/test/validation-options.test.js b/test/validation-options.test.js index b1e8e49ee..a9b0eebe0 100644 --- a/test/validation-options.test.js +++ b/test/validation-options.test.js @@ -55,6 +55,18 @@ describe("validation", () => { success: [true, false, "normal", "verbose", { all: false, assets: true }], failure: [0], }, + mimeTypeDefault: { + success: ["text/plain"], + failure: [0], + }, + modifyResponseData: { + success: [ + (_ignore, _ignore1, foo, bar) => { + return { foo, bar }; + }, + ], + failure: [true], + }, }; function stringifyValue(value) { diff --git a/types/index.d.ts b/types/index.d.ts index e4e96841c..133d49bac 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -7,6 +7,7 @@ export = wdm; /** @typedef {import("webpack").Configuration} Configuration */ /** @typedef {import("webpack").Stats} Stats */ /** @typedef {import("webpack").MultiStats} MultiStats */ +/** @typedef {import("fs").ReadStream} ReadStream */ /** * @typedef {Object} ExtendedServerResponse * @property {{ webpack?: { devMiddleware?: Context } }} [locals] @@ -35,6 +36,21 @@ export = wdm; * @callback Callback * @param {Stats | MultiStats} [stats] */ +/** + * @typedef {Object} ResponseData + * @property {string | Buffer | ReadStream} data + * @property {number} byteLength + */ +/** + * @template {IncomingMessage} RequestInternal + * @template {ServerResponse} ResponseInternal + * @callback ModifyResponseData + * @param {RequestInternal} req + * @param {ResponseInternal} res + * @param {string | Buffer | ReadStream} data + * @param {number} byteLength + * @return {ResponseData} + */ /** * @template {IncomingMessage} RequestInternal * @template {ServerResponse} ResponseInternal @@ -67,6 +83,7 @@ export = wdm; * @property {boolean} [serverSideRender] * @property {OutputFileSystem} [outputFileSystem] * @property {boolean | string} [index] + * @property {ModifyResponseData} [modifyResponseData] */ /** * @template {IncomingMessage} RequestInternal @@ -131,6 +148,7 @@ declare namespace wdm { Configuration, Stats, MultiStats, + ReadStream, ExtendedServerResponse, IncomingMessage, ServerResponse, @@ -141,6 +159,8 @@ declare namespace wdm { OutputFileSystem, Logger, Callback, + ResponseData, + ModifyResponseData, Context, Headers, Options, @@ -174,6 +194,9 @@ type Options< serverSideRender?: boolean | undefined; outputFileSystem?: OutputFileSystem | undefined; index?: string | boolean | undefined; + modifyResponseData?: + | ModifyResponseData + | undefined; }; type API< RequestInternal extends import("http").IncomingMessage, @@ -184,6 +207,7 @@ type Schema = import("schema-utils/declarations/validate").Schema; type Configuration = import("webpack").Configuration; type Stats = import("webpack").Stats; type MultiStats = import("webpack").MultiStats; +type ReadStream = import("fs").ReadStream; type ExtendedServerResponse = { locals?: | { @@ -212,6 +236,19 @@ type Logger = ReturnType; type Callback = ( stats?: import("webpack").Stats | import("webpack").MultiStats | undefined ) => any; +type ResponseData = { + data: string | Buffer | ReadStream; + byteLength: number; +}; +type ModifyResponseData< + RequestInternal extends import("http").IncomingMessage, + ResponseInternal extends ServerResponse +> = ( + req: RequestInternal, + res: ResponseInternal, + data: string | Buffer | ReadStream, + byteLength: number +) => ResponseData; type Context< RequestInternal extends import("http").IncomingMessage, ResponseInternal extends ServerResponse