From fff38ec65ff42f27f14c008969e243e07f66d406 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 11 Jul 2026 16:01:05 -0500 Subject: [PATCH 1/4] feat(plugin): implement close method for plugin mode handling --- src/index.js | 9 +++++++++ test/pluginMode.test.js | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 test/pluginMode.test.js diff --git a/src/index.js b/src/index.js index 611ae7c60..5af180efe 100644 --- a/src/index.js +++ b/src/index.js @@ -597,10 +597,19 @@ function wdm(compiler, options = {}, isPlugin = false) { instance.invalidate = (callback = noop) => { middleware.ready(filledContext, callback); + // TODO for plugin usage (`isPlugin = true`) `watching` is `undefined` and this throws — + // invalidate the host's `compiler.watching` (each child's one for a `MultiCompiler`) instead filledContext.watching.invalidate(); }; instance.close = (callback = noop) => { + // For plugin usage the host (webpack-cli, webpack-dev-server, etc.) owns `compiler.watch()`, + // so there is no `watching` of our own to close (`compiler.close()` on the host handles it) + if (!filledContext.watching) { + callback(null); + return; + } + filledContext.watching.close(callback); }; diff --git a/test/pluginMode.test.js b/test/pluginMode.test.js new file mode 100644 index 000000000..e960a87e4 --- /dev/null +++ b/test/pluginMode.test.js @@ -0,0 +1,39 @@ +import middleware from "../src"; + +import webpackConfig from "./fixtures/webpack.config"; +import getCompiler from "./helpers/getCompiler"; + +jest.spyOn(globalThis.console, "log").mockImplementation(); + +// When used as a plugin (`isPlugin = true`) the host (webpack-cli, +// webpack-dev-server, etc.) owns `compiler.watch()`, so the middleware has no +// `watching` of its own +describe("plugin mode", () => { + describe("close method", () => { + it("should not throw and call the callback when the host is not watching", (done) => { + const compiler = getCompiler(webpackConfig); + const instance = middleware(compiler, {}, true); + + instance.close((error) => { + expect(error).toBeNull(); + + done(); + }); + }); + + it("should not close the watching owned by the host", (done) => { + const compiler = getCompiler(webpackConfig); + const watching = compiler.watch({}, () => {}); + const instance = middleware(compiler, {}, true); + + instance.waitUntilValid(() => { + instance.close((error) => { + expect(error).toBeNull(); + expect(watching.closed).toBe(false); + + watching.close(done); + }); + }); + }); + }); +}); From 6bff2913174609da2a6fdf30e588a1b0ac155435 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 11 Jul 2026 16:08:41 -0500 Subject: [PATCH 2/4] fix(plugin): resolve crash on close() in plugin mode --- .changeset/plugin-mode-close.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/plugin-mode-close.md diff --git a/.changeset/plugin-mode-close.md b/.changeset/plugin-mode-close.md new file mode 100644 index 000000000..5ad413429 --- /dev/null +++ b/.changeset/plugin-mode-close.md @@ -0,0 +1,5 @@ +--- +"webpack-dev-middleware": patch +--- + +Fixed a crash when calling `close()` in plugin mode (`isPlugin = true`). Since the host (webpack-cli, webpack-dev-server, etc.) owns `compiler.watch()`, the middleware has no `watching` of its own to close, so `close()` now just calls the callback instead of throwing. From 899744ac5f3a9f51ef62a919f417e45d5c74e823 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 28 Jul 2026 21:32:02 -0500 Subject: [PATCH 3/4] fix(plugin): add warning for missing watching instance in plugin mode --- src/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.js b/src/index.js index 5af180efe..c1f485ff4 100644 --- a/src/index.js +++ b/src/index.js @@ -606,6 +606,9 @@ function wdm(compiler, options = {}, isPlugin = false) { // For plugin usage the host (webpack-cli, webpack-dev-server, etc.) owns `compiler.watch()`, // so there is no `watching` of our own to close (`compiler.close()` on the host handles it) if (!filledContext.watching) { + filledContext.logger.warn( + "The `close` method was called, but there is no own `watching` instance to close. When using the middleware as a plugin, the host owns watching, so use `compiler.close()` instead.", + ); callback(null); return; } From c82442d7a4712fb225f17d99c22c4b3e6f72e6b1 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 28 Jul 2026 21:37:53 -0500 Subject: [PATCH 4/4] test(plugin): add snapshots and warnings for close method in plugin mode --- test/__snapshots__/pluginMode.test.js.snap.webpack5 | 5 +++++ test/pluginMode.test.js | 10 ++++++++++ 2 files changed, 15 insertions(+) create mode 100644 test/__snapshots__/pluginMode.test.js.snap.webpack5 diff --git a/test/__snapshots__/pluginMode.test.js.snap.webpack5 b/test/__snapshots__/pluginMode.test.js.snap.webpack5 new file mode 100644 index 000000000..f7a8402c3 --- /dev/null +++ b/test/__snapshots__/pluginMode.test.js.snap.webpack5 @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`plugin mode close method should not close the watching owned by the host: warning 1`] = `"The \`close\` method was called, but there is no own \`watching\` instance to close. When using the middleware as a plugin, the host owns watching, so use \`compiler.close()\` instead."`; + +exports[`plugin mode close method should not throw and call the callback when the host is not watching: warning 1`] = `"The \`close\` method was called, but there is no own \`watching\` instance to close. When using the middleware as a plugin, the host owns watching, so use \`compiler.close()\` instead."`; diff --git a/test/pluginMode.test.js b/test/pluginMode.test.js index e960a87e4..7600c8758 100644 --- a/test/pluginMode.test.js +++ b/test/pluginMode.test.js @@ -13,9 +13,14 @@ describe("plugin mode", () => { it("should not throw and call the callback when the host is not watching", (done) => { const compiler = getCompiler(webpackConfig); const instance = middleware(compiler, {}, true); + const warnSpy = jest + .spyOn(instance.context.logger, "warn") + .mockImplementation(); instance.close((error) => { expect(error).toBeNull(); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls[0][0]).toMatchSnapshot("warning"); done(); }); @@ -25,11 +30,16 @@ describe("plugin mode", () => { const compiler = getCompiler(webpackConfig); const watching = compiler.watch({}, () => {}); const instance = middleware(compiler, {}, true); + const warnSpy = jest + .spyOn(instance.context.logger, "warn") + .mockImplementation(); instance.waitUntilValid(() => { instance.close((error) => { expect(error).toBeNull(); expect(watching.closed).toBe(false); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls[0][0]).toMatchSnapshot("warning"); watching.close(done); });