Skip to content

Commit 2dbdfc9

Browse files
committed
fix: assign distinct automatic ports in dev server plugin mode
1 parent 07eade8 commit 2dbdfc9

5 files changed

Lines changed: 140 additions & 9 deletions

File tree

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/webpack-cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"commander": "^14.0.3",
3636
"cross-spawn": "^7.0.6",
3737
"envinfo": "^7.21.0",
38+
"get-port": "^7.2.0",
3839
"import-local": "^3.2.0",
3940
"interpret": "^3.1.1",
4041
"rechoir": "^0.8.0",

packages/webpack-cli/src/webpack-cli.ts

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2240,6 +2240,20 @@ class WebpackCLI {
22402240
const compilersForDevServer =
22412241
possibleCompilers.length > 0 ? possibleCompilers : [compilers[0]];
22422242
const usedPorts: number[] = [];
2243+
const devServerConfigurations: DevServerConfiguration[] = [];
2244+
const validatePort = ({ port }: DevServerConfiguration): void => {
2245+
if (port && port !== "auto") {
2246+
const portNumber = Number(port);
2247+
2248+
if (usedPorts.includes(portNumber)) {
2249+
throw new Error(
2250+
"Unique ports must be specified for each devServer option in your webpack configuration. Alternatively, run only 1 devServer config using the --config-name flag to specify your desired config.",
2251+
);
2252+
}
2253+
2254+
usedPorts.push(portNumber);
2255+
}
2256+
};
22432257
// @ts-expect-error different versions of the `Schema` type
22442258
const devServerArgs = this.#getArguments(webpack, devServer.schema);
22452259
let appliedDevServers = 0;
@@ -2273,20 +2287,45 @@ class WebpackCLI {
22732287
this.#processArguments(webpack, args, devServerConfiguration, values);
22742288
}
22752289

2276-
if (devServerConfiguration.port) {
2277-
const portNumber = Number(devServerConfiguration.port);
2290+
if (isDevServerPlugin) {
2291+
validatePort(devServerConfiguration);
2292+
}
22782293

2279-
if (usedPorts.includes(portNumber)) {
2280-
throw new Error(
2281-
"Unique ports must be specified for each devServer option in your webpack configuration. Alternatively, run only 1 devServer config using the --config-name flag to specify your desired config.",
2282-
);
2283-
}
2294+
devServerConfigurations.push(devServerConfiguration);
2295+
}
22842296

2285-
usedPorts.push(portNumber);
2297+
for (const devServerConfiguration of devServerConfigurations) {
2298+
if (!isDevServerPlugin) {
2299+
validatePort(devServerConfiguration);
22862300
}
22872301

22882302
try {
22892303
if (isDevServerPlugin) {
2304+
let { port } = devServerConfiguration;
2305+
2306+
if (
2307+
devServerConfigurations.length > 1 &&
2308+
!devServerConfiguration.ipc &&
2309+
(typeof port === "undefined" || port === "auto")
2310+
) {
2311+
const { default: getPort, portNumbers } = await import("get-port");
2312+
const basePort = Number.parseInt(
2313+
process.env.WEBPACK_DEV_SERVER_BASE_PORT ?? "8080",
2314+
10,
2315+
);
2316+
const host = devServerConfiguration.host
2317+
? await DevServer.getHostname(devServerConfiguration.host)
2318+
: undefined;
2319+
2320+
// Plugins select ports before any server starts listening.
2321+
port = await getPort({
2322+
port: portNumbers(basePort, 65535),
2323+
host,
2324+
exclude: usedPorts,
2325+
});
2326+
usedPorts.push(port);
2327+
}
2328+
22902329
// v5 typings lack the plugin constructor.
22912330
const DevServerPlugin = DevServer as unknown as new (
22922331
options: DevServerConfiguration,
@@ -2295,6 +2334,7 @@ class WebpackCLI {
22952334
// Serve all child compilers, regardless of which defines devServer.
22962335
new DevServerPlugin({
22972336
...devServerConfiguration,
2337+
port,
22982338
setupExitSignals: false,
22992339
}).apply(compiler);
23002340
} else {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/* eslint-disable jest/require-top-level-describe -- Version-gated describe */
2+
3+
const net = require("node:net");
4+
const [devServerVersion] = require("webpack-dev-server/package.json").version;
5+
const { runWatch } = require("../../utils/test-utils");
6+
7+
const getGetPort = () => import("get-port");
8+
9+
const describeDevServer6 = devServerVersion === "5" ? describe.skip : describe;
10+
11+
describeDevServer6("automatic dev server ports", () => {
12+
let occupied;
13+
let basePort;
14+
15+
beforeEach(async () => {
16+
occupied = net.createServer();
17+
basePort = await (await getGetPort()).default();
18+
await new Promise((resolve, reject) => {
19+
occupied.once("error", reject);
20+
occupied.listen(basePort, "127.0.0.1", resolve);
21+
});
22+
});
23+
24+
afterEach(async () => {
25+
await new Promise((resolve, reject) => {
26+
occupied.close((error) => (error ? reject(error) : resolve()));
27+
});
28+
});
29+
30+
test.each(["omitted", "auto", "explicit"])(
31+
"should assign distinct available ports with %s ports",
32+
async (mode) => {
33+
const explicitPort = await (await getGetPort()).default();
34+
const args = ["serve", "--watch-options-stdin"];
35+
36+
if (mode === "auto") {
37+
args.push("--env", "auto=true");
38+
} else if (mode === "explicit") {
39+
args.push("--env", `explicit=${explicitPort}`);
40+
}
41+
42+
const { exitCode, stdout, stderr } = await runWatch(__dirname, args, {
43+
env: {
44+
WEBPACK_DEV_SERVER_BASE_PORT: String(mode === "explicit" ? explicitPort : basePort),
45+
},
46+
handler: (proc) => {
47+
let output = "";
48+
let stopping = false;
49+
proc.stdout.on("data", (chunk) => {
50+
output += chunk.toString();
51+
52+
if (!stopping && [...output.matchAll(/Listening \d: \d+\n/g)].length === 2) {
53+
stopping = true;
54+
proc.stdin.end();
55+
}
56+
});
57+
},
58+
});
59+
60+
const ports = [...stdout.matchAll(/Listening \d: (\d+)/g)].map((match) => Number(match[1]));
61+
expect(exitCode).toBe(0);
62+
expect(ports).toHaveLength(2);
63+
expect(new Set(ports).size).toBe(2);
64+
expect(ports).not.toContain(basePort);
65+
expect(stderr).not.toContain("EADDRINUSE");
66+
67+
if (mode === "explicit") {
68+
expect(ports).toContain(explicitPort);
69+
}
70+
},
71+
);
72+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module.exports = (env) =>
2+
[0, 1].map((index) => ({
3+
name: `server-${index}`,
4+
mode: "development",
5+
entry: "../rebuild/src/index.js",
6+
output: { filename: `server-${index}.js` },
7+
devServer: {
8+
host: "127.0.0.1",
9+
static: false,
10+
hot: false,
11+
client: false,
12+
...(env.explicit && index === 1 ? { port: Number(env.explicit) } : {}),
13+
...(env.auto ? { port: "auto" } : {}),
14+
onListening(server) {
15+
console.log(`Listening ${index}: ${server.server.address().port}`);
16+
},
17+
},
18+
}));

0 commit comments

Comments
 (0)