From 87f596f8f3a4e3564fe3368bc81b2130db57b50c Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 5 Mar 2026 21:00:38 +0300 Subject: [PATCH 01/22] refactor: make `needWatchStdin` private --- packages/webpack-cli/src/webpack-cli.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index cf0f172159b..48e59558761 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1638,7 +1638,7 @@ class WebpackCLI { const servers: InstanceType[] = []; - if (this.needWatchStdin(compiler)) { + if (this.#needWatchStdin(compiler)) { process.stdin.on("end", () => { Promise.all(servers.map((server) => server.stop())).then(() => { process.exit(0); @@ -2760,7 +2760,7 @@ class WebpackCLI { return compiler; } - needWatchStdin(compiler: Compiler | MultiCompiler): boolean { + #needWatchStdin(compiler: Compiler | MultiCompiler): boolean { if (this.isMultipleCompiler(compiler)) { return Boolean( compiler.compilers.some((compiler: Compiler) => compiler.options.watchOptions?.stdin), @@ -2894,7 +2894,7 @@ class WebpackCLI { process.on(signal, listener); } - if (this.needWatchStdin(compiler)) { + if (this.#needWatchStdin(compiler)) { process.stdin.on("end", () => { process.exit(0); }); From 2e09a08fa31f83c699f2e058a5e5f3e809807f6a Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 5 Mar 2026 21:21:08 +0300 Subject: [PATCH 02/22] refactor: preload webpack for options --- packages/webpack-cli/src/webpack-cli.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 48e59558761..462733e5ed3 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -838,11 +838,13 @@ class WebpackCLI { } } - getBuiltInOptions(): CommandOption[] { + async getBuiltInOptions(): Promise { if (this.#builtInOptionsCache) { return this.#builtInOptionsCache; } + this.webpack = await this.loadWebpack(); + const builtInFlags: CommandOption[] = [ // For configs { @@ -1379,7 +1381,9 @@ class WebpackCLI { this.logger.raw(`${bold("Default value:")} ${JSON.stringify(option.defaultValue)}`); } - const flag = this.getBuiltInOptions().find((flag) => option.long === `--${flag.name}`); + const flag = (await this.getBuiltInOptions()).find( + (flag) => option.long === `--${flag.name}`, + ); if (flag?.configs) { const possibleValues = flag.configs.reduce((accumulator, currentValue) => { @@ -1531,11 +1535,7 @@ class WebpackCLI { if (isBuildCommandUsed || isWatchCommandUsed) { await this.makeCommand( isBuildCommandUsed ? WebpackCLI.#commands.build : WebpackCLI.#commands.watch, - async () => { - this.webpack = await this.loadWebpack(); - - return this.getBuiltInOptions(); - }, + async () => this.getBuiltInOptions(), async (entries: string[], options: CommanderArgs) => { if (entries.length > 0) { options.entry = [...entries, ...(options.entry || [])]; @@ -1564,8 +1564,6 @@ class WebpackCLI { await this.makeCommand( WebpackCLI.#commands.serve, async () => { - this.webpack = await this.loadWebpack(); - let devServerOptions = []; try { @@ -1577,12 +1575,12 @@ class WebpackCLI { process.exit(2); } - const webpackOptions = this.getBuiltInOptions(); + const webpackOptions = await this.getBuiltInOptions(); return [...webpackOptions, ...devServerOptions]; }, async (entries: string[], options: CommanderArgs) => { - const builtInOptions = this.getBuiltInOptions(); + const builtInOptions = await this.getBuiltInOptions(); let devServerFlags: CommandOption[] = []; try { @@ -2537,11 +2535,11 @@ class WebpackCLI { const { default: CLIPlugin } = (await import("./plugins/cli-plugin.js")).default; + const builtInOptions = await this.getBuiltInOptions(); const internalBuildConfig = (configuration: Configuration) => { const originalWatchValue = configuration.watch; // Apply options - const builtInOptions = this.getBuiltInOptions(); const args: Record = {}; const values: ProcessedArguments = {}; From 858351c1d8bc56cb60aa79ae20922dc99b8466ae Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 5 Mar 2026 21:31:47 +0300 Subject: [PATCH 03/22] refactor: avoid extra unnecessary call --- packages/webpack-cli/src/webpack-cli.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 462733e5ed3..6712c088afa 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -2505,13 +2505,6 @@ class WebpackCLI { config.path.set(config.options, mergedConfigPaths); } - return config; - } - - async buildConfig( - config: ConfigurationsAndPaths, - options: Options, - ): Promise { if (options.analyze && !(await this.isPackageInstalled("webpack-bundle-analyzer"))) { await this.installPackage("webpack-bundle-analyzer", { preMessage: () => { @@ -2729,9 +2722,7 @@ class WebpackCLI { process.env.NODE_ENV = options.configNodeEnv; } - let config = await this.loadConfig(options); - config = await this.buildConfig(config, options); - + const config = await this.loadConfig(options); let compiler: Compiler | MultiCompiler; try { From 579dbf30a1b82f4271422df63c85f0d443693cae Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 5 Mar 2026 21:40:30 +0300 Subject: [PATCH 04/22] refactor: loadConfig --- packages/webpack-cli/src/webpack-cli.ts | 147 +++++++++++------------- 1 file changed, 69 insertions(+), 78 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 6712c088afa..b1449e703c7 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -838,6 +838,12 @@ class WebpackCLI { } } + isValidationError(error: unknown): error is WebpackError { + return ( + error instanceof this.webpack.ValidationError || (error as Error).name === "ValidationError" + ); + } + async getBuiltInOptions(): Promise { if (this.#builtInOptionsCache) { return this.#builtInOptionsCache; @@ -2140,93 +2146,84 @@ class WebpackCLI { await this.program.parseAsync(args, parseOptions); } - async #loadConfigurationFile( - configPath: string, - disableInterpret = false, - ): Promise { - let pkg: LoadableWebpackConfiguration | undefined; - - let loadingError; + async loadConfig(options: Options) { + const disableInterpret = + typeof options.disableInterpret !== "undefined" && options.disableInterpret; - try { - // eslint-disable-next-line no-eval - pkg = (await eval(`import("${pathToFileURL(configPath)}")`)).default; - } catch (err) { - if (this.isValidationError(err) || process.env?.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) { - throw err; - } + const loadConfigByPath = async ( + configPath: string, + argv: Argv = { env: {} }, + ): Promise<{ options: Configuration | MultiConfiguration; path: string }> => { + let options: LoadableWebpackConfiguration | undefined; - loadingError = err; - } + try { + let loadingError; - // Fallback logic when we can't use `import(...)` - if (loadingError) { - const { jsVariants, extensions } = await import("interpret"); - const ext = path.extname(configPath).toLowerCase(); + try { + // eslint-disable-next-line no-eval + options = (await eval(`import("${pathToFileURL(configPath)}")`)).default; + } catch (err) { + if (this.isValidationError(err) || process.env?.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) { + throw err; + } - let interpreted = Object.keys(jsVariants).find((variant) => variant === ext); + loadingError = err; + } - if (!interpreted && ext.endsWith(".cts")) { - interpreted = jsVariants[".ts"] as string; - } + // Fallback logic when we can't use `import(...)` + if (loadingError) { + const { jsVariants, extensions } = await import("interpret"); + const ext = path.extname(configPath).toLowerCase(); - if (interpreted && !disableInterpret) { - const rechoir: Rechoir = (await import("rechoir")).default; + let interpreted = Object.keys(jsVariants).find((variant) => variant === ext); - try { - rechoir.prepare(extensions, configPath); - } catch (error) { - if ((error as RechoirError)?.failures) { - 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); - } - this.logger.error("Please install one of them"); - process.exit(2); + if (!interpreted && ext.endsWith(".cts")) { + interpreted = jsVariants[".ts"] as string; } - this.logger.error(error); - process.exit(2); - } - } - try { - pkg = require(configPath); - } catch (err) { - if (this.isValidationError(err)) { - throw err; - } + if (interpreted && !disableInterpret) { + const rechoir: Rechoir = (await import("rechoir")).default; - throw new ConfigurationLoadingError([loadingError, err]); - } - } - - // To handle `babel`/`module.exports.default = {};` - if (pkg && typeof pkg === "object" && "default" in pkg) { - pkg = pkg.default as LoadableWebpackConfiguration | undefined; - } + try { + rechoir.prepare(extensions, configPath); + } catch (error) { + if ((error as RechoirError)?.failures) { + 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); + } + this.logger.error("Please install one of them"); + process.exit(2); + } + this.logger.error(error); + process.exit(2); + } + } - 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.`, - ); - } + try { + options = require(configPath); + } catch (err) { + if (this.isValidationError(err)) { + throw err; + } - return pkg || {}; - } + throw new ConfigurationLoadingError([loadingError, err]); + } + } - async loadConfig(options: Options) { - const disableInterpret = - typeof options.disableInterpret !== "undefined" && options.disableInterpret; + // To handle `babel`/`module.exports.default = {};` + if (options && typeof options === "object" && "default" in options) { + options = options.default as LoadableWebpackConfiguration | undefined; + } - const loadConfigByPath = async ( - configPath: string, - argv: Argv = { env: {} }, - ): Promise<{ options: Configuration | MultiConfiguration; path: string }> => { - let options: LoadableWebpackConfiguration | undefined; + if (!options) { + 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.`, + ); - try { - options = await this.#loadConfigurationFile(configPath, disableInterpret); + options = {}; + } } catch (error) { if (error instanceof ConfigurationLoadingError) { this.logger.error(`Failed to load '${configPath}' config\n${error.message}`); @@ -2708,12 +2705,6 @@ class WebpackCLI { return config; } - isValidationError(error: unknown): error is WebpackError { - return ( - error instanceof this.webpack.ValidationError || (error as Error).name === "ValidationError" - ); - } - async createCompiler( options: Options, callback?: WebpackCallback, From c659325b5e80e19861a350893652367f837dbde6 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 5 Mar 2026 21:48:32 +0300 Subject: [PATCH 05/22] refactor: order methods and properties by common, webpack related and webpack-cli related --- packages/webpack-cli/src/webpack-cli.ts | 170 ++++++++++++------------ 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index b1449e703c7..78f9b996bf8 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -267,16 +267,6 @@ class WebpackCLI { return { ...createColors({ useColor: shouldUseColor }), isColorSupported: shouldUseColor }; } - isMultipleConfiguration( - config: Configuration | MultiConfiguration, - ): config is MultiConfiguration { - return Array.isArray(config); - } - - isMultipleCompiler(compiler: Compiler | MultiCompiler): compiler is MultiCompiler { - return (compiler as MultiCompiler).compilers as unknown as boolean; - } - isPromise(value: Promise): value is Promise { return typeof (value as unknown as Promise).then === "function"; } @@ -838,6 +828,16 @@ class WebpackCLI { } } + isMultipleConfiguration( + config: Configuration | MultiConfiguration, + ): config is MultiConfiguration { + return Array.isArray(config); + } + + isMultipleCompiler(compiler: Compiler | MultiCompiler): compiler is MultiCompiler { + return (compiler as MultiCompiler).compilers as unknown as boolean; + } + isValidationError(error: unknown): error is WebpackError { return ( error instanceof this.webpack.ValidationError || (error as Error).name === "ValidationError" @@ -1063,64 +1063,6 @@ class WebpackCLI { return options; } - static #commands: Record< - "build" | "watch" | "version" | "help" | "serve" | "info" | "configtest", - CommandOptions - > = { - build: { - rawName: "build", - name: "build [entries...]", - alias: ["bundle", "b"], - description: "Run webpack (default command, can be omitted).", - usage: "[entries...] [options]", - dependencies: [WEBPACK_PACKAGE], - }, - watch: { - rawName: "watch", - name: "watch [entries...]", - alias: "w", - description: "Run webpack and watch for files changes.", - usage: "[entries...] [options]", - dependencies: [WEBPACK_PACKAGE], - }, - serve: { - rawName: "serve", - name: "serve [entries...]", - alias: ["server", "s"], - description: "Run the webpack dev server and watch for source file changes while serving.", - usage: "[entries...] [options]", - dependencies: [WEBPACK_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE], - }, - version: { - rawName: "version", - name: "version", - alias: "v", - usage: "[options]", - description: - "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and other packages.", - }, - info: { - rawName: "info", - name: "info", - alias: "i", - usage: "[options]", - description: "Outputs information about your system.", - }, - help: { - rawName: "help", - name: "help [command] [option]", - alias: "h", - description: "Display help for commands and options.", - }, - configtest: { - rawName: "configtest", - name: "configtest [config-path]", - alias: "t", - description: "Validate a webpack configuration.", - dependencies: [WEBPACK_PACKAGE], - }, - }; - async #outputHelp( options: string[], isVerbose: boolean, @@ -1496,12 +1438,79 @@ class WebpackCLI { return info; } - #findCommandByName(name: string) { - return this.program.commands.find( - (command) => name === command.name() || command.aliases().includes(name), - ); + async #loadPackage(pkg: string, isCustom: boolean): Promise { + const importTarget = + isCustom && /^(?:[A-Za-z]:(\\|\/)|\\\\|\/)/.test(pkg) ? pathToFileURL(pkg).toString() : pkg; + + return (await import(importTarget)).default; + } + + async loadWebpack(): Promise { + return this.#loadPackage(WEBPACK_PACKAGE, WEBPACK_PACKAGE_IS_CUSTOM); } + async loadWebpackDevServer(): Promise { + return this.#loadPackage(WEBPACK_DEV_SERVER_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM); + } + + static #commands: Record< + "build" | "watch" | "version" | "help" | "serve" | "info" | "configtest", + CommandOptions + > = { + build: { + rawName: "build", + name: "build [entries...]", + alias: ["bundle", "b"], + description: "Run webpack (default command, can be omitted).", + usage: "[entries...] [options]", + dependencies: [WEBPACK_PACKAGE], + }, + watch: { + rawName: "watch", + name: "watch [entries...]", + alias: "w", + description: "Run webpack and watch for files changes.", + usage: "[entries...] [options]", + dependencies: [WEBPACK_PACKAGE], + }, + serve: { + rawName: "serve", + name: "serve [entries...]", + alias: ["server", "s"], + description: "Run the webpack dev server and watch for source file changes while serving.", + usage: "[entries...] [options]", + dependencies: [WEBPACK_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE], + }, + version: { + rawName: "version", + name: "version", + alias: "v", + usage: "[options]", + description: + "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and other packages.", + }, + info: { + rawName: "info", + name: "info", + alias: "i", + usage: "[options]", + description: "Outputs information about your system.", + }, + help: { + rawName: "help", + name: "help [command] [option]", + alias: "h", + description: "Display help for commands and options.", + }, + configtest: { + rawName: "configtest", + name: "configtest [config-path]", + alias: "t", + description: "Validate a webpack configuration.", + dependencies: [WEBPACK_PACKAGE], + }, + }; + #isCommand(input: string, commandOptions: CommandOptions) { const longName = commandOptions.rawName; @@ -1519,19 +1528,10 @@ class WebpackCLI { return false; } - async #loadPackage(pkg: string, isCustom: boolean): Promise { - const importTarget = - isCustom && /^(?:[A-Za-z]:(\\|\/)|\\\\|\/)/.test(pkg) ? pathToFileURL(pkg).toString() : pkg; - - return (await import(importTarget)).default; - } - - async loadWebpack(): Promise { - return this.#loadPackage(WEBPACK_PACKAGE, WEBPACK_PACKAGE_IS_CUSTOM); - } - - async loadWebpackDevServer(): Promise { - return this.#loadPackage(WEBPACK_DEV_SERVER_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM); + #findCommandByName(name: string) { + return this.program.commands.find( + (command) => name === command.name() || command.aliases().includes(name), + ); } async #loadCommandByName(commandName: string, allowToInstall = false) { From 18745c107a6b568a043209bf0cba0d7222c53a6d Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 5 Mar 2026 23:07:39 +0300 Subject: [PATCH 06/22] refactor: order --- packages/webpack-cli/src/webpack-cli.ts | 110 ++++++++++++------------ 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 78f9b996bf8..e6e10a02ba8 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -298,61 +298,6 @@ class WebpackCLI { }; } - async isPackageInstalled(packageName: string): Promise { - if (process.versions.pnp) { - return true; - } - - try { - require.resolve(packageName); - return true; - } catch { - // Nothing - } - - // Fallback using fs - let dir = __dirname; - - do { - try { - const stats = await fs.promises.stat(path.join(dir, "node_modules", packageName)); - - if (stats.isDirectory()) { - return true; - } - } catch { - // Nothing - } - } while (dir !== (dir = path.dirname(dir))); - - // Extra fallback using fs and hidden API - // @ts-expect-error No types, private API - const { globalPaths } = await import("node:module"); - - // https://github.com/nodejs/node/blob/v18.9.1/lib/internal/modules/cjs/loader.js#L1274 - const results = await Promise.all( - (globalPaths as string[]).map(async (internalPath) => { - try { - const stats = await fs.promises.stat(path.join(internalPath, packageName)); - - if (stats.isDirectory()) { - return true; - } - } catch { - // Nothing - } - - return false; - }), - ); - - if (results.includes(true)) { - return true; - } - - return false; - } - async getDefaultPackageManager(): Promise { const { sync } = await import("cross-spawn"); @@ -410,6 +355,61 @@ class WebpackCLI { } } + async isPackageInstalled(packageName: string): Promise { + if (process.versions.pnp) { + return true; + } + + try { + require.resolve(packageName); + return true; + } catch { + // Nothing + } + + // Fallback using fs + let dir = __dirname; + + do { + try { + const stats = await fs.promises.stat(path.join(dir, "node_modules", packageName)); + + if (stats.isDirectory()) { + return true; + } + } catch { + // Nothing + } + } while (dir !== (dir = path.dirname(dir))); + + // Extra fallback using fs and hidden API + // @ts-expect-error No types, private API + const { globalPaths } = await import("node:module"); + + // https://github.com/nodejs/node/blob/v18.9.1/lib/internal/modules/cjs/loader.js#L1274 + const results = await Promise.all( + (globalPaths as string[]).map(async (internalPath) => { + try { + const stats = await fs.promises.stat(path.join(internalPath, packageName)); + + if (stats.isDirectory()) { + return true; + } + } catch { + // Nothing + } + + return false; + }), + ); + + if (results.includes(true)) { + return true; + } + + return false; + } + async installPackage( packageName: string, options: { preMessage?: () => void } = {}, From 2346c2c4e28bbcbea7b445f6aee643d68c354760 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Fri, 6 Mar 2026 16:37:15 +0300 Subject: [PATCH 07/22] fix: performance for serve --- packages/webpack-cli/src/webpack-cli.ts | 42 +++++++++---------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index e6e10a02ba8..a8eeb75d64f 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1551,6 +1551,7 @@ class WebpackCLI { }, ); } else if (this.#isCommand(commandName, WebpackCLI.#commands.serve)) { + const webpackOptions = await this.getBuiltInOptions(); const loadDevServerOptions = async () => { const devServer = await this.loadWebpackDevServer(); @@ -1567,40 +1568,27 @@ class WebpackCLI { }); }; - await this.makeCommand( - WebpackCLI.#commands.serve, - async () => { - let devServerOptions = []; + let devServerOptions = []; - try { - devServerOptions = await loadDevServerOptions(); - } catch (error) { - this.logger.error( - `You need to install 'webpack-dev-server' for running 'webpack serve'.\n${error}`, - ); - process.exit(2); - } - - const webpackOptions = await this.getBuiltInOptions(); + try { + devServerOptions = await loadDevServerOptions(); + } catch (error) { + this.logger.error( + `You need to install 'webpack-dev-server' for running 'webpack serve'.\n${error}`, + ); + process.exit(2); + } - return [...webpackOptions, ...devServerOptions]; - }, + await this.makeCommand( + WebpackCLI.#commands.serve, + async () => [...webpackOptions, ...devServerOptions], async (entries: string[], options: CommanderArgs) => { - const builtInOptions = await this.getBuiltInOptions(); - let devServerFlags: CommandOption[] = []; - - try { - devServerFlags = await loadDevServerOptions(); - } catch { - // Nothing, to prevent future updates - } - const webpackCLIOptions: Partial = {}; const devServerCLIOptions: CommanderArgs = {}; for (const optionName in options) { const kebabedOption = this.toKebabCase(optionName); - const isBuiltInOption = builtInOptions.find( + const isBuiltInOption = webpackOptions.find( (builtInOption) => builtInOption.name === kebabedOption, ); @@ -1672,7 +1660,7 @@ class WebpackCLI { if (name === "argv") continue; const kebabName = this.toKebabCase(name); - const arg = devServerFlags.find((item) => item.name === kebabName); + const arg = devServerOptions.find((item) => item.name === kebabName); if (arg) { args[name] = arg as unknown as WebpackArgument; From 3a7e7f959c6141616e6eb865eab2d364387a8bd1 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Fri, 6 Mar 2026 21:00:49 +0300 Subject: [PATCH 08/22] refactor: commands code --- packages/webpack-cli/src/webpack-cli.ts | 780 +++++++++++++----------- 1 file changed, 413 insertions(+), 367 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index a8eeb75d64f..20e8c9ac08f 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -78,12 +78,20 @@ interface Colors extends WebpackColors { isColorSupported: boolean; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Context = Record; + interface Command extends CommanderCommand { pkg?: string; forHelp?: boolean; + context: Context; } -interface CommandOptions extends CommanderCommandOptions { +interface CommandOptions< + A = void, + O extends CommanderArgs = CommanderArgs, + C extends Context = Context, +> extends CommanderCommandOptions { rawName: string; name: string; alias: string | string[]; @@ -92,6 +100,48 @@ interface CommandOptions extends CommanderCommandOptions { dependencies?: string[]; pkg?: string; external?: boolean; + preload?: () => Promise; + options?: + | CommandOption[] + | ((command: Command & { context: C }) => CommandOption[]) + | ((command: Command & { context: C }) => Promise); + action: A extends void + ? (options: O, cmd: Command & { context: C }) => void | Promise + : (args: A, options: O, cmd: Command & { context: C }) => void | Promise; +} + +interface WebpackContext { + webpack: typeof webpack; +} + +interface WebpackOptionsContext { + webpackOptions: CommandOption[]; +} + +interface WebpackDevServerContext { + devServer: typeof import("webpack-dev-server"); +} + +interface WebpackDevServerOptionsContext { + devServerOptions: CommandOption[]; +} + +interface KnownWebpackCLICommands { + build: CommandOptions; + serve: CommandOptions< + string[], + CommanderArgs, + WebpackContext & + WebpackOptionsContext & + WebpackDevServerContext & + WebpackDevServerOptionsContext & + Context + >; + watch: CommandOptions; + version: CommandOptions; + help: CommandOptions; + info: CommandOptions; + configtest: CommandOptions; } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -230,7 +280,7 @@ class WebpackCLI { this.logger = this.getLogger(); // Initialize program - this.program = program; + this.program = program as Command; this.program.name("webpack"); this.program.configureOutput({ writeErr: (str) => { @@ -498,46 +548,44 @@ class WebpackCLI { process.exit(2); } - async makeCommand( - commandOptions: CommandOptions, - options: CommandOption[] | (() => CommandOption[]) | (() => Promise), - action: Parameters[0], + async makeCommand( + options: CommandOptions, ): Promise { const alreadyLoaded = this.program.commands.find( - (command) => command.name() === commandOptions.rawName, + (command) => command.name() === options.rawName, ); if (alreadyLoaded) { return; } - const command = this.program.command(commandOptions.name, { - hidden: commandOptions.hidden, - isDefault: commandOptions.isDefault, - }) as Command; + const command = this.program.command(options.name, { + hidden: options.hidden, + isDefault: options.isDefault, + }) as Command & { context: C }; - if (commandOptions.description) { - command.description(commandOptions.description); + if (options.description) { + command.description(options.description); } - if (commandOptions.usage) { - command.usage(commandOptions.usage); + if (options.usage) { + command.usage(options.usage); } - if (Array.isArray(commandOptions.alias)) { - command.aliases(commandOptions.alias); + if (Array.isArray(options.alias)) { + command.aliases(options.alias); } else { - command.alias(commandOptions.alias); + command.alias(options.alias); } - command.pkg = commandOptions.pkg || "webpack-cli"; + command.pkg = options.pkg || "webpack-cli"; const { forHelp } = this.program; let allDependenciesInstalled = true; - if (commandOptions.dependencies && commandOptions.dependencies.length > 0) { - for (const dependency of commandOptions.dependencies) { + if (options.dependencies && options.dependencies.length > 0) { + for (const dependency of options.dependencies) { if ( // Allow to use `./path/to/webpack.js` outside `node_modules` (dependency === WEBPACK_PACKAGE && WEBPACK_PACKAGE_IS_CUSTOM) || @@ -563,7 +611,7 @@ class WebpackCLI { preMessage: () => { this.logger.error( `For using '${this.colors.green( - commandOptions.rawName, + options.rawName, )}' command you need to install: '${this.colors.green(dependency)}' package.`, ); }, @@ -571,33 +619,43 @@ class WebpackCLI { } } - if (options) { - if (typeof options === "function") { + command.context = {} as C; + + if (typeof options.preload === "function") { + const data = await options.preload(); + + command.context = { ...command.context, ...data }; + } + + if (options.options) { + let commandOptions: CommandOption[] = []; + + if (typeof options.options === "function") { if ( forHelp && !allDependenciesInstalled && - commandOptions.dependencies && - commandOptions.dependencies.length > 0 + options.dependencies && + options.dependencies.length > 0 ) { command.description( `${ - commandOptions.description - } To see all available options you need to install ${commandOptions.dependencies + options.description + } To see all available options you need to install ${options.dependencies .map((dependency) => `'${dependency}'`) .join(", ")}.`, ); - options = []; + commandOptions = []; } else { - options = await options(); + commandOptions = await options.options(command); } } - for (const option of options) { + for (const option of commandOptions) { this.makeOption(command, option); } } - command.action(action); + command.action(options.action); return command; } @@ -1229,12 +1287,12 @@ class WebpackCLI { if (isGlobalHelp) { await Promise.all( - Object.values(WebpackCLI.#commands).map((knownCommand) => + Object.values(this.#commands).map((knownCommand) => this.#loadCommandByName(knownCommand.rawName), ), ); - const buildCommand = this.#findCommandByName(WebpackCLI.#commands.build.rawName); + const buildCommand = this.#findCommandByName(this.#commands.build.rawName); if (buildCommand) { this.logger.raw(buildCommand.helpInformation()); @@ -1247,7 +1305,7 @@ class WebpackCLI { const command = this.#findCommandByName(name); if (!command) { - const builtInCommandUsed = Object.values(WebpackCLI.#commands).find( + const builtInCommandUsed = Object.values(this.#commands).find( (command) => command.name.includes(name) || name === command.alias, ); if (typeof builtInCommandUsed !== "undefined") { @@ -1265,7 +1323,7 @@ class WebpackCLI { } } else if (isHelpCommandSyntax) { let isCommandSpecified = false; - let commandName = WebpackCLI.#commands.build.rawName; + let commandName = this.#commands.build.rawName; let optionName = ""; if (options.length === 1) { @@ -1453,10 +1511,7 @@ class WebpackCLI { return this.#loadPackage(WEBPACK_DEV_SERVER_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM); } - static #commands: Record< - "build" | "watch" | "version" | "help" | "serve" | "info" | "configtest", - CommandOptions - > = { + #commands: KnownWebpackCLICommands = { build: { rawName: "build", name: "build [entries...]", @@ -1464,6 +1519,14 @@ class WebpackCLI { description: "Run webpack (default command, can be omitted).", usage: "[entries...] [options]", dependencies: [WEBPACK_PACKAGE], + options: async () => this.getBuiltInOptions(), + action: async (entries: string[], options: CommanderArgs) => { + if (entries.length > 0) { + options.entry = [...entries, ...(options.entry || [])]; + } + + await this.runWebpack(options, false); + }, }, watch: { rawName: "watch", @@ -1472,6 +1535,14 @@ class WebpackCLI { description: "Run webpack and watch for files changes.", usage: "[entries...] [options]", dependencies: [WEBPACK_PACKAGE], + options: async () => this.getBuiltInOptions(), + action: async (entries: string[], options: CommanderArgs) => { + if (entries.length > 0) { + options.entry = [...entries, ...(options.entry || [])]; + } + + await this.runWebpack(options, true); + }, }, serve: { rawName: "serve", @@ -1480,397 +1551,372 @@ class WebpackCLI { description: "Run the webpack dev server and watch for source file changes while serving.", usage: "[entries...] [options]", dependencies: [WEBPACK_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE], - }, - version: { - rawName: "version", - name: "version", - alias: "v", - usage: "[options]", - description: - "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and other packages.", - }, - info: { - rawName: "info", - name: "info", - alias: "i", - usage: "[options]", - description: "Outputs information about your system.", - }, - help: { - rawName: "help", - name: "help [command] [option]", - alias: "h", - description: "Display help for commands and options.", - }, - configtest: { - rawName: "configtest", - name: "configtest [config-path]", - alias: "t", - description: "Validate a webpack configuration.", - dependencies: [WEBPACK_PACKAGE], - }, - }; - - #isCommand(input: string, commandOptions: CommandOptions) { - const longName = commandOptions.rawName; - - if (input === longName) { - return true; - } - - if (commandOptions.alias) { - if (Array.isArray(commandOptions.alias)) { - return commandOptions.alias.includes(input); - } - return commandOptions.alias === input; - } + preload: async () => { + const webpack = await this.loadWebpack(); + const webpackOptions = await this.getBuiltInOptions(); - return false; - } - - #findCommandByName(name: string) { - return this.program.commands.find( - (command) => name === command.name() || command.aliases().includes(name), - ); - } - - async #loadCommandByName(commandName: string, allowToInstall = false) { - const isBuildCommandUsed = this.#isCommand(commandName, WebpackCLI.#commands.build); - const isWatchCommandUsed = this.#isCommand(commandName, WebpackCLI.#commands.watch); - - if (isBuildCommandUsed || isWatchCommandUsed) { - await this.makeCommand( - isBuildCommandUsed ? WebpackCLI.#commands.build : WebpackCLI.#commands.watch, - async () => this.getBuiltInOptions(), - async (entries: string[], options: CommanderArgs) => { - if (entries.length > 0) { - options.entry = [...entries, ...(options.entry || [])]; - } - - await this.runWebpack(options, isWatchCommandUsed); - }, - ); - } else if (this.#isCommand(commandName, WebpackCLI.#commands.serve)) { - const webpackOptions = await this.getBuiltInOptions(); - const loadDevServerOptions = async () => { const devServer = await this.loadWebpackDevServer(); + const devServerOptions: Record = + // @ts-expect-error different types in dev server + webpack.cli.getArguments(devServer.schema) as unknown as Record; + Object.keys(devServerOptions).map((key) => { + devServerOptions[key].name = key; - // @ts-expect-error different schema types - const options = this.webpack.cli.getArguments(devServer.schema) as unknown as Record< - string, - CommandOption - >; - - return Object.keys(options).map((key) => { - options[key].name = key; - - return options[key]; + return devServerOptions[key]; }); - }; - - let devServerOptions = []; - - try { - devServerOptions = await loadDevServerOptions(); - } catch (error) { - this.logger.error( - `You need to install 'webpack-dev-server' for running 'webpack serve'.\n${error}`, - ); - process.exit(2); - } - - await this.makeCommand( - WebpackCLI.#commands.serve, - async () => [...webpackOptions, ...devServerOptions], - async (entries: string[], options: CommanderArgs) => { - const webpackCLIOptions: Partial = {}; - const devServerCLIOptions: CommanderArgs = {}; - for (const optionName in options) { - const kebabedOption = this.toKebabCase(optionName); - const isBuiltInOption = webpackOptions.find( - (builtInOption) => builtInOption.name === kebabedOption, - ); + return { + webpack, + webpackOptions, + devServer, + devServerOptions: Object.values(devServerOptions), + }; + }, + options: (cmd) => { + const { webpackOptions, devServerOptions } = cmd.context; - if (isBuiltInOption) { - webpackCLIOptions[optionName as keyof Options] = options[optionName]; - } else { - devServerCLIOptions[optionName] = options[optionName]; - } - } + return [...webpackOptions, ...devServerOptions]; + }, + action: async (entries: string[], options: CommanderArgs, cmd) => { + const { webpackOptions, devServerOptions } = cmd.context; + const webpackCLIOptions: Partial = {}; + const devServerCLIOptions: CommanderArgs = {}; + + for (const optionName in options) { + const kebabedOption = this.toKebabCase(optionName); + const isBuiltInOption = webpackOptions.find( + (builtInOption) => builtInOption.name === kebabedOption, + ); - if (entries.length > 0) { - webpackCLIOptions.entry = [...entries, ...(options.entry || [])]; + if (isBuiltInOption) { + webpackCLIOptions[optionName as keyof Options] = options[optionName]; + } else { + devServerCLIOptions[optionName] = options[optionName]; } + } - webpackCLIOptions.argv = { - ...options, - env: { WEBPACK_SERVE: true, ...options.env }, - }; + if (entries.length > 0) { + webpackCLIOptions.entry = [...entries, ...(options.entry || [])]; + } - webpackCLIOptions.isWatchingLikeCommand = true; + webpackCLIOptions.argv = { + ...options, + env: { WEBPACK_SERVE: true, ...options.env }, + }; - const compiler = await this.createCompiler(webpackCLIOptions); + webpackCLIOptions.isWatchingLikeCommand = true; - if (!compiler) { - return; - } + const compiler = await this.createCompiler(webpackCLIOptions); - type DevServerConstructor = typeof import("webpack-dev-server"); - let DevServer: DevServerConstructor; + if (!compiler) { + return; + } - try { - DevServer = await this.loadWebpackDevServer(); - } catch (err) { - this.logger.error( - `You need to install 'webpack-dev-server' for running 'webpack serve'.\n${err}`, - ); - process.exit(2); - } + type DevServerConstructor = typeof import("webpack-dev-server"); - const servers: InstanceType[] = []; + const DevServer: DevServerConstructor = cmd.context.devServer; + const servers: InstanceType[] = []; - if (this.#needWatchStdin(compiler)) { - process.stdin.on("end", () => { - Promise.all(servers.map((server) => server.stop())).then(() => { - process.exit(0); - }); + if (this.#needWatchStdin(compiler)) { + process.stdin.on("end", () => { + Promise.all(servers.map((server) => server.stop())).then(() => { + process.exit(0); }); - process.stdin.resume(); - } + }); + process.stdin.resume(); + } - const compilers = this.isMultipleCompiler(compiler) ? compiler.compilers : [compiler]; - const possibleCompilers = compilers.filter((compiler) => compiler.options.devServer); - const compilersForDevServer = - possibleCompilers.length > 0 ? possibleCompilers : [compilers[0]]; - const usedPorts: number[] = []; + const compilers = this.isMultipleCompiler(compiler) ? compiler.compilers : [compiler]; + const possibleCompilers = compilers.filter((compiler) => compiler.options.devServer); + const compilersForDevServer = + possibleCompilers.length > 0 ? possibleCompilers : [compilers[0]]; + const usedPorts: number[] = []; - for (const compilerForDevServer of compilersForDevServer) { - if (compilerForDevServer.options.devServer === false) { - continue; - } + for (const compilerForDevServer of compilersForDevServer) { + if (compilerForDevServer.options.devServer === false) { + continue; + } - const devServerConfiguration: DevServerConfiguration = - compilerForDevServer.options.devServer || {}; + const devServerConfiguration: DevServerConfiguration = + compilerForDevServer.options.devServer || {}; - const args: Record = {}; - const values: ProcessedArguments = {}; + const args: Record = {}; + const values: ProcessedArguments = {}; - for (const name of Object.keys(options)) { - if (name === "argv") continue; + for (const name of Object.keys(options)) { + if (name === "argv") continue; - const kebabName = this.toKebabCase(name); - const arg = devServerOptions.find((item) => item.name === kebabName); + const kebabName = this.toKebabCase(name); + const arg = devServerOptions.find((item) => item.name === kebabName); - if (arg) { - args[name] = arg as unknown as WebpackArgument; - // We really don't know what the value is - // eslint-disable-next-line @typescript-eslint/no-explicit-any - values[name] = options[name as keyof Options] as any; - } + if (arg) { + args[name] = arg as unknown as WebpackArgument; + // We really don't know what the value is + // eslint-disable-next-line @typescript-eslint/no-explicit-any + values[name] = options[name as keyof Options] as any; } + } - if (Object.keys(values).length > 0) { - const problems = this.webpack.cli.processArguments( - args, - devServerConfiguration, - values, - ); + if (Object.keys(values).length > 0) { + const problems = this.webpack.cli.processArguments( + args, + devServerConfiguration, + values, + ); - if (problems) { - const groupBy = >( - xs: Problem[], - key: K, - ) => - xs.reduce( - (rv, problem) => { - const path = problem[key]; + if (problems) { + const groupBy = >( + xs: Problem[], + key: K, + ) => + xs.reduce( + (rv, problem) => { + const path = problem[key]; - (rv[path] ||= []).push(problem); + (rv[path] ||= []).push(problem); - return rv; - }, - {} as Record, - ); + return rv; + }, + {} as Record, + ); - const problemsByPath = groupBy<"path">(problems, "path"); + const problemsByPath = groupBy<"path">(problems, "path"); - for (const path in problemsByPath) { - const problems = problemsByPath[path]; + for (const path in problemsByPath) { + const problems = problemsByPath[path]; - for (const problem of problems) { - this.logger.error( - `${this.capitalizeFirstLetter(problem.type.replace("-", " "))}${ - problem.value ? ` '${problem.value}'` : "" - } for the '--${problem.argument.replaceAll(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}' option${ - problem.index ? ` by index '${problem.index}'` : "" - }`, - ); + for (const problem of problems) { + this.logger.error( + `${this.capitalizeFirstLetter(problem.type.replace("-", " "))}${ + problem.value ? ` '${problem.value}'` : "" + } for the '--${problem.argument.replaceAll(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}' option${ + problem.index ? ` by index '${problem.index}'` : "" + }`, + ); - if (problem.expected) { - this.logger.error(`Expected: '${problem.expected}'`); - } + if (problem.expected) { + this.logger.error(`Expected: '${problem.expected}'`); } } - - process.exit(2); } - } - if (devServerConfiguration.port) { - const portNumber = Number(devServerConfiguration.port); + process.exit(2); + } + } - if (usedPorts.includes(portNumber)) { - throw new Error( - "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.", - ); - } + if (devServerConfiguration.port) { + const portNumber = Number(devServerConfiguration.port); - usedPorts.push(portNumber); + if (usedPorts.includes(portNumber)) { + throw new Error( + "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.", + ); } - try { - const server = new DevServer(devServerConfiguration, compiler); + usedPorts.push(portNumber); + } - await server.start(); + try { + const server = new DevServer(devServerConfiguration, compiler); - servers.push(server as unknown as InstanceType); - } catch (error) { - if (this.isValidationError(error as Error)) { - this.logger.error((error as Error).message); - } else { - this.logger.error(error); - } + await server.start(); - process.exit(2); + servers.push(server as unknown as InstanceType); + } catch (error) { + if (this.isValidationError(error as Error)) { + this.logger.error((error as Error).message); + } else { + this.logger.error(error); } - } - if (servers.length === 0) { - this.logger.error("No dev server configurations to run"); process.exit(2); } + } + + if (servers.length === 0) { + this.logger.error("No dev server configurations to run"); + process.exit(2); + } + }, + }, + help: { + rawName: "help", + name: "help [command] [option]", + alias: "h", + description: "Display help for commands and options.", + action: () => { + // Nothing, just stub + }, + }, + version: { + rawName: "version", + name: "version", + alias: "v", + usage: "[options]", + description: + "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and other packages.", + options: [ + { + name: "output", + alias: "o", + configs: [ + { + type: "string", + }, + ], + description: "To get the output in a specified format (accept json or markdown)", + hidden: false, }, - ); - } else if (this.#isCommand(commandName, WebpackCLI.#commands.help)) { - await this.makeCommand(WebpackCLI.#commands.help, [], () => { - // Stub for the `help` command - }); - } else if (this.#isCommand(commandName, WebpackCLI.#commands.version)) { - await this.makeCommand( - WebpackCLI.#commands.version, - [ - { - name: "output", - alias: "o", - configs: [ - { - type: "string", - }, - ], - description: "To get the output in a specified format (accept json or markdown)", - hidden: false, + ], + action: async (options: { output?: string }) => { + let info = await this.#getInfoOutput({ + ...options, + information: { + npmPackages: `{${DEFAULT_WEBPACK_PACKAGES.map((item) => `*${item}*`).join(",")}}`, }, - ], - async (options: { output?: string }) => { - let info = await this.#getInfoOutput({ - ...options, - information: { - npmPackages: `{${DEFAULT_WEBPACK_PACKAGES.map((item) => `*${item}*`).join(",")}}`, - }, - }); + }); - if (typeof options.output === "undefined") { - info = info.replace("Packages:", "").replaceAll(/^\s+/gm, "").trim(); - } + if (typeof options.output === "undefined") { + info = info.replace("Packages:", "").replaceAll(/^\s+/gm, "").trim(); + } - this.logger.raw(info); + this.logger.raw(info); + }, + }, + info: { + rawName: "info", + name: "info", + alias: "i", + usage: "[options]", + description: "Outputs information about your system.", + options: [ + { + name: "output", + alias: "o", + configs: [ + { + type: "string", + }, + ], + description: "To get the output in a specified format (accept json or markdown)", + hidden: false, }, - ); - } else if (this.#isCommand(commandName, WebpackCLI.#commands.info)) { - await this.makeCommand( - WebpackCLI.#commands.info, - [ - { - name: "output", - alias: "o", - configs: [ - { - type: "string", - }, - ], - description: "To get the output in a specified format (accept json or markdown)", - hidden: false, - }, - { - name: "additional-package", - alias: "a", - configs: [{ type: "string" }], - multiple: true, - description: "Adds additional packages to the output", - hidden: false, - }, - ], - async (options: { output?: string; additionalPackage?: string[] }) => { - const info = await this.#getInfoOutput(options); - - this.logger.raw(info); + { + name: "additional-package", + alias: "a", + configs: [{ type: "string" }], + multiple: true, + description: "Adds additional packages to the output", + hidden: false, }, - ); - } else if (this.#isCommand(commandName, WebpackCLI.#commands.configtest)) { - await this.makeCommand( - WebpackCLI.#commands.configtest, - [], - async (configPath: string | undefined) => { - this.webpack = await this.loadWebpack(); - - const env: Env = {}; - const argv: Argv = { env }; - const config = await this.loadConfig( - configPath ? { config: [configPath] } : { env, argv }, - ); - const configPaths = new Set(); + ], + action: async (options: { output?: string; additionalPackage?: string[] }) => { + const info = await this.#getInfoOutput(options); - if (Array.isArray(config.options)) { - for (const options of config.options) { - const loadedConfigPaths = config.path.get(options); + this.logger.raw(info); + }, + }, + configtest: { + rawName: "configtest", + name: "configtest [config-path]", + alias: "t", + description: "Validate a webpack configuration.", + dependencies: [WEBPACK_PACKAGE], + options: [], + preload: async () => { + const webpack = await this.loadWebpack(); + return { webpack }; + }, + action: async (configPath: string | undefined, _options: CommanderArgs, cmd) => { + const env: Env = {}; + const argv: Argv = { env }; + const config = await this.loadConfig(configPath ? { config: [configPath] } : { env, argv }); + const configPaths = new Set(); - if (loadedConfigPaths) { - for (const path of loadedConfigPaths) configPaths.add(path); - } - } - } else if (config.path.get(config.options)) { - const loadedConfigPaths = config.path.get(config.options); + if (Array.isArray(config.options)) { + for (const options of config.options) { + const loadedConfigPaths = config.path.get(options); if (loadedConfigPaths) { for (const path of loadedConfigPaths) configPaths.add(path); } } + } else if (config.path.get(config.options)) { + const loadedConfigPaths = config.path.get(config.options); - if (configPaths.size === 0) { - this.logger.error("No configuration found."); - process.exit(2); + if (loadedConfigPaths) { + for (const path of loadedConfigPaths) configPaths.add(path); } + } - this.logger.info(`Validate '${[...configPaths].join(" ,")}'.`); + if (configPaths.size === 0) { + this.logger.error("No configuration found."); + process.exit(2); + } - try { - this.webpack.validate(config.options); - } catch (error) { - if (this.isValidationError(error as Error)) { - this.logger.error((error as Error).message); - } else { - this.logger.error(error); - } + this.logger.info(`Validate '${[...configPaths].join(" ,")}'.`); - process.exit(2); + try { + cmd.context.webpack.validate(config.options); + } catch (error) { + if (this.isValidationError(error as Error)) { + this.logger.error((error as Error).message); + } else { + this.logger.error(error); } - this.logger.success("There are no validation errors in the given webpack configuration."); - }, - ); + process.exit(2); + } + + this.logger.success("There are no validation errors in the given webpack configuration."); + }, + }, + }; + + #isCommand( + input: string, + commandOptions: CommandOptions, + ) { + const longName = commandOptions.rawName; + + if (input === longName) { + return true; + } + + if (commandOptions.alias) { + if (Array.isArray(commandOptions.alias)) { + return commandOptions.alias.includes(input); + } + return commandOptions.alias === input; + } + + return false; + } + + #findCommandByName(name: string) { + return this.program.commands.find( + (command) => name === command.name() || command.aliases().includes(name), + ); + } + + async #loadCommandByName(commandName: string, allowToInstall = false) { + if (this.#isCommand(commandName, this.#commands.build)) { + await this.makeCommand(this.#commands.build); + } else if (this.#isCommand(commandName, this.#commands.serve)) { + await this.makeCommand(this.#commands.serve); + } else if (this.#isCommand(commandName, this.#commands.watch)) { + await this.makeCommand(this.#commands.watch); + } else if (this.#isCommand(commandName, this.#commands.help)) { + // Stub for the `help` command + await this.makeCommand(this.#commands.help); + } else if (this.#isCommand(commandName, this.#commands.version)) { + await this.makeCommand(this.#commands.version); + } else if (this.#isCommand(commandName, this.#commands.info)) { + await this.makeCommand(this.#commands.info); + } else if (this.#isCommand(commandName, this.#commands.configtest)) { + await this.makeCommand(this.#commands.configtest); } else { - const builtInExternalCommandInfo = Object.values(WebpackCLI.#commands) + const builtInExternalCommandInfo = Object.values(this.#commands) .filter((item) => item.external) .find( (externalBuiltInCommandInfo) => @@ -1954,7 +2000,7 @@ class WebpackCLI { const { operands } = this.program.parseOptions(this.program.args); const operand = - typeof operands[0] !== "undefined" ? operands[0] : WebpackCLI.#commands.build.rawName; + typeof operands[0] !== "undefined" ? operands[0] : this.#commands.build.rawName; if (operand) { const command = this.#findCommandByName(operand); @@ -2018,11 +2064,11 @@ class WebpackCLI { this.program.allowExcessArguments(true); this.program.action(async (options) => { const { operands, unknown } = this.program.parseOptions(this.program.args); - const defaultCommandNameToRun = WebpackCLI.#commands.build.rawName; + const defaultCommandNameToRun = this.#commands.build.rawName; const hasOperand = typeof operands[0] !== "undefined"; const operand = hasOperand ? operands[0] : defaultCommandNameToRun; const isHelpOption = typeof options.help !== "undefined"; - const isHelpCommandSyntax = this.#isCommand(operand, WebpackCLI.#commands.help); + const isHelpCommandSyntax = this.#isCommand(operand, this.#commands.help); if (isHelpOption || isHelpCommandSyntax) { let isVerbose = false; @@ -2073,7 +2119,7 @@ class WebpackCLI { let isKnownCommand = false; - for (const command of Object.values(WebpackCLI.#commands)) { + for (const command of Object.values(this.#commands)) { if ( command.rawName === commandNameToRun || (Array.isArray(command.alias) @@ -2105,7 +2151,7 @@ class WebpackCLI { } else { this.logger.error(`Unknown command or entry '${operand}'`); - const found = Object.values(WebpackCLI.#commands).find( + const found = Object.values(this.#commands).find( (commandOptions) => distance(operand, commandOptions.rawName) < 3, ); From 51223ab90174b8bff296e09c92bdb2ccf7092761 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Fri, 6 Mar 2026 21:07:20 +0300 Subject: [PATCH 09/22] refactor: move webpack-cli options to own --- packages/webpack-cli/src/webpack-cli.ts | 336 ++++++++++++------------ 1 file changed, 169 insertions(+), 167 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 20e8c9ac08f..f2876dadc32 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -909,173 +909,7 @@ class WebpackCLI { this.webpack = await this.loadWebpack(); - const builtInFlags: CommandOption[] = [ - // For configs - { - name: "config", - alias: "c", - configs: [ - { - type: "string", - }, - ], - multiple: true, - valueName: "pathToConfigFile", - description: - 'Provide path to one or more webpack configuration files to process, e.g. "./webpack.config.js".', - hidden: false, - }, - { - name: "config-name", - configs: [ - { - type: "string", - }, - ], - multiple: true, - valueName: "name", - description: - "Name(s) of particular configuration(s) to use if configuration file exports an array of multiple configurations.", - hidden: false, - }, - { - name: "merge", - alias: "m", - configs: [ - { - type: "enum", - values: [true], - }, - ], - description: "Merge two or more configurations using 'webpack-merge'.", - hidden: false, - }, - // Complex configs - { - name: "env", - type: ( - value: string, - previous: Record = {}, - ): Record => { - // This ensures we're only splitting by the first `=` - const [allKeys, val] = value.split(/[=](.+)/, 2); - const splitKeys = allKeys.split(/\.(?!$)/); - - let prevRef = previous; - - for (let [index, someKey] of splitKeys.entries()) { - // https://github.com/webpack/webpack-cli/issues/3284 - if (someKey.endsWith("=")) { - // remove '=' from key - someKey = someKey.slice(0, -1); - // @ts-expect-error we explicitly want to set it to undefined - prevRef[someKey] = undefined; - continue; - } - - if (!prevRef[someKey]) { - prevRef[someKey] = {}; - } - - if (typeof prevRef[someKey] === "string") { - prevRef[someKey] = {}; - } - - if (index === splitKeys.length - 1) { - prevRef[someKey] = typeof val === "string" ? val : true; - } - - prevRef = prevRef[someKey] as Record; - } - - return previous; - }, - multiple: true, - description: - 'Environment variables passed to the configuration when it is a function, e.g. "myvar" or "myvar=myval".', - hidden: false, - }, - { - name: "config-node-env", - configs: [ - { - type: "string", - }, - ], - multiple: false, - description: - "Sets process.env.NODE_ENV to the specified value for access within the configuration.", - hidden: false, - }, - - // Adding more plugins - { - name: "analyze", - configs: [ - { - type: "enum", - values: [true], - }, - ], - multiple: false, - description: "It invokes webpack-bundle-analyzer plugin to get bundle information.", - hidden: false, - }, - { - name: "progress", - configs: [ - { - type: "string", - }, - { - type: "enum", - values: [true], - }, - ], - description: "Print compilation progress during build.", - hidden: false, - }, - - // Output options - { - name: "json", - configs: [ - { - type: "string", - }, - { - type: "enum", - values: [true], - }, - ], - alias: "j", - valueName: "pathToJsonFile", - description: "Prints result as JSON or store it in a file.", - hidden: false, - }, - { - name: "fail-on-warnings", - configs: [ - { - type: "enum", - values: [true], - }, - ], - description: "Stop webpack-cli process with non-zero exit code on warnings from webpack.", - hidden: false, - }, - { - name: "disable-interpret", - configs: [ - { - type: "enum", - values: [true], - }, - ], - description: "Disable interpret for loading the config file.", - hidden: false, - }, - ]; + const builtInFlags: CommandOption[] = this.#CLIOptions; // Extract all the flags being exported from core. // A list of cli flags generated by core can be found here https://github.com/webpack/webpack/blob/main/test/__snapshots__/Cli.basictest.js.snap @@ -1511,6 +1345,174 @@ class WebpackCLI { return this.#loadPackage(WEBPACK_DEV_SERVER_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM); } + #CLIOptions: CommandOption[] = [ + // For configs + { + name: "config", + alias: "c", + configs: [ + { + type: "string", + }, + ], + multiple: true, + valueName: "pathToConfigFile", + description: + 'Provide path to one or more webpack configuration files to process, e.g. "./webpack.config.js".', + hidden: false, + }, + { + name: "config-name", + configs: [ + { + type: "string", + }, + ], + multiple: true, + valueName: "name", + description: + "Name(s) of particular configuration(s) to use if configuration file exports an array of multiple configurations.", + hidden: false, + }, + { + name: "merge", + alias: "m", + configs: [ + { + type: "enum", + values: [true], + }, + ], + description: "Merge two or more configurations using 'webpack-merge'.", + hidden: false, + }, + // Complex configs + { + name: "env", + type: ( + value: string, + previous: Record = {}, + ): Record => { + // This ensures we're only splitting by the first `=` + const [allKeys, val] = value.split(/[=](.+)/, 2); + const splitKeys = allKeys.split(/\.(?!$)/); + + let prevRef = previous; + + for (let [index, someKey] of splitKeys.entries()) { + // https://github.com/webpack/webpack-cli/issues/3284 + if (someKey.endsWith("=")) { + // remove '=' from key + someKey = someKey.slice(0, -1); + // @ts-expect-error we explicitly want to set it to undefined + prevRef[someKey] = undefined; + continue; + } + + if (!prevRef[someKey]) { + prevRef[someKey] = {}; + } + + if (typeof prevRef[someKey] === "string") { + prevRef[someKey] = {}; + } + + if (index === splitKeys.length - 1) { + prevRef[someKey] = typeof val === "string" ? val : true; + } + + prevRef = prevRef[someKey] as Record; + } + + return previous; + }, + multiple: true, + description: + 'Environment variables passed to the configuration when it is a function, e.g. "myvar" or "myvar=myval".', + hidden: false, + }, + { + name: "config-node-env", + configs: [ + { + type: "string", + }, + ], + multiple: false, + description: + "Sets process.env.NODE_ENV to the specified value for access within the configuration.", + hidden: false, + }, + + // Adding more plugins + { + name: "analyze", + configs: [ + { + type: "enum", + values: [true], + }, + ], + multiple: false, + description: "It invokes webpack-bundle-analyzer plugin to get bundle information.", + hidden: false, + }, + { + name: "progress", + configs: [ + { + type: "string", + }, + { + type: "enum", + values: [true], + }, + ], + description: "Print compilation progress during build.", + hidden: false, + }, + + // Output options + { + name: "json", + configs: [ + { + type: "string", + }, + { + type: "enum", + values: [true], + }, + ], + alias: "j", + valueName: "pathToJsonFile", + description: "Prints result as JSON or store it in a file.", + hidden: false, + }, + { + name: "fail-on-warnings", + configs: [ + { + type: "enum", + values: [true], + }, + ], + description: "Stop webpack-cli process with non-zero exit code on warnings from webpack.", + hidden: false, + }, + { + name: "disable-interpret", + configs: [ + { + type: "enum", + values: [true], + }, + ], + description: "Disable interpret for loading the config file.", + hidden: false, + }, + ]; + #commands: KnownWebpackCLICommands = { build: { rawName: "build", From 0165073dbf6a1393967e5b43628b45ee16a3d830 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Fri, 6 Mar 2026 22:02:30 +0300 Subject: [PATCH 10/22] refactor: options --- packages/webpack-cli/src/webpack-cli.ts | 152 ++++++++++++------------ 1 file changed, 73 insertions(+), 79 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index f2876dadc32..acbae1b3594 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -181,7 +181,6 @@ interface CommandOption { defaultValue?: string; hidden?: boolean; negativeHidden?: boolean; - group?: "core"; } interface Env { @@ -217,6 +216,7 @@ declare interface WebpackCallback { } type ProcessedArguments = Parameters<(typeof webpack)["cli"]["processArguments"]>[2]; +type Schema = Parameters<(typeof webpack)["cli"]["getArguments"]>[0]; interface KnownOptions { config?: string[]; @@ -235,6 +235,7 @@ interface KnownOptions { configName?: string[]; disableInterpret?: boolean; extends?: string[]; + webpack: typeof webpack; } type Options = @@ -269,10 +270,6 @@ class WebpackCLI { #isColorSupportChanged: boolean | undefined; - #builtInOptionsCache: CommandOption[] | undefined; - - webpack!: typeof webpack; - program: Command; constructor() { @@ -897,45 +894,23 @@ class WebpackCLI { } isValidationError(error: unknown): error is WebpackError { - return ( - error instanceof this.webpack.ValidationError || (error as Error).name === "ValidationError" - ); + return (error as Error).name === "ValidationError"; } - async getBuiltInOptions(): Promise { - if (this.#builtInOptionsCache) { - return this.#builtInOptionsCache; - } - - this.webpack = await this.loadWebpack(); - - const builtInFlags: CommandOption[] = this.#CLIOptions; - - // Extract all the flags being exported from core. - // A list of cli flags generated by core can be found here https://github.com/webpack/webpack/blob/main/test/__snapshots__/Cli.basictest.js.snap - // Fast search, `includes` is slow - const minHelpSet = new Set([ - "mode", - "watch", - "watch-options-stdin", - "stats", - "devtool", - "entry", - "target", - "name", - "output-path", - "extends", - ]); - const minimumNegativeHelpFlags = new Set(["devtool"]); - const coreArgs = this.webpack.cli.getArguments(); + schemaToOptions( + webpackMod: typeof webpack, + schema: Schema = undefined, + additionalOptions: CommandOption[] = [], + ): CommandOption[] { + const coreArgs = webpackMod.cli.getArguments(schema); // Take memory const options: CommandOption[] = Array.from({ - length: builtInFlags.length + Object.keys(coreArgs).length, + length: additionalOptions.length + Object.keys(coreArgs).length, }); let i = 0; // Adding own options - for (; i < builtInFlags.length; i++) options[i] = builtInFlags[i]; + for (; i < additionalOptions.length; i++) options[i] = additionalOptions[i]; // Adding core options for (const name in coreArgs) { @@ -944,14 +919,11 @@ class WebpackCLI { ...meta, name, description: meta.description, - group: "core", - hidden: !minHelpSet.has(name), - negativeHidden: !minimumNegativeHelpFlags.has(name), + hidden: !this.#minimumHelpOptions.has(name), + negativeHidden: !this.#minimumNegativeHelpOptions.has(name), }; } - this.#builtInOptionsCache = options; - return options; } @@ -1221,7 +1193,9 @@ class WebpackCLI { this.logger.raw(`${bold("Default value:")} ${JSON.stringify(option.defaultValue)}`); } - const flag = (await this.getBuiltInOptions()).find( + // TODO maybe bug here + const webpack = await this.loadWebpack(); + const flag = this.schemaToOptions(webpack, undefined, this.#CLIOptions).find( (flag) => option.long === `--${flag.name}`, ); @@ -1345,6 +1319,21 @@ class WebpackCLI { return this.#loadPackage(WEBPACK_DEV_SERVER_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM); } + #minimumHelpOptions = new Set([ + "mode", + "watch", + "watch-options-stdin", + "stats", + "devtool", + "entry", + "target", + "name", + "output-path", + "extends", + ]); + + #minimumNegativeHelpOptions = new Set(["devtool"]); + #CLIOptions: CommandOption[] = [ // For configs { @@ -1521,13 +1510,22 @@ class WebpackCLI { description: "Run webpack (default command, can be omitted).", usage: "[entries...] [options]", dependencies: [WEBPACK_PACKAGE], - options: async () => this.getBuiltInOptions(), - action: async (entries: string[], options: CommanderArgs) => { + preload: async () => { + const webpack = await this.loadWebpack(); + return { webpack }; + }, + options: async (cmd) => + this.schemaToOptions(cmd.context.webpack, undefined, this.#CLIOptions), + action: async (entries, options, cmd) => { + const { webpack } = cmd.context; + if (entries.length > 0) { options.entry = [...entries, ...(options.entry || [])]; } - await this.runWebpack(options, false); + options.webpack = webpack; + + await this.runWebpack(options as Options, false); }, }, watch: { @@ -1537,13 +1535,22 @@ class WebpackCLI { description: "Run webpack and watch for files changes.", usage: "[entries...] [options]", dependencies: [WEBPACK_PACKAGE], - options: async () => this.getBuiltInOptions(), - action: async (entries: string[], options: CommanderArgs) => { + preload: async () => { + const webpack = await this.loadWebpack(); + return { webpack }; + }, + options: async (cmd) => + this.schemaToOptions(cmd.context.webpack, undefined, this.#CLIOptions), + action: async (entries, options, cmd) => { + const { webpack } = cmd.context; + if (entries.length > 0) { options.entry = [...entries, ...(options.entry || [])]; } - await this.runWebpack(options, true); + options.webpack = webpack; + + await this.runWebpack(options as Options, true); }, }, serve: { @@ -1555,24 +1562,12 @@ class WebpackCLI { dependencies: [WEBPACK_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE], preload: async () => { const webpack = await this.loadWebpack(); - const webpackOptions = await this.getBuiltInOptions(); - + const webpackOptions = this.schemaToOptions(webpack, undefined, this.#CLIOptions); const devServer = await this.loadWebpackDevServer(); - const devServerOptions: Record = - // @ts-expect-error different types in dev server - webpack.cli.getArguments(devServer.schema) as unknown as Record; - Object.keys(devServerOptions).map((key) => { - devServerOptions[key].name = key; - - return devServerOptions[key]; - }); + // @ts-expect-error different versions of the `Schema` type + const devServerOptions = this.schemaToOptions(webpack, devServer.schema); - return { - webpack, - webpackOptions, - devServer, - devServerOptions: Object.values(devServerOptions), - }; + return { webpack, webpackOptions, devServer, devServerOptions }; }, options: (cmd) => { const { webpackOptions, devServerOptions } = cmd.context; @@ -1580,8 +1575,8 @@ class WebpackCLI { return [...webpackOptions, ...devServerOptions]; }, action: async (entries: string[], options: CommanderArgs, cmd) => { - const { webpackOptions, devServerOptions } = cmd.context; - const webpackCLIOptions: Partial = {}; + const { webpack, webpackOptions, devServerOptions } = cmd.context; + const webpackCLIOptions: Options = { webpack, isWatchingLikeCommand: true }; const devServerCLIOptions: CommanderArgs = {}; for (const optionName in options) { @@ -1606,8 +1601,6 @@ class WebpackCLI { env: { WEBPACK_SERVE: true, ...options.env }, }; - webpackCLIOptions.isWatchingLikeCommand = true; - const compiler = await this.createCompiler(webpackCLIOptions); if (!compiler) { @@ -1660,11 +1653,7 @@ class WebpackCLI { } if (Object.keys(values).length > 0) { - const problems = this.webpack.cli.processArguments( - args, - devServerConfiguration, - values, - ); + const problems = webpack.cli.processArguments(args, devServerConfiguration, values); if (problems) { const groupBy = >( @@ -1830,9 +1819,12 @@ class WebpackCLI { return { webpack }; }, action: async (configPath: string | undefined, _options: CommanderArgs, cmd) => { + const { webpack } = cmd.context; const env: Env = {}; const argv: Argv = { env }; - const config = await this.loadConfig(configPath ? { config: [configPath] } : { env, argv }); + const config = await this.loadConfig( + configPath ? { env, argv, webpack, config: [configPath] } : { env, argv, webpack }, + ); const configPaths = new Set(); if (Array.isArray(config.options)) { @@ -2561,7 +2553,7 @@ class WebpackCLI { const { default: CLIPlugin } = (await import("./plugins/cli-plugin.js")).default; - const builtInOptions = await this.getBuiltInOptions(); + const builtInOptions = this.schemaToOptions(options.webpack); const internalBuildConfig = (configuration: Configuration) => { const originalWatchValue = configuration.watch; @@ -2573,7 +2565,7 @@ class WebpackCLI { if (name === "argv") continue; const kebabName = this.toKebabCase(name); - const arg = builtInOptions.find((item) => item.group === "core" && item.name === kebabName); + const arg = builtInOptions.find((item) => item.name === kebabName); if (arg) { args[name] = arg as unknown as WebpackArgument; @@ -2584,7 +2576,7 @@ class WebpackCLI { } if (Object.keys(values).length > 0) { - const problems = this.webpack.cli.processArguments(args, configuration, values); + const problems = options.webpack.cli.processArguments(args, configuration, values); if (problems) { const groupBy = >(xs: Problem[], key: K) => @@ -2745,6 +2737,8 @@ class WebpackCLI { options: Options, callback?: WebpackCallback, ): Promise { + const { webpack } = options; + if (typeof options.configNodeEnv === "string") { process.env.NODE_ENV = options.configNodeEnv; } @@ -2754,7 +2748,7 @@ class WebpackCLI { try { compiler = callback - ? this.webpack(config.options, (error, stats) => { + ? webpack(config.options, (error, stats) => { if (error && this.isValidationError(error)) { this.logger.error(error.message); process.exit(2); @@ -2762,7 +2756,7 @@ class WebpackCLI { callback(error as Error | null, stats); })! - : this.webpack(config.options); + : webpack(config.options); } catch (error) { if (this.isValidationError(error)) { this.logger.error(error.message); From 621af898892e187647bf82cb68e3602923f3b895 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Fri, 6 Mar 2026 22:22:49 +0300 Subject: [PATCH 11/22] refactor: options --- packages/webpack-cli/src/webpack-cli.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index acbae1b3594..9fc11e6719f 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -5,7 +5,6 @@ import { pathToFileURL } from "node:url"; import util from "node:util"; import { type stringifyChunked as stringifyChunkedType } from "@discoveryjs/json-ext"; import { - type Argument, type Command as CommanderCommand, type CommandOptions as CommanderCommandOptions, type Help, @@ -981,18 +980,8 @@ class WebpackCLI { }, // Support multiple aliases subcommandTerm: (command) => { - const humanReadableArgumentName = (argument: Argument) => { - const nameOutput = argument.name() + (argument.variadic ? "..." : ""); - - return argument.required ? `<${nameOutput}>` : `[${nameOutput}]`; - }; - const args = command.registeredArguments - .map((arg) => humanReadableArgumentName(arg)) - .join(" "); - - return `${command.name()}|${command.aliases().join("|")}${args ? ` ${args}` : ""}${ - command.options.length > 0 ? " [options]" : "" - }`; + const usage = command.usage(); + return `${command.name()}|${command.aliases().join("|")}${usage.length > 0 ? ` ${usage}` : ""}`; }, visibleOptions: function visibleOptions(command) { return command.options.filter((option) => { From 57f28c8491ad634f9b92f0d7856674d89c73cb6c Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Fri, 6 Mar 2026 22:37:31 +0300 Subject: [PATCH 12/22] refactor: fix logic --- packages/webpack-cli/src/webpack-cli.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 9fc11e6719f..51bb1b62f09 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -624,7 +624,7 @@ class WebpackCLI { } if (options.options) { - let commandOptions: CommandOption[] = []; + let commandOptions: CommandOption[]; if (typeof options.options === "function") { if ( @@ -644,6 +644,8 @@ class WebpackCLI { } else { commandOptions = await options.options(command); } + } else { + commandOptions = options.options; } for (const option of commandOptions) { From ef74e80dd326725918da586d4b879da01ca24cc6 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Fri, 6 Mar 2026 23:08:36 +0300 Subject: [PATCH 13/22] refactor: fix logic --- packages/webpack-cli/src/webpack-cli.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 51bb1b62f09..9d9f0dbbf41 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -902,11 +902,12 @@ class WebpackCLI { webpackMod: typeof webpack, schema: Schema = undefined, additionalOptions: CommandOption[] = [], + override: Partial = {}, ): CommandOption[] { - const coreArgs = webpackMod.cli.getArguments(schema); + const args = webpackMod.cli.getArguments(schema); // Take memory const options: CommandOption[] = Array.from({ - length: additionalOptions.length + Object.keys(coreArgs).length, + length: additionalOptions.length + Object.keys(args).length, }); let i = 0; @@ -914,14 +915,15 @@ class WebpackCLI { for (; i < additionalOptions.length; i++) options[i] = additionalOptions[i]; // Adding core options - for (const name in coreArgs) { - const meta = coreArgs[name]; + for (const name in args) { + const meta = args[name]; options[i++] = { ...meta, name, description: meta.description, hidden: !this.#minimumHelpOptions.has(name), negativeHidden: !this.#minimumNegativeHelpOptions.has(name), + ...override, }; } @@ -1556,7 +1558,10 @@ class WebpackCLI { const webpackOptions = this.schemaToOptions(webpack, undefined, this.#CLIOptions); const devServer = await this.loadWebpackDevServer(); // @ts-expect-error different versions of the `Schema` type - const devServerOptions = this.schemaToOptions(webpack, devServer.schema); + const devServerOptions = this.schemaToOptions(webpack, devServer.schema, undefined, { + hidden: false, + negativeHidden: false, + }); return { webpack, webpackOptions, devServer, devServerOptions }; }, From 8bd56d33a8d1c813a78b81b0b7ad3375f66b938a Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Sat, 7 Mar 2026 14:14:29 +0300 Subject: [PATCH 14/22] test: refactor --- test/api/CLI.test.js | 766 ++++++++++++++++++------------------------- 1 file changed, 324 insertions(+), 442 deletions(-) diff --git a/test/api/CLI.test.js b/test/api/CLI.test.js index 30e5f946c20..59974ba55f0 100644 --- a/test/api/CLI.test.js +++ b/test/api/CLI.test.js @@ -19,8 +19,12 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand({ name: "command" }, [], (options) => { - expect(options).toEqual({}); + const command = await cli.makeCommand({ + name: "command", + options: [], + action: (options) => { + expect(options).toEqual({}); + }, }); command.parseAsync([], { from: "user" }); @@ -31,20 +35,18 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: true }); }, - ); + }); command.parseAsync(["--boolean"], { from: "user" }); }); @@ -54,21 +56,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: Boolean, description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: true }); }, - ); + }); command.parseAsync(["--boolean"], { from: "user" }); }); @@ -78,22 +78,22 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ - { - name: "boolean", - type: Boolean, - description: "description", - negative: true, - }, + const command = await cli.makeCommand({ + name: "command", + options: [ + [ + { + name: "boolean", + type: Boolean, + description: "description", + negative: true, + }, + ], ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: false }); }, - ); + }); command.parseAsync(["--no-boolean"], { from: "user" }); }); @@ -103,11 +103,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "configs-boolean", configs: [ @@ -118,10 +116,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ configsBoolean: false }); }, - ); + }); command.parseAsync(["--no-configs-boolean"], { from: "user" }); }); @@ -131,11 +129,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "configs-number", configs: [ @@ -146,10 +142,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ configsNumber: 42 }); }, - ); + }); command.parseAsync(["--configs-number", "42"], { from: "user" }); }); @@ -159,11 +155,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "configs-string", configs: [ @@ -174,10 +168,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ configsString: "foo" }); }, - ); + }); command.parseAsync(["--configs-string", "foo"], { from: "user" }); }); @@ -187,11 +181,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "configs-path", configs: [ @@ -202,10 +194,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ configsPath: "/root/foo" }); }, - ); + }); command.parseAsync(["--configs-path", "/root/foo"], { from: "user", @@ -217,11 +209,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "configs-regexp", configs: [ @@ -232,10 +222,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ configsRegexp: "\\w+" }); }, - ); + }); command.parseAsync(["--configs-regexp", "\\w+"], { from: "user" }); }); @@ -245,11 +235,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "enum-string", configs: [ @@ -261,10 +249,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ enumString: "foo" }); }, - ); + }); command.parseAsync(["--enum-string", "foo"], { from: "user" }); }); @@ -274,11 +262,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "enum-number", configs: [ @@ -290,10 +276,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ enumNumber: 42 }); }, - ); + }); command.parseAsync(["--enum-number", "42"], { from: "user" }); }); @@ -303,11 +289,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "enum-boolean", configs: [ @@ -319,10 +303,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ enumBoolean: false }); }, - ); + }); command.parseAsync(["--no-enum-boolean"], { from: "user" }); }); @@ -332,11 +316,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: Boolean, @@ -344,10 +326,10 @@ describe("CLI API", () => { negative: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: false }); }, - ); + }); command.parseAsync(["--boolean", "--no-boolean"], { from: "user" }); }); @@ -357,11 +339,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: Boolean, @@ -369,10 +349,10 @@ describe("CLI API", () => { negative: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: true }); }, - ); + }); command.parseAsync(["--no-boolean", "--boolean"], { from: "user" }); }); @@ -382,11 +362,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: Boolean, @@ -394,10 +372,10 @@ describe("CLI API", () => { defaultValue: false, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: false }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -407,21 +385,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", type: String, description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: "bar" }); }, - ); + }); command.parseAsync(["--string", "bar"], { from: "user" }); }); @@ -431,11 +407,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", alias: "s", @@ -443,10 +417,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: "foo" }); }, - ); + }); command.parseAsync(["-s", "foo"], { from: "user" }); }); @@ -456,11 +430,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", type: String, @@ -468,10 +440,10 @@ describe("CLI API", () => { defaultValue: "default-value", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: "default-value" }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -481,11 +453,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", type: String, @@ -493,10 +463,10 @@ describe("CLI API", () => { defaultValue: "default-value", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: "foo" }); }, - ); + }); command.parseAsync(["--string", "foo"], { from: "user" }); }); @@ -506,21 +476,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", type: String, description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: "bar" }); }, - ); + }); command.parseAsync(["--string=bar"], { from: "user" }); }); @@ -530,11 +498,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", multiple: true, @@ -542,10 +508,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: ["foo", "bar"] }); }, - ); + }); command.parseAsync(["--string", "foo", "bar"], { from: "user" }); }); @@ -555,11 +521,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", multiple: true, @@ -568,10 +532,10 @@ describe("CLI API", () => { defaultValue: "string", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: "string" }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -581,11 +545,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", multiple: true, @@ -594,10 +556,10 @@ describe("CLI API", () => { defaultValue: "string", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: ["foo", "bar"] }); }, - ); + }); command.parseAsync(["--string", "foo", "--string", "bar"], { from: "user", @@ -609,11 +571,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", multiple: true, @@ -621,10 +581,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: ["foo", "bar"] }); }, - ); + }); command.parseAsync(["--string", "foo", "--string", "bar"], { from: "user", @@ -636,21 +596,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "number", type: Number, description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ number: 12 }); }, - ); + }); command.parseAsync(["--number", "12"], { from: "user" }); }); @@ -660,11 +618,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "number", type: Number, @@ -672,10 +628,10 @@ describe("CLI API", () => { defaultValue: 20, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ number: 20 }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -685,11 +641,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "number", multiple: true, @@ -697,10 +651,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ number: [1, 2] }); }, - ); + }); command.parseAsync(["--number", "1", "--number", "2"], { from: "user", @@ -712,11 +666,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "number", multiple: true, @@ -725,10 +677,10 @@ describe("CLI API", () => { defaultValue: 50, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ number: [1, 2] }); }, - ); + }); command.parseAsync(["--number", "1", "--number", "2"], { from: "user", @@ -740,11 +692,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + option: [ { name: "number", multiple: true, @@ -753,10 +703,10 @@ describe("CLI API", () => { defaultValue: 50, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ number: 50 }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -766,21 +716,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "custom", type: () => "function", description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ custom: "function" }); }, - ); + }); command.parseAsync(["--custom", "value"], { from: "user" }); }); @@ -790,11 +738,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "custom", type: () => "function", @@ -802,10 +748,10 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ custom: "default" }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -815,11 +761,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "custom", type: (value, previous = []) => [...previous, value], @@ -827,10 +771,10 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ custom: ["value", "other"] }); }, - ); + }); command.parseAsync(["--custom", "value", "--custom", "other"], { from: "user", @@ -842,11 +786,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "custom", type: (value, previous = []) => [...previous, value], @@ -855,10 +797,10 @@ describe("CLI API", () => { defaultValue: 50, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ custom: 50 }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -870,11 +812,9 @@ describe("CLI API", () => { let skipDefault = true; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "custom", type: (value, previous = []) => { @@ -890,10 +830,10 @@ describe("CLI API", () => { defaultValue: 50, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ custom: ["foo"] }); }, - ); + }); command.parseAsync(["--custom", "foo"], { from: "user" }); }); @@ -903,21 +843,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-string", type: [Boolean, String], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndString: true }); }, - ); + }); command.parseAsync(["--boolean-and-string"], { from: "user" }); }); @@ -927,21 +865,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-string", type: [Boolean, String], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndString: "value" }); }, - ); + }); command.parseAsync(["--boolean-and-string", "value"], { from: "user", @@ -953,11 +889,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-string", type: [Boolean, String], @@ -965,10 +899,10 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndString: true }); }, - ); + }); command.parseAsync(["--boolean-and-string"], { from: "user" }); }); @@ -978,11 +912,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-string", type: [Boolean, String], @@ -990,12 +922,12 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndString: ["bar", "baz"], }); }, - ); + }); command.parseAsync(["--boolean-and-string", "bar", "--boolean-and-string", "baz"], { from: "user", @@ -1007,11 +939,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-string", type: [Boolean, String], @@ -1019,10 +949,10 @@ describe("CLI API", () => { negative: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndString: true }); }, - ); + }); command.parseAsync(["--boolean-and-string"], { from: "user" }); }); @@ -1032,11 +962,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-string", type: [Boolean, String], @@ -1044,10 +972,10 @@ describe("CLI API", () => { negative: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndString: "foo" }); }, - ); + }); command.parseAsync(["--boolean-and-string", "foo"], { from: "user", @@ -1059,11 +987,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-string", type: [Boolean, String], @@ -1071,10 +997,10 @@ describe("CLI API", () => { negative: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndString: false }); }, - ); + }); command.parseAsync(["--no-boolean-and-string"], { from: "user" }); }); @@ -1084,21 +1010,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number", type: [Boolean, Number], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumber: true }); }, - ); + }); command.parseAsync(["--boolean-and-number"], { from: "user" }); }); @@ -1108,21 +1032,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number", type: [Boolean, Number], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumber: 12 }); }, - ); + }); command.parseAsync(["--boolean-and-number", "12"], { from: "user", @@ -1134,21 +1056,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: [Boolean], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: true }); }, - ); + }); command.parseAsync(["--boolean"], { from: "user" }); }); @@ -1158,23 +1078,21 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: true, }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string"], { from: "user", @@ -1186,21 +1104,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: 12 }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "12"], { from: "user", @@ -1212,23 +1128,21 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: "bar", }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "bar"], { from: "user", @@ -1240,11 +1154,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1252,12 +1164,12 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: "default", }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -1267,11 +1179,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1279,12 +1189,12 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: "foo", }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "foo"], { from: "user", @@ -1296,11 +1206,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1308,10 +1216,10 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: 12 }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "12"], { from: "user", @@ -1323,11 +1231,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1335,12 +1241,12 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: true, }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string"], { from: "user", @@ -1352,11 +1258,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1364,12 +1268,12 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: true, }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string"], { from: "user", @@ -1381,11 +1285,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1393,12 +1295,12 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: ["foo"], }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "foo"], { from: "user", @@ -1410,11 +1312,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1422,12 +1322,12 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: [12], }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "12"], { from: "user", @@ -1439,11 +1339,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1451,12 +1349,12 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: ["foo", "bar"], }); }, - ); + }); command.parseAsync( ["--boolean-and-number-and-string", "foo", "--boolean-and-number-and-string", "bar"], @@ -1469,11 +1367,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1481,12 +1377,12 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: ["foo", 12], }); }, - ); + }); command.parseAsync( ["--boolean-and-number-and-string", "foo", "--boolean-and-number-and-string", "12"], @@ -1499,11 +1395,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1512,12 +1406,12 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: "default", }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -1527,11 +1421,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1540,12 +1432,12 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: ["foo"], }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "foo"], { from: "user", @@ -1557,11 +1449,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1570,12 +1460,12 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: [12], }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "12"], { from: "user", @@ -1587,11 +1477,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1600,12 +1488,12 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: ["foo", 12], }); }, - ); + }); command.parseAsync( ["--boolean-and-number-and-string", "foo", "--boolean-and-number-and-string", "12"], @@ -1618,21 +1506,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "unknown", type: [Boolean, Symbol], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ unknown: "foo" }); }, - ); + }); command.parseAsync(["--unknown", "foo"], { from: "user" }); }); @@ -1642,11 +1528,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: Boolean, @@ -1654,10 +1538,10 @@ describe("CLI API", () => { negatedDescription: "Negated description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: true }); }, - ); + }); command.parseAsync(["--boolean"], { from: "user" }); @@ -1669,11 +1553,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: Boolean, @@ -1682,10 +1564,10 @@ describe("CLI API", () => { negatedDescription: "Negated description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: false }); }, - ); + }); command.parseAsync(["--no-boolean"], { from: "user" }); From 76b92e5346f4bac3619b64176d3472a550c3e95d Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Sat, 7 Mar 2026 14:31:06 +0300 Subject: [PATCH 15/22] test: avoid extra output --- test/build/custom-webpack/custom-webpack.test.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/build/custom-webpack/custom-webpack.test.js b/test/build/custom-webpack/custom-webpack.test.js index a8764c4c1f8..94b2c54486e 100644 --- a/test/build/custom-webpack/custom-webpack.test.js +++ b/test/build/custom-webpack/custom-webpack.test.js @@ -19,9 +19,6 @@ describe("custom-webpack", () => { env: { WEBPACK_PACKAGE: resolve(__dirname, "./custom-webpack.js") }, }); - console.log(stderr); - console.log(stdout); - expect(exitCode).toBe(0); expect(stderr).toBeFalsy(); expect(stdout).toContain("main.js"); From 1d0049de9e543d3be597927e7a2b410a80015ce8 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Sat, 7 Mar 2026 15:12:30 +0300 Subject: [PATCH 16/22] test: fix --- test/api/CLI.test.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/api/CLI.test.js b/test/api/CLI.test.js index 59974ba55f0..2dd11521a72 100644 --- a/test/api/CLI.test.js +++ b/test/api/CLI.test.js @@ -81,14 +81,12 @@ describe("CLI API", () => { const command = await cli.makeCommand({ name: "command", options: [ - [ - { - name: "boolean", - type: Boolean, - description: "description", - negative: true, - }, - ], + { + name: "boolean", + type: Boolean, + description: "description", + negative: true, + }, ], action: (options) => { expect(options).toEqual({ boolean: false }); @@ -694,7 +692,7 @@ describe("CLI API", () => { const command = await cli.makeCommand({ name: "command", - option: [ + options: [ { name: "number", multiple: true, From a1df7ed6fb2a6953105ebb47a09e940006909ad8 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Sat, 7 Mar 2026 15:36:12 +0300 Subject: [PATCH 17/22] test: rewrite --- .../api/resolve-config/resolve-config.test.js | 83 ------------------ test/api/resolve-config/webpack.config.cjs | 21 ----- test/api/resolve-config/webpack.config1.cjs | 7 -- test/api/resolve-config/webpack.config2.cjs | 8 -- .../resolve-config/webpack.promise.config.cjs | 14 ---- .../build/config/function/async-env.config.js | 10 +++ .../function/async-multi-webpack.config.js | 20 +++++ .../function/async-single-webpack.config.js | 7 ++ .../config/function/env.config.js} | 5 +- .../function-each-multi-webpack.config.js | 20 +++++ .../config/function/functional-config.test.js | 84 ++++++++++++++++++- .../config/function/multi-webpack.config.js | 4 +- .../config/function/promise.webpack.config.js | 9 ++ .../config/function/single-webpack.config.js | 2 +- .../index.js | 0 .../multi-compiler-options.test.js} | 0 .../webpack.config.js | 0 test/build/config/multi-compiler/a.js | 1 + test/build/config/multi-compiler/b.js | 1 + .../multi-compiler/multi-compiler.test.js | 16 ++++ .../config/multi-compiler/webpack.config.js | 18 ++++ 21 files changed, 190 insertions(+), 140 deletions(-) delete mode 100644 test/api/resolve-config/resolve-config.test.js delete mode 100644 test/api/resolve-config/webpack.config.cjs delete mode 100644 test/api/resolve-config/webpack.config1.cjs delete mode 100644 test/api/resolve-config/webpack.config2.cjs delete mode 100644 test/api/resolve-config/webpack.promise.config.cjs create mode 100644 test/build/config/function/async-env.config.js create mode 100644 test/build/config/function/async-multi-webpack.config.js create mode 100644 test/build/config/function/async-single-webpack.config.js rename test/{api/resolve-config/env.webpack.config.cjs => build/config/function/env.config.js} (56%) create mode 100644 test/build/config/function/function-each-multi-webpack.config.js create mode 100644 test/build/config/function/promise.webpack.config.js rename test/build/config/{top-multi-compilers-options => multi-compiler-options}/index.js (100%) rename test/build/config/{top-multi-compilers-options/top-multi-compilers-options.test.js => multi-compiler-options/multi-compiler-options.test.js} (100%) rename test/build/config/{top-multi-compilers-options => multi-compiler-options}/webpack.config.js (100%) create mode 100644 test/build/config/multi-compiler/a.js create mode 100644 test/build/config/multi-compiler/b.js create mode 100644 test/build/config/multi-compiler/multi-compiler.test.js create mode 100644 test/build/config/multi-compiler/webpack.config.js diff --git a/test/api/resolve-config/resolve-config.test.js b/test/api/resolve-config/resolve-config.test.js deleted file mode 100644 index 1d3fce10fdf..00000000000 --- a/test/api/resolve-config/resolve-config.test.js +++ /dev/null @@ -1,83 +0,0 @@ -const { resolve } = require("node:path"); -const WebpackCLI = require("../../../packages/webpack-cli/lib/webpack-cli").default; -const arrayConfig = require("./webpack.config.cjs"); -const config1 = require("./webpack.config1.cjs"); -const config2 = require("./webpack.config2.cjs"); -const promiseConfig = require("./webpack.promise.config.cjs"); - -const cli = new WebpackCLI(); - -describe("resolveConfig", () => { - it("should handle merge properly", async () => { - const result = await cli.loadConfig({ - merge: true, - config: [resolve(__dirname, "./webpack.config.cjs")], - }); - - const expectedOptions = { - output: { - filename: "./dist-commonjs.js", - libraryTarget: "commonjs", - }, - entry: "./a.js", - name: "amd", - mode: "production", - devtool: "eval-cheap-module-source-map", - target: "node", - }; - - expect(result.options).toEqual(expectedOptions); - }); - - it("should return array for multiple config", async () => { - const result = await cli.loadConfig({ - config: [ - resolve(__dirname, "./webpack.config1.cjs"), - resolve(__dirname, "./webpack.config2.cjs"), - ], - }); - const expectedOptions = [config1, config2]; - - expect(result.options).toEqual(expectedOptions); - }); - - it("should return config object for single config", async () => { - const result = await cli.loadConfig({ - config: [resolve(__dirname, "./webpack.config1.cjs")], - }); - - expect(result.options).toEqual(config1); - }); - - it("should return resolved config object for promise config", async () => { - const result = await cli.loadConfig({ - config: [resolve(__dirname, "./webpack.promise.config.cjs")], - }); - const expectedOptions = await promiseConfig(); - - expect(result.options).toEqual(expectedOptions); - }); - - it("should handle configs returning different types", async () => { - const result = await cli.loadConfig({ - config: [ - resolve(__dirname, "./webpack.promise.config.cjs"), - resolve(__dirname, "./webpack.config.cjs"), - ], - }); - const resolvedPromiseConfig = await promiseConfig(); - const expectedOptions = [resolvedPromiseConfig, ...arrayConfig]; - - expect(result.options).toEqual(expectedOptions); - }); - - it("should handle different env formats", async () => { - const result = await cli.loadConfig({ - argv: { env: { test: true, name: "Hisoka" } }, - config: [resolve(__dirname, "./env.webpack.config.cjs")], - }); - const expectedOptions = { mode: "staging", name: "Hisoka" }; - - expect(result.options).toEqual(expectedOptions); - }); -}); diff --git a/test/api/resolve-config/webpack.config.cjs b/test/api/resolve-config/webpack.config.cjs deleted file mode 100644 index 3d7d3ef7ce6..00000000000 --- a/test/api/resolve-config/webpack.config.cjs +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = [ - { - output: { - filename: "./dist-amd.js", - libraryTarget: "amd", - }, - entry: "./a.js", - name: "amd", - mode: "development", - devtool: "eval-cheap-module-source-map", - }, - { - output: { - filename: "./dist-commonjs.js", - libraryTarget: "commonjs", - }, - entry: "./a.js", - mode: "production", - target: "node", - }, -]; diff --git a/test/api/resolve-config/webpack.config1.cjs b/test/api/resolve-config/webpack.config1.cjs deleted file mode 100644 index c4b7df891f7..00000000000 --- a/test/api/resolve-config/webpack.config1.cjs +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - output: { - libraryTarget: "amd", - }, - entry: "./a.js", - name: "amd", -}; diff --git a/test/api/resolve-config/webpack.config2.cjs b/test/api/resolve-config/webpack.config2.cjs deleted file mode 100644 index 854b414229c..00000000000 --- a/test/api/resolve-config/webpack.config2.cjs +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - output: { - libraryTarget: "commonjs", - }, - entry: "./a.js", - mode: "production", - target: "node", -}; diff --git a/test/api/resolve-config/webpack.promise.config.cjs b/test/api/resolve-config/webpack.promise.config.cjs deleted file mode 100644 index f1083b0702e..00000000000 --- a/test/api/resolve-config/webpack.promise.config.cjs +++ /dev/null @@ -1,14 +0,0 @@ -const path = require("node:path"); - -module.exports = () => - new Promise((resolve) => { - setTimeout(() => { - resolve({ - entry: "./a", - output: { - path: path.resolve(__dirname, "./binary"), - filename: "promise.js", - }, - }); - }, 500); - }); diff --git a/test/build/config/function/async-env.config.js b/test/build/config/function/async-env.config.js new file mode 100644 index 00000000000..4b6bb6b47e6 --- /dev/null +++ b/test/build/config/function/async-env.config.js @@ -0,0 +1,10 @@ +module.exports = function configuration(env) { + const configName = env.name; + return { + name: configName, + mode: "development", + output: { + filename: `./async-${configName}-single.js`, + }, + }; +}; diff --git a/test/build/config/function/async-multi-webpack.config.js b/test/build/config/function/async-multi-webpack.config.js new file mode 100644 index 00000000000..3a20f17aff1 --- /dev/null +++ b/test/build/config/function/async-multi-webpack.config.js @@ -0,0 +1,20 @@ +module.exports = async () => [ + { + output: { + filename: "./multi-async-first.js", + }, + name: "first", + entry: "./src/first.js", + mode: "development", + stats: "minimal", + }, + { + output: { + filename: "./multi-async-second.js", + }, + name: "second", + entry: "./src/second.js", + mode: "development", + stats: "minimal", + }, +]; diff --git a/test/build/config/function/async-single-webpack.config.js b/test/build/config/function/async-single-webpack.config.js new file mode 100644 index 00000000000..353b4d20640 --- /dev/null +++ b/test/build/config/function/async-single-webpack.config.js @@ -0,0 +1,7 @@ +module.exports = async () => ({ + output: { + filename: "./async-single.js", + }, + name: "single", + mode: "development", +}); diff --git a/test/api/resolve-config/env.webpack.config.cjs b/test/build/config/function/env.config.js similarity index 56% rename from test/api/resolve-config/env.webpack.config.cjs rename to test/build/config/function/env.config.js index 4707f9e6c86..04fa62be056 100644 --- a/test/api/resolve-config/env.webpack.config.cjs +++ b/test/build/config/function/env.config.js @@ -2,6 +2,9 @@ module.exports = function configuration(env) { const configName = env.name; return { name: configName, - mode: env.test ? "staging" : "production", + mode: "development", + output: { + filename: `./${configName}-single.js`, + }, }; }; diff --git a/test/build/config/function/function-each-multi-webpack.config.js b/test/build/config/function/function-each-multi-webpack.config.js new file mode 100644 index 00000000000..86c3df51b3b --- /dev/null +++ b/test/build/config/function/function-each-multi-webpack.config.js @@ -0,0 +1,20 @@ +module.exports = [ + () => ({ + output: { + filename: "./function-each-first.js", + }, + name: "first", + entry: "./src/first.js", + mode: "development", + stats: "minimal", + }), + async () => ({ + output: { + filename: "./function-each-second.js", + }, + name: "second", + entry: "./src/second.js", + mode: "development", + stats: "minimal", + }), +]; diff --git a/test/build/config/function/functional-config.test.js b/test/build/config/function/functional-config.test.js index 2408e1e1300..db00488d1b0 100644 --- a/test/build/config/function/functional-config.test.js +++ b/test/build/config/function/functional-config.test.js @@ -14,7 +14,19 @@ describe("functional config", () => { expect(exitCode).toBe(0); expect(stderr).toBeFalsy(); expect(stdout).toContain("./src/index.js"); - expect(existsSync(resolve(__dirname, "./dist/dist-single.js"))).toBeTruthy(); + expect(existsSync(resolve(__dirname, "./dist/single.js"))).toBeTruthy(); + }); + + it("should build and not throw error with async single configuration", async () => { + const { stderr, stdout, exitCode } = await run(__dirname, [ + "--config", + resolve(__dirname, "async-single-webpack.config.js"), + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("./src/index.js"); + expect(existsSync(resolve(__dirname, "./dist/async-single.js"))).toBeTruthy(); }); it("should build and not throw errors with multiple configurations", async () => { @@ -27,7 +39,73 @@ describe("functional config", () => { expect(stderr).toBeFalsy(); expect(stdout).toContain("first"); expect(stdout).toContain("second"); - expect(existsSync(resolve(__dirname, "./dist/dist-first.js"))).toBeTruthy(); - expect(existsSync(resolve(__dirname, "./dist/dist-second.js"))).toBeTruthy(); + expect(existsSync(resolve(__dirname, "./dist/multi-first.js"))).toBeTruthy(); + expect(existsSync(resolve(__dirname, "./dist/multi-second.js"))).toBeTruthy(); + }); + + it("should build and not throw errors with async multiple configurations", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "--config", + resolve(__dirname, "async-multi-webpack.config.js"), + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("first"); + expect(stdout).toContain("second"); + expect(existsSync(resolve(__dirname, "./dist/multi-async-first.js"))).toBeTruthy(); + expect(existsSync(resolve(__dirname, "./dist/multi-async-second.js"))).toBeTruthy(); + }); + + it("should build and not throw errors with promise configuration", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "--config", + resolve(__dirname, "promise.webpack.config.js"), + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("./src/index.js"); + expect(existsSync(resolve(__dirname, "./dist/promise-single.js"))).toBeTruthy(); + }); + + it("should build and not throw errors with env configuration", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "--config", + resolve(__dirname, "env.config.js"), + "--env=name=env", + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("./src/index.js"); + expect(existsSync(resolve(__dirname, "./dist/env-single.js"))).toBeTruthy(); + }); + + it("should build and not throw errors with async env configuration", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "--config", + resolve(__dirname, "async-env.config.js"), + "--env=name=env", + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("./src/index.js"); + expect(existsSync(resolve(__dirname, "./dist/async-env-single.js"))).toBeTruthy(); + }); + + it("should build and not throw errors with function each multiple configurations", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "--config", + resolve(__dirname, "function-each-multi-webpack.config.js"), + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("first"); + expect(stdout).toContain("second"); + expect(existsSync(resolve(__dirname, "./dist/function-each-first.js"))).toBeTruthy(); + expect(existsSync(resolve(__dirname, "./dist/function-each-second.js"))).toBeTruthy(); }); }); diff --git a/test/build/config/function/multi-webpack.config.js b/test/build/config/function/multi-webpack.config.js index f82f79ada0a..73de2b98411 100644 --- a/test/build/config/function/multi-webpack.config.js +++ b/test/build/config/function/multi-webpack.config.js @@ -1,7 +1,7 @@ module.exports = () => [ { output: { - filename: "./dist-first.js", + filename: "./multi-first.js", }, name: "first", entry: "./src/first.js", @@ -10,7 +10,7 @@ module.exports = () => [ }, { output: { - filename: "./dist-second.js", + filename: "./multi-second.js", }, name: "second", entry: "./src/second.js", diff --git a/test/build/config/function/promise.webpack.config.js b/test/build/config/function/promise.webpack.config.js new file mode 100644 index 00000000000..4f55e4152ab --- /dev/null +++ b/test/build/config/function/promise.webpack.config.js @@ -0,0 +1,9 @@ +module.exports = new Promise((resolve) => { + resolve({ + output: { + filename: "./promise-single.js", + }, + name: "promise-single", + mode: "development", + }); +}); diff --git a/test/build/config/function/single-webpack.config.js b/test/build/config/function/single-webpack.config.js index 0c93cbb0193..4df8fa9ad26 100644 --- a/test/build/config/function/single-webpack.config.js +++ b/test/build/config/function/single-webpack.config.js @@ -1,6 +1,6 @@ module.exports = () => ({ output: { - filename: "./dist-single.js", + filename: "./single.js", }, name: "single", mode: "development", diff --git a/test/build/config/top-multi-compilers-options/index.js b/test/build/config/multi-compiler-options/index.js similarity index 100% rename from test/build/config/top-multi-compilers-options/index.js rename to test/build/config/multi-compiler-options/index.js diff --git a/test/build/config/top-multi-compilers-options/top-multi-compilers-options.test.js b/test/build/config/multi-compiler-options/multi-compiler-options.test.js similarity index 100% rename from test/build/config/top-multi-compilers-options/top-multi-compilers-options.test.js rename to test/build/config/multi-compiler-options/multi-compiler-options.test.js diff --git a/test/build/config/top-multi-compilers-options/webpack.config.js b/test/build/config/multi-compiler-options/webpack.config.js similarity index 100% rename from test/build/config/top-multi-compilers-options/webpack.config.js rename to test/build/config/multi-compiler-options/webpack.config.js diff --git a/test/build/config/multi-compiler/a.js b/test/build/config/multi-compiler/a.js new file mode 100644 index 00000000000..0e9a8dc5145 --- /dev/null +++ b/test/build/config/multi-compiler/a.js @@ -0,0 +1 @@ +module.exports = "a.js"; diff --git a/test/build/config/multi-compiler/b.js b/test/build/config/multi-compiler/b.js new file mode 100644 index 00000000000..e3ec26f3f74 --- /dev/null +++ b/test/build/config/multi-compiler/b.js @@ -0,0 +1 @@ +module.exports = "b.js"; diff --git a/test/build/config/multi-compiler/multi-compiler.test.js b/test/build/config/multi-compiler/multi-compiler.test.js new file mode 100644 index 00000000000..88e1d57fe54 --- /dev/null +++ b/test/build/config/multi-compiler/multi-compiler.test.js @@ -0,0 +1,16 @@ +"use strict"; + +const { resolve } = require("node:path"); +const { run } = require("../../../utils/test-utils"); + +describe("basic config file", () => { + it("should build and not throw error with a basic configuration file", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "-c", + resolve(__dirname, "webpack.config.js"), + ]); + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toBeTruthy(); + }); +}); diff --git a/test/build/config/multi-compiler/webpack.config.js b/test/build/config/multi-compiler/webpack.config.js new file mode 100644 index 00000000000..cc56b56a848 --- /dev/null +++ b/test/build/config/multi-compiler/webpack.config.js @@ -0,0 +1,18 @@ +const { resolve } = require("node:path"); + +module.exports = [ + { + entry: "./a.js", + output: { + path: resolve(__dirname, "binary"), + filename: "a.bundle.js", + }, + }, + { + entry: "./b.js", + output: { + path: resolve(__dirname, "binary"), + filename: "b.bundle.js", + }, + }, +]; From 0677fa5458f31bb1b512dae7ecc406ca633abe74 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Sat, 7 Mar 2026 16:12:10 +0300 Subject: [PATCH 18/22] refactor: fix smoketests --- packages/webpack-cli/src/webpack-cli.ts | 44 ++++++++++++++----------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 9d9f0dbbf41..129e8117dc5 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -600,6 +600,13 @@ class WebpackCLI { allDependenciesInstalled = false; if (forHelp) { + command.description( + `${ + options.description + } To see all available options you need to install ${options.dependencies + .map((dependency) => `'${dependency}'`) + .join(", ")}.`, + ); continue; } @@ -618,7 +625,15 @@ class WebpackCLI { command.context = {} as C; if (typeof options.preload === "function") { - const data = await options.preload(); + let data; + + try { + data = await options.preload(); + } catch (err) { + if (!forHelp) { + throw err; + } + } command.context = { ...command.context, ...data }; } @@ -626,24 +641,15 @@ class WebpackCLI { if (options.options) { let commandOptions: CommandOption[]; - if (typeof options.options === "function") { - if ( - forHelp && - !allDependenciesInstalled && - options.dependencies && - options.dependencies.length > 0 - ) { - command.description( - `${ - options.description - } To see all available options you need to install ${options.dependencies - .map((dependency) => `'${dependency}'`) - .join(", ")}.`, - ); - commandOptions = []; - } else { - commandOptions = await options.options(command); - } + if ( + forHelp && + !allDependenciesInstalled && + options.dependencies && + options.dependencies.length > 0 + ) { + commandOptions = []; + } else if (typeof options.options === "function") { + commandOptions = await options.options(command); } else { commandOptions = options.options; } From fea5abc21f52ac8afbb51757d4c1882e81921ceb Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Sat, 7 Mar 2026 17:23:22 +0300 Subject: [PATCH 19/22] refactor: resolve todo --- packages/webpack-cli/src/webpack-cli.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 129e8117dc5..6ee293a37c2 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -667,7 +667,7 @@ class WebpackCLI { makeOption(command: Command, option: CommandOption) { type MainOption = Pick< CommandOption, - "valueName" | "description" | "defaultValue" | "multiple" + "valueName" | "description" | "defaultValue" | "multiple" | "configs" > & { flags: string; type: Set; @@ -749,6 +749,7 @@ class WebpackCLI { type: mainOptionType, multiple: option.multiple, defaultValue: option.defaultValue, + configs: option.configs, }; if (needNegativeOption) { @@ -822,6 +823,10 @@ class WebpackCLI { optionForCommand.hidden = option.hidden || false; + if (option.configs) { + (optionForCommand as Option & { configs: ArgumentConfig[] }).configs = option.configs; + } + command.addOption(optionForCommand); } else if (mainOption.type.has(Boolean)) { const optionForCommand = new Option(mainOption.flags, mainOption.description).default( @@ -838,6 +843,10 @@ class WebpackCLI { optionForCommand.hidden = option.hidden || false; + if (option.configs) { + (optionForCommand as Option & { configs: ArgumentConfig[] }).configs = option.configs; + } + command.addOption(optionForCommand); } } else if (mainOption.type.size > 1) { @@ -868,6 +877,10 @@ class WebpackCLI { optionForCommand.hidden = option.hidden || false; + if (option.configs) { + (optionForCommand as Option & { configs: ArgumentConfig[] }).configs = option.configs; + } + command.addOption(optionForCommand); } else if (mainOption.type.size === 0 && negativeOption) { const optionForCommand = new Option(mainOption.flags, mainOption.description); @@ -1192,14 +1205,10 @@ class WebpackCLI { this.logger.raw(`${bold("Default value:")} ${JSON.stringify(option.defaultValue)}`); } - // TODO maybe bug here - const webpack = await this.loadWebpack(); - const flag = this.schemaToOptions(webpack, undefined, this.#CLIOptions).find( - (flag) => option.long === `--${flag.name}`, - ); + const { configs } = option as Option & { configs?: ArgumentConfig[] }; - if (flag?.configs) { - const possibleValues = flag.configs.reduce((accumulator, currentValue) => { + if (configs) { + const possibleValues = configs.reduce((accumulator, currentValue) => { if (currentValue.values) { return [...accumulator, ...currentValue.values]; } From 3dca6322cd7a2d3a8f10bd745eabd0963edc337e Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Sat, 7 Mar 2026 17:38:33 +0300 Subject: [PATCH 20/22] refactor: code --- packages/webpack-cli/src/webpack-cli.ts | 137 ++++++++++-------------- 1 file changed, 56 insertions(+), 81 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 6ee293a37c2..c4169d9deca 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -48,6 +48,9 @@ const DEFAULT_CONFIGURATION_FILES = [ ".webpack/webpackfile", ]; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type RecordAny = Record; + interface Rechoir { prepare: typeof prepare; } @@ -77,8 +80,7 @@ interface Colors extends WebpackColors { isColorSupported: boolean; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Context = Record; +type Context = RecordAny; interface Command extends CommanderCommand { pkg?: string; @@ -189,8 +191,7 @@ interface Env { WEBPACK_SERVE?: boolean; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -interface Argv extends Record { +interface Argv extends RecordAny { env: Env; } @@ -241,7 +242,7 @@ type Options = // Webpack CLI own options KnownOptions & // Webpack and webpack-dev-server options - Record; + RecordAny; const DEFAULT_WEBPACK_PACKAGES: string[] = ["webpack", "loader"]; @@ -949,6 +950,54 @@ class WebpackCLI { return options; } + #processArguments( + webpackMod: typeof webpack, + args: Record, + configuration: RecordAny, + values: ProcessedArguments, + ) { + const problems = webpackMod.cli.processArguments(args, configuration, values); + + if (problems) { + const groupBy = >(xs: Problem[], key: K) => + xs.reduce( + (rv, problem) => { + const path = problem[key]; + + (rv[path] ||= []).push(problem); + + return rv; + }, + {} as Record, + ); + const problemsByPath = groupBy<"path">(problems, "path"); + + for (const path in problemsByPath) { + const problems = problemsByPath[path]; + + for (const problem of problems) { + this.logger.error( + `${this.capitalizeFirstLetter(problem.type.replaceAll("-", " "))}${ + problem.value ? ` '${problem.value}'` : "" + } for the '--${problem.argument.replaceAll(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}' option${ + problem.index ? ` by index '${problem.index}'` : "" + }`, + ); + + if (problem.expected) { + if (problem.expected === "true | false") { + this.logger.error("Expected: without value or negative option"); + } else { + this.logger.error(`Expected: '${problem.expected}'`); + } + } + } + } + + process.exit(2); + } + } + async #outputHelp( options: string[], isVerbose: boolean, @@ -1664,46 +1713,7 @@ class WebpackCLI { } if (Object.keys(values).length > 0) { - const problems = webpack.cli.processArguments(args, devServerConfiguration, values); - - if (problems) { - const groupBy = >( - xs: Problem[], - key: K, - ) => - xs.reduce( - (rv, problem) => { - const path = problem[key]; - - (rv[path] ||= []).push(problem); - - return rv; - }, - {} as Record, - ); - - const problemsByPath = groupBy<"path">(problems, "path"); - - for (const path in problemsByPath) { - const problems = problemsByPath[path]; - - for (const problem of problems) { - this.logger.error( - `${this.capitalizeFirstLetter(problem.type.replace("-", " "))}${ - problem.value ? ` '${problem.value}'` : "" - } for the '--${problem.argument.replaceAll(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}' option${ - problem.index ? ` by index '${problem.index}'` : "" - }`, - ); - - if (problem.expected) { - this.logger.error(`Expected: '${problem.expected}'`); - } - } - } - - process.exit(2); - } + this.#processArguments(webpack, args, devServerConfiguration, values); } if (devServerConfiguration.port) { @@ -2587,42 +2597,7 @@ class WebpackCLI { } if (Object.keys(values).length > 0) { - const problems = options.webpack.cli.processArguments(args, configuration, values); - - if (problems) { - const groupBy = >(xs: Problem[], key: K) => - xs.reduce( - (rv, problem) => { - const path = problem[key]; - - (rv[path] ||= []).push(problem); - - return rv; - }, - {} as Record, - ); - const problemsByPath = groupBy(problems, "path"); - - for (const path in problemsByPath) { - const problems = problemsByPath[path]; - - for (const problem of problems) { - this.logger.error( - `${this.capitalizeFirstLetter(problem.type.replaceAll("-", " "))}${ - problem.value ? ` '${problem.value}'` : "" - } for the '--${problem.argument.replaceAll(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}' option${ - problem.index ? ` by index '${problem.index}'` : "" - }`, - ); - - if (problem.expected) { - this.logger.error(`Expected: '${problem.expected}'`); - } - } - } - - process.exit(2); - } + this.#processArguments(webpack, args, configuration, values); } // Output warnings From 05324a2393e529bc3325b377d250b87cbd3e65b0 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Sat, 7 Mar 2026 17:47:00 +0300 Subject: [PATCH 21/22] refactor: fix --- 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 c4169d9deca..b2033151184 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -2597,7 +2597,7 @@ class WebpackCLI { } if (Object.keys(values).length > 0) { - this.#processArguments(webpack, args, configuration, values); + this.#processArguments(options.webpack, args, configuration, values); } // Output warnings From 195bcc6aaefe242eca579336adb1bd8e8c894782 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Sat, 7 Mar 2026 18:01:57 +0300 Subject: [PATCH 22/22] test: more --- .../__snapshots__/stats.test.js.snap.webpack5 | 2 +- .../help.test.js.snap.devServer5.webpack5 | 28 +++++++++++++++++++ test/help/help.test.js | 16 +++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/test/build/stats/flags/__snapshots__/stats.test.js.snap.webpack5 b/test/build/stats/flags/__snapshots__/stats.test.js.snap.webpack5 index ecc4b892b59..157da2650f0 100644 --- a/test/build/stats/flags/__snapshots__/stats.test.js.snap.webpack5 +++ b/test/build/stats/flags/__snapshots__/stats.test.js.snap.webpack5 @@ -4,7 +4,7 @@ exports[`stats flag should log error when an unknown flag stats value is passed: "[webpack-cli] Invalid value 'foo' for the '--stats' option [webpack-cli] Expected: 'none | summary | errors-only | errors-warnings | minimal | normal | detailed | verbose' [webpack-cli] Invalid value 'foo' for the '--stats' option -[webpack-cli] Expected: 'true | false'" +[webpack-cli] Expected: without value or negative option" `; exports[`stats flag should log error when an unknown flag stats value is passed: stdout 1`] = `""`; diff --git a/test/help/__snapshots__/help.test.js.snap.devServer5.webpack5 b/test/help/__snapshots__/help.test.js.snap.devServer5.webpack5 index 9b5adec60c9..17e22931ffc 100644 --- a/test/help/__snapshots__/help.test.js.snap.devServer5.webpack5 +++ b/test/help/__snapshots__/help.test.js.snap.devServer5.webpack5 @@ -2469,6 +2469,20 @@ CLI documentation: https://webpack.js.org/api/cli/. Made with ♥ by the webpack team." `; +exports[`help should show help information using the "help --cache-type" option when using serve: stderr 1`] = `""`; + +exports[`help should show help information using the "help --cache-type" option when using serve: stdout 1`] = ` +"Usage: webpack serve --cache-type +Description: In memory caching. Filesystem caching. +Possible values: 'memory' | 'filesystem' + +To see list of all supported commands and options run 'webpack --help=verbose'. + +Webpack documentation: https://webpack.js.org/. +CLI documentation: https://webpack.js.org/api/cli/. +Made with ♥ by the webpack team." +`; + exports[`help should show help information using the "help --cache-type" option: stderr 1`] = `""`; exports[`help should show help information using the "help --cache-type" option: stdout 1`] = ` @@ -2550,6 +2564,20 @@ CLI documentation: https://webpack.js.org/api/cli/. Made with ♥ by the webpack team." `; +exports[`help should show help information using the "help --server-type" option when using serve: stderr 1`] = `""`; + +exports[`help should show help information using the "help --server-type" option when using serve: stdout 1`] = ` +"Usage: webpack serve --server-type +Description: Allows to set server and options (by default 'http'). +Possible values: 'http' | 'https' | 'spdy' | 'http2' + +To see list of all supported commands and options run 'webpack --help=verbose'. + +Webpack documentation: https://webpack.js.org/. +CLI documentation: https://webpack.js.org/api/cli/. +Made with ♥ by the webpack team." +`; + exports[`help should show help information using the "help --stats" option: stderr 1`] = `""`; exports[`help should show help information using the "help --stats" option: stdout 1`] = ` diff --git a/test/help/help.test.js b/test/help/help.test.js index b8d844a7c1d..56a32fe918e 100644 --- a/test/help/help.test.js +++ b/test/help/help.test.js @@ -245,6 +245,22 @@ describe("help", () => { expect(normalizeStdout(stdout)).toMatchSnapshot("stdout"); }); + it('should show help information using the "help --cache-type" option when using serve', async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["help", "serve", "--cache-type"]); + + expect(exitCode).toBe(0); + expect(normalizeStderr(stderr)).toMatchSnapshot("stderr"); + expect(normalizeStdout(stdout)).toMatchSnapshot("stdout"); + }); + + it('should show help information using the "help --server-type" option when using serve', async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["help", "serve", "--server-type"]); + + expect(exitCode).toBe(0); + expect(normalizeStderr(stderr)).toMatchSnapshot("stderr"); + expect(normalizeStdout(stdout)).toMatchSnapshot("stdout"); + }); + it('should show help information using the "help --output-chunk-format" option', async () => { const { exitCode, stderr, stdout } = await run(__dirname, ["help", "--output-chunk-format"]);