From c13aa560651a3bb4c4a7b1b4363c04383596c7e9 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Sat, 18 Dec 2021 20:25:05 +0530 Subject: [PATCH] feat: added the `setupMiddlewares` option (#4068) --- examples/setup-middlewares/README.md | 49 ++ examples/setup-middlewares/app.js | 6 + examples/setup-middlewares/webpack.config.js | 36 ++ lib/Server.js | 463 +++++++++--------- lib/options.json | 8 + .../validate-options.test.js.snap.webpack4 | 21 + .../validate-options.test.js.snap.webpack5 | 21 + .../headers.test.js.snap.webpack4 | 8 + .../headers.test.js.snap.webpack5 | 8 + .../setup-middlewares.test.js.snap.webpack4 | 39 ++ .../setup-middlewares.test.js.snap.webpack5 | 39 ++ test/e2e/headers.test.js | 64 +++ test/e2e/on-after-setup-middleware.test.js | 13 + test/e2e/on-before-setup-middleware.test.js | 13 + test/e2e/setup-middlewares.test.js | 169 +++++++ test/ports-map.js | 1 + test/validate-options.test.js | 4 + 17 files changed, 732 insertions(+), 230 deletions(-) create mode 100644 examples/setup-middlewares/README.md create mode 100644 examples/setup-middlewares/app.js create mode 100644 examples/setup-middlewares/webpack.config.js create mode 100644 test/e2e/__snapshots__/setup-middlewares.test.js.snap.webpack4 create mode 100644 test/e2e/__snapshots__/setup-middlewares.test.js.snap.webpack5 create mode 100644 test/e2e/setup-middlewares.test.js diff --git a/examples/setup-middlewares/README.md b/examples/setup-middlewares/README.md new file mode 100644 index 0000000000..84aa2cc506 --- /dev/null +++ b/examples/setup-middlewares/README.md @@ -0,0 +1,49 @@ +# setupMiddlewares + +Provides the ability to execute a custom function and apply custom middleware(s). + +**webpack.config.js** + +```js +module.exports = { + // ... + devServer: { + setupMiddlewares: (middlewares, devServer) => { + if (!devServer) { + throw new Error("webpack-dev-server is not defined"); + } + + devServer.app.get("/setup-middleware/some/path", (_, response) => { + response.send("setup-middlewares option GET"); + }); + + middlewares.push({ + name: "hello-world-test-one", + // `path` is optional + path: "/foo/bar", + middleware: (req, res) => { + res.send("Foo Bar!"); + }, + }); + + middlewares.push((req, res) => { + res.send("Hello World!"); + }); + + return middlewares; + }, + }, +}; +``` + +To run this example use the following command: + +```console +npx webpack serve --open +``` + +## What Should Happen + +1. The script should open `http://localhost:8080/` in your default browser. +2. You should see the text on the page itself change to read `Success!`. +3. Go to `http://localhost:8080/setup-middleware/some/path`, you should see the text on the page itself change to read `setup-middlewares option GET`. diff --git a/examples/setup-middlewares/app.js b/examples/setup-middlewares/app.js new file mode 100644 index 0000000000..51cf4a396b --- /dev/null +++ b/examples/setup-middlewares/app.js @@ -0,0 +1,6 @@ +"use strict"; + +const target = document.querySelector("#target"); + +target.classList.add("pass"); +target.innerHTML = "Success!"; diff --git a/examples/setup-middlewares/webpack.config.js b/examples/setup-middlewares/webpack.config.js new file mode 100644 index 0000000000..df9d9fb74a --- /dev/null +++ b/examples/setup-middlewares/webpack.config.js @@ -0,0 +1,36 @@ +"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: { + setupMiddlewares: (middlewares, devServer) => { + if (!devServer) { + throw new Error("webpack-dev-server is not defined"); + } + + devServer.app.get("/setup-middleware/some/path", (_, response) => { + response.send("setup-middlewares option GET"); + }); + + middlewares.push({ + name: "hello-world-test-one", + // `path` is optional + path: "/foo/bar", + middleware: (req, res) => { + res.send("Foo Bar!"); + }, + }); + + middlewares.push((req, res) => { + res.send("Hello World!"); + }); + + return middlewares; + }, + }, +}); diff --git a/lib/Server.js b/lib/Server.js index 3185b34586..4a1ce11a6a 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -965,6 +965,24 @@ class Server { options.open = [...getOpenItemsFromObject(options.open)]; } + if (options.onAfterSetupMiddleware) { + // TODO: remove in the next major release + util.deprecate( + () => {}, + "'onAfterSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.", + `DEP_WEBPACK_DEV_SERVER_ON_AFTER_SETUP_MIDDLEWARE` + )(); + } + + if (options.onBeforeSetupMiddleware) { + // TODO: remove in the next major release + util.deprecate( + () => {}, + "'onBeforeSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.", + `DEP_WEBPACK_DEV_SERVER_ON_BEFORE_SETUP_MIDDLEWARE` + )(); + } + if (typeof options.port === "string" && options.port !== "auto") { options.port = Number(options.port); } @@ -1305,7 +1323,8 @@ class Server { // Should be after `webpack-dev-middleware`, otherwise other middlewares might rewrite response this.setupBuiltInRoutes(); this.setupWatchFiles(); - this.setupFeatures(); + this.setupWatchStaticFiles(); + this.setupMiddlewares(); this.createServer(); if (this.options.setupExitSignals) { @@ -1466,279 +1485,259 @@ class Server { }); } - setupCompressFeature() { - const compress = require("compression"); + setupWatchStaticFiles() { + if (this.options.static.length > 0) { + this.options.static.forEach((staticOption) => { + if (staticOption.watch) { + this.watchFiles(staticOption.directory, staticOption.watch); + } + }); + } + } + + setupWatchFiles() { + const { watchFiles } = this.options; - this.app.use(compress()); + if (watchFiles.length > 0) { + watchFiles.forEach((item) => { + this.watchFiles(item.paths, item.options); + }); + } } - setupProxyFeature() { - const { createProxyMiddleware } = require("http-proxy-middleware"); + setupMiddlewares() { + let middlewares = []; - const getProxyMiddleware = (proxyConfig) => { - // It is possible to use the `bypass` method without a `target` or `router`. - // However, the proxy middleware has no use in this case, and will fail to instantiate. - if (proxyConfig.target) { - const context = proxyConfig.context || proxyConfig.path; + // compress is placed last and uses unshift so that it will be the first middleware used + if (this.options.compress) { + const compression = require("compression"); - return createProxyMiddleware(context, proxyConfig); - } + middlewares.push({ name: "compression", middleware: compression() }); + } - if (proxyConfig.router) { - return createProxyMiddleware(proxyConfig); - } - }; - /** - * Assume a proxy configuration specified as: - * proxy: [ - * { - * context: "value", - * ...options, - * }, - * // or: - * function() { - * return { - * context: "context", - * ...options, - * }; - * } - * ] - */ - this.options.proxy.forEach((proxyConfigOrCallback) => { - let proxyMiddleware; + if (this.options.onBeforeSetupMiddleware) { + this.options.onBeforeSetupMiddleware(this); + } - let proxyConfig = - typeof proxyConfigOrCallback === "function" - ? proxyConfigOrCallback() - : proxyConfigOrCallback; + if (typeof this.options.headers !== "undefined") { + middlewares.push({ + name: "set-headers", + path: "*", + middleware: this.setHeaders.bind(this), + }); + } - proxyMiddleware = getProxyMiddleware(proxyConfig); + middlewares.push({ + name: "webpack-dev-middleware", + middleware: this.middleware, + }); - if (proxyConfig.ws) { - this.webSocketProxies.push(proxyMiddleware); - } + if (this.options.proxy) { + const { createProxyMiddleware } = require("http-proxy-middleware"); - const handle = async (req, res, next) => { - if (typeof proxyConfigOrCallback === "function") { - const newProxyConfig = proxyConfigOrCallback(req, res, next); + const getProxyMiddleware = (proxyConfig) => { + // It is possible to use the `bypass` method without a `target` or `router`. + // However, the proxy middleware has no use in this case, and will fail to instantiate. + if (proxyConfig.target) { + const context = proxyConfig.context || proxyConfig.path; - if (newProxyConfig !== proxyConfig) { - proxyConfig = newProxyConfig; - proxyMiddleware = getProxyMiddleware(proxyConfig); - } + return createProxyMiddleware(context, proxyConfig); } - // - Check if we have a bypass function defined - // - In case the bypass function is defined we'll retrieve the - // bypassUrl from it otherwise bypassUrl would be null - // TODO remove in the next major in favor `context` and `router` options - const isByPassFuncDefined = typeof proxyConfig.bypass === "function"; - const bypassUrl = isByPassFuncDefined - ? await proxyConfig.bypass(req, res, proxyConfig) - : null; - - if (typeof bypassUrl === "boolean") { - // skip the proxy - req.url = null; - next(); - } else if (typeof bypassUrl === "string") { - // byPass to that url - req.url = bypassUrl; - next(); - } else if (proxyMiddleware) { - return proxyMiddleware(req, res, next); - } else { - next(); + if (proxyConfig.router) { + return createProxyMiddleware(proxyConfig); } }; - this.app.use(handle); - // Also forward error requests to the proxy so it can handle them. - this.app.use((error, req, res, next) => handle(req, res, next)); - }); - } - - setupHistoryApiFallbackFeature() { - const { historyApiFallback } = this.options; - - if ( - typeof historyApiFallback.logger === "undefined" && - !historyApiFallback.verbose - ) { - historyApiFallback.logger = this.logger.log.bind( - this.logger, - "[connect-history-api-fallback]" - ); - } - - // Fall back to /index.html if nothing else matches. - this.app.use(require("connect-history-api-fallback")(historyApiFallback)); - } - - setupStaticFeature() { - this.options.static.forEach((staticOption) => { - staticOption.publicPath.forEach((publicPath) => { - this.app.use( - publicPath, - express.static(staticOption.directory, staticOption.staticOptions) - ); - }); - }); - } + /** + * Assume a proxy configuration specified as: + * proxy: [ + * { + * context: "value", + * ...options, + * }, + * // or: + * function() { + * return { + * context: "context", + * ...options, + * }; + * } + * ] + */ + this.options.proxy.forEach((proxyConfigOrCallback) => { + let proxyMiddleware; - setupStaticServeIndexFeature() { - const serveIndex = require("serve-index"); + let proxyConfig = + typeof proxyConfigOrCallback === "function" + ? proxyConfigOrCallback() + : proxyConfigOrCallback; - this.options.static.forEach((staticOption) => { - staticOption.publicPath.forEach((publicPath) => { - if (staticOption.serveIndex) { - this.app.use(publicPath, (req, res, next) => { - // serve-index doesn't fallthrough non-get/head request to next middleware - if (req.method !== "GET" && req.method !== "HEAD") { - return next(); - } + proxyMiddleware = getProxyMiddleware(proxyConfig); - serveIndex(staticOption.directory, staticOption.serveIndex)( - req, - res, - next - ); - }); + if (proxyConfig.ws) { + this.webSocketProxies.push(proxyMiddleware); } - }); - }); - } - setupStaticWatchFeature() { - this.options.static.forEach((staticOption) => { - if (staticOption.watch) { - this.watchFiles(staticOption.directory, staticOption.watch); - } - }); - } + const handler = async (req, res, next) => { + if (typeof proxyConfigOrCallback === "function") { + const newProxyConfig = proxyConfigOrCallback(req, res, next); - setupOnBeforeSetupMiddlewareFeature() { - this.options.onBeforeSetupMiddleware(this); - } + if (newProxyConfig !== proxyConfig) { + proxyConfig = newProxyConfig; + proxyMiddleware = getProxyMiddleware(proxyConfig); + } + } - setupWatchFiles() { - const { watchFiles } = this.options; + // - Check if we have a bypass function defined + // - In case the bypass function is defined we'll retrieve the + // bypassUrl from it otherwise bypassUrl would be null + // TODO remove in the next major in favor `context` and `router` options + const isByPassFuncDefined = typeof proxyConfig.bypass === "function"; + const bypassUrl = isByPassFuncDefined + ? await proxyConfig.bypass(req, res, proxyConfig) + : null; + + if (typeof bypassUrl === "boolean") { + // skip the proxy + req.url = null; + next(); + } else if (typeof bypassUrl === "string") { + // byPass to that url + req.url = bypassUrl; + next(); + } else if (proxyMiddleware) { + return proxyMiddleware(req, res, next); + } else { + next(); + } + }; - if (watchFiles.length > 0) { - watchFiles.forEach((item) => { - this.watchFiles(item.paths, item.options); + middlewares.push({ + name: "http-proxy-middleware", + middleware: handler, + }); + // Also forward error requests to the proxy so it can handle them. + middlewares.push({ + name: "http-proxy-middleware-error-handler", + middleware: (error, req, res, next) => handler(req, res, next), + }); }); - } - } - - setupMiddleware() { - this.app.use(this.middleware); - } - - setupOnAfterSetupMiddlewareFeature() { - this.options.onAfterSetupMiddleware(this); - } - - setupHeadersFeature() { - this.app.all("*", this.setHeaders.bind(this)); - } - - setupMagicHtmlFeature() { - this.app.get("*", this.serveMagicHtml.bind(this)); - } - - setupFeatures() { - const features = { - compress: () => { - if (this.options.compress) { - this.setupCompressFeature(); - } - }, - proxy: () => { - if (this.options.proxy) { - this.setupProxyFeature(); - } - }, - historyApiFallback: () => { - if (this.options.historyApiFallback) { - this.setupHistoryApiFallbackFeature(); - } - }, - static: () => { - this.setupStaticFeature(); - }, - staticServeIndex: () => { - this.setupStaticServeIndexFeature(); - }, - staticWatch: () => { - this.setupStaticWatchFeature(); - }, - onBeforeSetupMiddleware: () => { - if (typeof this.options.onBeforeSetupMiddleware === "function") { - this.setupOnBeforeSetupMiddlewareFeature(); - } - }, - onAfterSetupMiddleware: () => { - if (typeof this.options.onAfterSetupMiddleware === "function") { - this.setupOnAfterSetupMiddlewareFeature(); - } - }, - middleware: () => { - // include our middleware to ensure - // it is able to handle '/index.html' request after redirect - this.setupMiddleware(); - }, - headers: () => { - this.setupHeadersFeature(); - }, - magicHtml: () => { - this.setupMagicHtmlFeature(); - }, - }; - const runnableFeatures = []; - - // compress is placed last and uses unshift so that it will be the first middleware used - if (this.options.compress) { - runnableFeatures.push("compress"); + middlewares.push({ + name: "webpack-dev-middleware", + middleware: this.middleware, + }); } - if (this.options.onBeforeSetupMiddleware) { - runnableFeatures.push("onBeforeSetupMiddleware"); + if (this.options.static.length > 0) { + this.options.static.forEach((staticOption) => { + staticOption.publicPath.forEach((publicPath) => { + middlewares.push({ + name: "express-static", + path: publicPath, + middleware: express.static( + staticOption.directory, + staticOption.staticOptions + ), + }); + }); + }); } - runnableFeatures.push("headers", "middleware"); + if (this.options.historyApiFallback) { + const connectHistoryApiFallback = require("connect-history-api-fallback"); + const { historyApiFallback } = this.options; - if (this.options.proxy) { - runnableFeatures.push("proxy", "middleware"); - } + if ( + typeof historyApiFallback.logger === "undefined" && + !historyApiFallback.verbose + ) { + historyApiFallback.logger = this.logger.log.bind( + this.logger, + "[connect-history-api-fallback]" + ); + } - if (this.options.static) { - runnableFeatures.push("static"); - } + // Fall back to /index.html if nothing else matches. + middlewares.push({ + name: "connect-history-api-fallback", + middleware: connectHistoryApiFallback(historyApiFallback), + }); - if (this.options.historyApiFallback) { - runnableFeatures.push("historyApiFallback", "middleware"); + // include our middleware to ensure + // it is able to handle '/index.html' request after redirect + middlewares.push({ + name: "webpack-dev-middleware", + middleware: this.middleware, + }); - if (this.options.static) { - runnableFeatures.push("static"); + if (this.options.static.length > 0) { + this.options.static.forEach((staticOption) => { + staticOption.publicPath.forEach((publicPath) => { + middlewares.push({ + name: "express-static", + path: publicPath, + middleware: express.static( + staticOption.directory, + staticOption.staticOptions + ), + }); + }); + }); } } - if (this.options.static) { - runnableFeatures.push("staticServeIndex", "staticWatch"); + if (this.options.static.length > 0) { + const serveIndex = require("serve-index"); + + this.options.static.forEach((staticOption) => { + staticOption.publicPath.forEach((publicPath) => { + if (staticOption.serveIndex) { + middlewares.push({ + name: "serve-index", + path: publicPath, + middleware: (req, res, next) => { + // serve-index doesn't fallthrough non-get/head request to next middleware + if (req.method !== "GET" && req.method !== "HEAD") { + return next(); + } + + serveIndex(staticOption.directory, staticOption.serveIndex)( + req, + res, + next + ); + }, + }); + } + }); + }); } if (this.options.magicHtml) { - runnableFeatures.push("magicHtml"); + middlewares.push({ + name: "serve-magic-html", + middleware: this.serveMagicHtml.bind(this), + }); } if (this.options.onAfterSetupMiddleware) { - runnableFeatures.push("onAfterSetupMiddleware"); + this.options.onAfterSetupMiddleware(this); + } + + if (typeof this.options.setupMiddlewares === "function") { + middlewares = this.options.setupMiddlewares(middlewares, this); } - runnableFeatures.forEach((feature) => { - features[feature](); + middlewares.forEach((middleware) => { + if (typeof middleware.path !== "undefined") { + this.app.use(middleware.path, middleware.middleware); + } else if (typeof middleware === "function") { + this.app.use(middleware); + } else { + this.app.use(middleware.middleware); + } }); } @@ -2016,7 +2015,7 @@ class Server { } } - if (this.options.static && this.options.static.length > 0) { + if (this.options.static.length > 0) { this.logger.info( `Content not from webpack is served from '${colors.info( useColor, @@ -2170,6 +2169,10 @@ class Server { } serveMagicHtml(req, res, next) { + if (req.method !== "GET" && req.method !== "HEAD") { + return next(); + } + this.middleware.waitUntilValid(() => { const _path = req.path; diff --git a/lib/options.json b/lib/options.json index cca24f9b8f..13516aa3bf 100644 --- a/lib/options.json +++ b/lib/options.json @@ -856,6 +856,11 @@ "exclude": true } }, + "SetupMiddlewares": { + "instanceof": "Function", + "description": "Provides the ability to execute a custom function and apply custom middleware(s).", + "link": "https://webpack.js.org/configuration/dev-server/#devserversetupmiddlewares" + }, "Static": { "anyOf": [ { @@ -1142,6 +1147,9 @@ "setupExitSignals": { "$ref": "#/definitions/SetupExitSignals" }, + "setupMiddlewares": { + "$ref": "#/definitions/SetupMiddlewares" + }, "static": { "$ref": "#/definitions/Static" }, diff --git a/test/__snapshots__/validate-options.test.js.snap.webpack4 b/test/__snapshots__/validate-options.test.js.snap.webpack4 index 90222720fa..aa3e62015a 100644 --- a/test/__snapshots__/validate-options.test.js.snap.webpack4 +++ b/test/__snapshots__/validate-options.test.js.snap.webpack4 @@ -714,6 +714,27 @@ exports[`options validate should throw an error on the "server" option with '{"t -> Request for an SSL certificate." `; +exports[`options validate should throw an error on the "setupMiddlewares" option with '10' value 1`] = ` +"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. + - options.setupMiddlewares should be an instance of function. + -> Provides the ability to execute a custom function and apply custom middleware(s). + -> Read more at https://webpack.js.org/configuration/dev-server/#devserversetupmiddlewares" +`; + +exports[`options validate should throw an error on the "setupMiddlewares" 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.setupMiddlewares should be an instance of function. + -> Provides the ability to execute a custom function and apply custom middleware(s). + -> Read more at https://webpack.js.org/configuration/dev-server/#devserversetupmiddlewares" +`; + +exports[`options validate should throw an error on the "setupMiddlewares" option with 'true' value 1`] = ` +"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. + - options.setupMiddlewares should be an instance of function. + -> Provides the ability to execute a custom function and apply custom middleware(s). + -> Read more at https://webpack.js.org/configuration/dev-server/#devserversetupmiddlewares" +`; + exports[`options validate should throw an error on the "static" 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.static should be a non-empty string." diff --git a/test/__snapshots__/validate-options.test.js.snap.webpack5 b/test/__snapshots__/validate-options.test.js.snap.webpack5 index 90222720fa..aa3e62015a 100644 --- a/test/__snapshots__/validate-options.test.js.snap.webpack5 +++ b/test/__snapshots__/validate-options.test.js.snap.webpack5 @@ -714,6 +714,27 @@ exports[`options validate should throw an error on the "server" option with '{"t -> Request for an SSL certificate." `; +exports[`options validate should throw an error on the "setupMiddlewares" option with '10' value 1`] = ` +"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. + - options.setupMiddlewares should be an instance of function. + -> Provides the ability to execute a custom function and apply custom middleware(s). + -> Read more at https://webpack.js.org/configuration/dev-server/#devserversetupmiddlewares" +`; + +exports[`options validate should throw an error on the "setupMiddlewares" 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.setupMiddlewares should be an instance of function. + -> Provides the ability to execute a custom function and apply custom middleware(s). + -> Read more at https://webpack.js.org/configuration/dev-server/#devserversetupmiddlewares" +`; + +exports[`options validate should throw an error on the "setupMiddlewares" option with 'true' value 1`] = ` +"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. + - options.setupMiddlewares should be an instance of function. + -> Provides the ability to execute a custom function and apply custom middleware(s). + -> Read more at https://webpack.js.org/configuration/dev-server/#devserversetupmiddlewares" +`; + exports[`options validate should throw an error on the "static" 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.static should be a non-empty string." diff --git a/test/e2e/__snapshots__/headers.test.js.snap.webpack4 b/test/e2e/__snapshots__/headers.test.js.snap.webpack4 index 2deab3832d..291c9b3933 100644 --- a/test/e2e/__snapshots__/headers.test.js.snap.webpack4 +++ b/test/e2e/__snapshots__/headers.test.js.snap.webpack4 @@ -21,6 +21,14 @@ key2=value2" exports[`headers option as a function should handle GET request with headers as a function: response status 1`] = `200`; +exports[`headers option as a string and support HEAD request should handle HEAD request with headers: console messages 1`] = `Array []`; + +exports[`headers option as a string and support HEAD request should handle HEAD request with headers: page errors 1`] = `Array []`; + +exports[`headers option as a string and support HEAD request should handle HEAD request with headers: response headers x-foo 1`] = `"dev-server headers"`; + +exports[`headers option as a string and support HEAD request should handle HEAD request with headers: response status 1`] = `200`; + exports[`headers option as a string should handle GET request with headers: console messages 1`] = `Array []`; exports[`headers option as a string should handle GET request with headers: 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 2deab3832d..291c9b3933 100644 --- a/test/e2e/__snapshots__/headers.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/headers.test.js.snap.webpack5 @@ -21,6 +21,14 @@ key2=value2" exports[`headers option as a function should handle GET request with headers as a function: response status 1`] = `200`; +exports[`headers option as a string and support HEAD request should handle HEAD request with headers: console messages 1`] = `Array []`; + +exports[`headers option as a string and support HEAD request should handle HEAD request with headers: page errors 1`] = `Array []`; + +exports[`headers option as a string and support HEAD request should handle HEAD request with headers: response headers x-foo 1`] = `"dev-server headers"`; + +exports[`headers option as a string and support HEAD request should handle HEAD request with headers: response status 1`] = `200`; + exports[`headers option as a string should handle GET request with headers: console messages 1`] = `Array []`; exports[`headers option as a string should handle GET request with headers: page errors 1`] = `Array []`; diff --git a/test/e2e/__snapshots__/setup-middlewares.test.js.snap.webpack4 b/test/e2e/__snapshots__/setup-middlewares.test.js.snap.webpack4 new file mode 100644 index 0000000000..53fc795d44 --- /dev/null +++ b/test/e2e/__snapshots__/setup-middlewares.test.js.snap.webpack4 @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: console messages 1`] = `Array []`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: page errors 1`] = `Array []`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response headers content-type 1`] = `"text/html; charset=utf-8"`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response headers content-type 2`] = `"text/html; charset=utf-8"`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response headers content-type 3`] = `"text/html; charset=utf-8"`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response headers content-type 4`] = `"text/html; charset=utf-8"`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response status 1`] = `200`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response status 2`] = `200`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response status 3`] = `200`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response status 4`] = `200`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response text 1`] = `"setup-middlewares option GET"`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response text 2`] = `"Hello World with path!"`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response text 3`] = `"Hello World without path!"`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response text 4`] = `"Hello World as function!"`; + +exports[`setupMiddlewares option should handle POST request to /setup-middleware/some/path route: console messages 1`] = `Array []`; + +exports[`setupMiddlewares option should handle POST request to /setup-middleware/some/path route: page errors 1`] = `Array []`; + +exports[`setupMiddlewares option should handle POST request to /setup-middleware/some/path route: response headers content-type 1`] = `"text/html; charset=utf-8"`; + +exports[`setupMiddlewares option should handle POST request to /setup-middleware/some/path route: response status 1`] = `200`; + +exports[`setupMiddlewares option should handle POST request to /setup-middleware/some/path route: response text 1`] = `"setup-middlewares option POST"`; diff --git a/test/e2e/__snapshots__/setup-middlewares.test.js.snap.webpack5 b/test/e2e/__snapshots__/setup-middlewares.test.js.snap.webpack5 new file mode 100644 index 0000000000..53fc795d44 --- /dev/null +++ b/test/e2e/__snapshots__/setup-middlewares.test.js.snap.webpack5 @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: console messages 1`] = `Array []`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: page errors 1`] = `Array []`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response headers content-type 1`] = `"text/html; charset=utf-8"`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response headers content-type 2`] = `"text/html; charset=utf-8"`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response headers content-type 3`] = `"text/html; charset=utf-8"`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response headers content-type 4`] = `"text/html; charset=utf-8"`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response status 1`] = `200`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response status 2`] = `200`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response status 3`] = `200`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response status 4`] = `200`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response text 1`] = `"setup-middlewares option GET"`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response text 2`] = `"Hello World with path!"`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response text 3`] = `"Hello World without path!"`; + +exports[`setupMiddlewares option should handle GET request to /setup-middleware/some/path route: response text 4`] = `"Hello World as function!"`; + +exports[`setupMiddlewares option should handle POST request to /setup-middleware/some/path route: console messages 1`] = `Array []`; + +exports[`setupMiddlewares option should handle POST request to /setup-middleware/some/path route: page errors 1`] = `Array []`; + +exports[`setupMiddlewares option should handle POST request to /setup-middleware/some/path route: response headers content-type 1`] = `"text/html; charset=utf-8"`; + +exports[`setupMiddlewares option should handle POST request to /setup-middleware/some/path route: response status 1`] = `200`; + +exports[`setupMiddlewares option should handle POST request to /setup-middleware/some/path route: response text 1`] = `"setup-middlewares option POST"`; diff --git a/test/e2e/headers.test.js b/test/e2e/headers.test.js index 8c1b216cdb..496c23b7a6 100644 --- a/test/e2e/headers.test.js +++ b/test/e2e/headers.test.js @@ -1,6 +1,7 @@ "use strict"; const webpack = require("webpack"); +const request = require("supertest"); const Server = require("../../lib/Server"); const config = require("../fixtures/simple-config/webpack.config"); const runBrowser = require("../helpers/run-browser"); @@ -391,4 +392,67 @@ describe("headers option", () => { expect(pageErrors).toMatchSnapshot("page errors"); }); }); + + describe("as a string and support HEAD request", () => { + let compiler; + let server; + let page; + let browser; + let pageErrors; + let consoleMessages; + let req; + + beforeEach(async () => { + compiler = webpack(config); + + server = new Server( + { + headers: { "X-Foo": "dev-server headers" }, + port, + }, + compiler + ); + + await server.start(); + + req = request(server.app); + + ({ page, browser } = await runBrowser()); + + pageErrors = []; + consoleMessages = []; + }); + + afterEach(async () => { + await browser.close(); + await server.stop(); + }); + + it("should handle HEAD 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.status()).toMatchSnapshot("response status"); + expect(consoleMessages.map((message) => message.text())).toMatchSnapshot( + "console messages" + ); + expect(pageErrors).toMatchSnapshot("page errors"); + + const responseForHead = await req.get(`/main.js`); + + expect(responseForHead.headers["x-foo"]).toBe("dev-server headers"); + }); + }); }); diff --git a/test/e2e/on-after-setup-middleware.test.js b/test/e2e/on-after-setup-middleware.test.js index e24ddcefbb..f7534b8db2 100644 --- a/test/e2e/on-after-setup-middleware.test.js +++ b/test/e2e/on-after-setup-middleware.test.js @@ -1,5 +1,6 @@ "use strict"; +const util = require("util"); const webpack = require("webpack"); const Server = require("../../lib/Server"); const config = require("../fixtures/client-config/webpack.config"); @@ -13,9 +14,13 @@ describe("onAfterSetupMiddleware option", () => { let browser; let pageErrors; let consoleMessages; + let utilSpy; beforeEach(async () => { compiler = webpack(config); + + utilSpy = jest.spyOn(util, "deprecate"); + server = new Server( { onAfterSetupMiddleware: (devServer) => { @@ -58,6 +63,10 @@ describe("onAfterSetupMiddleware option", () => { pageErrors.push(error); }); + expect(utilSpy.mock.calls[0][1]).toBe( + "'onAfterSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option." + ); + const response = await page.goto( `http://127.0.0.1:${port}/after/some/path`, { @@ -94,6 +103,10 @@ describe("onAfterSetupMiddleware option", () => { interceptedRequest.continue({ method: "POST" }); }); + expect(utilSpy.mock.calls[0][1]).toBe( + "'onAfterSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option." + ); + const response = await page.goto( `http://127.0.0.1:${port}/after/some/path`, { diff --git a/test/e2e/on-before-setup-middleware.test.js b/test/e2e/on-before-setup-middleware.test.js index 7d8efc446f..579c882ea1 100644 --- a/test/e2e/on-before-setup-middleware.test.js +++ b/test/e2e/on-before-setup-middleware.test.js @@ -1,5 +1,6 @@ "use strict"; +const util = require("util"); const webpack = require("webpack"); const Server = require("../../lib/Server"); const config = require("../fixtures/client-config/webpack.config"); @@ -13,9 +14,13 @@ describe("onBeforeSetupMiddleware option", () => { let browser; let pageErrors; let consoleMessages; + let utilSpy; beforeEach(async () => { compiler = webpack(config); + + utilSpy = jest.spyOn(util, "deprecate"); + server = new Server( { onBeforeSetupMiddleware: (devServer) => { @@ -58,6 +63,10 @@ describe("onBeforeSetupMiddleware option", () => { pageErrors.push(error); }); + expect(utilSpy.mock.calls[0][1]).toBe( + "'onBeforeSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option." + ); + const response = await page.goto( `http://127.0.0.1:${port}/before/some/path`, { @@ -94,6 +103,10 @@ describe("onBeforeSetupMiddleware option", () => { interceptedRequest.continue({ method: "POST" }); }); + expect(utilSpy.mock.calls[0][1]).toBe( + "'onBeforeSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option." + ); + const response = await page.goto( `http://127.0.0.1:${port}/before/some/path`, { diff --git a/test/e2e/setup-middlewares.test.js b/test/e2e/setup-middlewares.test.js new file mode 100644 index 0000000000..d063aa1d07 --- /dev/null +++ b/test/e2e/setup-middlewares.test.js @@ -0,0 +1,169 @@ +"use strict"; + +const webpack = require("webpack"); +const Server = require("../../lib/Server"); +const config = require("../fixtures/client-config/webpack.config"); +const runBrowser = require("../helpers/run-browser"); +const port = require("../ports-map")["setup-middlewares-option"]; + +describe("setupMiddlewares option", () => { + let compiler; + let server; + let page; + let browser; + let pageErrors; + let consoleMessages; + + beforeEach(async () => { + compiler = webpack(config); + server = new Server( + { + setupMiddlewares: (middlewares, devServer) => { + if (!devServer) { + throw new Error("webpack-dev-server is not defined"); + } + + devServer.app.get("/setup-middleware/some/path", (_, response) => { + response.send("setup-middlewares option GET"); + }); + + devServer.app.post("/setup-middleware/some/path", (_, response) => { + response.send("setup-middlewares option POST"); + }); + + middlewares.push({ + name: "hello-world-test-two", + middleware: (req, res, next) => { + if (req.path !== "/foo/bar/baz") { + next(); + + return; + } + + res.send("Hello World without path!"); + }, + }); + middlewares.push({ + name: "hello-world-test-one", + path: "/foo/bar", + middleware: (req, res) => { + res.send("Hello World with path!"); + }, + }); + middlewares.push((req, res) => { + res.send("Hello World as function!"); + }); + + return middlewares; + }, + port, + }, + compiler + ); + + await server.start(); + + ({ page, browser } = await runBrowser()); + + pageErrors = []; + consoleMessages = []; + }); + + afterEach(async () => { + await browser.close(); + await server.stop(); + }); + + it("should handle GET request to /setup-middleware/some/path route", 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}/setup-middleware/some/path`, + { + waitUntil: "networkidle0", + } + ); + + expect(response.headers()["content-type"]).toMatchSnapshot( + "response headers content-type" + ); + expect(response.status()).toMatchSnapshot("response status"); + expect(await response.text()).toMatchSnapshot("response text"); + + const response1 = await page.goto(`http://127.0.0.1:${port}/foo/bar`, { + waitUntil: "networkidle0", + }); + + expect(response1.headers()["content-type"]).toMatchSnapshot( + "response headers content-type" + ); + expect(response1.status()).toMatchSnapshot("response status"); + expect(await response1.text()).toMatchSnapshot("response text"); + + const response2 = await page.goto(`http://127.0.0.1:${port}/foo/bar/baz`, { + waitUntil: "networkidle0", + }); + + expect(response2.headers()["content-type"]).toMatchSnapshot( + "response headers content-type" + ); + expect(response2.status()).toMatchSnapshot("response status"); + expect(await response2.text()).toMatchSnapshot("response text"); + + const response3 = await page.goto( + `http://127.0.0.1:${port}/setup-middleware/unknown`, + { + waitUntil: "networkidle0", + } + ); + + expect(response3.headers()["content-type"]).toMatchSnapshot( + "response headers content-type" + ); + expect(response3.status()).toMatchSnapshot("response status"); + expect(await response3.text()).toMatchSnapshot("response text"); + + expect(consoleMessages.map((message) => message.text())).toMatchSnapshot( + "console messages" + ); + expect(pageErrors).toMatchSnapshot("page errors"); + }); + + it("should handle POST request to /setup-middleware/some/path route", async () => { + await page.setRequestInterception(true); + + page + .on("console", (message) => { + consoleMessages.push(message); + }) + .on("pageerror", (error) => { + pageErrors.push(error); + }) + .on("request", (interceptedRequest) => { + interceptedRequest.continue({ method: "POST" }); + }); + + const response = await page.goto( + `http://127.0.0.1:${port}/setup-middleware/some/path`, + { + waitUntil: "networkidle0", + } + ); + + expect(response.headers()["content-type"]).toMatchSnapshot( + "response headers content-type" + ); + expect(response.status()).toMatchSnapshot("response status"); + expect(await response.text()).toMatchSnapshot("response text"); + expect(consoleMessages.map((message) => message.text())).toMatchSnapshot( + "console messages" + ); + expect(pageErrors).toMatchSnapshot("page errors"); + }); +}); diff --git a/test/ports-map.js b/test/ports-map.js index b90c1f0406..00f8fe9437 100644 --- a/test/ports-map.js +++ b/test/ports-map.js @@ -78,6 +78,7 @@ const listOfTests = { "cli-server": 1, "server-option": 1, "normalize-option": 1, + "setup-middlewares-option": 1, }; let startPort = 8089; diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 60880d572c..9b306c3ddd 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -514,6 +514,10 @@ const tests = { }, ], }, + setupMiddlewares: { + success: [() => {}], + failure: [false, 10, "true"], + }, webSocketServer: { success: [ false,