Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3ea59f3
feat: plugin support
bjohansebas Feb 16, 2026
cff074e
fix: correct errors when a compiler is not passed to the constructor
bjohansebas Feb 21, 2026
d7e0cec
feat: adapt webpack-dev-middleware.plugin
bjohansebas Mar 6, 2026
f5b39f4
feat: enhance plugin API support and update tests for new compile beh…
bjohansebas Mar 7, 2026
07c25c9
feat: add isPlugin flag to Server class for plugin identification
bjohansebas Mar 30, 2026
2d6b91f
feat: prevent multiple server starts on recompilation and ensure clea…
bjohansebas Mar 30, 2026
72fd93c
fixup!
bjohansebas Mar 30, 2026
616b803
chore: more tests
bjohansebas Mar 30, 2026
c8b6c2c
feat: enhance server setup process
bjohansebas Apr 26, 2026
0583b3f
refactor: remove unnecessary compiler checks in setup methods
bjohansebas Apr 26, 2026
de2a8d0
test: ensure server setup and listen methods are called once on recom…
bjohansebas Apr 26, 2026
6ea0928
test: add API plugin tests and snapshots for webpack config integration
bjohansebas May 1, 2026
4829d1d
feat: enhance MultiCompiler support in server setup and add related t…
bjohansebas May 1, 2026
ebbeb03
feat: add support for multiple independent plugin servers and enhance…
bjohansebas May 1, 2026
fd72668
test: add test for passive behavior in build mode with compiler.run
bjohansebas May 1, 2026
b304863
fixup!
bjohansebas May 1, 2026
2caad57
feat: add example for using webpack-dev-server as a plugin with confi…
bjohansebas May 1, 2026
351f88f
refactor: update README and remove redundant standalone server handli…
bjohansebas May 1, 2026
ab97baf
fixup!
bjohansebas May 1, 2026
3e742d1
test: add more tests
bjohansebas May 1, 2026
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
46 changes: 46 additions & 0 deletions examples/api/plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# API: Plugin

Use `webpack-dev-server` as a webpack plugin by adding an instance to
`plugins[]`. The dev server starts when the first compilation finishes and
stops when the compiler closes — no separate `server.start()` call is needed.

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

module.exports = {
// ...
plugins: [new WebpackDevServer({ port: 8080, open: true })],
};
```

If you have existing `devServer` options in your config, spread them into the
plugin instance — the plugin reads its options from its constructor argument,
not from `config.devServer`:

```js
const devServerOptions = { ...config.devServer, open: true };
config.plugins.push(new WebpackDevServer(devServerOptions));
```

## Run

```console
npx webpack --watch
```

## What should happen

1. Open `http://localhost:8080/` in your preferred browser.
2. You should see the text on the page itself change to read `Success!`.
3. Press `Ctrl+C` in the terminal — `webpack-cli` closes the compiler, which
fires the plugin's `shutdown` hook, stopping the dev server cleanly.

## Notes

- The plugin works with both `webpack --watch` and `webpack serve`. With
`webpack serve`, `webpack-cli` already creates its own standalone dev server
for the same compiler, so you would end up with two servers running. If
that's intentional (e.g. different ports/hosts), make sure the plugin's
`port` does not clash with the one `webpack-cli` resolves from
`config.devServer` and CLI args. Otherwise prefer one or the other.
6 changes: 6 additions & 0 deletions examples/api/plugin/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"use strict";

const target = document.querySelector("#target");

target.classList.add("pass");
target.innerHTML = "Success!";
27 changes: 27 additions & 0 deletions examples/api/plugin/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use strict";

const WebpackDevServer = require("../../../lib/Server");
// our setup function adds behind-the-scenes bits to the config that all of our
// examples need
const { setup } = require("../../util");

const config = setup({
context: __dirname,
entry: "./app.js",
output: {
filename: "bundle.js",
},
stats: {
colors: true,
},
});

