From 81d131d610b88a1f4b6e72f85dd03d8bfd39ea6f Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 16 Feb 2026 03:31:15 +0000 Subject: [PATCH 1/8] feat: plugin support --- lib/Server.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/Server.js b/lib/Server.js index cb7da1a6de..6e9628873c 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -311,6 +311,8 @@ const DEFAULT_ALLOWED_PROTOCOLS = /^(file|.+-extension):/i; * @property {typeof useFn} use */ +const pluginName = "webpack-dev-server"; + /** * @template {BasicApplication} [A=ExpressApplication] * @template {BasicServer} [S=HTTPServer] @@ -330,7 +332,7 @@ class Server { /** * @type {ReturnType} */ - this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server"); + this.logger = this.compiler.getInfrastructureLogger(pluginName); this.options = options; /** * @type {FSWatcher[]} @@ -3414,6 +3416,23 @@ class Server { .then(() => callback(), callback) .catch(callback); } + + /** + * @param {Compiler} compiler compiler + * @returns {void} + */ + apply(compiler) { + const pluginName = this.constructor.name; + this.compiler = compiler; + + this.compiler.hooks.watchRun.tapPromise(pluginName, async () => { + await this.start(); + }); + + this.compiler.hooks.watchClose.tap(pluginName, async () => { + await this.stop(); + }); + } } module.exports = Server; From 20460e3b81382758cb026b82beb6f7ddd7c33123 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 21 Feb 2026 18:12:12 +0000 Subject: [PATCH 2/8] fix: correct errors when a compiler is not passed to the constructor --- lib/Server.js | 26 +++++++++++++++++++------- test/e2e/api.test.js | 34 ++++++++++++++++++++++++++++++++++ test/helpers/compile.js | 12 ++++++++++++ 3 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 test/helpers/compile.js diff --git a/lib/Server.js b/lib/Server.js index 6e9628873c..5e1299e6be 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -328,11 +328,14 @@ class Server { baseDataPath: "options", }); - this.compiler = compiler; - /** - * @type {ReturnType} - */ - this.logger = this.compiler.getInfrastructureLogger(pluginName); + if (compiler) { + this.compiler = compiler; + + /** + * @type {ReturnType} + */ + this.logger = this.compiler.getInfrastructureLogger(pluginName); + } this.options = options; /** * @type {FSWatcher[]} @@ -1589,6 +1592,9 @@ class Server { * @returns {void} */ setupProgressPlugin() { + // In the case where there is no compiler and it’s not being used as a plugin. + if (this.compiler === undefined) return; + const { ProgressPlugin } = /** @type {MultiCompiler} */ (this.compiler).compilers @@ -1633,6 +1639,7 @@ class Server { * @returns {Promise} */ async initialize() { + if (this.compiler === undefined) return; this.setupHooks(); await this.setupApp(); @@ -1708,7 +1715,7 @@ class Server { needForceShutdown = true; this.stopCallback(() => { - if (typeof this.compiler.close === "function") { + if (typeof this.compiler?.close === "function") { this.compiler.close(() => { // eslint-disable-next-line n/no-process-exit process.exit(); @@ -1783,11 +1790,14 @@ class Server { * @returns {void} */ setupHooks() { + if (this.compiler === undefined) return; + this.compiler.hooks.invalid.tap("webpack-dev-server", () => { if (this.webSocketServer) { this.sendMessage(this.webSocketServer.clients, "invalid"); } }); + this.compiler.hooks.done.tap( "webpack-dev-server", /** @@ -1842,6 +1852,7 @@ class Server { * @returns {void} */ setupMiddlewares() { + if (this.compiler === undefined) return; /** * @type {Array} */ @@ -2348,6 +2359,7 @@ class Server { // middleware for serving webpack bundle /** @type {import("webpack-dev-middleware").API} */ this.middleware = webpackDevMiddleware( + // @ts-expect-error this.compiler, this.options.devMiddleware, ); @@ -3422,8 +3434,8 @@ class Server { * @returns {void} */ apply(compiler) { - const pluginName = this.constructor.name; this.compiler = compiler; + this.logger = this.compiler.getInfrastructureLogger(pluginName); this.compiler.hooks.watchRun.tapPromise(pluginName, async () => { await this.start(); diff --git a/test/e2e/api.test.js b/test/e2e/api.test.js index ee09842d04..4f66eb67be 100644 --- a/test/e2e/api.test.js +++ b/test/e2e/api.test.js @@ -4,11 +4,45 @@ const path = require("node:path"); const webpack = require("webpack"); const Server = require("../../lib/Server"); const config = require("../fixtures/client-config/webpack.config"); +const compile = require("../helpers/compile"); const runBrowser = require("../helpers/run-browser"); const sessionSubscribe = require("../helpers/session-subscribe"); const port = require("../ports-map").api; describe("API", () => { + it("should work with plugin API", async () => { + const compiler = webpack(config); + const server = new Server({ port }); + + server.apply(compiler); + await compile(compiler); + + const { page, browser } = await runBrowser(); + + const pageErrors = []; + const consoleMessages = []; + + page + .on("console", (message) => { + consoleMessages.push(message); + }) + .on("pageerror", (error) => { + pageErrors.push(error); + }); + + await page.goto(`http://127.0.0.1:${port}/`, { + waitUntil: "networkidle0", + }); + + expect(consoleMessages.map((message) => message.text())).toMatchSnapshot( + "console messages", + ); + expect(pageErrors).toMatchSnapshot("page errors"); + + await browser.close(); + compiler.watching.close(); + }); + describe("WEBPACK_SERVE environment variable", () => { const OLD_ENV = process.env; let server; diff --git a/test/helpers/compile.js b/test/helpers/compile.js new file mode 100644 index 0000000000..2696e4b053 --- /dev/null +++ b/test/helpers/compile.js @@ -0,0 +1,12 @@ +"use strict"; + +module.exports = (compiler) => + new Promise((resolve, reject) => { + compiler.run((error, stats) => { + if (error) { + return reject(error); + } + + return resolve(stats); + }); + }); From 0fdb03f81e43bd6db833918d682ab55275b60f34 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Fri, 6 Mar 2026 20:34:57 +0000 Subject: [PATCH 3/8] feat: adapt webpack-dev-middleware.plugin --- lib/Server.js | 1 + types/lib/Server.d.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index 5e1299e6be..98a80a0ed6 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -2362,6 +2362,7 @@ class Server { // @ts-expect-error this.compiler, this.options.devMiddleware, + true, ); } diff --git a/types/lib/Server.d.ts b/types/lib/Server.d.ts index dd895e2e3c..649bb6b617 100644 --- a/types/lib/Server.d.ts +++ b/types/lib/Server.d.ts @@ -1,8 +1,4 @@ export = Server; -/** - * @typedef {object} BasicApplication - * @property {typeof useFn} use - */ /** * @template {BasicApplication} [A=ExpressApplication] * @template {BasicServer} [S=HTTPServer] @@ -1167,7 +1163,10 @@ declare class Server< * @param {Compiler | MultiCompiler} compiler compiler */ constructor(options: Configuration, compiler: Compiler | MultiCompiler); - compiler: import("webpack").Compiler | import("webpack").MultiCompiler; + compiler: + | import("webpack").Compiler + | import("webpack").MultiCompiler + | undefined; /** * @type {ReturnType} */ @@ -1409,6 +1408,11 @@ declare class Server< * @param {((err?: Error) => void)=} callback callback */ stopCallback(callback?: ((err?: Error) => void) | undefined): void; + /** + * @param {Compiler} compiler compiler + * @returns {void} + */ + apply(compiler: Compiler): void; #private; } declare namespace Server { From 8664549e2f34fcb0f017de31d270c23ea2d19440 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Fri, 6 Mar 2026 22:03:14 -0500 Subject: [PATCH 4/8] feat: enhance plugin API support and update tests for new compile behavior --- .../__snapshots__/api.test.js.snap.webpack5 | 10 +++++ test/e2e/api.test.js | 6 ++- test/helpers/compile.js | 38 +++++++++++++++++-- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/test/e2e/__snapshots__/api.test.js.snap.webpack5 b/test/e2e/__snapshots__/api.test.js.snap.webpack5 index 14c74e33ea..7c143f91c9 100644 --- a/test/e2e/__snapshots__/api.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/api.test.js.snap.webpack5 @@ -165,3 +165,13 @@ exports[`API latest async API should work with callback API: console messages 1` `; exports[`API latest async API should work with callback API: page errors 1`] = `[]`; + +exports[`API should work with plugin API: console messages 1`] = ` +[ + "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.", + "[HMR] Waiting for update signal from WDS...", + "Hey.", +] +`; + +exports[`API should work with plugin API: page errors 1`] = `[]`; diff --git a/test/e2e/api.test.js b/test/e2e/api.test.js index 4f66eb67be..aa9dcdd8a2 100644 --- a/test/e2e/api.test.js +++ b/test/e2e/api.test.js @@ -15,7 +15,9 @@ describe("API", () => { const server = new Server({ port }); server.apply(compiler); - await compile(compiler); + + // Use compile helper which waits for the server to be ready + const { watching } = await compile(compiler, port); const { page, browser } = await runBrowser(); @@ -40,7 +42,7 @@ describe("API", () => { expect(pageErrors).toMatchSnapshot("page errors"); await browser.close(); - compiler.watching.close(); + watching.close(); }); describe("WEBPACK_SERVE environment variable", () => { diff --git a/test/helpers/compile.js b/test/helpers/compile.js index 2696e4b053..36f1c480c9 100644 --- a/test/helpers/compile.js +++ b/test/helpers/compile.js @@ -1,12 +1,44 @@ "use strict"; -module.exports = (compiler) => +// Helper function to check if server is ready using fetch +const waitForServer = async (port, timeout = 10000) => { + const start = Date.now(); + + while (Date.now() - start < timeout) { + try { + // eslint-disable-next-line n/no-unsupported-features/node-builtins + await fetch(`http://127.0.0.1:${port}/`); + return; // Server is ready + } catch { + // Server not ready yet, wait and retry + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + } + } + + throw new Error(`Server on port ${port} not ready after ${timeout}ms`); +}; + +module.exports = (compiler, port = null) => new Promise((resolve, reject) => { - compiler.run((error, stats) => { + const watching = compiler.watch({}, async (error, stats) => { if (error) { + watching.close(); return reject(error); } - return resolve(stats); + // If a port is provided, wait for the server to be ready + if (port) { + try { + await waitForServer(port); + } catch (err) { + watching.close(); + return reject(err); + } + } + + // Return both stats and watching for caller to manage + resolve({ stats, watching }); }); }); From 0e9081f1b3d51f58790ca7baa8b925041722c6fc Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 30 Mar 2026 15:00:58 -0500 Subject: [PATCH 5/8] feat: add isPlugin flag to Server class for plugin identification --- lib/Server.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/Server.js b/lib/Server.js index 98a80a0ed6..b8e7989f0d 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -362,6 +362,11 @@ class Server { */ this.currentHash = undefined; + /** + * @private + * @type {boolean} + */ + this.isPlugin = false; } static get schema() { @@ -2362,7 +2367,7 @@ class Server { // @ts-expect-error this.compiler, this.options.devMiddleware, - true, + this.isPlugin, ); } @@ -3436,6 +3441,7 @@ class Server { */ apply(compiler) { this.compiler = compiler; + this.isPlugin = true; this.logger = this.compiler.getInfrastructureLogger(pluginName); this.compiler.hooks.watchRun.tapPromise(pluginName, async () => { From c22d8700fdddb0812eccfa64d885c9342730eb84 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 30 Mar 2026 15:37:14 -0500 Subject: [PATCH 6/8] feat: prevent multiple server starts on recompilation and ensure clean shutdown --- lib/Server.js | 10 +++++++-- test/e2e/api.test.js | 53 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index b8e7989f0d..b82789eaae 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -3444,11 +3444,17 @@ class Server { this.isPlugin = true; this.logger = this.compiler.getInfrastructureLogger(pluginName); + let started = false; + this.compiler.hooks.watchRun.tapPromise(pluginName, async () => { - await this.start(); + if (!started) { + started = true; + await this.start(); + } }); - this.compiler.hooks.watchClose.tap(pluginName, async () => { + this.compiler.hooks.shutdown.tapPromise(pluginName, async () => { + started = false; await this.stop(); }); } diff --git a/test/e2e/api.test.js b/test/e2e/api.test.js index aa9dcdd8a2..56543611d0 100644 --- a/test/e2e/api.test.js +++ b/test/e2e/api.test.js @@ -16,8 +16,7 @@ describe("API", () => { server.apply(compiler); - // Use compile helper which waits for the server to be ready - const { watching } = await compile(compiler, port); + await compile(compiler, port); const { page, browser } = await runBrowser(); @@ -42,7 +41,55 @@ describe("API", () => { expect(pageErrors).toMatchSnapshot("page errors"); await browser.close(); - watching.close(); + await new Promise((resolve) => { + compiler.close(resolve); + }); + }); + + it("should not start the server multiple times on recompilation", async () => { + const compiler = webpack(config); + const server = new Server({ port }); + const startSpy = jest.spyOn(server, "start"); + + server.apply(compiler); + + const { watching } = await compile(compiler, port); + + // Trigger a recompilation by invalidating + await new Promise((resolve) => { + watching.invalidate(() => { + resolve(); + }); + }); + + // Wait for the recompilation to finish + await new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + + expect(startSpy).toHaveBeenCalledTimes(1); + + startSpy.mockRestore(); + await new Promise((resolve) => { + compiler.close(resolve); + }); + }); + + it("should stop the server cleanly via compiler.close()", async () => { + const compiler = webpack(config); + const server = new Server({ port }); + const stopSpy = jest.spyOn(server, "stop"); + + server.apply(compiler); + + await compile(compiler, port); + + await new Promise((resolve) => { + compiler.close(resolve); + }); + + expect(stopSpy).toHaveBeenCalledTimes(1); + stopSpy.mockRestore(); }); describe("WEBPACK_SERVE environment variable", () => { From 379b3da2e525b4ec0486504fc05565ab1f658924 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 30 Mar 2026 15:39:46 -0500 Subject: [PATCH 7/8] fixup! --- types/lib/Server.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/types/lib/Server.d.ts b/types/lib/Server.d.ts index 649bb6b617..b7d842dd21 100644 --- a/types/lib/Server.d.ts +++ b/types/lib/Server.d.ts @@ -1195,6 +1195,11 @@ declare class Server< * @type {string | undefined} */ private currentHash; + /** + * @private + * @type {boolean} + */ + private isPlugin; /** * @private * @param {Compiler} compiler compiler From d8084a706316ab2daacc0245bc5efdf9e505b671 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 30 Mar 2026 16:03:20 -0500 Subject: [PATCH 8/8] chore: more tests --- .../logging.test.js.snap.webpack5 | 50 ++++++++ test/e2e/logging.test.js | 121 ++++++++++++++++++ 2 files changed, 171 insertions(+) diff --git a/test/e2e/__snapshots__/logging.test.js.snap.webpack5 b/test/e2e/__snapshots__/logging.test.js.snap.webpack5 index 4c8cc479f7..f9d55c0f6c 100644 --- a/test/e2e/__snapshots__/logging.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/logging.test.js.snap.webpack5 @@ -1,5 +1,55 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`logging plugin mode should work and do not log messages about hot and live reloading is enabled 1`] = ` +[ + "[webpack-dev-server] Server started: Hot Module Replacement disabled, Live Reloading disabled, Progress disabled, Overlay enabled.", + "Hey.", +] +`; + +exports[`logging plugin mode should work and log errors by default 1`] = ` +[ + "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.", + "[HMR] Waiting for update signal from WDS...", + "Hey.", + "[webpack-dev-server] Errors while compiling. Reload prevented.", + "[webpack-dev-server] ERROR +Error from compilation", +] +`; + +exports[`logging plugin mode should work and log message about live reloading is enabled 1`] = ` +[ + "[webpack-dev-server] Server started: Hot Module Replacement disabled, Live Reloading enabled, Progress disabled, Overlay enabled.", + "Hey.", +] +`; + +exports[`logging plugin mode should work and log messages about hot and live reloading is enabled 1`] = ` +[ + "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.", + "[HMR] Waiting for update signal from WDS...", + "Hey.", +] +`; + +exports[`logging plugin mode should work and log warnings by default 1`] = ` +[ + "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.", + "[HMR] Waiting for update signal from WDS...", + "Hey.", + "[webpack-dev-server] Warnings while compiling.", + "[webpack-dev-server] WARNING +Warning from compilation", +] +`; + +exports[`logging plugin mode should work when the "client.logging" is "none" 1`] = ` +[ + "Hey.", +] +`; + exports[`logging should work and do not log messages about hot and live reloading is enabled (ws) 1`] = ` [ "[webpack-dev-server] Server started: Hot Module Replacement disabled, Live Reloading disabled, Progress disabled, Overlay enabled.", diff --git a/test/e2e/logging.test.js b/test/e2e/logging.test.js index 9ba530ba3d..aed449463f 100644 --- a/test/e2e/logging.test.js +++ b/test/e2e/logging.test.js @@ -5,6 +5,7 @@ const fs = require("graceful-fs"); const webpack = require("webpack"); const Server = require("../../lib/Server"); const config = require("../fixtures/client-config/webpack.config"); +const compile = require("../helpers/compile"); const HTMLGeneratorPlugin = require("../helpers/html-generator-plugin"); const runBrowser = require("../helpers/run-browser"); const port = require("../ports-map").logging; @@ -241,4 +242,124 @@ describe("logging", () => { }); } } + + describe("plugin mode", () => { + const pluginCases = [ + { + title: + "should work and log messages about hot and live reloading is enabled", + devServerOptions: { + hot: true, + }, + }, + { + title: "should work and log message about live reloading is enabled", + devServerOptions: { + hot: false, + }, + }, + { + title: + "should work and do not log messages about hot and live reloading is enabled", + devServerOptions: { + liveReload: false, + hot: false, + }, + }, + { + title: "should work and log warnings by default", + webpackOptions: { + plugins: [ + { + apply(compiler) { + compiler.hooks.thisCompilation.tap( + "warnings-webpack-plugin", + (compilation) => { + compilation.warnings.push( + new Error("Warning from compilation"), + ); + }, + ); + }, + }, + new HTMLGeneratorPlugin(), + ], + }, + }, + { + title: "should work and log errors by default", + webpackOptions: { + plugins: [ + { + apply(compiler) { + compiler.hooks.thisCompilation.tap( + "warnings-webpack-plugin", + (compilation) => { + compilation.errors.push( + new Error("Error from compilation"), + ); + }, + ); + }, + }, + new HTMLGeneratorPlugin(), + ], + }, + }, + { + title: 'should work when the "client.logging" is "none"', + devServerOptions: { + client: { + logging: "none", + }, + }, + }, + ]; + + for (const testCase of pluginCases) { + it(`${testCase.title}`, async () => { + const compiler = webpack({ ...config, ...testCase.webpackOptions }); + const devServerOptions = { + port, + ...testCase.devServerOptions, + }; + const server = new Server(devServerOptions); + + server.apply(compiler); + + await compile(compiler, port); + + const { page, browser } = await runBrowser(); + + try { + const consoleMessages = []; + + page.on("console", (message) => { + consoleMessages.push(message); + }); + + await page.goto(`http://127.0.0.1:${port}/`, { + waitUntil: "networkidle0", + }); + + expect( + consoleMessages.map((message) => + message + .text() + .replaceAll("\\", "/") + .replaceAll( + new RegExp(process.cwd().replaceAll("\\", "/"), "g"), + "", + ), + ), + ).toMatchSnapshot(); + } finally { + await browser.close(); + await new Promise((resolve) => { + compiler.close(resolve); + }); + } + }); + } + }); });