From f5a7bb865541b50cee6ce2ff970a6f7ff089ba9c Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:34:23 +0000 Subject: [PATCH 1/9] test: make the generator and dev server tests stable The `create-webpack-app` tests install real packages, and a registry hiccup made npm fail, which the tests only reported as a missing line in stdout. npm now prefers its cache and retries requests more, without the audit, fund and update-notifier requests, and a failed install fails the test with npm's error. The dev server tests took ports from the ephemeral range, where outgoing connections or another Jest worker could take them before the server listened. Each worker now takes ports from its own range below the ephemeral ones. Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL --- test/create-webpack-app/test.utils.js | 48 +++++++++++++++---- test/serve/basic/multi-dev-server.config.js | 7 ++- test/serve/basic/serve-basic.test.js | 11 +++-- .../serve-variable/serve-variable.test.js | 6 +-- .../serve-custom-config.test.js | 6 +-- test/utils/test-utils.js | 17 +++++++ 6 files changed, 71 insertions(+), 24 deletions(-) diff --git a/test/create-webpack-app/test.utils.js b/test/create-webpack-app/test.utils.js index b495cecea48..da3b2644dff 100644 --- a/test/create-webpack-app/test.utils.js +++ b/test/create-webpack-app/test.utils.js @@ -8,6 +8,31 @@ const nodeVersion = Number.parseInt(process.versions.node.split(".")[0], 10); // eslint-disable-next-line jsdoc/reject-any-type /** @typedef {Record} TestOptions */ +// The generators install real packages. Let npm reuse what is in its cache instead of +// asking the registry about every package again, and retry the requests it does make +// more times, so a network error doesn't fail the test. +const packageManagerEnv = { + npm_config_prefer_offline: "true", + npm_config_fetch_retries: "5", + npm_config_audit: "false", + npm_config_fund: "false", + npm_config_update_notifier: "false", +}; + +/** + * Fail with the package manager's error, rather than with the output the test misses. + * @template {{ stderr: string }} T + * @param {T} result result of a CLI run + * @returns {T} the same result + */ +const checkInstall = (result) => { + if (result.stderr && result.stderr.includes("Error occurred while installing packages")) { + throw new Error(`Installing packages failed:\n${result.stderr}`); + } + + return result; +}; + function createPathDependentUtils(cli) { const CLI_PATH = path.resolve(__dirname, `../../packages/${cli}/bin/cli.js`); @@ -19,10 +44,13 @@ function createPathDependentUtils(cli) { * @returns {Promise} child process */ const run = async (cwd, args = [], options = {}) => - testUtilsPkg.run(cwd, args, { - ...options, - executorPath: CLI_PATH, - }); + checkInstall( + await testUtilsPkg.run(cwd, args, { + ...options, + env: { ...packageManagerEnv, ...options.env }, + executorPath: CLI_PATH, + }), + ); /** * Run the webpack CLI in watch mode for a test case. @@ -34,6 +62,7 @@ function createPathDependentUtils(cli) { const runWatch = async (cwd, args = [], options = {}) => testUtilsPkg.runWatch(cwd, args, { ...options, + env: { ...packageManagerEnv, ...options.env }, executorPath: CLI_PATH, }); @@ -46,10 +75,13 @@ function createPathDependentUtils(cli) { * @returns {Promise<{ stdout: string, stderr: string }>} result */ const runPromptWithAnswers = async (cwd, args, answers = [], options = {}) => - testUtilsPkg.runPromptWithAnswers(cwd, args, answers, { - ...options, - executorPath: CLI_PATH, - }); + checkInstall( + await testUtilsPkg.runPromptWithAnswers(cwd, args, answers, { + ...options, + env: { ...packageManagerEnv, ...options.env }, + executorPath: CLI_PATH, + }), + ); return { run, runWatch, runPromptWithAnswers }; } diff --git a/test/serve/basic/multi-dev-server.config.js b/test/serve/basic/multi-dev-server.config.js index 26405fe26a6..7519a9c3a96 100644 --- a/test/serve/basic/multi-dev-server.config.js +++ b/test/serve/basic/multi-dev-server.config.js @@ -1,11 +1,10 @@ +const { getFreePort } = require("../../utils/test-utils"); const WebpackCLITestPlugin = require("../../utils/webpack-cli-test-plugin"); const { devServerConfig } = require("./helper/base-dev-server.config"); -const getGetPort = () => import("get-port"); - module.exports = async () => { - const port1 = await (await getGetPort()).default(); - const port2 = await (await getGetPort()).default(); + const port1 = await getFreePort(); + const port2 = await getFreePort(); return [ { diff --git a/test/serve/basic/serve-basic.test.js b/test/serve/basic/serve-basic.test.js index 3fb5c9ad521..c796b9bee73 100644 --- a/test/serve/basic/serve-basic.test.js +++ b/test/serve/basic/serve-basic.test.js @@ -1,9 +1,12 @@ "use strict"; const path = require("node:path"); -const { normalizeStderr, normalizeStdout, runWatch } = require("../../utils/test-utils"); - -const getGetPort = () => import("get-port"); +const { + getFreePort, + normalizeStderr, + normalizeStdout, + runWatch, +} = require("../../utils/test-utils"); const testPath = path.resolve(__dirname); @@ -16,7 +19,7 @@ describe("basic serve usage", () => { let port; beforeEach(async () => { - port = await (await getGetPort()).default(); + port = await getFreePort(); }); it("should work", async () => { diff --git a/test/serve/serve-variable/serve-variable.test.js b/test/serve/serve-variable/serve-variable.test.js index 2066defbaff..3730b73f73f 100644 --- a/test/serve/serve-variable/serve-variable.test.js +++ b/test/serve/serve-variable/serve-variable.test.js @@ -1,9 +1,7 @@ "use strict"; const path = require("node:path"); -const { normalizeStderr, runWatch } = require("../../utils/test-utils"); - -const getGetPort = () => import("get-port"); +const { getFreePort, normalizeStderr, runWatch } = require("../../utils/test-utils"); const testPath = path.resolve(__dirname); @@ -11,7 +9,7 @@ describe("serve variable", () => { let port; beforeEach(async () => { - port = await (await getGetPort()).default(); + port = await getFreePort(); }); it("compiles without flags and export variable", async () => { diff --git a/test/serve/with-custom-port/serve-custom-config.test.js b/test/serve/with-custom-port/serve-custom-config.test.js index d9f437b7924..bf48f790437 100644 --- a/test/serve/with-custom-port/serve-custom-config.test.js +++ b/test/serve/with-custom-port/serve-custom-config.test.js @@ -1,9 +1,7 @@ "use strict"; const path = require("node:path"); -const { normalizeStderr, runWatch } = require("../../utils/test-utils"); - -const getGetPort = () => import("get-port"); +const { getFreePort, normalizeStderr, runWatch } = require("../../utils/test-utils"); const testPath = path.resolve(__dirname); @@ -11,7 +9,7 @@ describe("serve with devServer in config", () => { let port; beforeEach(async () => { - port = await (await getGetPort()).default(); + port = await getFreePort(); }); it("should pick up the host and port from config", async () => { diff --git a/test/utils/test-utils.js b/test/utils/test-utils.js index addd613a11b..1e6332c36f4 100644 --- a/test/utils/test-utils.js +++ b/test/utils/test-utils.js @@ -401,6 +401,22 @@ const cleanStderr = (stderr) => ? stderr.replaceAll(NODE_WARNING_RE, "").replaceAll(INFRASTRUCTURE_INFO_RE, "").trim() : stderr; +/** + * Get a free port for a dev server. + * + * A port from the OS comes from the ephemeral range, where outgoing connections (e.g. of + * 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. + * @returns {Promise} free port + */ +const getFreePort = async () => { + const { default: getPort, portNumbers } = await import("get-port"); + const from = 20000 + (Number(process.env.JEST_WORKER_ID || 1) - 1) * 500; + + return getPort({ port: portNumbers(from, from + 499) }); +}; + const uniqueDirectoryForTest = async () => { const result = path.resolve(path.resolve(__dirname, "../create-webpack-app-testing"), uuid()); @@ -413,6 +429,7 @@ const uniqueDirectoryForTest = async () => { module.exports = { cleanStderr, + getFreePort, getWebpackCliArguments, hyphenToUpperCase, isWindows, From 3c1d9a630c4b423c72fdc4af74bef2754baf117f Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:23:17 +0000 Subject: [PATCH 2/9] test: keep npm's default retries for the generator tests More retries made a failing install outlast the test timeout, which then hid npm's error. Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL --- test/create-webpack-app/test.utils.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/create-webpack-app/test.utils.js b/test/create-webpack-app/test.utils.js index da3b2644dff..a557dcf54ed 100644 --- a/test/create-webpack-app/test.utils.js +++ b/test/create-webpack-app/test.utils.js @@ -9,11 +9,10 @@ const nodeVersion = Number.parseInt(process.versions.node.split(".")[0], 10); /** @typedef {Record} TestOptions */ // The generators install real packages. Let npm reuse what is in its cache instead of -// asking the registry about every package again, and retry the requests it does make -// more times, so a network error doesn't fail the test. +// asking the registry about every package again, and skip the requests the tests don't +// need, so fewer requests can fail. const packageManagerEnv = { npm_config_prefer_offline: "true", - npm_config_fetch_retries: "5", npm_config_audit: "false", npm_config_fund: "false", npm_config_update_notifier: "false", From 0236d0a62d2341733024fc6d559d2418ce0825b5 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:29:49 +0000 Subject: [PATCH 3/9] test: keep the port ranges of Jest workers within valid ports Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL --- test/utils/test-utils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/utils/test-utils.js b/test/utils/test-utils.js index 1e6332c36f4..09798485c3d 100644 --- a/test/utils/test-utils.js +++ b/test/utils/test-utils.js @@ -412,7 +412,8 @@ const cleanStderr = (stderr) => */ const getFreePort = async () => { const { default: getPort, portNumbers } = await import("get-port"); - const from = 20000 + (Number(process.env.JEST_WORKER_ID || 1) - 1) * 500; + // 24 ranges keep ports below 32768, where the ephemeral range of Linux starts + const from = 20000 + ((Number(process.env.JEST_WORKER_ID || 1) - 1) % 24) * 500; return getPort({ port: portNumbers(from, from + 499) }); }; From 9bbf4a6e18cc9e5a8e79f7ecac574e26c83eec79 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:37:43 +0000 Subject: [PATCH 4/9] test: give up to 120 Jest workers their own port range Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL --- test/utils/test-utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/utils/test-utils.js b/test/utils/test-utils.js index 09798485c3d..7e87ddc67e4 100644 --- a/test/utils/test-utils.js +++ b/test/utils/test-utils.js @@ -412,10 +412,10 @@ const cleanStderr = (stderr) => */ const getFreePort = async () => { const { default: getPort, portNumbers } = await import("get-port"); - // 24 ranges keep ports below 32768, where the ephemeral range of Linux starts - const from = 20000 + ((Number(process.env.JEST_WORKER_ID || 1) - 1) % 24) * 500; + // 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 + 499) }); + return getPort({ port: portNumbers(from, from + 99) }); }; const uniqueDirectoryForTest = async () => { From 8612f1c1698dd432e6ed9aa8a0c82abc5b43bb22 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:09:14 +0000 Subject: [PATCH 5/9] fix: don't detect browsers in `webpack info` on Windows On Windows, `envinfo` starts the browsers themselves to get their versions, which can hang, e.g. the `info` tests time out on Windows CI runners. Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL --- packages/webpack-cli/src/webpack-cli.ts | 30 +++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 971ce6b18e3..f6de9f120a3 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1876,18 +1876,24 @@ class WebpackCLI { } else { const defaultInformation: EnvinfoConfig = { Binaries: ["Node", "Yarn", "npm", "pnpm"], - Browsers: [ - "Brave Browser", - "Chrome", - "Chrome Canary", - "Edge", - "Firefox", - "Firefox Developer Edition", - "Firefox Nightly", - "Internet Explorer", - "Safari", - "Safari Technology Preview", - ], + // On Windows, `envinfo` starts the browsers themselves to get their versions, + // which can hang (e.g. on CI), so they are skipped there + ...(process.platform === "win32" + ? {} + : { + Browsers: [ + "Brave Browser", + "Chrome", + "Chrome Canary", + "Edge", + "Firefox", + "Firefox Developer Edition", + "Firefox Nightly", + "Internet Explorer", + "Safari", + "Safari Technology Preview", + ], + }), // @ts-expect-error No in types Monorepos: ["Yarn Workspaces", "Lerna"], System: ["OS", "CPU", "Memory"], From b78111d21d7e24332971420edea0f63026819ca3 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:04:54 +0000 Subject: [PATCH 6/9] test: don't let npm prefer its cache in the generator tests With `prefer-offline`, npm used cached metadata that lacked a version a freshly fetched package depended on, and the install failed with ETARGET (`@babel/generator@^8.0.10`) on Windows CI. Also add a changeset for not detecting browsers in `webpack info` on Windows. Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL --- .changeset/info-skip-browsers-windows.md | 5 +++++ test/create-webpack-app/test.utils.js | 7 +++---- 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 .changeset/info-skip-browsers-windows.md diff --git a/.changeset/info-skip-browsers-windows.md b/.changeset/info-skip-browsers-windows.md new file mode 100644 index 00000000000..01ef73fac2f --- /dev/null +++ b/.changeset/info-skip-browsers-windows.md @@ -0,0 +1,5 @@ +--- +"webpack-cli": patch +--- + +fix: don't detect browsers in `webpack info` on Windows, `envinfo` starts them to get their versions, which can hang diff --git a/test/create-webpack-app/test.utils.js b/test/create-webpack-app/test.utils.js index a557dcf54ed..a2c9bfe9cb4 100644 --- a/test/create-webpack-app/test.utils.js +++ b/test/create-webpack-app/test.utils.js @@ -8,11 +8,10 @@ const nodeVersion = Number.parseInt(process.versions.node.split(".")[0], 10); // eslint-disable-next-line jsdoc/reject-any-type /** @typedef {Record} TestOptions */ -// The generators install real packages. Let npm reuse what is in its cache instead of -// asking the registry about every package again, and skip the requests the tests don't -// need, so fewer requests can fail. +// The generators install real packages. Skip the requests the tests don't need, so fewer +// requests can fail. `prefer-offline` isn't used: with a stale cache, npm misses new +// versions that other packages already depend on. const packageManagerEnv = { - npm_config_prefer_offline: "true", npm_config_audit: "false", npm_config_fund: "false", npm_config_update_notifier: "false", From 77665c5d2b6eb81f49b4f52623514b7f5ff75f16 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:29:44 +0000 Subject: [PATCH 7/9] chore: move the `webpack info` changeset to #4847 Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL --- .changeset/info-skip-browsers-windows.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/info-skip-browsers-windows.md diff --git a/.changeset/info-skip-browsers-windows.md b/.changeset/info-skip-browsers-windows.md deleted file mode 100644 index 01ef73fac2f..00000000000 --- a/.changeset/info-skip-browsers-windows.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-cli": patch ---- - -fix: don't detect browsers in `webpack info` on Windows, `envinfo` starts them to get their versions, which can hang From f3c80dd7563c9acc1e36c59f853ed2acff673f86 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Fri, 30 Oct 2026 08:16:51 +0000 Subject: [PATCH 8/9] revert: detect browsers in `webpack info` on Windows again The `info` tests time out on Windows only at random, so keep detecting browsers there. Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL --- packages/webpack-cli/src/webpack-cli.ts | 30 ++++++++++--------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index f6de9f120a3..971ce6b18e3 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1876,24 +1876,18 @@ class WebpackCLI { } else { const defaultInformation: EnvinfoConfig = { Binaries: ["Node", "Yarn", "npm", "pnpm"], - // On Windows, `envinfo` starts the browsers themselves to get their versions, - // which can hang (e.g. on CI), so they are skipped there - ...(process.platform === "win32" - ? {} - : { - Browsers: [ - "Brave Browser", - "Chrome", - "Chrome Canary", - "Edge", - "Firefox", - "Firefox Developer Edition", - "Firefox Nightly", - "Internet Explorer", - "Safari", - "Safari Technology Preview", - ], - }), + Browsers: [ + "Brave Browser", + "Chrome", + "Chrome Canary", + "Edge", + "Firefox", + "Firefox Developer Edition", + "Firefox Nightly", + "Internet Explorer", + "Safari", + "Safari Technology Preview", + ], // @ts-expect-error No in types Monorepos: ["Yarn Workspaces", "Lerna"], System: ["OS", "CPU", "Memory"], From ed4db6ef36a82403932a5322fd5ff39aa9e65e74 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Fri, 30 Oct 2026 09:15:39 +0000 Subject: [PATCH 9/9] test: give each Jest worker its own Yarn cache in the generator tests The init, loader and plugin tests install with Yarn in parallel Jest workers. Yarn 1 can't fill one cache from two installs at the same time, and an install failed on macOS CI reading a half-written package.json from the shared cache. Claude-Session: https://claude.ai/code/session_013FhutRFjuQemBzekJMYcqL --- test/create-webpack-app/test.utils.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/create-webpack-app/test.utils.js b/test/create-webpack-app/test.utils.js index a2c9bfe9cb4..541bcfad14b 100644 --- a/test/create-webpack-app/test.utils.js +++ b/test/create-webpack-app/test.utils.js @@ -1,5 +1,6 @@ "use strict"; +const os = require("node:os"); const path = require("node:path"); const testUtilsPkg = require("../utils/test-utils"); @@ -15,6 +16,12 @@ const packageManagerEnv = { npm_config_audit: "false", npm_config_fund: "false", npm_config_update_notifier: "false", + // Yarn 1 breaks its cache when two installs fill it at the same time, as tests in parallel + // Jest workers do, so each worker gets its own + YARN_CACHE_FOLDER: path.join( + os.tmpdir(), + `webpack-cli-yarn-cache-${process.env.JEST_WORKER_ID || 1}`, + ), }; /**