// `setup()` populates `config.devServer.setupMiddlewares` so that the example
// layout assets (CSS, favicon, icons under `.assets/`) are served by the dev
// server. Forward those options to the plugin instance — without them the
// `<link rel="stylesheet">` from the shared layout would 404.
config.plugins.push(
new WebpackDevServer({ ...config.devServer, port: 8090, open: true }),
);

module.exports = config;
137 changes: 121 additions & 16 deletions lib/Server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -326,11 +328,14 @@ class Server {
baseDataPath: "options",
});

this.compiler = compiler;
/**
* @type {ReturnType<Compiler["getInfrastructureLogger"]>}
*/
this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server");
if (compiler) {
this.compiler = compiler;

/**
* @type {ReturnType<Compiler["getInfrastructureLogger"]>}
*/
this.logger = this.compiler.getInfrastructureLogger(pluginName);
}
this.options = options;
/**
* @type {FSWatcher[]}
Expand All @@ -357,6 +362,11 @@ class Server {
*/

this.currentHash = undefined;
/**
* @private
* @type {boolean}
*/
this.isPlugin = false;
}

static get schema() {
Expand Down Expand Up @@ -544,14 +554,14 @@ class Server {
}

if (!dir) {
return path.resolve(cwd, ".cache/webpack-dev-server");
return path.resolve(cwd, `.cache/${pluginName}`);
} else if (process.versions.pnp === "1") {
return path.resolve(dir, ".pnp/.cache/webpack-dev-server");
return path.resolve(dir, `.pnp/.cache/${pluginName}`);
} else if (process.versions.pnp === "3") {
return path.resolve(dir, ".yarn/.cache/webpack-dev-server");
return path.resolve(dir, `.yarn/.cache/${pluginName}`);
}

return path.resolve(dir, "node_modules/.cache/webpack-dev-server");
return path.resolve(dir, `node_modules/.cache/${pluginName}`);
}

/**
Expand Down Expand Up @@ -1231,7 +1241,7 @@ class Server {
if (typeof options.ipc === "boolean") {
const isWindows = process.platform === "win32";
const pipePrefix = isWindows ? "\\\\.\\pipe\\" : os.tmpdir();
const pipeName = "webpack-dev-server.sock";
const pipeName = `${pluginName}.sock`;

options.ipc = path.join(pipePrefix, pipeName);
}
Expand Down Expand Up @@ -1330,7 +1340,12 @@ class Server {
}

if (typeof options.setupExitSignals === "undefined") {
options.setupExitSignals = true;
// In plugin mode, the host (e.g. `webpack-cli`) usually owns process
// signal handling and calls `compiler.close()` on shutdown, which fires
// our `shutdown` hook. Adding our own SIGINT/SIGTERM listeners on top of
// that would race with the host's handler and call `compiler.close()`
// twice.
options.setupExitSignals = !this.isPlugin;
}

if (typeof options.static === "undefined") {
Expand Down Expand Up @@ -1621,7 +1636,7 @@ class Server {
this.server.emit("progress-update", { percent, msg, pluginName });
}
},
).apply(this.compiler);
).apply(/** @type {Compiler | MultiCompiler} */ (this.compiler));
}

