From cf0363fa356a465d411fd19802410f4726dd3ba6 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 25 Feb 2026 19:07:59 +0300 Subject: [PATCH 01/23] refactor: remove `getAvailablePackageManagers` in favor of `getDefaultPackageManager` --- packages/webpack-cli/src/types.ts | 1 - packages/webpack-cli/src/webpack-cli.ts | 27 ------------------------- 2 files changed, 28 deletions(-) diff --git a/packages/webpack-cli/src/types.ts b/packages/webpack-cli/src/types.ts index ff53a762a18..d3429e30bf1 100644 --- a/packages/webpack-cli/src/types.ts +++ b/packages/webpack-cli/src/types.ts @@ -46,7 +46,6 @@ interface IWebpackCLI { toKebabCase: StringFormatter; capitalizeFirstLetter: StringFormatter; checkPackageExists(packageName: string): boolean; - getAvailablePackageManagers(): PackageManager[]; getDefaultPackageManager(): Promise; doInstall(packageName: string, options?: PackageInstallOptions): Promise; loadJSONFile(path: Path, handleError: boolean): Promise; diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index dd97feca1a0..e31e8153cd6 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -226,33 +226,6 @@ class WebpackCLI implements IWebpackCLI { return false; } - // TODO remove me - getAvailablePackageManagers(): PackageManager[] { - const { sync } = require("cross-spawn"); - - const installers: PackageManager[] = ["npm", "yarn", "pnpm"]; - const hasPackageManagerInstalled = (packageManager: PackageManager) => { - try { - sync(packageManager, ["--version"]); - - return packageManager; - } catch { - return false; - } - }; - const availableInstallers = installers.filter((installer) => - hasPackageManagerInstalled(installer), - ); - - if (!availableInstallers.length) { - this.logger.error("No package manager found."); - - process.exit(2); - } - - return availableInstallers; - } - async getDefaultPackageManager(): Promise { const { sync } = await import("cross-spawn"); From 62e2238f9d12dc3a43339a2cd4d01c148434cb7d Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 25 Feb 2026 19:16:47 +0300 Subject: [PATCH 02/23] refactor: remove outdated `tryRequireThenImport` --- packages/webpack-cli/src/types.ts | 11 --- .../src/utils/dynamic-import-loader.ts | 16 ---- packages/webpack-cli/src/webpack-cli.ts | 95 +------------------ 3 files changed, 2 insertions(+), 120 deletions(-) delete mode 100644 packages/webpack-cli/src/utils/dynamic-import-loader.ts diff --git a/packages/webpack-cli/src/types.ts b/packages/webpack-cli/src/types.ts index d3429e30bf1..3c0a8b27333 100644 --- a/packages/webpack-cli/src/types.ts +++ b/packages/webpack-cli/src/types.ts @@ -49,7 +49,6 @@ interface IWebpackCLI { getDefaultPackageManager(): Promise; doInstall(packageName: string, options?: PackageInstallOptions): Promise; loadJSONFile(path: Path, handleError: boolean): Promise; - tryRequireThenImport(module: ModuleName, handleError: boolean): Promise; getInfoOptions(): WebpackCLIBuiltInOption[]; getInfoOutput(options: { output: string; additionalPackage: string[] }): Promise; makeCommand( @@ -247,7 +246,6 @@ type Instantiable< ConstructorParameters extends unknown[] = unknown[], > = new (...args: ConstructorParameters) => InstanceType; type PotentialPromise = T | Promise; -type ModuleName = string; type Path = string; // eslint-disable-next-line @typescript-eslint/no-explicit-any type LogHandler = (value: any) => void; @@ -267,12 +265,6 @@ interface Env { WEBPACK_DEV_SERVER_PACKAGE?: string; } -type DynamicImport = (url: string) => Promise<{ default: T }>; - -interface ImportLoaderError extends Error { - code?: string; -} - /** * External libraries types */ @@ -304,14 +296,11 @@ export { type CallableWebpackConfiguration, type CommandAction, type CommanderOption, - type DynamicImport, type EnumValue, type FileSystemCacheOptions, type IWebpackCLI, - type ImportLoaderError, type Instantiable, type LoadableWebpackConfiguration, - type ModuleName, type PackageInstallOptions, type PackageManager, type Path, diff --git a/packages/webpack-cli/src/utils/dynamic-import-loader.ts b/packages/webpack-cli/src/utils/dynamic-import-loader.ts deleted file mode 100644 index 558299d23c9..00000000000 --- a/packages/webpack-cli/src/utils/dynamic-import-loader.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type DynamicImport } from "../types.js"; - -function dynamicImportLoader(): DynamicImport | null { - let importESM; - - try { - // eslint-disable-next-line no-new-func - importESM = new Function("id", "return import(id);"); - } catch { - importESM = null; - } - - return importESM as DynamicImport; -} - -module.exports = dynamicImportLoader; diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index e31e8153cd6..82e36cf096f 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -17,14 +17,11 @@ import { type BasicPrimitive, type CallableWebpackConfiguration, type CommandAction, - type DynamicImport, type EnumValue, type FileSystemCacheOptions, type IWebpackCLI, - type ImportLoaderError, type Instantiable, type LoadableWebpackConfiguration, - type ModuleName, type PackageInstallOptions, type PackageManager, type Path, @@ -358,93 +355,6 @@ class WebpackCLI implements IWebpackCLI { process.exit(2); } - // TODO remove me in the next major release - async tryRequireThenImport( - module: ModuleName, - handleError = true, - moduleType: "unknown" | "commonjs" | "esm" = "unknown", - ): Promise { - let result; - - switch (moduleType) { - case "unknown": { - try { - result = require(module); - } catch (error) { - const dynamicImportLoader: null | DynamicImport = - require("./utils/dynamic-import-loader")(); - - if ( - ((error as ImportLoaderError).code === "ERR_REQUIRE_ESM" || - (error as ImportLoaderError).code === "ERR_REQUIRE_ASYNC_MODULE" || - process.env.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) && - pathToFileURL && - dynamicImportLoader - ) { - const urlForConfig = pathToFileURL(module); - - result = await dynamicImportLoader(urlForConfig); - result = result.default; - - return result; - } - - if (handleError) { - this.logger.error(error); - process.exit(2); - } else { - throw error; - } - } - break; - } - case "commonjs": { - try { - result = require(module); - } catch (error) { - if (handleError) { - this.logger.error(error); - process.exit(2); - } else { - throw error; - } - } - break; - } - case "esm": { - try { - const dynamicImportLoader: null | DynamicImport = - require("./utils/dynamic-import-loader")(); - - if (pathToFileURL && dynamicImportLoader) { - const urlForConfig = pathToFileURL(module); - - result = await dynamicImportLoader(urlForConfig); - result = result.default; - - return result; - } - } catch (error) { - if (handleError) { - this.logger.error(error); - process.exit(2); - } else { - throw error; - } - } - - break; - } - } - - // For babel and other, only commonjs - if (result && typeof result === "object" && "default" in result) { - result = result.default || {}; - } - - return result || {}; - } - // TODO remove me loadJSONFile(pathToFile: Path, handleError = true): T { let result; @@ -1525,13 +1435,12 @@ class WebpackCLI implements IWebpackCLI { }); } - let loadedCommand; + let loadedCommand: Instantiable<() => void>; try { - loadedCommand = await this.tryRequireThenImport void>>(pkg, false); + loadedCommand = (await import(pkg)).default; } catch { // Ignore, command is not installed - return; } From 96863ba2b394ee6d9573db16fbf94da8e020d2a8 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 25 Feb 2026 19:22:59 +0300 Subject: [PATCH 03/23] refactor: remove outdated `loadJSONFile` --- packages/webpack-cli/src/types.ts | 3 --- packages/webpack-cli/src/webpack-cli.ts | 21 +-------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/packages/webpack-cli/src/types.ts b/packages/webpack-cli/src/types.ts index 3c0a8b27333..29870269794 100644 --- a/packages/webpack-cli/src/types.ts +++ b/packages/webpack-cli/src/types.ts @@ -48,7 +48,6 @@ interface IWebpackCLI { checkPackageExists(packageName: string): boolean; getDefaultPackageManager(): Promise; doInstall(packageName: string, options?: PackageInstallOptions): Promise; - loadJSONFile(path: Path, handleError: boolean): Promise; getInfoOptions(): WebpackCLIBuiltInOption[]; getInfoOutput(options: { output: string; additionalPackage: string[] }): Promise; makeCommand( @@ -246,7 +245,6 @@ type Instantiable< ConstructorParameters extends unknown[] = unknown[], > = new (...args: ConstructorParameters) => InstanceType; type PotentialPromise = T | Promise; -type Path = string; // eslint-disable-next-line @typescript-eslint/no-explicit-any type LogHandler = (value: any) => void; type StringFormatter = (value: string) => string; @@ -303,7 +301,6 @@ export { type LoadableWebpackConfiguration, type PackageInstallOptions, type PackageManager, - type Path, type PotentialPromise, type ProcessedArguments, type PromptOptions, diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 82e36cf096f..ebc251f5cb2 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -24,7 +24,6 @@ import { type LoadableWebpackConfiguration, type PackageInstallOptions, type PackageManager, - type Path, type PotentialPromise, type Problem, type ProcessedArguments, @@ -355,24 +354,6 @@ class WebpackCLI implements IWebpackCLI { process.exit(2); } - // TODO remove me - loadJSONFile(pathToFile: Path, handleError = true): T { - let result; - - try { - result = require(pathToFile); - } catch (error) { - if (handleError) { - this.logger.error(error); - process.exit(2); - } else { - throw error; - } - } - - return result; - } - getInfoOptions(): WebpackCLIBuiltInOption[] { return [ { @@ -1093,7 +1074,7 @@ class WebpackCLI implements IWebpackCLI { } async loadWebpack(): Promise { - return require(WEBPACK_PACKAGE); + return (await import(WEBPACK_PACKAGE)).default; } async #loadCommandByName(commandName: WebpackCLIOptions["name"], allowToInstall = false) { From c6fc1ba14bf77ef326a06a11458df8ad5a4f1ff2 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 25 Feb 2026 19:31:32 +0300 Subject: [PATCH 04/23] refactor: reduce count of `require` --- packages/webpack-cli/bin/cli.js | 14 +++++++++++++- packages/webpack-cli/src/bootstrap.ts | 19 ------------------- packages/webpack-cli/src/index.ts | 5 ----- packages/webpack-cli/src/webpack-cli.ts | 3 --- 4 files changed, 13 insertions(+), 28 deletions(-) delete mode 100644 packages/webpack-cli/src/bootstrap.ts diff --git a/packages/webpack-cli/bin/cli.js b/packages/webpack-cli/bin/cli.js index 42e9ba26588..1e98935cefe 100755 --- a/packages/webpack-cli/bin/cli.js +++ b/packages/webpack-cli/bin/cli.js @@ -3,7 +3,18 @@ "use strict"; const importLocal = require("import-local"); -const runCLI = require("../lib/bootstrap"); +const WebpackCLI = require("../lib/webpack-cli").default; + +const runCLI = async (args) => { + const cli = new WebpackCLI(); + + try { + await cli.run(args); + } catch (error) { + cli.logger.error(error); + process.exit(2); + } +}; if ( !process.env.WEBPACK_CLI_SKIP_IMPORT_LOCAL && // Prefer the local installation of `webpack-cli` @@ -14,4 +25,5 @@ if ( process.title = "webpack"; +// eslint-disable-next-line unicorn/prefer-top-level-await runCLI(process.argv); diff --git a/packages/webpack-cli/src/bootstrap.ts b/packages/webpack-cli/src/bootstrap.ts deleted file mode 100644 index e9a3ea3d322..00000000000 --- a/packages/webpack-cli/src/bootstrap.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type IWebpackCLI } from "./types.js"; -import WebpackCLI from "./webpack-cli.js"; - -const runCLI = async (args: Parameters[0]) => { - // Create a new instance of the CLI object - const cli: IWebpackCLI = new WebpackCLI(); - - try { - await cli.run(args); - } catch (error) { - cli.logger.error(error); - process.exit(2); - } -}; - -export default runCLI; - -// TODO remove me in the next major release and use `default` export -module.exports = runCLI; diff --git a/packages/webpack-cli/src/index.ts b/packages/webpack-cli/src/index.ts index 4e0f8a9a802..9619380e1d2 100644 --- a/packages/webpack-cli/src/index.ts +++ b/packages/webpack-cli/src/index.ts @@ -1,7 +1,2 @@ -import WebpackCLI from "./webpack-cli.js"; - export type * from "./types.js"; export { default } from "./webpack-cli.js"; - -// TODO remove me in the next major release and use `default` export -module.exports = WebpackCLI; diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index ebc251f5cb2..87af85be761 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -2721,6 +2721,3 @@ class WebpackCLI implements IWebpackCLI { } export default WebpackCLI; - -// TODO remove me in the next major release and use `default` export -module.exports = WebpackCLI; From 9d309bd765cd04972f43866a207609f06fabf83b Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 25 Feb 2026 19:49:47 +0300 Subject: [PATCH 05/23] refactor: reduce count of `require` --- packages/webpack-cli/src/webpack-cli.ts | 34 +++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 87af85be761..d941af277ef 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1,4 +1,5 @@ -import { type stringifyChunked } from "@discoveryjs/json-ext"; +import { type Readable as ReadableType } from "node:stream"; +import { type stringifyChunked as stringifyChunkedType } from "@discoveryjs/json-ext"; import { type Help, type ParseOptions } from "commander"; import { type Compiler, @@ -49,7 +50,6 @@ import { const fs = require("node:fs"); const path = require("node:path"); -const { Readable } = require("node:stream"); const { pathToFileURL } = require("node:url"); const util = require("node:util"); const { Option, program } = require("commander"); @@ -225,20 +225,28 @@ class WebpackCLI implements IWebpackCLI { async getDefaultPackageManager(): Promise { const { sync } = await import("cross-spawn"); - // TODO use async methods - const hasLocalNpm = fs.existsSync(path.resolve(process.cwd(), "package-lock.json")); + const hasLocalNpm = await fs.promises.access( + path.resolve(process.cwd(), "package-lock.json"), + fs.constants.F_OK, + ); if (hasLocalNpm) { return "npm"; } - const hasLocalYarn = fs.existsSync(path.resolve(process.cwd(), "yarn.lock")); + const hasLocalYarn = await fs.promises.access( + path.resolve(process.cwd(), "yarn.lock"), + fs.constants.F_OK, + ); if (hasLocalYarn) { return "yarn"; } - const hasLocalPnpm = fs.existsSync(path.resolve(process.cwd(), "pnpm-lock.yaml")); + const hasLocalPnpm = await fs.promises.access( + path.resolve(process.cwd(), "pnpm-lock.yaml"), + fs.constants.F_OK, + ); if (hasLocalPnpm) { return "pnpm"; @@ -2584,12 +2592,12 @@ class WebpackCLI implements IWebpackCLI { async runWebpack(options: WebpackRunOptions, isWatchCommand: boolean): Promise { let compiler: Compiler | MultiCompiler; - let createStringifyChunked: typeof stringifyChunked; + let stringifyChunked: typeof stringifyChunkedType; + let Readable: typeof ReadableType; if (options.json) { - const { stringifyChunked } = await import("@discoveryjs/json-ext"); - - createStringifyChunked = stringifyChunked; + ({ stringifyChunked } = await import("@discoveryjs/json-ext")); + ({ Readable } = await import("node:stream")); } const callback: WebpackCallback = (error, stats): void => { @@ -2616,20 +2624,20 @@ class WebpackCLI implements IWebpackCLI { ? (compiler.options.stats as StatsOptions) : undefined; - if (options.json && createStringifyChunked) { + if (options.json) { const handleWriteError = (error: WebpackError) => { this.logger.error(error); process.exit(2); }; if (options.json === true) { - Readable.from(createStringifyChunked(stats.toJson(statsOptions as StatsOptions))) + Readable.from(stringifyChunked(stats.toJson(statsOptions as StatsOptions))) .on("error", handleWriteError) .pipe(process.stdout) .on("error", handleWriteError) .on("close", () => process.stdout.write("\n")); } else { - Readable.from(createStringifyChunked(stats.toJson(statsOptions as StatsOptions))) + Readable.from(stringifyChunked(stats.toJson(statsOptions as StatsOptions))) .on("error", handleWriteError) .pipe(fs.createWriteStream(options.json)) .on("error", handleWriteError) From 8b77206db6b50bcfd12c41c944e12c19816017fc Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 25 Feb 2026 20:06:59 +0300 Subject: [PATCH 06/23] refactor: remove `colorette` in favor of build-in webpack colors --- packages/webpack-cli/package.json | 6 +----- packages/webpack-cli/src/webpack-cli.ts | 25 ++++++++++++------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/webpack-cli/package.json b/packages/webpack-cli/package.json index 0195e9a963d..5e10065c20b 100644 --- a/packages/webpack-cli/package.json +++ b/packages/webpack-cli/package.json @@ -32,10 +32,6 @@ ], "dependencies": { "@discoveryjs/json-ext": "^0.6.1", - "@webpack-cli/configtest": "^3.0.1", - "@webpack-cli/info": "^3.0.1", - "@webpack-cli/serve": "^3.0.1", - "colorette": "^2.0.14", "commander": "^12.1.0", "cross-spawn": "^7.0.6", "envinfo": "^7.14.0", @@ -49,7 +45,7 @@ "@types/envinfo": "^7.8.1" }, "peerDependencies": { - "webpack": "^5.82.0", + "webpack": "^5.101.0", "webpack-bundle-analyzer": "^4.0.0 || ^5.0.0", "webpack-dev-server": "^5.0.0" }, diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index d941af277ef..c7b82d5e623 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -159,23 +159,22 @@ class WebpackCLI implements IWebpackCLI { } createColors(useColor?: boolean): WebpackCLIColors { - try { - const { cli } = require("webpack"); - - if (typeof cli.createColors === "function") { - const { createColors, isColorSupported } = cli; - const shouldUseColor = useColor || isColorSupported(); + let cli: (typeof webpack)["cli"]; - return { ...createColors({ useColor: shouldUseColor }), isColorSupported: shouldUseColor }; - } + try { + cli = require("webpack").cli; } catch { - // Nothing + // Some big repos can have a problem with update webpack everywhere, so let's create a simple proxy for colors + return new Proxy({} as WebpackCLIColors, { + get() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (...args: any[]) => [...args]; + }, + }); } - // TODO remove `colorette` and set webpack@5.101.0 as the minimum supported version in the next major release - const { createColors, isColorSupported } = require("colorette"); - - const shouldUseColor = useColor || isColorSupported; + const { createColors, isColorSupported } = cli; + const shouldUseColor = useColor || isColorSupported(); return { ...createColors({ useColor: shouldUseColor }), isColorSupported: shouldUseColor }; } From d59b5784720dc204fe56ed58491fbd7be621f1a4 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 25 Feb 2026 20:17:40 +0300 Subject: [PATCH 07/23] refactor: reduce unnecessary types --- packages/webpack-cli/src/types.ts | 56 +------------------------ packages/webpack-cli/src/webpack-cli.ts | 14 +++---- 2 files changed, 7 insertions(+), 63 deletions(-) diff --git a/packages/webpack-cli/src/types.ts b/packages/webpack-cli/src/types.ts index 29870269794..69b4681a20d 100644 --- a/packages/webpack-cli/src/types.ts +++ b/packages/webpack-cli/src/types.ts @@ -1,19 +1,15 @@ -import { type Command, type CommandOptions, type Option, type ParseOptions } from "commander"; +import { type Command, type CommandOptions, type Option } from "commander"; import { type prepare } from "rechoir"; import { type AssetEmittedInfo, type Colors, - type Compiler, type Configuration, type EntryOptions, type FileCacheOptions, - type MultiCompiler, type MultiConfiguration, type MultiStats, type Stats, - type WebpackError, type WebpackOptionsNormalized, - default as webpack, } from "webpack"; import { @@ -30,52 +26,6 @@ declare interface WebpackCallback { (err: null | Error, result?: MultiStats): void; } -// TODO remove me in the next major release, we don't need extra interface -// TODO also revisit all methods - remove unused or make private -interface IWebpackCLI { - colors: WebpackCLIColors; - logger: WebpackCLILogger; - isColorSupportChanged: boolean | undefined; - webpack: typeof webpack; - program: WebpackCLICommand; - isMultipleCompiler(compiler: WebpackCompiler): compiler is MultiCompiler; - isPromise(value: Promise): value is Promise; - isFunction(value: unknown): value is CallableFunction; - getLogger(): WebpackCLILogger; - createColors(useColors?: boolean): WebpackCLIColors; - toKebabCase: StringFormatter; - capitalizeFirstLetter: StringFormatter; - checkPackageExists(packageName: string): boolean; - getDefaultPackageManager(): Promise; - doInstall(packageName: string, options?: PackageInstallOptions): Promise; - getInfoOptions(): WebpackCLIBuiltInOption[]; - getInfoOutput(options: { output: string; additionalPackage: string[] }): Promise; - makeCommand( - commandOptions: WebpackCLIOptions, - options: WebpackCLICommandOptions, - action: CommandAction, - ): Promise; - makeOption(command: WebpackCLICommand, option: WebpackCLIBuiltInOption): void; - run( - args: Parameters[0], - parseOptions?: ParseOptions, - ): Promise; - getBuiltInOptions(): WebpackCLIBuiltInOption[]; - loadWebpack(handleError?: boolean): Promise; - loadConfig(options: Partial): Promise; - buildConfig( - config: WebpackCLIConfig, - options: WebpackDevServerOptions, - ): Promise; - isValidationError(error: Error): error is WebpackError; - createCompiler( - options: Partial, - callback?: WebpackCallback, - ): Promise; - needWatchStdin(compiler: Compiler | MultiCompiler): boolean; - runWebpack(options: WebpackRunOptions, isWatchCommand: boolean): Promise; -} - interface WebpackCLIColors extends Colors { isColorSupported: boolean; } @@ -182,7 +132,6 @@ type WebpackDevServerOptions = DevServerConfig & */ type LoadableWebpackConfiguration = PotentialPromise; type CallableWebpackConfiguration = (env: Env | undefined, argv: Argv) => Configuration; -type WebpackCompiler = Compiler | MultiCompiler; interface EnumValueObject { [key: string]: EnumValue; @@ -247,7 +196,6 @@ type Instantiable< type PotentialPromise = T | Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any type LogHandler = (value: any) => void; -type StringFormatter = (value: string) => string; // eslint-disable-next-line @typescript-eslint/no-explicit-any interface Argv extends Record { @@ -296,7 +244,6 @@ export { type CommanderOption, type EnumValue, type FileSystemCacheOptions, - type IWebpackCLI, type Instantiable, type LoadableWebpackConfiguration, type PackageInstallOptions, @@ -318,7 +265,6 @@ export { type WebpackCLIMainOption, type WebpackCLIOptions, type WebpackCallback, - type WebpackCompiler, type WebpackDevServerOptions, type WebpackRunOptions, }; diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index c7b82d5e623..9d153bae36c 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -20,7 +20,6 @@ import { type CommandAction, type EnumValue, type FileSystemCacheOptions, - type IWebpackCLI, type Instantiable, type LoadableWebpackConfiguration, type PackageInstallOptions, @@ -43,7 +42,6 @@ import { type WebpackCLIMainOption, type WebpackCLIOptions, type WebpackCallback, - type WebpackCompiler, type WebpackDevServerOptions, type WebpackRunOptions, } from "./types.js"; @@ -98,7 +96,7 @@ class ConfigurationLoadingError extends Error { } } -class WebpackCLI implements IWebpackCLI { +class WebpackCLI { colors: WebpackCLIColors; logger: WebpackCLILogger; @@ -134,7 +132,7 @@ class WebpackCLI implements IWebpackCLI { return Array.isArray(config); } - isMultipleCompiler(compiler: WebpackCompiler): compiler is MultiCompiler { + isMultipleCompiler(compiler: Compiler | MultiCompiler): compiler is MultiCompiler { return (compiler as MultiCompiler).compilers as unknown as boolean; } @@ -1768,7 +1766,7 @@ class WebpackCLI implements IWebpackCLI { async run(args: readonly string[], parseOptions: ParseOptions) { // Default `--color` and `--no-color` options // eslint-disable-next-line @typescript-eslint/no-this-alias - const self: IWebpackCLI = this; + const self: WebpackCLI = this; // Register own exit this.program.exitOverride((error) => { @@ -2543,7 +2541,7 @@ class WebpackCLI implements IWebpackCLI { async createCompiler( options: Partial, callback?: WebpackCallback, - ): Promise { + ): Promise { if (typeof options.configNodeEnv === "string") { process.env.NODE_ENV = options.configNodeEnv; } else if (typeof options.nodeEnv === "string") { @@ -2553,7 +2551,7 @@ class WebpackCLI implements IWebpackCLI { let config = await this.loadConfig(options); config = await this.buildConfig(config, options); - let compiler: WebpackCompiler; + let compiler: Compiler | MultiCompiler; try { compiler = callback @@ -2677,7 +2675,7 @@ class WebpackCLI implements IWebpackCLI { return; } - const needGracefulShutdown = (compiler: WebpackCompiler): boolean => + const needGracefulShutdown = (compiler: Compiler | MultiCompiler): boolean => Boolean( this.isMultipleCompiler(compiler) ? compiler.compilers.some( From 10dd60f3b6ea591fc9b214f61b79530bc595e6c5 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 25 Feb 2026 21:20:56 +0300 Subject: [PATCH 08/23] refactor: reduce extra types --- packages/webpack-cli/src/index.ts | 2 +- .../webpack-cli/src/plugins/cli-plugin.ts | 11 +- packages/webpack-cli/src/types.ts | 273 ------------------ packages/webpack-cli/src/webpack-cli.ts | 265 +++++++++++++---- 4 files changed, 224 insertions(+), 327 deletions(-) delete mode 100644 packages/webpack-cli/src/types.ts diff --git a/packages/webpack-cli/src/index.ts b/packages/webpack-cli/src/index.ts index 9619380e1d2..27ca235f72f 100644 --- a/packages/webpack-cli/src/index.ts +++ b/packages/webpack-cli/src/index.ts @@ -1,2 +1,2 @@ -export type * from "./types.js"; +export type * from "./webpack-cli.js"; export { default } from "./webpack-cli.js"; diff --git a/packages/webpack-cli/src/plugins/cli-plugin.ts b/packages/webpack-cli/src/plugins/cli-plugin.ts index f304271d4f8..fe5a06bae67 100644 --- a/packages/webpack-cli/src/plugins/cli-plugin.ts +++ b/packages/webpack-cli/src/plugins/cli-plugin.ts @@ -1,5 +1,14 @@ import { type Compiler } from "webpack"; -import { type CLIPluginOptions } from "../types.js"; + +interface CLIPluginOptions { + isMultiCompiler?: boolean; + configPath?: string[]; + helpfulOutput: boolean; + hot?: boolean | "only"; + progress?: boolean | "profile"; + prefetch?: string; + analyze?: boolean; +} export default class CLIPlugin { logger!: ReturnType; diff --git a/packages/webpack-cli/src/types.ts b/packages/webpack-cli/src/types.ts deleted file mode 100644 index 69b4681a20d..00000000000 --- a/packages/webpack-cli/src/types.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { type Command, type CommandOptions, type Option } from "commander"; -import { type prepare } from "rechoir"; -import { - type AssetEmittedInfo, - type Colors, - type Configuration, - type EntryOptions, - type FileCacheOptions, - type MultiConfiguration, - type MultiStats, - type Stats, - type WebpackOptionsNormalized, -} from "webpack"; - -import { - type ClientConfiguration, - type Configuration as DevServerConfig, -} from "webpack-dev-server"; - -/** - * Webpack CLI - */ - -declare interface WebpackCallback { - (err: null | Error, result?: Stats): void; - (err: null | Error, result?: MultiStats): void; -} - -interface WebpackCLIColors extends Colors { - isColorSupported: boolean; -} - -interface WebpackCLILogger { - error: LogHandler; - warn: LogHandler; - info: LogHandler; - success: LogHandler; - log: LogHandler; - raw: LogHandler; -} - -interface WebpackCLICommandOption extends CommanderOption { - helpLevel?: "minimum" | "verbose"; -} - -interface WebpackCLIConfig { - options: Configuration | MultiConfiguration; - path: WeakMap; -} - -interface WebpackCLICommand extends Command { - pkg: string | undefined; - forHelp: boolean | undefined; - _args: WebpackCLICommandOption[]; -} - -type WebpackCLIMainOption = Pick< - WebpackCLIBuiltInOption, - "valueName" | "description" | "defaultValue" | "multiple" -> & { - flags: string; - type: Set; -}; - -interface WebpackCLIOptions extends CommandOptions { - rawName: string; - name: string; - alias: string | string[]; - description?: string; - usage?: string; - dependencies?: string[]; - pkg?: string; - argsDescription?: Record; - external?: boolean; -} - -type WebpackCLICommandOptions = - | WebpackCLIBuiltInOption[] - | (() => Promise); - -interface WebpackCLIBuiltInFlag { - name: string; - alias?: string; - type?: ( - value: string, - previous: Record, - ) => Record; - configs?: ArgumentConfig[]; - negative?: boolean; - multiple?: boolean; - valueName?: string; - description?: string; - describe?: string; - negatedDescription?: string; - defaultValue?: string; - helpLevel: "minimum" | "verbose"; -} - -interface WebpackCLIBuiltInOption extends WebpackCLIBuiltInFlag { - hidden?: boolean; - group?: "core"; -} - -/** - * Webpack dev server - */ - -type WebpackDevServerOptions = DevServerConfig & - Configuration & - ClientConfiguration & - AssetEmittedInfo & - WebpackOptionsNormalized & - FileCacheOptions & - Argv & { - nodeEnv?: string; - watchOptionsStdin?: boolean; - progress?: boolean | "profile"; - analyze?: boolean; - prefetch?: string; - json?: boolean; - entry: EntryOptions; - merge?: boolean; - config: string[]; - configName?: string[]; - disableInterpret?: boolean; - extends?: string[]; - argv: Argv; - }; - -/** - * Webpack - */ -type LoadableWebpackConfiguration = PotentialPromise; -type CallableWebpackConfiguration = (env: Env | undefined, argv: Argv) => Configuration; - -interface EnumValueObject { - [key: string]: EnumValue; -} -type EnumValueArray = EnumValue[]; -type EnumValue = string | number | boolean | EnumValueObject | EnumValueArray | null; - -interface ArgumentConfig { - description?: string; - negatedDescription?: string; - path?: string; - multiple?: boolean; - type: "enum" | "string" | "path" | "number" | "boolean" | "RegExp" | "reset"; - values?: EnumValue[]; -} - -type FileSystemCacheOptions = Configuration & { - cache: FileCacheOptions & { defaultConfig: string[] }; -}; - -type ProcessedArguments = Record; - -type CommandAction = Parameters[0]; - -interface WebpackRunOptions extends WebpackOptionsNormalized { - progress?: boolean | "profile"; - json?: boolean; - argv?: Argv; - env: Env; - failOnWarnings?: boolean; - isWatchingLikeCommand?: boolean; -} - -/** - * Package management - */ - -type PackageManager = "pnpm" | "yarn" | "npm"; -interface PackageInstallOptions { - preMessage?: () => void; -} - -/** - * Plugins and util types - */ - -interface CLIPluginOptions { - isMultiCompiler?: boolean; - configPath?: string[]; - helpfulOutput: boolean; - hot?: boolean | "only"; - progress?: boolean | "profile"; - prefetch?: string; - analyze?: boolean; -} - -type BasicPrimitive = string | boolean | number; -type Instantiable< - InstanceType = unknown, - ConstructorParameters extends unknown[] = unknown[], -> = new (...args: ConstructorParameters) => InstanceType; -type PotentialPromise = T | Promise; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type LogHandler = (value: any) => void; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -interface Argv extends Record { - env?: Env; -} - -interface Env { - WEBPACK_BUNDLE?: boolean; - WEBPACK_BUILD?: boolean; - WEBPACK_WATCH?: boolean; - WEBPACK_SERVE?: boolean; - WEBPACK_PACKAGE?: string; - WEBPACK_DEV_SERVER_PACKAGE?: string; -} - -/** - * External libraries types - */ -type OptionConstructor = new (flags: string, description?: string) => Option; -type CommanderOption = InstanceType; - -interface Rechoir { - prepare: typeof prepare; -} - -interface RechoirError extends Error { - failures: RechoirError[]; - error: Error; -} - -interface PromptOptions { - message: string; - defaultResponse: string; - stream: NodeJS.WritableStream; -} - -type StringsKeys = { [K in keyof T]: T[K] extends string ? K : never }[keyof T]; - -export { - type ArgumentConfig, - type Argv, - type BasicPrimitive, - type CLIPluginOptions, - type CallableWebpackConfiguration, - type CommandAction, - type CommanderOption, - type EnumValue, - type FileSystemCacheOptions, - type Instantiable, - type LoadableWebpackConfiguration, - type PackageInstallOptions, - type PackageManager, - type PotentialPromise, - type ProcessedArguments, - type PromptOptions, - type Rechoir, - type RechoirError, - type StringsKeys, - type WebpackCLIBuiltInFlag, - type WebpackCLIBuiltInOption, - type WebpackCLIColors, - type WebpackCLICommand, - type WebpackCLICommandOption, - type WebpackCLICommandOptions, - type WebpackCLIConfig, - type WebpackCLILogger, - type WebpackCLIMainOption, - type WebpackCLIOptions, - type WebpackCallback, - type WebpackDevServerOptions, - type WebpackRunOptions, -}; - -export { type CommandOptions } from "commander"; -export { type Argument, type Problem } from "webpack"; diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 9d153bae36c..17020999024 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1,50 +1,30 @@ import { type Readable as ReadableType } from "node:stream"; import { type stringifyChunked as stringifyChunkedType } from "@discoveryjs/json-ext"; -import { type Help, type ParseOptions } from "commander"; +import { type Command, type CommandOptions, type Help, type ParseOptions } from "commander"; +import { type prepare } from "rechoir"; import { + type Argument, + type AssetEmittedInfo, + type Colors, type Compiler, type Configuration, + type EntryOptions, + type FileCacheOptions, type MultiCompiler, type MultiConfiguration, + type MultiStats, type MultiStatsOptions, + type Problem, + type Stats, type StatsOptions, type WebpackError, + type WebpackOptionsNormalized, default as webpack, } from "webpack"; - import { - type Argument, - type Argv, - type BasicPrimitive, - type CallableWebpackConfiguration, - type CommandAction, - type EnumValue, - type FileSystemCacheOptions, - type Instantiable, - type LoadableWebpackConfiguration, - type PackageInstallOptions, - type PackageManager, - type PotentialPromise, - type Problem, - type ProcessedArguments, - type PromptOptions, - type Rechoir, - type RechoirError, - type StringsKeys, - type WebpackCLIBuiltInFlag, - type WebpackCLIBuiltInOption, - type WebpackCLIColors, - type WebpackCLICommand, - type WebpackCLICommandOption, - type WebpackCLICommandOptions, - type WebpackCLIConfig, - type WebpackCLILogger, - type WebpackCLIMainOption, - type WebpackCLIOptions, - type WebpackCallback, - type WebpackDevServerOptions, - type WebpackRunOptions, -} from "./types.js"; + type ClientConfiguration, + type Configuration as DevServerConfig, +} from "webpack-dev-server"; const fs = require("node:fs"); const path = require("node:path"); @@ -77,6 +57,190 @@ interface Information { npmPackages?: string | string[]; } +interface Rechoir { + prepare: typeof prepare; +} + +interface RechoirError extends Error { + failures: RechoirError[]; + error: Error; +} + +type PackageManager = "pnpm" | "yarn" | "npm"; + +type StringsKeys = { [K in keyof T]: T[K] extends string ? K : never }[keyof T]; + +// TODO simplify all types and avoid WebpackCLI prefix + +interface PromptOptions { + message: string; + defaultResponse: string; + stream: NodeJS.WritableStream; +} + +interface PackageInstallOptions { + preMessage?: () => void; +} + +interface WebpackCLIColors extends Colors { + isColorSupported: boolean; +} + +type ProcessedArguments = Record; + +interface WebpackCLICommand extends Command { + pkg: string | undefined; + forHelp: boolean | undefined; + _args: WebpackCLICommandOption[]; +} + +type CommandAction = Parameters[0]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type LogHandler = (value: any) => void; + +interface WebpackCLILogger { + error: LogHandler; + warn: LogHandler; + info: LogHandler; + success: LogHandler; + log: LogHandler; + raw: LogHandler; +} + +type FileSystemCacheOptions = Configuration & { + cache: FileCacheOptions & { defaultConfig: string[] }; +}; + +interface WebpackCLIConfigurationObj { + options: Configuration | MultiConfiguration; + path: WeakMap; +} + +interface Env { + WEBPACK_BUNDLE?: boolean; + WEBPACK_BUILD?: boolean; + WEBPACK_WATCH?: boolean; + WEBPACK_SERVE?: boolean; + WEBPACK_PACKAGE?: string; + WEBPACK_DEV_SERVER_PACKAGE?: string; +} + +interface WebpackCLIOptions extends CommandOptions { + rawName: string; + name: string; + alias: string | string[]; + description?: string; + usage?: string; + dependencies?: string[]; + pkg?: string; + argsDescription?: Record; + external?: boolean; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +interface Argv extends Record { + env?: Env; +} +type CallableWebpackConfiguration = (env: Env | undefined, argv: Argv) => Configuration; +// TODO wrong type here +type PotentialPromise = T | Promise; +type LoadableWebpackConfiguration = PotentialPromise; + +declare interface WebpackCallback { + (err: null | Error, result?: Stats): void; + (err: null | Error, result?: MultiStats): void; +} + +interface EnumValueObject { + [key: string]: EnumValue; +} +type BasicPrimitive = string | boolean | number; +type EnumValueArray = EnumValue[]; +type EnumValue = string | number | boolean | EnumValueObject | EnumValueArray | null; + +interface ArgumentConfig { + description?: string; + negatedDescription?: string; + path?: string; + multiple?: boolean; + type: "enum" | "string" | "path" | "number" | "boolean" | "RegExp" | "reset"; + values?: EnumValue[]; +} + +interface WebpackCLIBuiltInFlag { + name: string; + alias?: string; + type?: ( + value: string, + previous: Record, + ) => Record; + configs?: ArgumentConfig[]; + negative?: boolean; + multiple?: boolean; + valueName?: string; + description?: string; + describe?: string; + negatedDescription?: string; + defaultValue?: string; + helpLevel: "minimum" | "verbose"; +} + +interface WebpackCLIBuiltInOption extends WebpackCLIBuiltInFlag { + hidden?: boolean; + group?: "core"; +} + +interface WebpackRunOptions extends WebpackOptionsNormalized { + progress?: boolean | "profile"; + json?: boolean; + argv?: Argv; + env: Env; + failOnWarnings?: boolean; + isWatchingLikeCommand?: boolean; +} + +type OptionConstructor = new (flags: string, description?: string) => typeof Option; +type CommanderOption = InstanceType; + +interface WebpackCLICommandOption extends CommanderOption { + helpLevel?: "minimum" | "verbose"; +} + +type WebpackCLIMainOption = Pick< + WebpackCLIBuiltInOption, + "valueName" | "description" | "defaultValue" | "multiple" +> & { + flags: string; + type: Set; +}; + +type WebpackDevServerOptions = DevServerConfig & + Configuration & + ClientConfiguration & + AssetEmittedInfo & + WebpackOptionsNormalized & + FileCacheOptions & + Argv & { + nodeEnv?: string; + watchOptionsStdin?: boolean; + progress?: boolean | "profile"; + analyze?: boolean; + prefetch?: string; + json?: boolean; + entry: EntryOptions; + merge?: boolean; + config: string[]; + configName?: string[]; + disableInterpret?: boolean; + extends?: string[]; + argv: Argv; + }; + +type WebpackCLICommandOptions = + | WebpackCLIBuiltInOption[] + | (() => Promise); + type LoadConfigOption = PotentialPromise; class ConfigurationLoadingError extends Error { @@ -661,10 +825,7 @@ class WebpackCLI { if (mainOption.type.has(Number)) { let skipDefault = true; - const optionForCommand: WebpackCLICommandOption = new Option( - mainOption.flags, - mainOption.description, - ) + const optionForCommand = new Option(mainOption.flags, mainOption.description) .argParser((value: string, prev = []) => { if (mainOption.defaultValue && mainOption.multiple && skipDefault) { prev = []; @@ -681,10 +842,7 @@ class WebpackCLI { } else if (mainOption.type.has(String)) { let skipDefault = true; - const optionForCommand: WebpackCLICommandOption = new Option( - mainOption.flags, - mainOption.description, - ) + const optionForCommand = new Option(mainOption.flags, mainOption.description) .argParser((value: string, prev = []) => { if (mainOption.defaultValue && mainOption.multiple && skipDefault) { prev = []; @@ -1421,6 +1579,11 @@ class WebpackCLI { }); } + type Instantiable< + InstanceType = unknown, + ConstructorParameters extends unknown[] = unknown[], + > = new (...args: ConstructorParameters) => InstanceType; + let loadedCommand: Instantiable<() => void>; try { @@ -1508,10 +1671,8 @@ class WebpackCLI { command.options.length > 0 ? " [options]" : "" }`; }, - visibleOptions: function visibleOptions( - command: WebpackCLICommand, - ): WebpackCLICommandOption[] { - return command.options.filter((option: WebpackCLICommandOption) => { + visibleOptions: function visibleOptions(command: WebpackCLICommand) { + return command.options.filter((option) => { if (option.hidden) { return false; } @@ -1524,7 +1685,7 @@ class WebpackCLI { return false; } - switch (option.helpLevel) { + switch ((option as unknown as WebpackCLIBuiltInFlag).helpLevel) { case "verbose": return isVerbose; case "minimum": @@ -1599,7 +1760,7 @@ class WebpackCLI { } // Global options - const globalOptionList = program.options.map((option: WebpackCLICommandOption) => + const globalOptionList = program.options.map((option) => formatItem(helper.optionTerm(option), helper.optionDescription(option)), ); @@ -2112,7 +2273,7 @@ class WebpackCLI { }; }; - const config: WebpackCLIConfig = { + const config: WebpackCLIConfigurationObj = { options: {}, path: new WeakMap(), }; @@ -2217,7 +2378,7 @@ class WebpackCLI { const resolveExtends = async ( config: Configuration, - configPaths: WebpackCLIConfig["path"], + configPaths: WebpackCLIConfigurationObj["path"], extendsPaths: string[], ): Promise => { delete config.extends; @@ -2330,9 +2491,9 @@ class WebpackCLI { } async buildConfig( - config: WebpackCLIConfig, + config: WebpackCLIConfigurationObj, options: Partial, - ): Promise { + ): Promise { if (options.analyze && !this.checkPackageExists("webpack-bundle-analyzer")) { await this.doInstall("webpack-bundle-analyzer", { preMessage: () => { From 7c0fcdab6b0cd2046762b981944737dca73f7f24 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 00:59:36 +0300 Subject: [PATCH 09/23] refactor: types and make a lot of fixes --- packages/webpack-cli/src/webpack-cli.ts | 399 +++++++++++------------- 1 file changed, 188 insertions(+), 211 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 17020999024..b5e341f2e32 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1,14 +1,17 @@ import { type Readable as ReadableType } from "node:stream"; import { type stringifyChunked as stringifyChunkedType } from "@discoveryjs/json-ext"; -import { type Command, type CommandOptions, type Help, type ParseOptions } from "commander"; +import { + type Command, + type CommandOptions as CommanderCommandOptions, + type Help, + type ParseOptions, +} from "commander"; import { type prepare } from "rechoir"; import { type Argument, - type AssetEmittedInfo, - type Colors, + type Colors as WebpackColors, type Compiler, type Configuration, - type EntryOptions, type FileCacheOptions, type MultiCompiler, type MultiConfiguration, @@ -18,13 +21,9 @@ import { type Stats, type StatsOptions, type WebpackError, - type WebpackOptionsNormalized, default as webpack, } from "webpack"; -import { - type ClientConfiguration, - type Configuration as DevServerConfig, -} from "webpack-dev-server"; +import { type Configuration as DevServerConfiguration } from "webpack-dev-server"; const fs = require("node:fs"); const path = require("node:path"); @@ -70,36 +69,10 @@ type PackageManager = "pnpm" | "yarn" | "npm"; type StringsKeys = { [K in keyof T]: T[K] extends string ? K : never }[keyof T]; -// TODO simplify all types and avoid WebpackCLI prefix - -interface PromptOptions { - message: string; - defaultResponse: string; - stream: NodeJS.WritableStream; -} - -interface PackageInstallOptions { - preMessage?: () => void; -} - -interface WebpackCLIColors extends Colors { - isColorSupported: boolean; -} - -type ProcessedArguments = Record; - -interface WebpackCLICommand extends Command { - pkg: string | undefined; - forHelp: boolean | undefined; - _args: WebpackCLICommandOption[]; -} - -type CommandAction = Parameters[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any type LogHandler = (value: any) => void; -interface WebpackCLILogger { +interface Logger { error: LogHandler; warn: LogHandler; info: LogHandler; @@ -108,25 +81,11 @@ interface WebpackCLILogger { raw: LogHandler; } -type FileSystemCacheOptions = Configuration & { - cache: FileCacheOptions & { defaultConfig: string[] }; -}; - -interface WebpackCLIConfigurationObj { - options: Configuration | MultiConfiguration; - path: WeakMap; -} - -interface Env { - WEBPACK_BUNDLE?: boolean; - WEBPACK_BUILD?: boolean; - WEBPACK_WATCH?: boolean; - WEBPACK_SERVE?: boolean; - WEBPACK_PACKAGE?: string; - WEBPACK_DEV_SERVER_PACKAGE?: string; +interface Colors extends WebpackColors { + isColorSupported: boolean; } -interface WebpackCLIOptions extends CommandOptions { +interface CommandOptions extends CommanderCommandOptions { rawName: string; name: string; alias: string | string[]; @@ -138,26 +97,13 @@ interface WebpackCLIOptions extends CommandOptions { external?: boolean; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -interface Argv extends Record { - env?: Env; -} -type CallableWebpackConfiguration = (env: Env | undefined, argv: Argv) => Configuration; -// TODO wrong type here -type PotentialPromise = T | Promise; -type LoadableWebpackConfiguration = PotentialPromise; +type BasicPrimitive = string | boolean | number; -declare interface WebpackCallback { - (err: null | Error, result?: Stats): void; - (err: null | Error, result?: MultiStats): void; -} +type EnumValue = string | number | boolean | EnumValueObject | EnumValue[] | null; interface EnumValueObject { [key: string]: EnumValue; } -type BasicPrimitive = string | boolean | number; -type EnumValueArray = EnumValue[]; -type EnumValue = string | number | boolean | EnumValueObject | EnumValueArray | null; interface ArgumentConfig { description?: string; @@ -168,7 +114,7 @@ interface ArgumentConfig { values?: EnumValue[]; } -interface WebpackCLIBuiltInFlag { +interface Option { name: string; alias?: string; type?: ( @@ -184,64 +130,63 @@ interface WebpackCLIBuiltInFlag { negatedDescription?: string; defaultValue?: string; helpLevel: "minimum" | "verbose"; -} - -interface WebpackCLIBuiltInOption extends WebpackCLIBuiltInFlag { hidden?: boolean; group?: "core"; } -interface WebpackRunOptions extends WebpackOptionsNormalized { - progress?: boolean | "profile"; - json?: boolean; - argv?: Argv; - env: Env; - failOnWarnings?: boolean; - isWatchingLikeCommand?: boolean; +interface Env { + WEBPACK_BUNDLE?: boolean; + WEBPACK_BUILD?: boolean; + WEBPACK_WATCH?: boolean; + WEBPACK_SERVE?: boolean; } -type OptionConstructor = new (flags: string, description?: string) => typeof Option; -type CommanderOption = InstanceType; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +interface Argv extends Record { + env: Env; +} -interface WebpackCLICommandOption extends CommanderOption { - helpLevel?: "minimum" | "verbose"; +type CallableWebpackConfiguration = (env: Env, argv: Argv) => T; +type PotentialPromise = T | Promise; +type LoadableWebpackConfiguration = PotentialPromise< + | Configuration + | MultiConfiguration + | CallableWebpackConfiguration + // TODO revisit this support in future + | CallableWebpackConfiguration[] +>; + +interface ConfigurationsAndPaths { + options: Configuration | MultiConfiguration; + path: WeakMap; } -type WebpackCLIMainOption = Pick< - WebpackCLIBuiltInOption, - "valueName" | "description" | "defaultValue" | "multiple" -> & { - flags: string; - type: Set; -}; - -type WebpackDevServerOptions = DevServerConfig & - Configuration & - ClientConfiguration & - AssetEmittedInfo & - WebpackOptionsNormalized & - FileCacheOptions & - Argv & { - nodeEnv?: string; - watchOptionsStdin?: boolean; - progress?: boolean | "profile"; - analyze?: boolean; - prefetch?: string; - json?: boolean; - entry: EntryOptions; - merge?: boolean; - config: string[]; - configName?: string[]; - disableInterpret?: boolean; - extends?: string[]; - argv: Argv; - }; +declare interface WebpackCallback { + (err: null | Error, result?: Stats): void; + (err: null | Error, result?: MultiStats): void; +} -type WebpackCLICommandOptions = - | WebpackCLIBuiltInOption[] - | (() => Promise); +type ProcessedArguments = Parameters<(typeof webpack)["cli"]["processArguments"]>[2]; -type LoadConfigOption = PotentialPromise; +interface Options { + config?: string[]; + argv?: Argv; + env?: Env; + nodeEnv?: string; + configNodeEnv?: string; + watchOptionsStdin?: boolean; + watch?: boolean; + isWatchingLikeCommand?: boolean; + progress?: boolean | "profile"; + analyze?: boolean; + prefetch?: string; + json?: boolean; + entry?: string | string[]; + merge?: boolean; + configName?: string[]; + disableInterpret?: boolean; + extends?: string[]; +} class ConfigurationLoadingError extends Error { name = "ConfigurationLoadingError"; @@ -261,17 +206,17 @@ class ConfigurationLoadingError extends Error { } class WebpackCLI { - colors: WebpackCLIColors; + colors: Colors; - logger: WebpackCLILogger; + logger: Logger; isColorSupportChanged: boolean | undefined; - #builtInOptionsCache: WebpackCLIBuiltInOption[] | undefined; + #builtInOptionsCache: Option[] | undefined; webpack!: typeof webpack; - program: WebpackCLICommand; + program: Command; constructor() { this.colors = this.createColors(); @@ -320,14 +265,14 @@ class WebpackCLI { return str.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); } - createColors(useColor?: boolean): WebpackCLIColors { + createColors(useColor?: boolean): Colors { let cli: (typeof webpack)["cli"]; try { cli = require("webpack").cli; } catch { // Some big repos can have a problem with update webpack everywhere, so let's create a simple proxy for colors - return new Proxy({} as WebpackCLIColors, { + return new Proxy({} as Colors, { get() { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (...args: any[]) => [...args]; @@ -341,7 +286,7 @@ class WebpackCLI { return { ...createColors({ useColor: shouldUseColor }), isColorSupported: shouldUseColor }; } - getLogger(): WebpackCLILogger { + getLogger(): Logger { return { error: (val) => console.error(`[webpack-cli] ${this.colors.red(util.format(val))}`), warn: (val) => console.warn(`[webpack-cli] ${this.colors.yellow(val)}`), @@ -446,7 +391,7 @@ class WebpackCLI { } } - async doInstall(packageName: string, options: PackageInstallOptions = {}): Promise { + async doInstall(packageName: string, options: { preMessage?: () => void } = {}): Promise { const packageManager = await this.getDefaultPackageManager(); if (!packageManager) { @@ -461,7 +406,15 @@ class WebpackCLI { const { createInterface } = await import("node:readline"); - const prompt = ({ message, defaultResponse, stream }: PromptOptions) => { + const prompt = ({ + message, + defaultResponse, + stream, + }: { + message: string; + defaultResponse: string; + stream: NodeJS.WritableStream; + }) => { const rl = createInterface({ input: process.stdin, output: stream, @@ -523,7 +476,7 @@ class WebpackCLI { process.exit(2); } - getInfoOptions(): WebpackCLIBuiltInOption[] { + getInfoOptions(): Option[] { return [ { name: "output", @@ -606,10 +559,10 @@ class WebpackCLI { } async makeCommand( - commandOptions: WebpackCLIOptions, - options: WebpackCLICommandOptions, - action: CommandAction, - ): Promise { + commandOptions: CommandOptions, + options: Option[] | (() => Promise), + action: Parameters[0], + ): Promise { const alreadyLoaded = this.program.commands.find( (command) => command.name() === commandOptions.name.split(" ")[0] || @@ -623,7 +576,7 @@ class WebpackCLI { const command = this.program.command(commandOptions.name, { hidden: commandOptions.hidden, isDefault: commandOptions.isDefault, - }) as WebpackCLICommand; + }); if (commandOptions.description) { command.description(commandOptions.description, commandOptions.argsDescription!); @@ -639,9 +592,11 @@ class WebpackCLI { command.alias(commandOptions.alias); } - command.pkg = commandOptions.pkg || "webpack-cli"; + // TODO search API for this + (command as Command & { pkg: string }).pkg = commandOptions.pkg || "webpack-cli"; - const { forHelp } = this.program; + // TODO search a new API for this + const { forHelp } = this.program as Command & { forHelp: boolean }; let allDependenciesInstalled = true; @@ -710,9 +665,19 @@ class WebpackCLI { return command; } - makeOption(command: WebpackCLICommand, option: WebpackCLIBuiltInOption) { - let mainOption: WebpackCLIMainOption; - let negativeOption; + makeOption(command: Command, option: Option) { + type MainOption = Pick & { + flags: string; + type: Set; + }; + type NegativeOption = Pick< + Option, + "valueName" | "description" | "defaultValue" | "multiple" + > & { + flags: string; + }; + let mainOption: MainOption; + let negativeOption: NegativeOption | undefined; const flagsWithAlias = ["devtool", "output-path", "target", "watch", "extends"]; if (flagsWithAlias.includes(option.name)) { @@ -722,7 +687,7 @@ class WebpackCLI { if (option.configs) { let needNegativeOption = false; let negatedDescription; - const mainOptionType: WebpackCLIMainOption["type"] = new Set(); + const mainOptionType: MainOption["type"] = new Set(); for (const config of option.configs) { switch (config.type) { @@ -925,12 +890,12 @@ class WebpackCLI { } } - getBuiltInOptions(): WebpackCLIBuiltInOption[] { + getBuiltInOptions(): Option[] { if (this.#builtInOptionsCache) { return this.#builtInOptionsCache; } - const builtInFlags: WebpackCLIBuiltInFlag[] = [ + const builtInFlags: Option[] = [ // For configs { name: "config", @@ -1130,7 +1095,7 @@ class WebpackCLI { const minHelpSet = new Set(minimumHelpFlags); const coreArgs = this.webpack.cli.getArguments(); // Take memory - const options: WebpackCLIBuiltInOption[] = Array.from({ + const options: Option[] = Array.from({ length: builtInFlags.length + Object.keys(coreArgs).length, }); @@ -1157,7 +1122,7 @@ class WebpackCLI { static #commands: Record< "build" | "watch" | "version" | "help" | "serve" | "info" | "configtest", - WebpackCLIOptions + CommandOptions > = { build: { rawName: "build", @@ -1219,7 +1184,7 @@ class WebpackCLI { ); } - #isCommand(input: string, commandOptions: WebpackCLIOptions) { + #isCommand(input: string, commandOptions: CommandOptions) { const longName = commandOptions.rawName; if (input === longName) { @@ -1240,7 +1205,7 @@ class WebpackCLI { return (await import(WEBPACK_PACKAGE)).default; } - async #loadCommandByName(commandName: WebpackCLIOptions["name"], allowToInstall = false) { + async #loadCommandByName(commandName: string, allowToInstall = false) { const isBuildCommandUsed = this.#isCommand(commandName, WebpackCLI.#commands.build); const isWatchCommandUsed = this.#isCommand(commandName, WebpackCLI.#commands.watch); @@ -1264,9 +1229,9 @@ class WebpackCLI { const loadDevServerOptions = async () => { const devServer = (await import(WEBPACK_DEV_SERVER_PACKAGE)).default; - const options: Record = this.webpack.cli.getArguments( + const options: Record = this.webpack.cli.getArguments( devServer.schema, - ) as unknown as Record; + ) as unknown as Record; return Object.keys(options).map((key) => { options[key].name = key; @@ -1297,7 +1262,7 @@ class WebpackCLI { }, async (entries: string[], options) => { const builtInOptions = this.getBuiltInOptions(); - let devServerFlags: WebpackCLIBuiltInOption[] = []; + let devServerFlags: Option[] = []; try { devServerFlags = await loadDevServerOptions(); @@ -1305,8 +1270,8 @@ class WebpackCLI { // Nothing, to prevent future updates } - const webpackCLIOptions: Partial = {}; - const devServerCLIOptions: Record = {}; + const webpackCLIOptions: Partial = {}; + const devServerCLIOptions: Record = {}; for (const optionName in options) { const kebabedOption = this.toKebabCase(optionName); @@ -1315,18 +1280,14 @@ class WebpackCLI { ); if (isBuiltInOption) { - webpackCLIOptions[optionName] = options[optionName]; + webpackCLIOptions[optionName as keyof Options] = options[optionName]; } else { devServerCLIOptions[optionName] = options[optionName]; } } if (entries.length > 0) { - // @ts-expect-error Need investigate - webpackCLIOptions.entry = [ - ...(entries as string[]), - ...((webpackCLIOptions.entry || []) as string[]), - ]; + webpackCLIOptions.entry = [...entries, ...(options.entry || [])]; } webpackCLIOptions.argv = { @@ -1336,7 +1297,7 @@ class WebpackCLI { webpackCLIOptions.isWatchingLikeCommand = true; - const compiler = await this.createCompiler(webpackCLIOptions); + const compiler = await this.createCompiler(webpackCLIOptions as Options); if (!compiler) { return; @@ -1376,7 +1337,9 @@ class WebpackCLI { continue; } - const result = { ...compilerForDevServer.options.devServer }; + const devServerConfiguration: DevServerConfiguration = { + ...compilerForDevServer.options.devServer, + }; const args: Record = {}; @@ -1395,7 +1358,11 @@ class WebpackCLI { } if (Object.keys(values).length > 0) { - const problems = this.webpack.cli.processArguments(args, result, values); + const problems = this.webpack.cli.processArguments( + args, + devServerConfiguration, + values, + ); if (problems) { const groupBy = >( @@ -1403,7 +1370,7 @@ class WebpackCLI { key: K, ) => xs.reduce( - (rv: Record, problem: Problem) => { + (rv, problem) => { const path = problem[key]; (rv[path] ||= []).push(problem); @@ -1437,10 +1404,8 @@ class WebpackCLI { } } - const devServerOptions: WebpackDevServerOptions = result as WebpackDevServerOptions; - - if (devServerOptions.port) { - const portNumber = Number(devServerOptions.port); + if (devServerConfiguration.port) { + const portNumber = Number(devServerConfiguration.port); if (usedPorts.includes(portNumber)) { throw new Error( @@ -1452,7 +1417,7 @@ class WebpackCLI { } try { - const server = new DevServer(devServerOptions, compiler); + const server = new DevServer(devServerConfiguration, compiler); await server.start(); @@ -1505,7 +1470,11 @@ class WebpackCLI { async (configPath: string | undefined) => { this.webpack = await this.loadWebpack(); - const config = await this.loadConfig(configPath ? { config: [configPath] } : {}); + const env: Env = {}; + const argv: Argv = { env }; + const config = await this.loadConfig( + configPath ? { config: [configPath] } : { env, argv }, + ); const configPaths = new Set(); if (Array.isArray(config.options)) { @@ -1560,7 +1529,7 @@ class WebpackCLI { let pkg: string; if (builtInExternalCommandInfo) { - ({ pkg } = builtInExternalCommandInfo as WebpackCLIOptions & { pkg: string }); + ({ pkg } = builtInExternalCommandInfo as CommandOptions & { pkg: string }); } else { pkg = commandName; } @@ -1612,7 +1581,7 @@ class WebpackCLI { options: string[], isVerbose: boolean, isHelpCommandSyntax: boolean, - program: WebpackCLICommand, + program: Command, ) { const isOption = (value: string): boolean => value.startsWith("-"); const isGlobalOption = (value: string) => @@ -1639,7 +1608,7 @@ class WebpackCLI { program.configureHelp({ sortSubcommands: true, // Support multiple aliases - commandUsage: (command: WebpackCLICommand) => { + commandUsage: (command) => { let parentCmdNames = ""; for (let parentCmd = command.parent; parentCmd; parentCmd = parentCmd.parent) { @@ -1657,21 +1626,21 @@ class WebpackCLI { .join("|")} ${command.usage()}`; }, // Support multiple aliases - subcommandTerm: (command: WebpackCLICommand) => { - const humanReadableArgumentName = (argument: WebpackCLICommandOption) => { + subcommandTerm: (command) => { + const humanReadableArgumentName = (argument: typeof Option) => { const nameOutput = argument.name() + (argument.variadic ? "..." : ""); return argument.required ? `<${nameOutput}>` : `[${nameOutput}]`; }; - const args = command._args - .map((arg: WebpackCLICommandOption) => humanReadableArgumentName(arg)) + const args = command.registeredArguments + .map((arg) => humanReadableArgumentName(arg)) .join(" "); return `${command.name()}|${command.aliases().join("|")}${args ? ` ${args}` : ""}${ command.options.length > 0 ? " [options]" : "" }`; }, - visibleOptions: function visibleOptions(command: WebpackCLICommand) { + visibleOptions: function visibleOptions(command) { return command.options.filter((option) => { if (option.hidden) { return false; @@ -1685,7 +1654,7 @@ class WebpackCLI { return false; } - switch ((option as unknown as WebpackCLIBuiltInFlag).helpLevel) { + switch ((option as unknown as Option).helpLevel) { case "verbose": return isVerbose; case "minimum": @@ -1694,7 +1663,7 @@ class WebpackCLI { } }); }, - padWidth(command: WebpackCLICommand, helper: Help) { + padWidth(command, helper: Help) { return Math.max( helper.longestArgumentTermLength(command, helper), helper.longestOptionTermLength(command, helper), @@ -1703,7 +1672,7 @@ class WebpackCLI { helper.longestSubcommandTermLength(isGlobalHelp ? program : command, helper), ); }, - formatHelp: (command: WebpackCLICommand, helper: Help) => { + formatHelp: (command, helper: Help) => { const termWidth = helper.padWidth(command, helper); const helpWidth = helper.helpWidth || (process.env.WEBPACK_CLI_HELP_WIDTH as unknown as number) || 80; @@ -1847,7 +1816,7 @@ class WebpackCLI { process.exit(2); } - const option = (command as WebpackCLICommand).options.find( + const option = command.options.find( (option) => option.short === optionName || option.long === optionName, ); @@ -1968,7 +1937,7 @@ class WebpackCLI { const { distance } = require("fastest-levenshtein"); - for (const option of (command as WebpackCLICommand).options) { + for (const option of command.options) { if (!option.hidden && distance(name, option.long?.slice(2) as string) < 3) { this.logger.error(`Did you mean '--${option.name()}'?`); } @@ -1988,14 +1957,14 @@ class WebpackCLI { }); this.program.option("--color", "Enable colors on console."); - this.program.on("option:color", function color(this: WebpackCLICommand) { + this.program.on("option:color", function color(this: Command) { const { color } = this.opts(); self.isColorSupportChanged = color; self.colors = self.createColors(color); }); this.program.option("--no-color", "Disable colors on console."); - this.program.on("option:no-color", function noColor(this: WebpackCLICommand) { + this.program.on("option:no-color", function noColor(this: Command) { const { color } = this.opts(); self.isColorSupportChanged = color; @@ -2040,7 +2009,7 @@ class WebpackCLI { isVerbose = true; } - this.program.forHelp = true; + (this.program as Command & { forHelp: boolean }).forHelp = true; const optionsForHelp = [ ...(isHelpOption && hasOperand ? [operand] : []), @@ -2127,8 +2096,8 @@ class WebpackCLI { async #loadConfigurationFile( configPath: string, disableInterpret = false, - ): Promise { - let pkg: LoadConfigOption | LoadConfigOption[] | undefined; + ): Promise { + let pkg: LoadableWebpackConfiguration | undefined; let loadingError; @@ -2187,7 +2156,7 @@ class WebpackCLI { // To handle `babel`/`module.exports.default = {};` if (pkg && typeof pkg === "object" && "default" in pkg) { - pkg = pkg.default as LoadConfigOption | LoadConfigOption[] | undefined; + pkg = pkg.default as LoadableWebpackConfiguration | undefined; } if (!pkg) { @@ -2199,15 +2168,15 @@ class WebpackCLI { return pkg || {}; } - async loadConfig(options: Partial) { + async loadConfig(options: Options) { const disableInterpret = typeof options.disableInterpret !== "undefined" && options.disableInterpret; const loadConfigByPath = async ( configPath: string, - argv: Argv = {}, - ): Promise<{ options: Configuration | Configuration[]; path: string }> => { - let options: LoadableWebpackConfiguration | LoadableWebpackConfiguration[] | undefined; + argv: Argv = { env: {} }, + ): Promise<{ options: Configuration | MultiConfiguration; path: string }> => { + let options: LoadableWebpackConfiguration | undefined; try { options = await this.#loadConfigurationFile(configPath, disableInterpret); @@ -2223,13 +2192,14 @@ class WebpackCLI { } if (Array.isArray(options)) { - // reassign the value to assert type const optionsArray: LoadableWebpackConfiguration[] = options; await Promise.all( optionsArray.map(async (_, i) => { if ( - this.isPromise( - optionsArray[i] as Promise, + this.isPromise>( + optionsArray[i] as Promise< + Configuration | CallableWebpackConfiguration + >, ) ) { optionsArray[i] = await optionsArray[i]; @@ -2241,7 +2211,7 @@ class WebpackCLI { } }), ); - options = optionsArray; + options = optionsArray as MultiConfiguration; } else { if ( this.isPromise( @@ -2268,12 +2238,12 @@ class WebpackCLI { } return { - options: options as Configuration | Configuration[], + options: options as Configuration | MultiConfiguration, path: configPath, }; }; - const config: WebpackCLIConfigurationObj = { + const config: ConfigurationsAndPaths = { options: {}, path: new WeakMap(), }; @@ -2364,7 +2334,7 @@ class WebpackCLI { } return found; - }) as Configuration[]; + }) as MultiConfiguration; if (notFoundConfigNames.length > 0) { this.logger.error( @@ -2378,7 +2348,7 @@ class WebpackCLI { const resolveExtends = async ( config: Configuration, - configPaths: WebpackCLIConfigurationObj["path"], + configPaths: ConfigurationsAndPaths["path"], extendsPaths: string[], ): Promise => { delete config.extends; @@ -2491,9 +2461,9 @@ class WebpackCLI { } async buildConfig( - config: WebpackCLIConfigurationObj, - options: Partial, - ): Promise { + config: ConfigurationsAndPaths, + options: Options, + ): Promise { if (options.analyze && !this.checkPackageExists("webpack-bundle-analyzer")) { await this.doInstall("webpack-bundle-analyzer", { preMessage: () => { @@ -2538,7 +2508,7 @@ class WebpackCLI { const kebabName = this.toKebabCase(name); if (args[kebabName] !== undefined) { - values[kebabName] = options[name]; + values[kebabName] = options[name as keyof Options] as string[]; } } @@ -2547,13 +2517,16 @@ class WebpackCLI { if (problems) { const groupBy = >(xs: Problem[], key: K) => - xs.reduce((rv: Record, problem: Problem) => { - const path = problem[key]; + xs.reduce( + (rv, problem) => { + const path = problem[key]; - (rv[path] ||= []).push(problem); + (rv[path] ||= []).push(problem); - return rv; - }, {}); + return rv; + }, + {} as Record, + ); const problemsByPath = groupBy(problems, "path"); for (const path in problemsByPath) { @@ -2599,8 +2572,12 @@ class WebpackCLI { } } - const isFileSystemCacheOptions = (config: Configuration): config is FileSystemCacheOptions => - Boolean(config.cache) && (config as FileSystemCacheOptions).cache.type === "filesystem"; + const isFileSystemCacheOptions = ( + config: Configuration, + ): config is Configuration & { cache: FileCacheOptions } => + typeof config.cache !== "undefined" && + typeof config.cache !== "boolean" && + config.cache.type === "filesystem"; // Setup default cache options if (isFileSystemCacheOptions(item) && Object.isExtensible(item.cache)) { @@ -2700,7 +2677,7 @@ class WebpackCLI { } async createCompiler( - options: Partial, + options: Options, callback?: WebpackCallback, ): Promise { if (typeof options.configNodeEnv === "string") { @@ -2748,7 +2725,7 @@ class WebpackCLI { return Boolean(compiler.options.watchOptions?.stdin); } - async runWebpack(options: WebpackRunOptions, isWatchCommand: boolean): Promise { + async runWebpack(options: Options, isWatchCommand: boolean): Promise { let compiler: Compiler | MultiCompiler; let stringifyChunked: typeof stringifyChunkedType; let Readable: typeof ReadableType; @@ -2764,7 +2741,7 @@ class WebpackCLI { process.exit(2); } - if (stats && (stats.hasErrors() || (options.failOnWarnings && stats.hasWarnings()))) { + if (stats && stats.hasErrors()) { process.exitCode = 1; } @@ -2783,7 +2760,7 @@ class WebpackCLI { : undefined; if (options.json) { - const handleWriteError = (error: WebpackError) => { + const handleWriteError = (error: Error) => { this.logger.error(error); process.exit(2); }; @@ -2830,7 +2807,7 @@ class WebpackCLI { options.isWatchingLikeCommand = true; } - compiler = await this.createCompiler(options as WebpackDevServerOptions, callback); + compiler = await this.createCompiler(options, callback); if (!compiler) { return; From ef99748da40225d652ee232c5b7bf9accbce4c6c Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 02:49:53 +0300 Subject: [PATCH 10/23] refactor: code --- packages/webpack-cli/src/webpack-cli.ts | 228 +++++++++++------------- 1 file changed, 102 insertions(+), 126 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index b5e341f2e32..830f98312f2 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1,14 +1,21 @@ +import fs from "node:fs"; +import path from "node:path"; import { type Readable as ReadableType } from "node:stream"; +import { pathToFileURL } from "node:url"; +import util from "node:util"; import { type stringifyChunked as stringifyChunkedType } from "@discoveryjs/json-ext"; import { + type Argument, type Command, type CommandOptions as CommanderCommandOptions, type Help, + Option, type ParseOptions, + program, } from "commander"; import { type prepare } from "rechoir"; import { - type Argument, + type Argument as WebpackArgument, type Colors as WebpackColors, type Compiler, type Configuration, @@ -25,12 +32,6 @@ import { } from "webpack"; import { type Configuration as DevServerConfiguration } from "webpack-dev-server"; -const fs = require("node:fs"); -const path = require("node:path"); -const { pathToFileURL } = require("node:url"); -const util = require("node:util"); -const { Option, program } = require("commander"); - const WEBPACK_PACKAGE_IS_CUSTOM = Boolean(process.env.WEBPACK_PACKAGE); const WEBPACK_PACKAGE = WEBPACK_PACKAGE_IS_CUSTOM ? (process.env.WEBPACK_PACKAGE as string) @@ -114,7 +115,7 @@ interface ArgumentConfig { values?: EnumValue[]; } -interface Option { +interface CommandOption { name: string; alias?: string; type?: ( @@ -129,6 +130,7 @@ interface Option { describe?: string; negatedDescription?: string; defaultValue?: string; + // TODO search API helpLevel: "minimum" | "verbose"; hidden?: boolean; group?: "core"; @@ -212,7 +214,7 @@ class WebpackCLI { isColorSupportChanged: boolean | undefined; - #builtInOptionsCache: Option[] | undefined; + #builtInOptionsCache: CommandOption[] | undefined; webpack!: typeof webpack; @@ -269,7 +271,7 @@ class WebpackCLI { let cli: (typeof webpack)["cli"]; try { - cli = require("webpack").cli; + cli = require(WEBPACK_PACKAGE).cli; } catch { // Some big repos can have a problem with update webpack everywhere, so let's create a simple proxy for colors return new Proxy({} as Colors, { @@ -297,7 +299,7 @@ class WebpackCLI { }; } - checkPackageExists(packageName: string): boolean { + async checkPackageExists(packageName: string): Promise { if (process.versions.pnp) { return true; } @@ -306,7 +308,9 @@ class WebpackCLI { do { try { - if (fs.statSync(path.join(dir, "node_modules", packageName)).isDirectory()) { + const stats = await fs.promises.stat(path.join(dir, "node_modules", packageName)); + + if (stats.isDirectory()) { return true; } } catch { @@ -314,10 +318,15 @@ class WebpackCLI { } } while (dir !== (dir = path.dirname(dir))); + // @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 - for (const internalPath of require("node:module").globalPaths) { + for (const internalPath of globalPaths) { try { - if (fs.statSync(path.join(internalPath, packageName)).isDirectory()) { + const stats = await fs.promises.stat(path.join(internalPath, packageName)); + + if (stats.isDirectory()) { return true; } } catch { @@ -331,31 +340,25 @@ class WebpackCLI { async getDefaultPackageManager(): Promise { const { sync } = await import("cross-spawn"); - const hasLocalNpm = await fs.promises.access( - path.resolve(process.cwd(), "package-lock.json"), - fs.constants.F_OK, - ); - - if (hasLocalNpm) { + try { + await fs.promises.access(path.resolve(process.cwd(), "package-lock.json"), fs.constants.F_OK); return "npm"; + } catch { + // Nothing } - const hasLocalYarn = await fs.promises.access( - path.resolve(process.cwd(), "yarn.lock"), - fs.constants.F_OK, - ); - - if (hasLocalYarn) { + try { + await fs.promises.access(path.resolve(process.cwd(), "yarn.lock"), fs.constants.F_OK); return "yarn"; + } catch { + // Nothing } - const hasLocalPnpm = await fs.promises.access( - path.resolve(process.cwd(), "pnpm-lock.yaml"), - fs.constants.F_OK, - ); - - if (hasLocalPnpm) { + try { + await fs.promises.access(path.resolve(process.cwd(), "pnpm-lock.yaml"), fs.constants.F_OK); return "pnpm"; + } catch { + // Nothing } try { @@ -476,7 +479,7 @@ class WebpackCLI { process.exit(2); } - getInfoOptions(): Option[] { + getInfoOptions(): CommandOption[] { return [ { name: "output", @@ -560,7 +563,7 @@ class WebpackCLI { async makeCommand( commandOptions: CommandOptions, - options: Option[] | (() => Promise), + options: CommandOption[] | (() => Promise), action: Parameters[0], ): Promise { const alreadyLoaded = this.program.commands.find( @@ -602,7 +605,8 @@ class WebpackCLI { if (commandOptions.dependencies && commandOptions.dependencies.length > 0) { for (const dependency of commandOptions.dependencies) { - const isPkgExist = this.checkPackageExists(dependency); + // TODO do we really need this for webpack itself? + const isPkgExist = await this.checkPackageExists(dependency); if (isPkgExist) { continue; @@ -665,13 +669,16 @@ class WebpackCLI { return command; } - makeOption(command: Command, option: Option) { - type MainOption = Pick & { + makeOption(command: Command, option: CommandOption) { + type MainOption = Pick< + CommandOption, + "valueName" | "description" | "defaultValue" | "multiple" + > & { flags: string; type: Set; }; type NegativeOption = Pick< - Option, + CommandOption, "valueName" | "description" | "defaultValue" | "multiple" > & { flags: string; @@ -791,34 +798,34 @@ class WebpackCLI { let skipDefault = true; const optionForCommand = new Option(mainOption.flags, mainOption.description) - .argParser((value: string, prev = []) => { + .argParser((value: string, prev: number | number[] = []) => { if (mainOption.defaultValue && mainOption.multiple && skipDefault) { prev = []; skipDefault = false; } - return mainOption.multiple ? [...prev, Number(value)] : Number(value); + return mainOption.multiple ? [...(prev as number[]), Number(value)] : Number(value); }) .default(mainOption.defaultValue); - optionForCommand.helpLevel = option.helpLevel; + (optionForCommand as Option & { helpLevel: string }).helpLevel = option.helpLevel; command.addOption(optionForCommand); } else if (mainOption.type.has(String)) { let skipDefault = true; const optionForCommand = new Option(mainOption.flags, mainOption.description) - .argParser((value: string, prev = []) => { + .argParser((value: string, prev: string | string[] = []) => { if (mainOption.defaultValue && mainOption.multiple && skipDefault) { prev = []; skipDefault = false; } - return mainOption.multiple ? [...prev, value] : value; + return mainOption.multiple ? [...(prev as string[]), value] : value; }) .default(mainOption.defaultValue); - optionForCommand.helpLevel = option.helpLevel; + (optionForCommand as Option & { helpLevel: string }).helpLevel = option.helpLevel; command.addOption(optionForCommand); } else if (mainOption.type.has(Boolean)) { @@ -826,27 +833,23 @@ class WebpackCLI { mainOption.defaultValue, ); - optionForCommand.helpLevel = option.helpLevel; + (optionForCommand as Option & { helpLevel: string }).helpLevel = option.helpLevel; command.addOption(optionForCommand); } else { const optionForCommand = new Option(mainOption.flags, mainOption.description) - .argParser([...mainOption.type][0]) + .argParser([...mainOption.type][0] as (value: string, previous: unknown) => unknown) .default(mainOption.defaultValue); - optionForCommand.helpLevel = option.helpLevel; + (optionForCommand as Option & { helpLevel: string }).helpLevel = option.helpLevel; command.addOption(optionForCommand); } } else if (mainOption.type.size > 1) { let skipDefault = true; - const optionForCommand = new Option( - mainOption.flags, - mainOption.description, - mainOption.defaultValue, - ) - .argParser((value: string, prev = []) => { + const optionForCommand = new Option(mainOption.flags, mainOption.description) + .argParser((value: string, prev: number | string | number[] | string[] = []) => { if (mainOption.defaultValue && mainOption.multiple && skipDefault) { prev = []; skipDefault = false; @@ -856,19 +859,19 @@ class WebpackCLI { const numberValue = Number(value); if (!Number.isNaN(numberValue)) { - return mainOption.multiple ? [...prev, numberValue] : numberValue; + return mainOption.multiple ? [...(prev as number[]), numberValue] : numberValue; } } if (mainOption.type.has(String)) { - return mainOption.multiple ? [...prev, value] : value; + return mainOption.multiple ? [...(prev as string[]), value] : value; } return value; }) .default(mainOption.defaultValue); - optionForCommand.helpLevel = option.helpLevel; + (optionForCommand as Option & { helpLevel: string }).helpLevel = option.helpLevel; command.addOption(optionForCommand); } else if (mainOption.type.size === 0 && negativeOption) { @@ -876,7 +879,7 @@ class WebpackCLI { // Hide stub option optionForCommand.hideHelp(); - optionForCommand.helpLevel = option.helpLevel; + (optionForCommand as Option & { helpLevel: string }).helpLevel = option.helpLevel; command.addOption(optionForCommand); } @@ -884,18 +887,18 @@ class WebpackCLI { if (negativeOption) { const optionForCommand = new Option(negativeOption.flags, negativeOption.description); - optionForCommand.helpLevel = option.helpLevel; + (optionForCommand as Option & { helpLevel: string }).helpLevel = option.helpLevel; command.addOption(optionForCommand); } } - getBuiltInOptions(): Option[] { + getBuiltInOptions(): CommandOption[] { if (this.#builtInOptionsCache) { return this.#builtInOptionsCache; } - const builtInFlags: Option[] = [ + const builtInFlags: CommandOption[] = [ // For configs { name: "config", @@ -1095,7 +1098,7 @@ class WebpackCLI { const minHelpSet = new Set(minimumHelpFlags); const coreArgs = this.webpack.cli.getArguments(); // Take memory - const options: Option[] = Array.from({ + const options: CommandOption[] = Array.from({ length: builtInFlags.length + Object.keys(coreArgs).length, }); @@ -1229,9 +1232,10 @@ class WebpackCLI { const loadDevServerOptions = async () => { const devServer = (await import(WEBPACK_DEV_SERVER_PACKAGE)).default; - const options: Record = this.webpack.cli.getArguments( - devServer.schema, - ) as unknown as Record; + const options = this.webpack.cli.getArguments(devServer.schema) as unknown as Record< + string, + CommandOption + >; return Object.keys(options).map((key) => { options[key].name = key; @@ -1262,7 +1266,7 @@ class WebpackCLI { }, async (entries: string[], options) => { const builtInOptions = this.getBuiltInOptions(); - let devServerFlags: Option[] = []; + let devServerFlags: CommandOption[] = []; try { devServerFlags = await loadDevServerOptions(); @@ -1271,7 +1275,7 @@ class WebpackCLI { } const webpackCLIOptions: Partial = {}; - const devServerCLIOptions: Record = {}; + const devServerCLIOptions: Record = {}; for (const optionName in options) { const kebabedOption = this.toKebabCase(optionName); @@ -1341,10 +1345,10 @@ class WebpackCLI { ...compilerForDevServer.options.devServer, }; - const args: Record = {}; + const args: Record = {}; for (const flag of devServerFlags) { - args[flag.name] = flag as unknown as Argument; + args[flag.name] = flag as unknown as WebpackArgument; } const values: ProcessedArguments = {}; @@ -1534,7 +1538,7 @@ class WebpackCLI { pkg = commandName; } - if (pkg !== "webpack-cli" && !this.checkPackageExists(pkg)) { + if (pkg !== "webpack-cli" && !(await this.checkPackageExists(pkg))) { if (!allowToInstall) { return; } @@ -1627,7 +1631,7 @@ class WebpackCLI { }, // Support multiple aliases subcommandTerm: (command) => { - const humanReadableArgumentName = (argument: typeof Option) => { + const humanReadableArgumentName = (argument: Argument) => { const nameOutput = argument.name() + (argument.variadic ? "..." : ""); return argument.required ? `<${nameOutput}>` : `[${nameOutput}]`; @@ -1654,7 +1658,7 @@ class WebpackCLI { return false; } - switch ((option as unknown as Option).helpLevel) { + switch ((option as unknown as CommandOption).helpLevel) { case "verbose": return isVerbose; case "minimum": @@ -1902,58 +1906,20 @@ class WebpackCLI { this.program.exitOverride((error) => { if (error.exitCode === 0) { process.exit(0); + return; } - if (error.code === "executeSubCommandAsync") { - process.exit(2); - } + const isInfo = ["commander.helpDisplayed", "commander.version"].includes(error.code); - if (error.code === "commander.help") { + if (isInfo) { process.exit(0); + return; } - if (error.code === "commander.unknownOption") { - let name = error.message.match(/'(.+)'/) as string | null; - - if (name) { - name = name[1].slice(2); - - if (name.includes("=")) { - [name] = name.split("="); - } - - const { operands } = this.program.parseOptions(this.program.args); - const operand = - typeof operands[0] !== "undefined" ? operands[0] : WebpackCLI.#commands.build.rawName; - - if (operand) { - const command = this.#findCommandByName(operand); - - if (!command) { - this.logger.error(`Can't find and load command '${operand}'`); - this.logger.error("Run 'webpack --help' to see available commands and options"); - process.exit(2); - } - - const { distance } = require("fastest-levenshtein"); - - for (const option of command.options) { - if (!option.hidden && distance(name, option.long?.slice(2) as string) < 3) { - this.logger.error(`Did you mean '--${option.name()}'?`); - } - } - } - } - } - - // Codes: - // - commander.unknownCommand - // - commander.missingArgument - // - commander.missingMandatoryOptionValue - // - commander.optionMissingArgument - this.logger.error("Run 'webpack --help' to see available commands and options"); process.exit(2); + + throw error; }); this.program.option("--color", "Enable colors on console."); @@ -1990,7 +1956,7 @@ class WebpackCLI { // That is why we need to set `allowUnknownOption` to `true`, otherwise commander will not work this.program.allowUnknownOption(true); this.program.action(async (options) => { - const { operands, unknown } = this.program.parseOptions(program.args); + const { operands, unknown } = this.program.parseOptions(this.program.args); const defaultCommandNameToRun = WebpackCLI.#commands.build.rawName; const hasOperand = typeof operands[0] !== "undefined"; const operand = hasOperand ? operands[0] : defaultCommandNameToRun; @@ -2021,7 +1987,7 @@ class WebpackCLI { ...(isHelpCommandSyntax && typeof options.version !== "undefined" ? ["--version"] : []), ]; - await this.#outputHelp(optionsForHelp, isVerbose, isHelpCommandSyntax, program); + await this.#outputHelp(optionsForHelp, isVerbose, isHelpCommandSyntax, this.program); } const isVersionOption = typeof options.version !== "undefined"; @@ -2052,7 +2018,14 @@ class WebpackCLI { if (isKnownCommand) { await this.#loadCommandByName(commandNameToRun, true); } else { - const isEntrySyntax = fs.existsSync(operand); + let isEntrySyntax: boolean; + + try { + await fs.promises.access(operand, fs.constants.F_OK); + isEntrySyntax = true; + } catch { + isEntrySyntax = false; + } if (isEntrySyntax) { commandNameToRun = defaultCommandNameToRun; @@ -2294,12 +2267,13 @@ class WebpackCLI { let foundDefaultConfigFile; for (const defaultConfigFile of defaultConfigFiles) { - if (!fs.existsSync(defaultConfigFile)) { + try { + await fs.promises.access(defaultConfigFile, fs.constants.F_OK); + foundDefaultConfigFile = defaultConfigFile; + break; + } catch { continue; } - - foundDefaultConfigFile = defaultConfigFile; - break; } if (foundDefaultConfigFile) { @@ -2464,7 +2438,7 @@ class WebpackCLI { config: ConfigurationsAndPaths, options: Options, ): Promise { - if (options.analyze && !this.checkPackageExists("webpack-bundle-analyzer")) { + if (options.analyze && !(await this.checkPackageExists("webpack-bundle-analyzer"))) { await this.doInstall("webpack-bundle-analyzer", { preMessage: () => { this.logger.error( @@ -2492,11 +2466,11 @@ class WebpackCLI { // Apply options const builtInOptions = this.getBuiltInOptions(); - const args: Record = {}; + const args: Record = {}; for (const flag of builtInOptions) { if (flag.group === "core") { - args[flag.name] = flag as unknown as Argument; + args[flag.name] = flag as unknown as WebpackArgument; } } @@ -2682,7 +2656,9 @@ class WebpackCLI { ): Promise { if (typeof options.configNodeEnv === "string") { process.env.NODE_ENV = options.configNodeEnv; - } else if (typeof options.nodeEnv === "string") { + } + // TODO remove in the next major release + else if (typeof options.nodeEnv === "string") { process.env.NODE_ENV = options.nodeEnv; } From 583501a7f31123f1a27da0a7574938c7a5a62a97 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 03:17:03 +0300 Subject: [PATCH 11/23] test: fix --- packages/webpack-cli/src/webpack-cli.ts | 1 + test/api/CLI.test.js | 19 ++++++++----------- test/api/capitalizeFirstLetter.test.js | 2 +- test/api/do-install.test.js | 2 +- test/api/get-default-package-manager.test.js | 2 +- .../api/resolve-config/resolve-config.test.js | 2 +- 6 files changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 830f98312f2..3a73b83bdb8 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1828,6 +1828,7 @@ class WebpackCLI { this.logger.error(`Unknown option '${optionName}'`); this.logger.error("Run 'webpack --help' to see available commands and options"); process.exit(2); + return; } const nameOutput = diff --git a/test/api/CLI.test.js b/test/api/CLI.test.js index 28de85d0cc2..30e5f946c20 100644 --- a/test/api/CLI.test.js +++ b/test/api/CLI.test.js @@ -1,4 +1,4 @@ -const CLI = require("../../packages/webpack-cli/lib/webpack-cli"); +const CLI = require("../../packages/webpack-cli/lib/webpack-cli").default; describe("CLI API", () => { let cli; @@ -1700,15 +1700,6 @@ describe("CLI API", () => { beforeEach(async () => { consoleSpy = jest.spyOn(globalThis.console, "log"); exitSpy = jest.spyOn(process, "exit").mockImplementation(() => {}); - - await new Promise((resolve, reject) => { - try { - cli.run(["help", "--mode"], { from: "user" }); - resolve(); - } catch (error) { - reject(error); - } - }); }); afterEach(async () => { @@ -1716,7 +1707,13 @@ describe("CLI API", () => { exitSpy.mockRestore(); }); - it("should display help information", () => { + it("should display help information", async () => { + try { + await cli.run(["help", "--mode"], { from: "user" }); + } catch { + // Nothing for tests + } + expect(exitSpy).toHaveBeenCalledWith(0); expect(consoleSpy.mock.calls).toMatchSnapshot(); }); diff --git a/test/api/capitalizeFirstLetter.test.js b/test/api/capitalizeFirstLetter.test.js index 26ae394c9f9..01d11b77b4c 100755 --- a/test/api/capitalizeFirstLetter.test.js +++ b/test/api/capitalizeFirstLetter.test.js @@ -1,4 +1,4 @@ -const CLI = require("../../packages/webpack-cli/lib/webpack-cli"); +const CLI = require("../../packages/webpack-cli/lib/webpack-cli").default; describe("capitalizeFirstLetter", () => { it("should capitalize first letter", () => { diff --git a/test/api/do-install.test.js b/test/api/do-install.test.js index cf939d0ff47..547feeca164 100644 --- a/test/api/do-install.test.js +++ b/test/api/do-install.test.js @@ -1,7 +1,7 @@ "use strict"; const { stripVTControlCharacters } = require("node:util"); -const CLI = require("../../packages/webpack-cli/lib/webpack-cli"); +const CLI = require("../../packages/webpack-cli/lib/webpack-cli").default; const readlineQuestionMock = jest.fn(); diff --git a/test/api/get-default-package-manager.test.js b/test/api/get-default-package-manager.test.js index f471acfd7e1..349a6f1cd66 100644 --- a/test/api/get-default-package-manager.test.js +++ b/test/api/get-default-package-manager.test.js @@ -1,6 +1,6 @@ const fs = require("node:fs"); const path = require("node:path"); -const CLI = require("../../packages/webpack-cli/lib/webpack-cli"); +const CLI = require("../../packages/webpack-cli/lib/webpack-cli").default; const syncMock = jest.fn(() => ({ stdout: "1.0.0", diff --git a/test/api/resolve-config/resolve-config.test.js b/test/api/resolve-config/resolve-config.test.js index 98c182c9446..1d3fce10fdf 100644 --- a/test/api/resolve-config/resolve-config.test.js +++ b/test/api/resolve-config/resolve-config.test.js @@ -1,5 +1,5 @@ const { resolve } = require("node:path"); -const WebpackCLI = require("../../../packages/webpack-cli/lib/webpack-cli"); +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"); From 281b2c71cbfe78399a4f956aeda0d0ef4276b930 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 04:00:46 +0300 Subject: [PATCH 12/23] test: fix --- packages/webpack-cli/src/webpack-cli.ts | 42 ++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 3a73b83bdb8..1fb17f03639 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1444,11 +1444,11 @@ class WebpackCLI { }, ); } else if (this.#isCommand(commandName, WebpackCLI.#commands.help)) { - this.makeCommand(WebpackCLI.#commands.help, [], () => { + await this.makeCommand(WebpackCLI.#commands.help, [], () => { // Stub for the `help` command }); } else if (this.#isCommand(commandName, WebpackCLI.#commands.version)) { - this.makeCommand( + await this.makeCommand( WebpackCLI.#commands.version, this.getInfoOptions(), async (options: { output: string; additionalPackage: string[] }) => { @@ -1458,7 +1458,7 @@ class WebpackCLI { }, ); } else if (this.#isCommand(commandName, WebpackCLI.#commands.info)) { - this.makeCommand( + await this.makeCommand( WebpackCLI.#commands.info, this.getInfoOptions(), async (options: { output: string; additionalPackage: string[] }) => { @@ -1468,7 +1468,7 @@ class WebpackCLI { }, ); } else if (this.#isCommand(commandName, WebpackCLI.#commands.configtest)) { - this.makeCommand( + await this.makeCommand( WebpackCLI.#commands.configtest, [], async (configPath: string | undefined) => { @@ -1917,6 +1917,40 @@ class WebpackCLI { return; } + if (error.code === "commander.unknownOption") { + let name = error.message.match(/'(.+)'/) as string | null; + + if (name) { + name = name[1].slice(2); + + if (name.includes("=")) { + [name] = name.split("="); + } + + const { operands } = this.program.parseOptions(this.program.args); + const operand = + typeof operands[0] !== "undefined" ? operands[0] : WebpackCLI.#commands.build.rawName; + + if (operand) { + const command = this.#findCommandByName(operand); + + if (!command) { + this.logger.error(`Can't find and load command '${operand}'`); + this.logger.error("Run 'webpack --help' to see available commands and options"); + process.exit(2); + } + + const { distance } = require("fastest-levenshtein"); + + for (const option of (command as Command).options) { + if (!option.hidden && distance(name, option.long?.slice(2) as string) < 3) { + this.logger.error(`Did you mean '--${option.name()}'?`); + } + } + } + } + } + this.logger.error("Run 'webpack --help' to see available commands and options"); process.exit(2); From 031e7da346b8e8494806740b282a34dc28011f50 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 04:04:39 +0300 Subject: [PATCH 13/23] refactor: remove unused API --- packages/webpack-cli/src/webpack-cli.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 1fb17f03639..83275de1052 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -598,9 +598,6 @@ class WebpackCLI { // TODO search API for this (command as Command & { pkg: string }).pkg = commandOptions.pkg || "webpack-cli"; - // TODO search a new API for this - const { forHelp } = this.program as Command & { forHelp: boolean }; - let allDependenciesInstalled = true; if (commandOptions.dependencies && commandOptions.dependencies.length > 0) { @@ -610,7 +607,7 @@ class WebpackCLI { if (isPkgExist) { continue; - } else if (!isPkgExist && forHelp) { + } else if (!isPkgExist) { allDependenciesInstalled = false; continue; } @@ -645,7 +642,7 @@ class WebpackCLI { if (options) { if (typeof options === "function") { - if (forHelp && !allDependenciesInstalled && commandOptions.dependencies) { + if (!allDependenciesInstalled && commandOptions.dependencies) { command.description( `${ commandOptions.description @@ -2010,8 +2007,6 @@ class WebpackCLI { isVerbose = true; } - (this.program as Command & { forHelp: boolean }).forHelp = true; - const optionsForHelp = [ ...(isHelpOption && hasOperand ? [operand] : []), ...operands.slice(1), From f6058f1ec9731feab0e3413cb98ff252b3e5eb44 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 04:10:05 +0300 Subject: [PATCH 14/23] refactor: fix more --- packages/webpack-cli/src/webpack-cli.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 83275de1052..f7a034aa9df 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -268,12 +268,16 @@ class WebpackCLI { } createColors(useColor?: boolean): Colors { - let cli: (typeof webpack)["cli"]; + let pkg: typeof webpack | undefined; try { - cli = require(WEBPACK_PACKAGE).cli; + pkg = require(WEBPACK_PACKAGE); } catch { - // Some big repos can have a problem with update webpack everywhere, so let's create a simple proxy for colors + // Nothing + } + + // Some big repos can have a problem with update webpack everywhere, so let's create a simple proxy for colors + if (!pkg || typeof pkg.cli.createColors === "undefined") { return new Proxy({} as Colors, { get() { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -282,7 +286,7 @@ class WebpackCLI { }); } - const { createColors, isColorSupported } = cli; + const { createColors, isColorSupported } = pkg.cli; const shouldUseColor = useColor || isColorSupported(); return { ...createColors({ useColor: shouldUseColor }), isColorSupported: shouldUseColor }; From 18ef9089c19c0387c8dbc858a4a5753e2be31944 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 04:21:06 +0300 Subject: [PATCH 15/23] refactor: fix more --- packages/webpack-cli/src/webpack-cli.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index f7a034aa9df..9ff7354f385 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -178,6 +178,7 @@ interface Options { configNodeEnv?: string; watchOptionsStdin?: boolean; watch?: boolean; + failOnWarnings?: boolean; isWatchingLikeCommand?: boolean; progress?: boolean | "profile"; analyze?: boolean; @@ -2751,7 +2752,7 @@ class WebpackCLI { process.exit(2); } - if (stats && stats.hasErrors()) { + if (stats && (stats.hasErrors() || (options.failOnWarnings && stats.hasWarnings()))) { process.exitCode = 1; } From 7b5d35720eacafd335675c97e507fd9aa147213d Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 04:23:43 +0300 Subject: [PATCH 16/23] refactor: fix more --- 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 9ff7354f385..59d717577a2 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -278,7 +278,7 @@ class WebpackCLI { } // Some big repos can have a problem with update webpack everywhere, so let's create a simple proxy for colors - if (!pkg || typeof pkg.cli.createColors === "undefined") { + if (!pkg || !pkg.cli || typeof pkg.cli.createColors === "undefined") { return new Proxy({} as Colors, { get() { // eslint-disable-next-line @typescript-eslint/no-explicit-any From 2a6ac2485ae90bde76a33b0f0d92e073d44a36a7 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 15:01:51 +0300 Subject: [PATCH 17/23] refactor: api --- packages/webpack-cli/src/webpack-cli.ts | 67 +++++++++++++++---------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 59d717577a2..1ae58f56484 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -278,7 +278,7 @@ class WebpackCLI { } // Some big repos can have a problem with update webpack everywhere, so let's create a simple proxy for colors - if (!pkg || !pkg.cli || typeof pkg.cli.createColors === "undefined") { + if (!pkg || !pkg.cli || typeof pkg.cli.createColors !== "function") { return new Proxy({} as Colors, { get() { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -309,6 +309,14 @@ class WebpackCLI { return true; } + try { + require.resolve(packageName); + return true; + } catch { + // Nothing + } + + // Fallback using fs let dir = __dirname; do { @@ -323,20 +331,29 @@ class WebpackCLI { } } 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 - for (const internalPath of globalPaths) { - try { - const stats = await fs.promises.stat(path.join(internalPath, packageName)); + 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; + if (stats.isDirectory()) { + return true; + } + } catch { + // Nothing } - } catch { - // Nothing - } + + return false; + }), + ); + + if (results.includes(true)) { + return true; } return false; @@ -607,7 +624,15 @@ class WebpackCLI { if (commandOptions.dependencies && commandOptions.dependencies.length > 0) { for (const dependency of commandOptions.dependencies) { - // TODO do we really need this for webpack itself? + if ( + // Allow to use `./path/to/webpack.js` outside `node_modules` + (dependency === WEBPACK_PACKAGE && WEBPACK_PACKAGE_IS_CUSTOM) || + // Allow to use `./path/to/webpack-dev-server.js` outside `node_modules` + (dependency === WEBPACK_DEV_SERVER_PACKAGE && WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM) + ) { + continue; + } + const isPkgExist = await this.checkPackageExists(dependency); if (isPkgExist) { @@ -617,22 +642,6 @@ class WebpackCLI { continue; } - let skipInstallation = false; - - // Allow to use `./path/to/webpack.js` outside `node_modules` - if (dependency === WEBPACK_PACKAGE && WEBPACK_PACKAGE_IS_CUSTOM) { - skipInstallation = true; - } - - // Allow to use `./path/to/webpack-dev-server.js` outside `node_modules` - if (dependency === WEBPACK_DEV_SERVER_PACKAGE && WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM) { - skipInstallation = true; - } - - if (skipInstallation) { - continue; - } - await this.doInstall(dependency, { preMessage: () => { this.logger.error( @@ -647,7 +656,11 @@ class WebpackCLI { if (options) { if (typeof options === "function") { - if (!allDependenciesInstalled && commandOptions.dependencies) { + if ( + !allDependenciesInstalled && + commandOptions.dependencies && + commandOptions.dependencies.length > 0 + ) { command.description( `${ commandOptions.description From 88dd1b037854702851825428a3709b9e3eabe9f8 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 15:29:10 +0300 Subject: [PATCH 18/23] test: fix --- packages/webpack-cli/src/webpack-cli.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 1ae58f56484..060d2fa3e7d 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -637,11 +637,10 @@ class WebpackCLI { if (isPkgExist) { continue; - } else if (!isPkgExist) { - allDependenciesInstalled = false; - continue; } + allDependenciesInstalled = false; + await this.doInstall(dependency, { preMessage: () => { this.logger.error( From 16bfa362de494794152a7e69878d54056319810c Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 15:30:20 +0300 Subject: [PATCH 19/23] test: debug --- test/build/custom-webpack/custom-webpack.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/build/custom-webpack/custom-webpack.test.js b/test/build/custom-webpack/custom-webpack.test.js index 94b2c54486e..a8764c4c1f8 100644 --- a/test/build/custom-webpack/custom-webpack.test.js +++ b/test/build/custom-webpack/custom-webpack.test.js @@ -19,6 +19,9 @@ 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 fb21563dab2dcdffd3791d270fa2cd42042d510a Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 15:50:31 +0300 Subject: [PATCH 20/23] test: fix --- package-lock.json | 50 +------------------------ packages/webpack-cli/src/webpack-cli.ts | 5 ++- 2 files changed, 4 insertions(+), 51 deletions(-) diff --git a/package-lock.json b/package-lock.json index e623f0153e0..2cce2bc8b03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8451,50 +8451,6 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@webpack-cli/configtest": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", - "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", - "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", - "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -29100,10 +29056,6 @@ "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "^0.6.1", - "@webpack-cli/configtest": "^3.0.1", - "@webpack-cli/info": "^3.0.1", - "@webpack-cli/serve": "^3.0.1", - "colorette": "^2.0.14", "commander": "^12.1.0", "cross-spawn": "^7.0.6", "envinfo": "^7.14.0", @@ -29127,7 +29079,7 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^5.82.0", + "webpack": "^5.101.0", "webpack-bundle-analyzer": "^4.0.0 || ^5.0.0", "webpack-dev-server": "^5.0.0" }, diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 060d2fa3e7d..5a0b92b9104 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1219,7 +1219,7 @@ class WebpackCLI { } async loadWebpack(): Promise { - return (await import(WEBPACK_PACKAGE)).default; + return (await import(pathToFileURL(WEBPACK_PACKAGE).toString())).default; } async #loadCommandByName(commandName: string, allowToInstall = false) { @@ -1244,7 +1244,8 @@ class WebpackCLI { ); } else if (this.#isCommand(commandName, WebpackCLI.#commands.serve)) { const loadDevServerOptions = async () => { - const devServer = (await import(WEBPACK_DEV_SERVER_PACKAGE)).default; + const devServer = (await import(pathToFileURL(WEBPACK_DEV_SERVER_PACKAGE).toString())) + .default; const options = this.webpack.cli.getArguments(devServer.schema) as unknown as Record< string, From a2fa9b5234b36e503cf641ae7f354785edcf0bc7 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 16:11:13 +0300 Subject: [PATCH 21/23] test: fix --- packages/webpack-cli/src/webpack-cli.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 5a0b92b9104..93c0eb16e20 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -1218,8 +1218,19 @@ 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 (await import(pathToFileURL(WEBPACK_PACKAGE).toString())).default; + 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); } async #loadCommandByName(commandName: string, allowToInstall = false) { @@ -1244,9 +1255,9 @@ class WebpackCLI { ); } else if (this.#isCommand(commandName, WebpackCLI.#commands.serve)) { const loadDevServerOptions = async () => { - const devServer = (await import(pathToFileURL(WEBPACK_DEV_SERVER_PACKAGE).toString())) - .default; + const devServer = await this.loadWebpackDevServer(); + // @ts-expect-error different schema types const options = this.webpack.cli.getArguments(devServer.schema) as unknown as Record< string, CommandOption @@ -1326,7 +1337,7 @@ class WebpackCLI { let DevServer: DevServerConstructor; try { - DevServer = (await import(WEBPACK_DEV_SERVER_PACKAGE)).default; + DevServer = await this.loadWebpackDevServer(); } catch (err) { this.logger.error( `You need to install 'webpack-dev-server' for running 'webpack serve'.\n${err}`, From 5b3530ccbeb0f770917460230c21c89c10bb2ae4 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 16:38:46 +0300 Subject: [PATCH 22/23] refactor: fix --- packages/webpack-cli/src/webpack-cli.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index 93c0eb16e20..e11430c750b 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -590,7 +590,7 @@ class WebpackCLI { ): Promise { const alreadyLoaded = this.program.commands.find( (command) => - command.name() === commandOptions.name.split(" ")[0] || + command.name() === commandOptions.rawName || command.aliases().includes(commandOptions.alias as string), ); @@ -620,7 +620,7 @@ class WebpackCLI { // TODO search API for this (command as Command & { pkg: string }).pkg = commandOptions.pkg || "webpack-cli"; - let allDependenciesInstalled = true; + const { forHelp } = this.program as Command & { forHelp?: boolean }; if (commandOptions.dependencies && commandOptions.dependencies.length > 0) { for (const dependency of commandOptions.dependencies) { @@ -635,17 +635,15 @@ class WebpackCLI { const isPkgExist = await this.checkPackageExists(dependency); - if (isPkgExist) { + if (isPkgExist || forHelp) { continue; } - allDependenciesInstalled = false; - await this.doInstall(dependency, { preMessage: () => { this.logger.error( `For using '${this.colors.green( - commandOptions.name.split(" ")[0], + commandOptions.rawName, )}' command you need to install: '${this.colors.green(dependency)}' package.`, ); }, @@ -655,11 +653,7 @@ class WebpackCLI { if (options) { if (typeof options === "function") { - if ( - !allDependenciesInstalled && - commandOptions.dependencies && - commandOptions.dependencies.length > 0 - ) { + if (forHelp && commandOptions.dependencies && commandOptions.dependencies.length > 0) { command.description( `${ commandOptions.description @@ -2036,6 +2030,8 @@ class WebpackCLI { isVerbose = true; } + (this.program as Command & { forHelp?: boolean }).forHelp = true; + const optionsForHelp = [ ...(isHelpOption && hasOperand ? [operand] : []), ...operands.slice(1), From 7204c61bdc29ee3f56433f70fce1fc6a5c313aa3 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 26 Feb 2026 17:01:45 +0300 Subject: [PATCH 23/23] test: again --- packages/webpack-cli/src/webpack-cli.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index e11430c750b..c9e5001ef70 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -622,6 +622,8 @@ class WebpackCLI { const { forHelp } = this.program as Command & { forHelp?: boolean }; + let allDependenciesInstalled = true; + if (commandOptions.dependencies && commandOptions.dependencies.length > 0) { for (const dependency of commandOptions.dependencies) { if ( @@ -635,7 +637,13 @@ class WebpackCLI { const isPkgExist = await this.checkPackageExists(dependency); - if (isPkgExist || forHelp) { + if (isPkgExist) { + continue; + } + + allDependenciesInstalled = false; + + if (forHelp) { continue; } @@ -653,7 +661,12 @@ class WebpackCLI { if (options) { if (typeof options === "function") { - if (forHelp && commandOptions.dependencies && commandOptions.dependencies.length > 0) { + if ( + forHelp && + !allDependenciesInstalled && + commandOptions.dependencies && + commandOptions.dependencies.length > 0 + ) { command.description( `${ commandOptions.description