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/shared-dev-server-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"webpack-cli": patch
---

fix: assign distinct ports to dev servers of configurations that share the same `devServer` object.
3 changes: 2 additions & 1 deletion packages/webpack-cli/src/webpack-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2245,7 +2245,8 @@ class WebpackCLI {
const getDevServerOptions = (
devServerOptions: DevServerConfiguration | undefined,
): DevServerConfiguration => {
const configuration: DevServerConfiguration = devServerOptions || {};
// Configurations can share options, so a port assigned to one isn't used by others
const configuration: DevServerConfiguration = { ...devServerOptions };
const args: Record<string, WebpackArgument> = {};
const values: ProcessedArguments = {};

Expand Down
13 changes: 1 addition & 12 deletions test/serve/automatic-ports/automatic-ports.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,7 @@

const net = require("node:net");
const [devServerVersion] = require("webpack-dev-server/package.json").version;
const { runWatch } = require("../../utils/test-utils");

/**
* Ports below the ephemeral ranges (32768+ on Linux, 49152+ on Windows and macOS), so outgoing
* connections can't take the next ports, which the CLI picks for dev servers without a port
* @returns {Promise<number>} free port
*/
const getFreePort = async () => {
const { default: getPort, portNumbers } = await import("get-port");

return getPort({ port: portNumbers(20000, 30000) });
};
const { getFreePort, runWatch } = require("../../utils/test-utils");

const describeDevServer6 = devServerVersion === "5" ? describe.skip : describe;

Expand Down
54 changes: 21 additions & 33 deletions test/serve/basic/multi-dev-server.config.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,25 @@
const { getFreePort } = require("../../utils/test-utils");
const WebpackCLITestPlugin = require("../../utils/webpack-cli-test-plugin");
const { devServerConfig } = require("./helper/base-dev-server.config");
const devServerConfig = require("./helper/base-dev-server.config");

module.exports = async () => {
const port1 = await getFreePort();
const port2 = await getFreePort();

return [
{
name: "one",
mode: "development",
devtool: false,
output: {
filename: "first-output/[name].js",
},
devServer: {
...devServerConfig,
port: port1,
},
plugins: [new WebpackCLITestPlugin(["mode", "output"], false, "hooks.compilation.taps")],
module.exports = [
{
name: "one",
mode: "development",
devtool: false,
output: {
filename: "first-output/[name].js",
},
{
name: "two",
mode: "development",
devtool: false,
entry: "./src/other.js",
output: {
filename: "second-output/[name].js",
},
devServer: {
...devServerConfig,
port: port2,
},
devServer: devServerConfig,
plugins: [new WebpackCLITestPlugin(["mode", "output"], false, "hooks.compilation.taps")],
},
{
name: "two",
mode: "development",
devtool: false,
entry: "./src/other.js",
output: {
filename: "second-output/[name].js",
},
];
};
devServer: devServerConfig,
},
];
74 changes: 60 additions & 14 deletions test/serve/basic/serve-basic.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use strict";

const path = require("node:path");
const [devServerVersion] = require("webpack-dev-server/package.json").version;
const {
getFreePort,
normalizeStderr,
Expand All @@ -10,6 +11,8 @@ const {

const testPath = path.resolve(__dirname);

const describeDevServer6 = devServerVersion === "5" ? describe.skip : describe;

const normalStdKillOptions = {
stdoutKillStr: /webpack \d+\.\d+\.\d/,
stderrKillStr: /Content not from webpack is served from/,
Expand Down Expand Up @@ -95,21 +98,64 @@ describe("basic serve usage", () => {
expect(stdout).toContain("second-output/main.js");
});

// TODO need fix in future, edge case
// eslint-disable-next-line jest/no-disabled-tests
it.skip("should work in multi compiler mode with multiple dev servers", async () => {
const { stderr, stdout } = await runWatch(
__dirname,
["serve", "--config", "multi-dev-server.config.js"],
normalStdKillOptions,
);
// TODO in the next major release, remove the condition, only dev servers with plugin support
// select distinct ports for configurations without them
describeDevServer6("with multiple dev servers", () => {
it("should work in multi compiler mode without ports", async () => {
const ports = [];
const responses = {};
const { exitCode, stdout } = await runWatch(
__dirname,
["serve", "--config", "multi-dev-server.config.js", "--watch-options-stdin"],
{
// Automatic ports start from the base one
env: { WEBPACK_DEV_SERVER_BASE_PORT: String(port) },
handler: (proc) => {
let output = "";

proc.stderr.on("data", (chunk) => {
output += chunk.toString();

if (ports.length > 0) {
return;
}

const found = [...output.matchAll(/Loopback: http:\/\/localhost:(\d+)\//g)].map(
([, found]) => found,
);

if (found.length < 2) {
return;
}

ports.push(...found);

// Every dev server serves all compilers
Promise.all(
ports.flatMap((port) =>
["first-output", "second-output"].map((directory) =>
fetch(
`http://127.0.0.1:${port}/dev-server-my-public-path/${directory}/main.js`,
).then((response) => {
responses[`${port}:${directory}`] = response.status;
}),
),
),
).finally(() => {
proc.stdin.end();
});
});
},
},
);

expect(normalizeStderr(stderr)).toMatchSnapshot("stderr");
expect(stdout).toContain("HotModuleReplacementPlugin");
expect(stdout).toContain("one");
expect(stdout).toContain("first-output/main.js");
expect(stdout).toContain("two");
expect(stdout).toContain("second-output/main.js");
expect(exitCode).toBe(0);
expect(new Set(ports).size).toBe(2);
expect(Object.values(responses)).toEqual([200, 200, 200, 200]);
expect(stdout).toContain("HotModuleReplacementPlugin");
expect(stdout).toContain("first-output/main.js");
expect(stdout).toContain("second-output/main.js");
});
});

it('should work with the "--mode" option', async () => {
Expand Down
6 changes: 2 additions & 4 deletions test/serve/error-handling/serve-error-handling.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ const net = require("node:net");
const os = require("node:os");
const path = require("node:path");
const [devServerVersion] = require("webpack-dev-server/package.json").version;
const { run, runWatch } = require("../../utils/test-utils");

const getGetPort = () => import("get-port");
const { getFreePort, run, runWatch } = require("../../utils/test-utils");

// Only plugin mode uses the CLI's watch callback.
const describeDevServer6 = devServerVersion === "5" ? describe.skip : describe;
Expand All @@ -18,7 +16,7 @@ describeDevServer6("serve error handling", () => {
let port;

beforeEach(async () => {
port = await (await getGetPort()).default();
port = await getFreePort();
});

test("should print the stats with errors and keep serving when the compilation fails", async () => {
Expand Down
9 changes: 3 additions & 6 deletions test/serve/per-compiler/serve-per-compiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

const path = require("node:path");
const [devServerVersion] = require("webpack-dev-server/package.json").version;
const { runWatch } = require("../../utils/test-utils");

const getGetPort = () => import("get-port");
const { getFreePort, runWatch } = require("../../utils/test-utils");

/**
* Starts `webpack serve` and requests every bundle from every dev server once they listen.
Expand All @@ -16,9 +14,8 @@ const getGetPort = () => import("get-port");
* @returns {Promise<{ exitCode: number, stdout: string, stderr: string, responses: Record<string, number> }>} result
*/
const serve = async (args, config = "webpack.config.js", env = {}) => {
const { default: getPort } = await getGetPort();
const appPort = await getPort();
const adminPort = await getPort({ exclude: [appPort] });
const appPort = await getFreePort();
const adminPort = await getFreePort([appPort]);
const names = config === "webpack.config.js" ? ["app", "admin"] : ["app"];
const ports = { app: appPort, admin: adminPort };
const responses = {};
Expand Down
6 changes: 2 additions & 4 deletions test/serve/rebuild/serve-rebuild.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
const { readFileSync, writeFileSync } = require("node:fs");
const { resolve } = require("node:path");
const [devServerVersion] = require("webpack-dev-server/package.json").version;
const { processKill, runWatch } = require("../../utils/test-utils");

const getGetPort = () => import("get-port");
const { getFreePort, processKill, runWatch } = require("../../utils/test-utils");

const entryPath = resolve(__dirname, "./src/index.js");
const originalEntry = readFileSync(entryPath, "utf8");
Expand All @@ -16,7 +14,7 @@ describe("serve recompilation", () => {
let port;

beforeEach(async () => {
port = await (await getGetPort()).default();
port = await getFreePort();
});

afterEach(() => {
Expand Down
5 changes: 3 additions & 2 deletions test/utils/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,14 +408,15 @@ const cleanStderr = (stderr) =>
* tests that install packages) can take it before the dev server listens, and Jest
* workers running in parallel can get the same one. So each worker takes ports from its
* own range below the ephemeral ones.
* @param {number[]=} exclude ports to skip
* @returns {Promise<number>} free port
*/
const getFreePort = async () => {
const getFreePort = async (exclude = []) => {
const { default: getPort, portNumbers } = await import("get-port");
// 120 ranges keep ports below 32768, where the ephemeral range of Linux starts
const from = 20000 + ((Number(process.env.JEST_WORKER_ID || 1) - 1) % 120) * 100;

return getPort({ port: portNumbers(from, from + 99) });
return getPort({ port: portNumbers(from, from + 99), exclude });
};

const uniqueDirectoryForTest = async () => {
Expand Down
Loading