Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/feat-multi-compiler-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"webpack-dev-server": minor
---

Run as a plugin from the top-level `plugins` of a multi-configuration, serving every configuration from one server.
16 changes: 16 additions & 0 deletions examples/api/plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ const devServerOptions = { ...config.devServer, open: true };
config.plugins.push(new WebpackDevServer(devServerOptions));
```

## Multiple configurations

To serve every configuration of a multi-configuration from one dev server, add
the instance to the top-level `plugins` of the exported array, next to
`parallelism`, rather than to one configuration's `plugins[]`. webpack applies
it once to the `MultiCompiler`; this needs webpack 5.112.0 or later.

```js
// webpack.config.js
const WebpackDevServer = require("webpack-dev-server");

module.exports = Object.assign([appConfig, workerConfig], {
plugins: [new WebpackDevServer({ port: 8080 })],
});
```

## Run

```console
Expand Down
25 changes: 16 additions & 9 deletions lib/Server.js
Original file line number Diff line number Diff line change
Expand Up @@ -4092,17 +4092,24 @@ class Server {
await this.stop();
};

// Teardown is the one place a loop is unavoidable. A `MultiCompiler` has no
// `shutdown` hook, and its aggregate `watchClose` does NOT fire on
// `compiler.close()` (only on `watching.close()`), so the only signal that
// survives `compiler.close()` is each child's own `shutdown`. Tapping it
// `shutdown` survives `compiler.close()`, unlike the aggregate `watchClose`
// (only `watching.close()` fires it). On a `MultiCompiler` it is a
// `MultiHook` firing once per child, so `onShutdown` is guarded. Tapping it
// with `tapPromise` also lets `compiler.close()` await the server actually
// stopping, so the port is released before the next start.
const childCompilers = /** @type {MultiCompiler} */ (compiler)
.compilers || [compiler];

for (const childCompiler of childCompilers) {
childCompiler.hooks.shutdown.tapPromise(pluginName, onShutdown);
// TODO in the next major, bump the webpack peer dependency to `^5.112.0`
// (`MultiCompiler.hooks.shutdown` since 5.110.0, top-level `plugins` of a
// multi-configuration since 5.112.0), remove this fallback and tap
// `compiler.hooks.shutdown` alone.
const shutdownCompilers = /** @type {Compiler} */ (compiler).hooks.shutdown
? [compiler]
: /** @type {MultiCompiler} */ (compiler).compilers;

for (const shutdownCompiler of shutdownCompilers) {
/** @type {Compiler} */ (shutdownCompiler).hooks.shutdown.tapPromise(
pluginName,
onShutdown,
);
}
}
}
Expand Down
34 changes: 25 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 69 additions & 0 deletions test/e2e/api-plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const port = portsMap["api-plugin"];
const [portA, portB] = portsMap["api-plugin-multi"];

// TODO in the next major, once the webpack peer dependency is `^5.112.0`, remove
// this check: the top-level `plugins` of a multi-configuration always apply.
const supportsMultiCompilerPlugins =
new webpack.MultiCompiler([], { plugins: [] }).options.plugins !== undefined;

const cssFilePath = path.resolve(
__dirname,
"../fixtures/reload-config/main.css",
Expand Down Expand Up @@ -657,6 +662,70 @@ describe("API (plugin)", () => {
stopSpy.mockRestore();
});

it(
"should work from the top-level `plugins` of a multi-configuration",
{ skip: !supportsMultiCompilerPlugins },
async () => {
const server = new Server({ port });
const setupSpy = spyOn(server, "setup");
const listenSpy = spyOn(server, "listen");
const stopSpy = spyOn(server, "stop");
const compiler = webpack(
Object.assign([...multiCompilerConfig], { plugins: [server] }),
);

await compile(compiler, port);

try {
const responseOne = await fetch(
`http://127.0.0.1:${port}/one-main.js`,
);
const responseTwo = await fetch(
`http://127.0.0.1:${port}/two-main.js`,
);

expect(responseOne.status).toBe(200);
expect(responseTwo.status).toBe(200);
expect(setupSpy).toHaveBeenCalledTimes(1);
expect(listenSpy).toHaveBeenCalledTimes(1);
} finally {
await new Promise((resolve) => {
compiler.close(resolve);
});
}

expect(stopSpy).toHaveBeenCalledTimes(1);
setupSpy.mockRestore();
listenSpy.mockRestore();
stopSpy.mockRestore();
},
);

// TODO remove in the next major, together with the fallback it covers in
// `Server#apply()`, once the webpack peer dependency is `^5.112.0`.
it("should stop the server once on webpack without `MultiCompiler.hooks.shutdown`", async () => {
const compiler = webpack(multiCompilerConfig);
// A webpack older than 5.110.0: the same compiler, minus the hook.
const { shutdown, ...hooks } = compiler.hooks;
const olderCompiler = Object.create(compiler, {
hooks: { value: hooks },
});
const server = new Server({ port });
const stopSpy = spyOn(server, "stop");

server.apply(olderCompiler);

await compile(compiler, port);

await new Promise((resolve) => {
compiler.close(resolve);
});

expect(shutdown).toBeDefined();
expect(stopSpy).toHaveBeenCalledTimes(1);
stopSpy.mockRestore();
});

it("should run two independent plugin servers on different child compilers", async () => {
const serverA = new Server({ port: portA });
const serverB = new Server({ port: portB });
Expand Down
Loading