Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5336c99
feat: plugin support
bjohansebas Feb 16, 2026
3746f72
fix: correct errors when a compiler is not passed to the constructor
bjohansebas Feb 21, 2026
264bf91
feat: adapt webpack-dev-middleware.plugin
bjohansebas Mar 6, 2026
c881f22
feat: enhance plugin API support and update tests for new compile beh…
bjohansebas Mar 7, 2026
11271ec
feat: add isPlugin flag to Server class for plugin identification
bjohansebas Mar 30, 2026
8a9fc1e
feat: prevent multiple server starts on recompilation and ensure clea…
bjohansebas Mar 30, 2026
0682910
fixup!
bjohansebas Mar 30, 2026
7c49d5e
chore: more tests
bjohansebas Mar 30, 2026
b0875a4
feat: enhance server setup process
bjohansebas Apr 26, 2026
a8d0688
refactor: remove unnecessary compiler checks in setup methods
bjohansebas Apr 26, 2026
0c0aea5
test: ensure server setup and listen methods are called once on recom…
bjohansebas Apr 26, 2026
d2bc352
test: add API plugin tests and snapshots for webpack config integration
bjohansebas May 1, 2026
28c9721
feat: enhance MultiCompiler support in server setup and add related t…
bjohansebas May 1, 2026
e7aa41a
feat: add support for multiple independent plugin servers and enhance…
bjohansebas May 1, 2026
baf1e4c
test: add test for passive behavior in build mode with compiler.run
bjohansebas May 1, 2026
5645069
fixup!
bjohansebas May 1, 2026
e3f8c1f
feat: add example for using webpack-dev-server as a plugin with confi…
bjohansebas May 1, 2026
85ca2ab
refactor: update README and remove redundant standalone server handli…
bjohansebas May 1, 2026
7adaeae
fixup!
bjohansebas May 1, 2026
7442587
test: add more tests
bjohansebas May 1, 2026
6dc4133
fixup!
bjohansebas May 14, 2026
b12f3f9
fixup!
bjohansebas May 14, 2026
9912722
chore: convert to esm
bjohansebas May 16, 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;
138 changes: 122 additions & 16 deletions lib/Server.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,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 @@ -341,11 +343,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 @@ -372,6 +377,11 @@ class Server {
*/

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

static get schema() {
Expand Down Expand Up @@ -558,14 +568,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 @@ -1250,7 +1260,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 @@ -1349,7 +1359,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 @@ -1645,7 +1660,7 @@ class Server {
this.server.emit("progress-update", { percent, msg, pluginName });
}
},
).apply(this.compiler);
).apply(/** @type {Compiler | MultiCompiler} */ (this.compiler));
}

/**
Expand Down Expand Up @@ -1732,7 +1747,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 @@ -1807,13 +1822,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 @@ -1866,6 +1884,7 @@ class Server {
* @returns {Promise<void>}
*/
async setupMiddlewares() {
if (this.compiler === undefined) return;
/**
* @type {Middleware[]}
*/
Expand Down Expand Up @@ -2361,8 +2380,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 @@ -3284,6 +3305,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 @@ -3335,7 +3365,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 @@ -3487,6 +3523,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);
}
}
}

export default Server;
47 changes: 47 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,47 @@
exports[`API (plugin) > MultiCompiler > should work with plugin API 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 2`] = `
[]
`;

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

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

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