/**
Expand Down Expand Up @@ -1704,7 +1719,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();
Expand Down Expand Up @@ -1779,13 +1794,16 @@ class Server {
* @returns {void}
*/
setupHooks() {
this.compiler.hooks.invalid.tap("webpack-dev-server", () => {
const compiler = /** @type {Compiler | MultiCompiler} */ (this.compiler);

compiler.hooks.invalid.tap(pluginName, () => {
if (this.webSocketServer) {
this.sendMessage(this.webSocketServer.clients, "invalid");
}
});
this.compiler.hooks.done.tap(
"webpack-dev-server",

compiler.hooks.done.tap(
pluginName,
/**
* @param {Stats | MultiStats} stats stats
*/
Expand Down Expand Up @@ -2329,8 +2347,10 @@ class Server {
// middleware for serving webpack bundle
/** @type {import("webpack-dev-middleware").API<Request, Response>} */
this.middleware = webpackDevMiddleware(
// @ts-expect-error
this.compiler,
this.options.devMiddleware,
this.isPlugin,
);
}

Expand Down Expand Up @@ -3237,6 +3257,15 @@ class Server {
* @returns {Promise<void>}
*/
async start() {
await this.setup();
await this.listen();
}

/**
* @private
* @returns {Promise<void>}
*/
async setup() {
await this.normalizeOptions();

if (this.options.ipc) {
Expand Down Expand Up @@ -3288,7 +3317,13 @@ class Server {
}

await this.initialize();
}

/**
* @private
* @returns {Promise<void>}
*/
async listen() {
const listenOptions = this.options.ipc
? { path: this.options.ipc }
: { host: this.options.host, port: this.options.port };
Expand Down Expand Up @@ -3429,6 +3464,76 @@ class Server {
.then(() => callback(), callback)
.catch(callback);
}

/**
* @param {Compiler | MultiCompiler} compiler compiler
* @returns {void}
*/
apply(compiler) {
this.compiler = compiler;
this.isPlugin = true;
this.logger = this.compiler.getInfrastructureLogger(pluginName);

/** @type {Promise<void> | undefined} */
let setupPromise;
let listening = false;
let stopped = false;

const childCompilers = /** @type {MultiCompiler} */ (compiler)
.compilers || [compiler];
const seenFirstDone = new WeakSet();
let firstDoneCount = 0;

// A one-shot `compiler.run()` (plain `webpack` build) is detected when no
// child compiler is in watch mode. In that case we skip both `setup()` and
// `listen()` so the build can finish and the process can exit normally —
// the user is not in control of the plugin lifecycle here, so we stay
// silent rather than logging a warning.
const isBuildMode = () =>
childCompilers.every((child) => !child.watching && !child.options.watch);

/**
* @returns {Promise<void>} promise
*/
const ensureSetup = () => {
if (isBuildMode()) return Promise.resolve();
if (!setupPromise) {
setupPromise = this.setup();
}
return setupPromise;
};

/**
* @param {Compiler} childCompiler child compiler
* @returns {Promise<void>} promise
*/
const onChildDone = async (childCompiler) => {
if (listening || isBuildMode()) return;
if (seenFirstDone.has(childCompiler)) return;
seenFirstDone.add(childCompiler);
firstDoneCount++;
if (firstDoneCount < childCompilers.length) return;
listening = true;
await ensureSetup();
await this.listen();
};

const onChildShutdown = async () => {
if (stopped) return;
stopped = true;
setupPromise = undefined;
listening = false;
await this.stop();
};

for (const childCompiler of childCompilers) {
childCompiler.hooks.beforeCompile.tapPromise(pluginName, ensureSetup);
childCompiler.hooks.done.tapPromise(pluginName, () =>
onChildDone(childCompiler),
);
childCompiler.hooks.shutdown.tapPromise(pluginName, onChildShutdown);
}
}
}

module.exports = Server;
41 changes: 41 additions & 0 deletions test/e2e/__snapshots__/api-plugin.test.js.snap.webpack5
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`API (plugin) MultiCompiler 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...",
"one",
]
`;

exports[`API (plugin) MultiCompiler should work with plugin API: page errors 1`] = `[]`;

exports[`API (plugin) plugin in webpack config should work when added to webpack config plugins array: 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 (plugin) plugin in webpack config should work when added to webpack config plugins array: page errors 1`] = `[]`;

exports[`API (plugin) plugin in webpack config should work with output.clean: true: 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 (plugin) plugin in webpack config should work with output.clean: true: page errors 1`] = `[]`;

exports[`API (plugin) 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 (plugin) should work with plugin API: page errors 1`] = `[]`;
Loading
Loading