diff --git a/.changeset/plugin-mode-invalidate.md b/.changeset/plugin-mode-invalidate.md new file mode 100644 index 000000000..00420bc27 --- /dev/null +++ b/.changeset/plugin-mode-invalidate.md @@ -0,0 +1,5 @@ +--- +"webpack-dev-middleware": patch +--- + +Fixed a crash when calling `invalidate()` in plugin mode (`isPlugin = true`). Since the host (webpack-cli, webpack-dev-server, etc.) owns `compiler.watch()`, the middleware now invalidates the host's `watching` instead (each child compiler's one for a `MultiCompiler` on webpack < 5.109), and logs a warning when the compiler is not watching at all. diff --git a/src/index.js b/src/index.js index c1f485ff4..bcb71108d 100644 --- a/src/index.js +++ b/src/index.js @@ -597,9 +597,36 @@ 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(); + if (filledContext.watching) { + filledContext.watching.invalidate(); + return; + } + + // For plugin usage the host (webpack-cli, webpack-dev-server, etc.) owns + // `compiler.watch()`, so invalidate the host's `watching` instead + const { compiler: contextCompiler } = filledContext; + + let invalidated = false; + + if (contextCompiler.watching) { + contextCompiler.watching.invalidate(); + invalidated = true; + } else if (isMultipleCompiler(contextCompiler)) { + // `MultiCompiler` exposes `watching` only since webpack 5.109, fall back + // to each child compiler's own `watching` + for (const childCompiler of contextCompiler.compilers) { + if (childCompiler.watching) { + childCompiler.watching.invalidate(); + invalidated = true; + } + } + } + + if (!invalidated) { + filledContext.logger.warn( + "The `invalidate` method was called, but the compiler is not watching, so there is nothing to invalidate.", + ); + } }; instance.close = (callback = noop) => { diff --git a/test/__snapshots__/pluginMode.test.js.snap.webpack5 b/test/__snapshots__/pluginMode.test.js.snap.webpack5 index f7a8402c3..360ea9a1a 100644 --- a/test/__snapshots__/pluginMode.test.js.snap.webpack5 +++ b/test/__snapshots__/pluginMode.test.js.snap.webpack5 @@ -3,3 +3,5 @@ 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."`; + +exports[`plugin mode invalidate method should not throw and warn when the host is not watching: warning 1`] = `"The \`invalidate\` method was called, but the compiler is not watching, so there is nothing to invalidate."`; diff --git a/test/pluginMode.test.js b/test/pluginMode.test.js index 7600c8758..266cc2b47 100644 --- a/test/pluginMode.test.js +++ b/test/pluginMode.test.js @@ -9,6 +9,37 @@ jest.spyOn(globalThis.console, "log").mockImplementation(); // webpack-dev-server, etc.) owns `compiler.watch()`, so the middleware has no // `watching` of its own describe("plugin mode", () => { + describe("invalidate method", () => { + it("should invalidate the watching owned by the host", (done) => { + const compiler = getCompiler(webpackConfig); + const watching = compiler.watch({}, () => {}); + const instance = middleware(compiler, {}, true); + const invalidateSpy = jest.spyOn(watching, "invalidate"); + + instance.waitUntilValid(() => { + instance.invalidate(); + + expect(invalidateSpy).toHaveBeenCalledTimes(1); + + watching.close(done); + }); + }); + + it("should not throw and warn when the host is not watching", () => { + const compiler = getCompiler(webpackConfig); + const instance = middleware(compiler, {}, true); + const warnSpy = jest + .spyOn(instance.context.logger, "warn") + .mockImplementation(); + + expect(() => { + instance.invalidate(); + }).not.toThrow(); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls[0][0]).toMatchSnapshot("warning"); + }); + }); + describe("close method", () => { it("should not throw and call the callback when the host is not watching", (done) => { const compiler = getCompiler(webpackConfig);