From a49c3274b0d03cbded74035fd669ae69db1c727e Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Mon, 16 Feb 2026 21:09:56 +0300 Subject: [PATCH 01/12] fix: fallback to `interpret` only when configuration can't be load --- .../webpack-cli/src/plugins/cli-plugin.ts | 4 +- packages/webpack-cli/src/types.ts | 7 +- packages/webpack-cli/src/webpack-cli.ts | 149 +++++++++++------- 3 files changed, 91 insertions(+), 69 deletions(-) diff --git a/packages/webpack-cli/src/plugins/cli-plugin.ts b/packages/webpack-cli/src/plugins/cli-plugin.ts index 33f06e8aaf4..0f4796a3ea1 100644 --- a/packages/webpack-cli/src/plugins/cli-plugin.ts +++ b/packages/webpack-cli/src/plugins/cli-plugin.ts @@ -1,7 +1,7 @@ import { type Compiler } from "webpack"; import { type CLIPluginOptions } from "../types.js"; -export class CLIPlugin { +export default class CLIPlugin { logger!: ReturnType; options: CLIPluginOptions; @@ -149,5 +149,3 @@ export class CLIPlugin { this.setupHelpfulOutput(compiler); } } - -module.exports = CLIPlugin; diff --git a/packages/webpack-cli/src/types.ts b/packages/webpack-cli/src/types.ts index 67acb7de486..96048a629f4 100644 --- a/packages/webpack-cli/src/types.ts +++ b/packages/webpack-cli/src/types.ts @@ -1,4 +1,3 @@ -import { type stringifyChunked } from "@discoveryjs/json-ext"; import { type Command, type CommandOptions, type Option, type ParseOptions } from "commander"; import { type prepare } from "rechoir"; import { @@ -32,6 +31,7 @@ declare interface WebpackCallback { } // TODO remove me in the next major release, we don't need extra interface +// TODO also revisit all methods - remove unused or make private interface IWebpackCLI { colors: WebpackCLIColors; logger: WebpackCLILogger; @@ -287,10 +287,6 @@ interface Rechoir { prepare: typeof prepare; } -interface JsonExt { - stringifyChunked: typeof stringifyChunked; -} - interface RechoirError extends Error { failures: RechoirError[]; error: Error; @@ -318,7 +314,6 @@ export { type IWebpackCLI, type ImportLoaderError, type Instantiable, - type JsonExt, type LoadableWebpackConfiguration, type ModuleName, type PackageInstallOptions, diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index ae26dff3317..35fd97fe08e 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -10,14 +10,11 @@ import { type WebpackError, default as webpack, } from "webpack"; -import type webpackMerge from "webpack-merge"; -import { type CLIPlugin as CLIPluginClass } from "./plugins/cli-plugin.js"; import { type Argument, type Argv, type BasicPrimitive, - type CLIPluginOptions, type CallableWebpackConfiguration, type CommandAction, type DynamicImport, @@ -26,7 +23,6 @@ import { type IWebpackCLI, type ImportLoaderError, type Instantiable, - type JsonExt, type LoadableWebpackConfiguration, type ModuleName, type PackageInstallOptions, @@ -83,6 +79,23 @@ interface Information { npmPackages?: string | string[]; } +class ConfigurationLoadingError extends Error { + name = "ConfigurationLoadingError"; + + constructor(errors: [unknown, unknown]) { + const message1 = errors[0] instanceof Error ? errors[0].message : String(errors[0]); + const message2 = util.stripVTControlCharacters( + errors[1] instanceof Error ? errors[1].message : String(errors[1]), + ); + const message = + `▶ ESM (\`import\`) failed:\n ${message1.split("\n").join("\n ")}\n\n▶ CJS (\`require\`) failed:\n ${message2.split("\n").join("\n ")}`.trim(); + + super(message); + + this.stack = ""; + } +} + class WebpackCLI implements IWebpackCLI { colors: WebpackCLIColors; @@ -348,7 +361,7 @@ class WebpackCLI implements IWebpackCLI { } if (needInstall) { - const { sync } = require("cross-spawn"); + const { sync } = await import("cross-spawn"); try { sync(packageManager, commandArguments, { stdio: "inherit" }); @@ -364,6 +377,7 @@ class WebpackCLI implements IWebpackCLI { process.exit(2); } + // TODO remove me in the next major release async tryRequireThenImport( module: ModuleName, handleError = true, @@ -539,7 +553,7 @@ class WebpackCLI implements IWebpackCLI { defaultInformation.npmPackages = `{${defaultPackages.map((item) => `*${item}*`).join(",")}}`; - const envinfo = await this.tryRequireThenImport("envinfo", false); + const envinfo = await import("envinfo"); let info = await envinfo.run(defaultInformation, envinfoConfig); @@ -1094,8 +1108,8 @@ class WebpackCLI implements IWebpackCLI { return options; } - async loadWebpack(handleError = true) { - return this.tryRequireThenImport(WEBPACK_PACKAGE, handleError); + async loadWebpack(): Promise { + return require(WEBPACK_PACKAGE); } async run(args: Parameters[0], parseOptions: ParseOptions) { @@ -1264,7 +1278,7 @@ class WebpackCLI implements IWebpackCLI { let loadedCommand; try { - loadedCommand = await this.tryRequireThenImport void>>(pkg, false); + loadedCommand = (await import(pkg)) as Instantiable<() => void>; } catch { // Ignore, command is not installed @@ -1325,10 +1339,10 @@ class WebpackCLI implements IWebpackCLI { process.exit(2); } - const levenshtein = require("fastest-levenshtein"); + const { distance } = await import("fastest-levenshtein"); for (const option of (command as WebpackCLICommand).options) { - if (!option.hidden && levenshtein.distance(name, option.long?.slice(2)) < 3) { + if (!option.hidden && distance(name, option.long?.slice(2) as string) < 3) { this.logger.error(`Did you mean '--${option.name()}'?`); } } @@ -1761,11 +1775,10 @@ class WebpackCLI implements IWebpackCLI { } else { this.logger.error(`Unknown command or entry '${operand}'`); - const levenshtein = require("fastest-levenshtein"); + const { distance } = await import("fastest-levenshtein"); const found = knownCommands.find( - (commandOptions) => - levenshtein.distance(operand, getCommandName(commandOptions.name)) < 3, + (commandOptions) => distance(operand, getCommandName(commandOptions.name)) < 3, ); if (found) { @@ -1789,33 +1802,41 @@ class WebpackCLI implements IWebpackCLI { await this.program.parseAsync(args, parseOptions); } - async loadConfig(options: Partial) { - const disableInterpret = - typeof options.disableInterpret !== "undefined" && options.disableInterpret; + async #loadConfigurationFile(module: ModuleName, disableInterpret = false): Promise { + let pkg; - const interpret = require("interpret"); + let loadingError; + + try { + // eslint-disable-next-line no-eval + pkg = await eval(`import("${module}")`); + } catch (err) { + if (this.isValidationError(err)) { + throw err; + } + + loadingError = err; + } + + // Fallback logic when we can't use `import(...)` + if (loadingError) { + const { jsVariants, extensions } = await import("interpret"); + const ext = path.extname(module).toLowerCase(); + + let interpreted = Object.keys(jsVariants).find((variant) => variant === ext); - const loadConfigByPath = async ( - configPath: string, - argv: Argv = {}, - ): Promise<{ options: Configuration | Configuration[]; path: string }> => { - const ext = path.extname(configPath).toLowerCase(); - let interpreted = Object.keys(interpret.jsVariants).find((variant) => variant === ext); - // Fallback `.cts` to `.ts` - // TODO implement good `.mts` support after https://github.com/gulpjs/rechoir/issues/43 - // For ESM and `.mts` you need to use: 'NODE_OPTIONS="--loader ts-node/esm" webpack-cli --config ./webpack.config.mts' if (!interpreted && ext.endsWith(".cts")) { - interpreted = interpret.jsVariants[".ts"]; + interpreted = jsVariants[".ts"] as string; } if (interpreted && !disableInterpret) { - const rechoir: Rechoir = require("rechoir"); + const rechoir: Rechoir = (await import("rechoir")).default; try { - rechoir.prepare(interpret.extensions, configPath); + rechoir.prepare(extensions, module); } catch (error) { if ((error as RechoirError)?.failures) { - this.logger.error(`Unable load '${configPath}'`); + this.logger.error(`Unable load '${module}'`); this.logger.error((error as RechoirError).message); for (const failure of (error as RechoirError).failures) { this.logger.error(failure.error.message); @@ -1823,41 +1844,51 @@ class WebpackCLI implements IWebpackCLI { this.logger.error("Please install one of them"); process.exit(2); } - this.logger.error(error); process.exit(2); } } - let options: LoadableWebpackConfiguration | LoadableWebpackConfiguration[]; + try { + pkg = require(module); + } catch (err) { + if (this.isValidationError(err)) { + throw err; + } - type LoadConfigOption = PotentialPromise; + throw new ConfigurationLoadingError([loadingError, err]); + } + } - let moduleType: "unknown" | "commonjs" | "esm" = "unknown"; + if (pkg && typeof pkg === "object" && "default" in pkg) { + pkg = pkg.default || {}; + } - switch (ext) { - case ".cjs": - case ".cts": - moduleType = "commonjs"; - break; - case ".mjs": - case ".mts": - moduleType = "esm"; - break; - } + return pkg || {}; + } + + async loadConfig(options: Partial) { + const disableInterpret = + typeof options.disableInterpret !== "undefined" && options.disableInterpret; + + const loadConfigByPath = async ( + configPath: string, + argv: Argv = {}, + ): Promise<{ options: Configuration | Configuration[]; path: string }> => { + let options: LoadableWebpackConfiguration | LoadableWebpackConfiguration[]; + + type LoadConfigOption = PotentialPromise; try { - options = await this.tryRequireThenImport( + options = await this.#loadConfigurationFile( configPath, - false, - moduleType, + disableInterpret, ); } catch (error) { - this.logger.error(`Failed to load '${configPath}' config`); - - if (this.isValidationError(error)) { - this.logger.error(error.message); + if (error instanceof ConfigurationLoadingError) { + this.logger.error(`Failed to load '${configPath}' config\n${error.message}`); } else { + this.logger.error(`Failed to load '${configPath}' config`); this.logger.error(error); } @@ -1950,6 +1981,7 @@ class WebpackCLI implements IWebpackCLI { } } } else { + const interpret = await import("interpret"); // Prioritize popular extensions first to avoid unnecessary fs calls const extensions = new Set([ ".js", @@ -2035,7 +2067,7 @@ class WebpackCLI implements IWebpackCLI { ), ); - const merge = await this.tryRequireThenImport("webpack-merge"); + const { merge } = await import("webpack-merge"); const loadedOptions = loadedConfigs.flatMap((config) => config.options); if (loadedOptions.length > 0) { @@ -2108,7 +2140,7 @@ class WebpackCLI implements IWebpackCLI { } if (options.merge) { - const merge = await this.tryRequireThenImport("webpack-merge"); + const { merge } = await import("webpack-merge"); // we can only merge when there are multiple configurations // either by passing multiple configs by flags or passing a @@ -2161,10 +2193,7 @@ class WebpackCLI implements IWebpackCLI { process.exit(2); } - const CLIPlugin = - await this.tryRequireThenImport>( - "./plugins/cli-plugin", - ); + const CLIPlugin = (await import("./plugins/cli-plugin.js")).default; const internalBuildConfig = (item: Configuration) => { const originalWatchValue = item.watch; @@ -2407,9 +2436,9 @@ class WebpackCLI implements IWebpackCLI { let createStringifyChunked: typeof stringifyChunked; if (options.json) { - const jsonExt = await this.tryRequireThenImport("@discoveryjs/json-ext"); + const { stringifyChunked } = await import("@discoveryjs/json-ext"); - createStringifyChunked = jsonExt.stringifyChunked; + createStringifyChunked = stringifyChunked; } const callback: WebpackCallback = (error, stats): void => { From 37c11ebb93a6a6e018affa56805f762d3d59874d Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Mon, 16 Feb 2026 21:43:56 +0300 Subject: [PATCH 02/12] refactor: fixes --- packages/webpack-cli/src/webpack-cli.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 35fd97fe08e..0815d55d265 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1815,6 +1815,10 @@ class WebpackCLI implements IWebpackCLI { throw err; } + if (process.env?.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) { + throw err; + } + loadingError = err; } From d696e76f853a0a3138a3c7e29ab26a904c93abd8 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Mon, 16 Feb 2026 21:46:37 +0300 Subject: [PATCH 03/12] refactor: fixes --- packages/webpack-cli/src/webpack-cli.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 0815d55d265..b226bb2887d 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -69,6 +69,11 @@ const WEBPACK_DEV_SERVER_PACKAGE = WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM : "webpack-dev-server"; const EXIT_SIGNALS = ["SIGINT", "SIGTERM"]; +const DEFAULT_CONFIGURATION_FILES = [ + "webpack.config", + ".webpack/webpack.config", + ".webpack/webpackfile", +]; interface Information { Binaries?: string[]; @@ -1278,7 +1283,7 @@ class WebpackCLI implements IWebpackCLI { let loadedCommand; try { - loadedCommand = (await import(pkg)) as Instantiable<() => void>; + loadedCommand = await this.tryRequireThenImport void>>(pkg, false); } catch { // Ignore, command is not installed @@ -1998,7 +2003,7 @@ class WebpackCLI implements IWebpackCLI { ]); // Order defines the priority, in decreasing order const defaultConfigFiles = new Set( - ["webpack.config", ".webpack/webpack.config", ".webpack/webpackfile"].flatMap((filename) => + DEFAULT_CONFIGURATION_FILES.flatMap((filename) => [...extensions].map((ext) => path.resolve(filename + ext)), ), ); From 806ac94e75e592d3c138d73beb98cb546da8b6c4 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Mon, 16 Feb 2026 21:59:56 +0300 Subject: [PATCH 04/12] refactor: fixes --- packages/webpack-cli/src/webpack-cli.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index b226bb2887d..a5c10208b6c 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1807,14 +1807,14 @@ class WebpackCLI implements IWebpackCLI { await this.program.parseAsync(args, parseOptions); } - async #loadConfigurationFile(module: ModuleName, disableInterpret = false): Promise { + async #loadConfigurationFile(configPath: string, disableInterpret = false): Promise { let pkg; let loadingError; try { // eslint-disable-next-line no-eval - pkg = await eval(`import("${module}")`); + pkg = await eval(`import("${configPath}")`); } catch (err) { if (this.isValidationError(err)) { throw err; @@ -1842,10 +1842,10 @@ class WebpackCLI implements IWebpackCLI { const rechoir: Rechoir = (await import("rechoir")).default; try { - rechoir.prepare(extensions, module); + rechoir.prepare(extensions, configPath); } catch (error) { if ((error as RechoirError)?.failures) { - this.logger.error(`Unable load '${module}'`); + this.logger.error(`Unable load '${configPath}'`); this.logger.error((error as RechoirError).message); for (const failure of (error as RechoirError).failures) { this.logger.error(failure.error.message); @@ -1859,7 +1859,7 @@ class WebpackCLI implements IWebpackCLI { } try { - pkg = require(module); + pkg = require(configPath); } catch (err) { if (this.isValidationError(err)) { throw err; From a19c96772b4d035073833daad364e284db90b2b1 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Mon, 16 Feb 2026 22:06:33 +0300 Subject: [PATCH 05/12] refactor: fixes --- packages/webpack-cli/src/webpack-cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index a5c10208b6c..7bf143c0874 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1830,7 +1830,7 @@ class WebpackCLI implements IWebpackCLI { // Fallback logic when we can't use `import(...)` if (loadingError) { const { jsVariants, extensions } = await import("interpret"); - const ext = path.extname(module).toLowerCase(); + const ext = path.extname(configPath).toLowerCase(); let interpreted = Object.keys(jsVariants).find((variant) => variant === ext); From e5a87b88e7a4fdf213b7af1da04b3498c7138592 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Mon, 16 Feb 2026 23:01:42 +0300 Subject: [PATCH 06/12] refactor: fix logic --- packages/webpack-cli/src/webpack-cli.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 7bf143c0874..4667716dcb2 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1306,7 +1306,7 @@ class WebpackCLI implements IWebpackCLI { }; // Register own exit - this.program.exitOverride(async (error) => { + this.program.exitOverride((error) => { if (error.exitCode === 0) { process.exit(0); } @@ -1344,7 +1344,7 @@ class WebpackCLI implements IWebpackCLI { process.exit(2); } - const { distance } = await import("fastest-levenshtein"); + const { distance } = require("fastest-levenshtein"); for (const option of (command as WebpackCLICommand).options) { if (!option.hidden && distance(name, option.long?.slice(2) as string) < 3) { From c4390a7021197480d6d49c17a0e28bbf64b06088 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Mon, 16 Feb 2026 23:53:27 +0300 Subject: [PATCH 07/12] test: refactor --- packages/webpack-cli/src/webpack-cli.ts | 34 +++++++++---------- .../typescript.test.mjs | 5 --- .../typescript.test.mjs | 2 -- .../typescript.test.mjs | 2 -- .../typescript.test.mjs | 2 -- .../config-error.test.js.snap.webpack5 | 10 ++++++ .../error-commonjs/config-error.test.js | 4 +-- test/build/config/named-export/mjs.test.js | 14 +++----- .../with-config-path.test.js.snap.webpack5 | 9 +++-- 9 files changed, 37 insertions(+), 45 deletions(-) create mode 100644 test/build/config/error-commonjs/__snapshots__/config-error.test.js.snap.webpack5 diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 4667716dcb2..342c7870f2b 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -84,6 +84,8 @@ interface Information { npmPackages?: string | string[]; } +type LoadConfigOption = PotentialPromise; + class ConfigurationLoadingError extends Error { name = "ConfigurationLoadingError"; @@ -1807,20 +1809,20 @@ class WebpackCLI implements IWebpackCLI { await this.program.parseAsync(args, parseOptions); } - async #loadConfigurationFile(configPath: string, disableInterpret = false): Promise { - let pkg; + async #loadConfigurationFile( + configPath: string, + disableInterpret = false, + ): Promise { + let pkg: LoadConfigOption | LoadConfigOption[] | undefined; let loadingError; try { // eslint-disable-next-line no-eval - pkg = await eval(`import("${configPath}")`); + pkg = (await eval(`import("${configPath}")`)).default; + return pkg; } catch (err) { - if (this.isValidationError(err)) { - throw err; - } - - if (process.env?.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) { + if (this.isValidationError(err) || process.env?.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) { throw err; } @@ -1867,10 +1869,11 @@ class WebpackCLI implements IWebpackCLI { throw new ConfigurationLoadingError([loadingError, err]); } - } - if (pkg && typeof pkg === "object" && "default" in pkg) { - pkg = pkg.default || {}; + // To handle `babel`/`module.exports.default = {};` + if (pkg && typeof pkg === "object" && "default" in pkg) { + pkg = pkg.default || {}; + } } return pkg || {}; @@ -1884,15 +1887,10 @@ class WebpackCLI implements IWebpackCLI { configPath: string, argv: Argv = {}, ): Promise<{ options: Configuration | Configuration[]; path: string }> => { - let options: LoadableWebpackConfiguration | LoadableWebpackConfiguration[]; - - type LoadConfigOption = PotentialPromise; + let options: LoadableWebpackConfiguration | LoadableWebpackConfiguration[] | undefined; try { - options = await this.#loadConfigurationFile( - configPath, - disableInterpret, - ); + options = await this.#loadConfigurationFile(configPath, disableInterpret); } catch (error) { if (error instanceof ConfigurationLoadingError) { this.logger.error(`Failed to load '${configPath}' config\n${error.message}`); diff --git a/test/build/config-format/typescript-cjs-using-nodejs/typescript.test.mjs b/test/build/config-format/typescript-cjs-using-nodejs/typescript.test.mjs index f521bc851f3..6cf4dd1c201 100644 --- a/test/build/config-format/typescript-cjs-using-nodejs/typescript.test.mjs +++ b/test/build/config-format/typescript-cjs-using-nodejs/typescript.test.mjs @@ -13,11 +13,6 @@ describe("webpack cli", () => { __dirname, ["-c", "./webpack.config.ts", "--disable-interpret"], { - env: { - NODE_NO_WARNINGS: 1, - // Due nyc logic - WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG: true, - }, // Fallback to `ts-node/esm` for old Node.js versions nodeOptions: major >= 24 ? [] : ["--require=ts-node/register"], }, diff --git a/test/build/config-format/typescript-mjs-using-nodejs/typescript.test.mjs b/test/build/config-format/typescript-mjs-using-nodejs/typescript.test.mjs index 62d0fdc5a58..33638220420 100644 --- a/test/build/config-format/typescript-mjs-using-nodejs/typescript.test.mjs +++ b/test/build/config-format/typescript-mjs-using-nodejs/typescript.test.mjs @@ -15,8 +15,6 @@ describe("webpack cli", () => { { env: { NODE_NO_WARNINGS: 1, - // Due nyc logic - WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG: true, }, // Fallback to `ts-node/esm` for old Node.js versions nodeOptions: major >= 24 ? [] : ["--experimental-loader=ts-node/esm"], diff --git a/test/build/config-format/typescript-ts-node-loader/typescript.test.mjs b/test/build/config-format/typescript-ts-node-loader/typescript.test.mjs index 89462548c7e..8653ad72ae8 100644 --- a/test/build/config-format/typescript-ts-node-loader/typescript.test.mjs +++ b/test/build/config-format/typescript-ts-node-loader/typescript.test.mjs @@ -12,8 +12,6 @@ describe("webpack cli", () => { const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.ts"], { env: { NODE_NO_WARNINGS: 1, - // Due nyc logic - WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG: true, }, nodeOptions: major >= 22 && minor >= 6 diff --git a/test/build/config-format/typescript-using-nodejs/typescript.test.mjs b/test/build/config-format/typescript-using-nodejs/typescript.test.mjs index 62d0fdc5a58..33638220420 100644 --- a/test/build/config-format/typescript-using-nodejs/typescript.test.mjs +++ b/test/build/config-format/typescript-using-nodejs/typescript.test.mjs @@ -15,8 +15,6 @@ describe("webpack cli", () => { { env: { NODE_NO_WARNINGS: 1, - // Due nyc logic - WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG: true, }, // Fallback to `ts-node/esm` for old Node.js versions nodeOptions: major >= 24 ? [] : ["--experimental-loader=ts-node/esm"], diff --git a/test/build/config/error-commonjs/__snapshots__/config-error.test.js.snap.webpack5 b/test/build/config/error-commonjs/__snapshots__/config-error.test.js.snap.webpack5 new file mode 100644 index 00000000000..cfa64d5bab4 --- /dev/null +++ b/test/build/config/error-commonjs/__snapshots__/config-error.test.js.snap.webpack5 @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`config with errors should throw syntax error and exit with non-zero exit code 1`] = ` +"[webpack-cli] Failed to load '/test/build/config/error-commonjs/syntax-error.js' config +▶ ESM (\`import\`) failed: + Unexpected token ';' + +▶ CJS (\`require\`) failed: + Unexpected token ';'" +`; diff --git a/test/build/config/error-commonjs/config-error.test.js b/test/build/config/error-commonjs/config-error.test.js index a4d4df2f420..f207df15881 100644 --- a/test/build/config/error-commonjs/config-error.test.js +++ b/test/build/config/error-commonjs/config-error.test.js @@ -1,7 +1,7 @@ "use strict"; const { resolve } = require("node:path"); -const { run } = require("../../../utils/test-utils"); +const { normalizeStderr, run } = require("../../../utils/test-utils"); describe("config with errors", () => { it("should throw error with invalid configuration", async () => { @@ -23,7 +23,7 @@ describe("config with errors", () => { ]); expect(exitCode).toBe(2); - expect(stderr).toContain("SyntaxError: Unexpected token"); + expect(normalizeStderr(stderr)).toMatchSnapshot(); expect(stdout).toBeFalsy(); }); }); diff --git a/test/build/config/named-export/mjs.test.js b/test/build/config/named-export/mjs.test.js index ab4336a35fc..5f854bccb3d 100644 --- a/test/build/config/named-export/mjs.test.js +++ b/test/build/config/named-export/mjs.test.js @@ -2,15 +2,11 @@ const { run } = require("../../../utils/test-utils"); describe("webpack cli", () => { it("should support mjs config format", async () => { - const { exitCode, stderr } = await run(__dirname, ["-c", "webpack.config.mjs"], { - env: { WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG: true }, - }); + const { exitCode, stderr } = await run(__dirname, ["-c", "webpack.config.mjs"]); - if (/Error: Not supported/.test(stderr)) { - expect(exitCode).toBe(2); - } else { - expect(exitCode).toBe(2); - expect(stderr).toMatch(/Unable to find default export./); - } + console.log(stderr); + + expect(exitCode).toBe(2); + expect(stderr).toMatch(/Unable to find default export./); }); }); diff --git a/test/configtest/with-config-path/__snapshots__/with-config-path.test.js.snap.webpack5 b/test/configtest/with-config-path/__snapshots__/with-config-path.test.js.snap.webpack5 index dec64f56516..8f64293cc3c 100644 --- a/test/configtest/with-config-path/__snapshots__/with-config-path.test.js.snap.webpack5 +++ b/test/configtest/with-config-path/__snapshots__/with-config-path.test.js.snap.webpack5 @@ -6,12 +6,11 @@ exports[`'configtest' command with the configuration path option should throw er exports[`'configtest' command with the configuration path option should throw syntax error: stderr 1`] = ` "[webpack-cli] Failed to load '/test/configtest/with-config-path/syntax-error.config.js' config -[webpack-cli] /test/configtest/with-config-path/syntax-error.config.js:5 - target: 'node'; - ^ +▶ ESM (\`import\`) failed: + Unexpected token ';' -SyntaxError: - at stack" +▶ CJS (\`require\`) failed: + Unexpected token ';'" `; exports[`'configtest' command with the configuration path option should throw syntax error: stdout 1`] = `""`; From 5741a97607d302eecbbc6b1b7eb0c2c82ae27dcd Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Tue, 17 Feb 2026 00:12:51 +0300 Subject: [PATCH 08/12] test: fix --- packages/webpack-cli/src/webpack-cli.ts | 1 - .../config-array-error.test.js.snap.webpack5 | 10 ++++++++++ .../config/error-array/config-array-error.test.js | 5 +++-- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 test/build/config/error-array/__snapshots__/config-array-error.test.js.snap.webpack5 diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 342c7870f2b..5e57093713f 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1820,7 +1820,6 @@ class WebpackCLI implements IWebpackCLI { try { // eslint-disable-next-line no-eval pkg = (await eval(`import("${configPath}")`)).default; - return pkg; } catch (err) { if (this.isValidationError(err) || process.env?.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) { throw err; diff --git a/test/build/config/error-array/__snapshots__/config-array-error.test.js.snap.webpack5 b/test/build/config/error-array/__snapshots__/config-array-error.test.js.snap.webpack5 new file mode 100644 index 00000000000..837aa696909 --- /dev/null +++ b/test/build/config/error-array/__snapshots__/config-array-error.test.js.snap.webpack5 @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`config with invalid array syntax should throw syntax error and exit with non-zero exit code when even 1 object has syntax error 1`] = ` +"[webpack-cli] Failed to load '/test/build/config/error-array/webpack.config.js' config +▶ ESM (\`import\`) failed: + Unexpected token ';' + +▶ CJS (\`require\`) failed: + Unexpected token ';'" +`; diff --git a/test/build/config/error-array/config-array-error.test.js b/test/build/config/error-array/config-array-error.test.js index c609231aeac..286b76ee0db 100644 --- a/test/build/config/error-array/config-array-error.test.js +++ b/test/build/config/error-array/config-array-error.test.js @@ -1,12 +1,13 @@ "use strict"; -const { run } = require("../../../utils/test-utils"); +const { normalizeStderr, run } = require("../../../utils/test-utils"); describe("config with invalid array syntax", () => { it("should throw syntax error and exit with non-zero exit code when even 1 object has syntax error", async () => { const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "./webpack.config.js"]); + expect(exitCode).toBe(2); - expect(stderr).toContain("SyntaxError: Unexpected token"); + expect(normalizeStderr(stderr)).toMatchSnapshot(); expect(stdout).toBeFalsy(); }); }); From 6d45f6d80b5ea5af823f11a5c50e2400af0a4b46 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Tue, 17 Feb 2026 00:57:46 +0300 Subject: [PATCH 09/12] test: fix --- packages/webpack-cli/src/webpack-cli.ts | 19 ++++++++++--------- test/build/config/named-export/mjs.test.js | 9 ++++----- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 5e57093713f..dbf330cae69 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1868,11 +1868,17 @@ class WebpackCLI implements IWebpackCLI { throw new ConfigurationLoadingError([loadingError, err]); } + } - // To handle `babel`/`module.exports.default = {};` - if (pkg && typeof pkg === "object" && "default" in pkg) { - pkg = pkg.default || {}; - } + // To handle `babel`/`module.exports.default = {};` + if (pkg && typeof pkg === "object" && "default" in pkg) { + pkg = pkg.default || {}; + } + + if (!pkg) { + this.logger.warn( + `Default export is missing or nullish at (from ${configPath}). Webpack will run with an empty configuration. Please double-check that this is what you want. If you want to run webpack with an empty config, \`export {}\`/\`module.exports = {};\` to remove this warning.`, + ); } return pkg || {}; @@ -1901,11 +1907,6 @@ class WebpackCLI implements IWebpackCLI { process.exit(2); } - if (!options) { - this.logger.error(`Failed to load '${configPath}' config. Unable to find default export.`); - process.exit(2); - } - if (Array.isArray(options)) { // reassign the value to assert type const optionsArray: LoadableWebpackConfiguration[] = options; diff --git a/test/build/config/named-export/mjs.test.js b/test/build/config/named-export/mjs.test.js index 5f854bccb3d..a2032aacc78 100644 --- a/test/build/config/named-export/mjs.test.js +++ b/test/build/config/named-export/mjs.test.js @@ -2,11 +2,10 @@ const { run } = require("../../../utils/test-utils"); describe("webpack cli", () => { it("should support mjs config format", async () => { - const { exitCode, stderr } = await run(__dirname, ["-c", "webpack.config.mjs"]); + const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "webpack.config.mjs"]); - console.log(stderr); - - expect(exitCode).toBe(2); - expect(stderr).toMatch(/Unable to find default export./); + expect(exitCode).toBe(1); + expect(stderr).toMatch(/Default export is missing or nullish at/); + expect(stdout).toBeTruthy(); }); }); From 146cfa8692b6ed981a3b0d73a4d1c271e5b6f71b Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Tue, 17 Feb 2026 01:17:52 +0300 Subject: [PATCH 10/12] test: refactor --- .../custom-name/custom-name.test.js | 22 ++++-------- .../mjs-config/default-mjs-config.test.js | 27 ++++++--------- .../config/error-mjs/config-error.test.js | 34 +++++++++---------- test/build/config/named-export/mjs.test.js | 1 + .../undefined-default.test.js | 2 +- test/build/config/undefined/undefined.test.js | 2 +- 6 files changed, 37 insertions(+), 51 deletions(-) diff --git a/test/build/config-lookup/custom-name/custom-name.test.js b/test/build/config-lookup/custom-name/custom-name.test.js index 7f83bed5f1d..617d16fc9ae 100644 --- a/test/build/config-lookup/custom-name/custom-name.test.js +++ b/test/build/config-lookup/custom-name/custom-name.test.js @@ -16,21 +16,13 @@ describe("custom config file", () => { }); it("should work with esm format", async () => { - const { exitCode, stderr, stdout } = await run( - __dirname, - ["--config", resolve(__dirname, "config.webpack.mjs")], - { - env: { WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG: true }, - }, - ); + const { exitCode, stderr, stdout } = await run(__dirname, [ + "--config", + resolve(__dirname, "config.webpack.mjs"), + ]); - if (/Error: Not supported/.test(stderr)) { - expect(exitCode).toBe(2); - expect(stdout).toBeFalsy(); - } else { - expect(exitCode).toBe(0); - expect(stderr).toBeFalsy(); - expect(stdout).toBeTruthy(); - } + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toBeTruthy(); }); }); diff --git a/test/build/config/defaults/mjs-config/default-mjs-config.test.js b/test/build/config/defaults/mjs-config/default-mjs-config.test.js index b1842c87628..7bd86af72dd 100644 --- a/test/build/config/defaults/mjs-config/default-mjs-config.test.js +++ b/test/build/config/defaults/mjs-config/default-mjs-config.test.js @@ -4,23 +4,16 @@ const { run } = require("../../../../utils/test-utils"); describe("default config with mjs extension", () => { it("should build and not throw error with mjs config by default", async () => { - const { exitCode, stderr, stdout } = await run(__dirname, [], { - env: { WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG: true }, - }); + const { exitCode, stderr, stdout } = await run(__dirname, []); - if (/Error: Not supported/.test(stderr)) { - expect(exitCode).toBe(2); - expect(stdout).toBeFalsy(); - } else { - expect(exitCode).toBe(0); - expect(stderr).toBeFalsy(); - // default entry should be used - expect(stdout).toContain("./src/index.js"); - // should pick up the output path from config - expect(stdout).toContain("test-output"); - expect(stdout).toContain("compiled successfully"); - // check that the output file exists - expect(fs.existsSync(path.join(__dirname, "/dist/test-output.js"))).toBeTruthy(); - } + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + // default entry should be used + expect(stdout).toContain("./src/index.js"); + // should pick up the output path from config + expect(stdout).toContain("test-output"); + expect(stdout).toContain("compiled successfully"); + // check that the output file exists + expect(fs.existsSync(path.join(__dirname, "/dist/test-output.js"))).toBeTruthy(); }); }); diff --git a/test/build/config/error-mjs/config-error.test.js b/test/build/config/error-mjs/config-error.test.js index c2ae7507281..3f98b4047bf 100644 --- a/test/build/config/error-mjs/config-error.test.js +++ b/test/build/config/error-mjs/config-error.test.js @@ -5,25 +5,29 @@ const { run } = require("../../../utils/test-utils"); describe("config error", () => { it("should throw error with invalid configuration", async () => { - const { exitCode, stderr, stdout } = await run( - __dirname, - ["-c", resolve(__dirname, "webpack.config.mjs")], - { - env: { WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG: true }, - }, - ); + const { exitCode, stderr, stdout } = await run(__dirname, [ + "-c", + resolve(__dirname, "webpack.config.mjs"), + ]); expect(exitCode).toBe(2); + expect(stderr).toContain("Invalid configuration object"); + expect(stderr).toContain('"development" | "production" | "none"'); + expect(stdout).toBeFalsy(); + }); - if (!/Error: Not supported/.test(stderr)) { - expect(stderr).toContain("Invalid configuration object"); - expect(stderr).toContain('"development" | "production" | "none"'); - } + it("should throw syntax error and exit with non-zero exit code", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "-c", + resolve(__dirname, "syntax-error.mjs"), + ]); + expect(exitCode).toBe(2); + expect(stderr).toContain("Unexpected token"); expect(stdout).toBeFalsy(); }); - it("should throw syntax error and exit with non-zero exit code", async () => { + it("should throw syntax error and exit with non-zero exit code (force ESM loading)", async () => { const { exitCode, stderr, stdout } = await run( __dirname, ["-c", resolve(__dirname, "syntax-error.mjs")], @@ -33,11 +37,7 @@ describe("config error", () => { ); expect(exitCode).toBe(2); - - if (!/Error: Not supported/.test(stderr)) { - expect(stderr).toContain("SyntaxError: Unexpected token"); - } - + expect(stderr).toContain("Unexpected token"); expect(stdout).toBeFalsy(); }); }); diff --git a/test/build/config/named-export/mjs.test.js b/test/build/config/named-export/mjs.test.js index a2032aacc78..d9e5578d5e0 100644 --- a/test/build/config/named-export/mjs.test.js +++ b/test/build/config/named-export/mjs.test.js @@ -4,6 +4,7 @@ describe("webpack cli", () => { it("should support mjs config format", async () => { const { exitCode, stderr, stdout } = await run(__dirname, ["-c", "webpack.config.mjs"]); + // Exit `1` because entry was not found expect(exitCode).toBe(1); expect(stderr).toMatch(/Default export is missing or nullish at/); expect(stdout).toBeTruthy(); diff --git a/test/build/config/undefined-default/undefined-default.test.js b/test/build/config/undefined-default/undefined-default.test.js index 53330b739a5..c398abbcf08 100644 --- a/test/build/config/undefined-default/undefined-default.test.js +++ b/test/build/config/undefined-default/undefined-default.test.js @@ -11,7 +11,7 @@ describe("config flag with undefined default export config file", () => { ]); expect(exitCode).toBe(0); - expect(stderr).toBeFalsy(); + expect(stderr).toMatch(/Default export is missing or nullish at/); expect(stdout).toBeTruthy(); }); }); diff --git a/test/build/config/undefined/undefined.test.js b/test/build/config/undefined/undefined.test.js index aa9f43244cf..98274a96473 100644 --- a/test/build/config/undefined/undefined.test.js +++ b/test/build/config/undefined/undefined.test.js @@ -11,7 +11,7 @@ describe("config flag with undefined export config file", () => { ]); expect(exitCode).toBe(0); - expect(stderr).toBeFalsy(); + expect(stderr).toMatch(/Default export is missing or nullish at/); expect(stdout).toBeTruthy(); }); }); From 64f907c2a73513e3288c1807cfe91ce36786f943 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Tue, 17 Feb 2026 01:36:15 +0300 Subject: [PATCH 11/12] refactor: code --- packages/webpack-cli/src/webpack-cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index dbf330cae69..47f93295735 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1872,7 +1872,7 @@ class WebpackCLI implements IWebpackCLI { // To handle `babel`/`module.exports.default = {};` if (pkg && typeof pkg === "object" && "default" in pkg) { - pkg = pkg.default || {}; + pkg = pkg.default as LoadConfigOption | LoadConfigOption[] | undefined; } if (!pkg) { From 0eab614dd3751a995d4ce0d7bb86bfcd5b3c14cf Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Tue, 17 Feb 2026 01:48:07 +0300 Subject: [PATCH 12/12] refactor: code --- packages/webpack-cli/src/webpack-cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 47f93295735..ac7431f79ed 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1819,7 +1819,7 @@ class WebpackCLI implements IWebpackCLI { try { // eslint-disable-next-line no-eval - pkg = (await eval(`import("${configPath}")`)).default; + pkg = (await eval(`import("${pathToFileURL(configPath)}")`)).default; } catch (err) { if (this.isValidationError(err) || process.env?.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) { throw err;