diff --git a/examples/setup-middlewares/README.md b/examples/setup-middlewares/README.md index 68cee1722c..84aa2cc506 100644 --- a/examples/setup-middlewares/README.md +++ b/examples/setup-middlewares/README.md @@ -13,13 +13,22 @@ module.exports = { throw new Error("webpack-dev-server is not defined"); } - const sendResponses = () => { - devServer.app.get("/setup-middleware/some/path", (_, response) => { - response.send("setup-middlewares option GET"); - }); - }; - - middlewares.push(sendResponses()); + 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/examples/setup-middlewares/webpack.config.js b/examples/setup-middlewares/webpack.config.js index 22ce45bd5f..df9d9fb74a 100644 --- a/examples/setup-middlewares/webpack.config.js +++ b/examples/setup-middlewares/webpack.config.js @@ -13,13 +13,22 @@ module.exports = setup({ throw new Error("webpack-dev-server is not defined"); } - const sendResponses = () => { - devServer.app.get("/setup-middleware/some/path", (_, response) => { - response.send("setup-middlewares option GET"); - }); - }; + devServer.app.get("/setup-middleware/some/path", (_, response) => { + response.send("setup-middlewares option GET"); + }); - middlewares.push(sendResponses()); + 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 2c63e8ba36..8f1752f3a1 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -1322,6 +1322,7 @@ class Server { this.setupDevMiddleware(); // Should be after `webpack-dev-middleware`, otherwise other middlewares might rewrite response this.setupBuiltInRoutes(); + this.setupWatchStaticFiles(); this.setupWatchFiles(); this.setupMiddlewares(); this.createServer(); @@ -1484,22 +1485,14 @@ class Server { }); } - setupStaticFeature() { - const staticMiddlewares = []; - - this.options.static.forEach((staticOption) => { - staticOption.publicPath.forEach((publicPath) => { - staticMiddlewares.push({ - publicPath, - middleware: express.static( - staticOption.directory, - staticOption.staticOptions - ), - }); + setupWatchStaticFiles() { + if (this.options.static.length > 0) { + this.options.static.forEach((staticOption) => { + if (staticOption.watch) { + this.watchFiles(staticOption.directory, staticOption.watch); + } }); - }); - - return staticMiddlewares; + } } setupWatchFiles() { @@ -1514,17 +1507,30 @@ class Server { setupMiddlewares() { let middlewares = []; + // compress is placed last and uses unshift so that it will be the first middleware used if (this.options.compress) { - const compress = require("compression"); - middlewares.push(compress()); + const compression = require("compression"); + + middlewares.push({ name: "compression", middleware: compression() }); } - if (typeof this.options.onBeforeSetupMiddleware === "function") { - middlewares.push(this.options.onBeforeSetupMiddleware(this)); + if (this.options.onBeforeSetupMiddleware) { + this.options.onBeforeSetupMiddleware(this); } - middlewares.push(this.setHeaders.bind(this), this.middleware); + if (typeof this.options.headers !== "undefined") { + middlewares.push({ + name: "set-headers", + path: "*", + middleware: this.setHeaders.bind(this), + }); + } + + middlewares.push({ + name: "webpack-dev-middleware", + middleware: this.middleware, + }); if (this.options.proxy) { const { createProxyMiddleware } = require("http-proxy-middleware"); @@ -1573,7 +1579,7 @@ class Server { this.webSocketProxies.push(proxyMiddleware); } - const handle = async (req, res, next) => { + const handler = async (req, res, next) => { if (typeof proxyConfigOrCallback === "function") { const newProxyConfig = proxyConfigOrCallback(req, res, next); @@ -1607,17 +1613,40 @@ class Server { } }; - middlewares.push(handle); + middlewares.push({ + name: "http-proxy-middleware", + middleware: handler, + }); // Also forward error requests to the proxy so it can handle them. - middlewares.push((error, req, res, next) => handle(req, res, next)); + middlewares.push({ + name: "http-proxy-middleware-error-handler", + middleware: (error, req, res, next) => handler(req, res, next), + }); + }); + + middlewares.push({ + name: "webpack-dev-middleware", + middleware: this.middleware, }); } if (this.options.static) { - middlewares = [...middlewares, ...this.setupStaticFeature()]; + 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.historyApiFallback) { + const connectHistoryApiFallback = require("connect-history-api-fallback"); const { historyApiFallback } = this.options; if ( @@ -1631,13 +1660,31 @@ class Server { } // Fall back to /index.html if nothing else matches. - middlewares.push( - require("connect-history-api-fallback")(historyApiFallback), - this.middleware - ); + middlewares.push({ + name: "connect-history-api-fallback", + middleware: connectHistoryApiFallback(historyApiFallback), + }); + + // 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) { - middlewares = [...middlewares, ...this.setupStaticFeature()]; + this.options.static.forEach((staticOption) => { + staticOption.publicPath.forEach((publicPath) => { + middlewares.push({ + name: "express-static", + path: publicPath, + middleware: express.static( + staticOption.directory, + staticOption.staticOptions + ), + }); + }); + }); } } @@ -1648,7 +1695,8 @@ class Server { staticOption.publicPath.forEach((publicPath) => { if (staticOption.serveIndex) { middlewares.push({ - publicPath, + 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") { @@ -1664,36 +1712,33 @@ class Server { }); } }); - - if (staticOption.watch) { - middlewares.push( - this.watchFiles(staticOption.directory, staticOption.watch) - ); - } }); } if (this.options.magicHtml) { - middlewares.push(this.serveMagicHtml.bind(this)); + middlewares.push({ + name: "serve-magic-html", + middleware: this.serveMagicHtml.bind(this), + }); } - if (typeof this.options.onAfterSetupMiddleware === "function") { - middlewares.push(this.options.onAfterSetupMiddleware(this)); + if (this.options.onAfterSetupMiddleware) { + this.options.onAfterSetupMiddleware(this); } if (typeof this.options.setupMiddlewares === "function") { middlewares = this.options.setupMiddlewares(middlewares, this); } - for (const middleware of middlewares) { - if (typeof middleware === "function") { + 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); } - - if (typeof middleware === "object") { - this.app.use(middleware.publicPath, middleware.middleware); - } - } + }); } createServer() { @@ -2124,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/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 index 0ebcc34a97..53fc795d44 100644 --- a/test/e2e/__snapshots__/setup-middlewares.test.js.snap.webpack4 +++ b/test/e2e/__snapshots__/setup-middlewares.test.js.snap.webpack4 @@ -6,10 +6,28 @@ exports[`setupMiddlewares option should handle GET request to /setup-middleware/ 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 []`; diff --git a/test/e2e/__snapshots__/setup-middlewares.test.js.snap.webpack5 b/test/e2e/__snapshots__/setup-middlewares.test.js.snap.webpack5 index 0ebcc34a97..53fc795d44 100644 --- a/test/e2e/__snapshots__/setup-middlewares.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/setup-middlewares.test.js.snap.webpack5 @@ -6,10 +6,28 @@ exports[`setupMiddlewares option should handle GET request to /setup-middleware/ 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 []`; 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/setup-middlewares.test.js b/test/e2e/setup-middlewares.test.js index 1af3c22a04..d063aa1d07 100644 --- a/test/e2e/setup-middlewares.test.js +++ b/test/e2e/setup-middlewares.test.js @@ -23,17 +23,36 @@ describe("setupMiddlewares option", () => { throw new Error("webpack-dev-server is not defined"); } - const sendResponses = () => { - 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(sendResponses()); + 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; }, @@ -74,15 +93,45 @@ describe("setupMiddlewares option", () => { 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"); }); @@ -110,15 +159,11 @@ describe("setupMiddlewares option", () => { 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"); }); });