"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.distance = distance; const node_fs_1 = __importDefault(require("node:fs")); const node_module_1 = require("node:module"); const node_path_1 = __importDefault(require("node:path")); const node_url_1 = require("node:url"); const node_util_1 = __importDefault(require("node:util")); const commander_1 = require("commander"); const WEBPACK_PACKAGE_IS_CUSTOM = Boolean(process.env.WEBPACK_PACKAGE); const WEBPACK_PACKAGE = WEBPACK_PACKAGE_IS_CUSTOM ? process.env.WEBPACK_PACKAGE : "webpack"; const WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM = Boolean(process.env.WEBPACK_DEV_SERVER_PACKAGE); const WEBPACK_DEV_SERVER_PACKAGE = WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM ? process.env.WEBPACK_DEV_SERVER_PACKAGE : "webpack-dev-server"; const EXIT_SIGNALS = ["SIGINT", "SIGTERM"]; const DEFAULT_CONFIGURATION_FILES = [ "webpack.config", ".webpack/webpack.config", ".webpack/webpackfile", ]; // Non-JavaScript configuration formats that Node.js cannot `import()`. // webpack-cli reads and parses these itself instead of relying on `interpret`. // The parsers are intentionally not shipped: the relevant package is imported // on demand from the user's project, and a helpful error is shown when it is // missing. `method` is the parser's entry point (`json5`/`toml` expose `parse`, // `js-yaml` exposes `load`). Hoisted to module scope so it is built only once. const DATA_FORMAT_LOADERS = { ".json5": { package: "json5", method: "parse" }, ".yaml": { package: "js-yaml", method: "load" }, ".yml": { package: "js-yaml", method: "load" }, ".toml": { package: "toml", method: "parse" }, }; // TypeScript and JSX configuration files that `tsx` can load through its // CommonJS require hook (`tsx/cjs`). Used as a fallback when the loaders // listed in `interpret` are unavailable (for `.mts` there is no `interpret` // entry at all, so `tsx` is the only fallback). const TSX_LOADABLE_EXTENSIONS = new Set([".ts", ".tsx", ".cts", ".mts", ".jsx"]); const DEFAULT_WEBPACK_PACKAGES = ["webpack", "loader"]; // Shared visual constants for the help/info/version/configtest "chrome". // Colors collapse to plain strings when disabled (piped output or `--no-color`), // so the textual structure stays stable and script-friendly. const UI_INDENT = " "; const UI_DIVIDER_WIDTH = 72; const UI_STATUS_ICONS = { success: "✔", error: "✖", warning: "⚠", info: "ℹ" }; // Options that get a single-character alias derived from their name. const FLAGS_WITH_ALIAS = new Set(["devtool", "output-path", "target", "watch", "extends"]); // Keys the CLI sets on the parsed options itself (never webpack arguments), so // they don't need to be forwarded to webpack's `processArguments`. const INTERNAL_OPTION_KEYS = new Set(["webpack", "argv", "isWatchingLikeCommand"]); // Levenshtein distance via Myers' bit-parallel algorithm, used only for "did you // mean" suggestions. Inspired by fastest-levenshtein (MIT, // https://github.com/ka-weihe/fastest-levenshtein). // // The 256 KB buffer is allocated lazily on first use: suggestions only run on // error paths, so a normal build never pays for it. let levenshteinPeq; function myers32(a, b, peq) { const n = a.length; const m = b.length; const lst = 1 << (n - 1); let pv = -1; let mv = 0; let sc = n; let i = n; while (i--) { peq[a.charCodeAt(i)] |= 1 << i; } for (i = 0; i < m; i++) { let eq = peq[b.charCodeAt(i)]; const xv = eq | mv; eq |= ((eq & pv) + pv) ^ pv; mv |= ~(eq | pv); pv &= eq; if (mv & lst) { sc++; } if (pv & lst) { sc--; } mv = (mv << 1) | 1; pv = (pv << 1) | ~(xv | mv); mv &= xv; } i = n; while (i--) { peq[a.charCodeAt(i)] = 0; } return sc; } function myersX(longer, shorter, peq) { const n = shorter.length; const m = longer.length; const mhc = []; const phc = []; const horizontalSize = Math.ceil(n / 32); const verticalSize = Math.ceil(m / 32); for (let i = 0; i < horizontalSize; i++) { phc[i] = -1; mhc[i] = 0; } let j = 0; for (; j < verticalSize - 1; j++) { let mv = 0; let pv = -1; const start = j * 32; const verticalLen = Math.min(32, m) + start; for (let k = start; k < verticalLen; k++) { peq[longer.charCodeAt(k)] |= 1 << k; } for (let i = 0; i < n; i++) { const eq = peq[shorter.charCodeAt(i)]; const pb = (phc[(i / 32) | 0] >>> i) & 1; const mb = (mhc[(i / 32) | 0] >>> i) & 1; const xv = eq | mv; const xh = ((((eq | mb) & pv) + pv) ^ pv) | eq | mb; let ph = mv | ~(xh | pv); let mh = pv & xh; if ((ph >>> 31) ^ pb) { phc[(i / 32) | 0] ^= 1 << i; } if ((mh >>> 31) ^ mb) { mhc[(i / 32) | 0] ^= 1 << i; } ph = (ph << 1) | pb; mh = (mh << 1) | mb; pv = mh | ~(xv | ph); mv = ph & xv; } for (let k = start; k < verticalLen; k++) { peq[longer.charCodeAt(k)] = 0; } } let mv = 0; let pv = -1; const start = j * 32; const verticalLen = Math.min(32, m - start) + start; for (let k = start; k < verticalLen; k++) { peq[longer.charCodeAt(k)] |= 1 << k; } let score = m; for (let i = 0; i < n; i++) { const eq = peq[shorter.charCodeAt(i)]; const pb = (phc[(i / 32) | 0] >>> i) & 1; const mb = (mhc[(i / 32) | 0] >>> i) & 1; const xv = eq | mv; const xh = ((((eq | mb) & pv) + pv) ^ pv) | eq | mb; let ph = mv | ~(xh | pv); let mh = pv & xh; score += (ph >>> (m - 1)) & 1; score -= (mh >>> (m - 1)) & 1; if ((ph >>> 31) ^ pb) { phc[(i / 32) | 0] ^= 1 << i; } if ((mh >>> 31) ^ mb) { mhc[(i / 32) | 0] ^= 1 << i; } ph = (ph << 1) | pb; mh = (mh << 1) | mb; pv = mh | ~(xv | ph); mv = ph & xv; } for (let k = start; k < verticalLen; k++) { peq[longer.charCodeAt(k)] = 0; } return score; } // Levenshtein edit distance between two strings, used for "did you mean" // suggestions. Exported only so it can be unit-tested directly; the CLI uses it // through the private `WebpackCLI.#distance`. function distance(first, second) { let a = first; let b = second; if (a.length < b.length) { const tmp = b; b = a; a = tmp; } if (b.length === 0) { return a.length; } levenshteinPeq ??= new Uint32Array(0x10000); return a.length <= 32 ? myers32(a, b, levenshteinPeq) : myersX(a, b, levenshteinPeq); } class ConfigurationLoadingError extends Error { name = "ConfigurationLoadingError"; constructor(errors) { const message = `▶ ESM (\`import\`) failed:\n${ConfigurationLoadingError.format(errors[0])}\n\n▶ CJS (\`require\`) failed:\n${ConfigurationLoadingError.format(errors[1])}`.trim(); super(message); this.stack = ""; } // Format a single underlying loading error as verbosely as possible. For // module-loading errors (e.g. a `SyntaxError`) `error.stack` begins with a // code frame (file, line, source and a caret) that points at the exact // location of the problem — far more useful than `error.message` alone. We // keep the full stack and surface the Node.js error `code` (e.g. // `MODULE_NOT_FOUND`) when present. static format(error) { return ConfigurationLoadingError.indent(ConfigurationLoadingError.describe(error)); } // Build the full, un-indented description of an error, walking the `cause` // chain. ES2022 error causes aren't reflected in `error.stack`, so they'd // otherwise be lost. A `seen` set guards against cyclic causes. static describe(error, seen = new Set()) { if (!(error instanceof Error)) { return node_util_1.default.stripVTControlCharacters(String(error)); } seen.add(error); let details = (error.stack ?? `${error.name}: ${error.message}`).trimEnd(); const { code } = error; if (code) { details += `\ncode: ${code}`; } details = node_util_1.default.stripVTControlCharacters(details); const { cause } = error; if (cause !== null && cause !== undefined && !seen.has(cause)) { details += `\n\nCaused by: ${ConfigurationLoadingError.describe(cause, seen)}`; } return details; } static indent(text) { return text .split("\n") .map((line) => (line ? ` ${line}` : line)) .join("\n"); } } class WebpackCLI { #colors; // Created lazily because `#createColors` loads the (large) webpack package, // which commands like `version`/`info` don't otherwise need. get colors() { return (this.#colors ??= this.#createColors()); } set colors(value) { this.#colors = value; } logger; #isColorSupportChanged; // Flag tokens of the current invocation, used to register only the options // actually present (instead of all ~850) when setting up a command. #argvForParsing; program; constructor() { this.logger = this.getLogger(); // Initialize program this.program = commander_1.program; this.program.name("webpack"); this.program.configureOutput({ writeErr: (str) => { this.logger.error(str); }, outputError: (str, write) => { write(`Error: ${this.capitalizeFirstLetter(str.replace(/^error:/, "").trim())}`); }, }); } #createColors(useColor) { let pkg; try { pkg = require(WEBPACK_PACKAGE); } catch { // Nothing } // 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 !== "function") { // webpack provides the color helpers; when it isn't available, fall back to // identity functions so output is plain (uncolored) text. Returning the // string unchanged (rather than an array) is important because the value is // passed to consumers — e.g. commander's `formatItem` — that expect a string. return new Proxy({}, { get(_target, property) { return property === "isColorSupported" ? false : (value) => value; }, }); } const { createColors, isColorSupported } = pkg.cli; const shouldUseColor = useColor || isColorSupported(); return { ...createColors({ useColor: shouldUseColor }), isColorSupported: shouldUseColor }; } isPromise(value) { return typeof value.then === "function"; } isFunction(value) { return typeof value === "function"; } capitalizeFirstLetter(str) { return str.length > 0 ? str.charAt(0).toUpperCase() + str.slice(1) : str; } toKebabCase(str) { return str.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); } // Levenshtein edit distance between two strings, for "did you mean" suggestions. static #distance(first, second) { return distance(first, second); } getLogger() { // Status icons brand every webpack-cli message (`✔`/`✖`/`⚠`/`ℹ`). They sit // between the `[webpack-cli]` prefix and the unchanged message text, so // tooling that greps the prefix or the message keeps working, and they // collapse to plain glyphs when colors are disabled. return { error: (val) => console.error(`[webpack-cli] ${this.colors.red(`✖ ${node_util_1.default.format(val)}`)}`), warn: (val) => console.warn(`[webpack-cli] ${this.colors.yellow(`⚠ ${val}`)}`), info: (val) => console.info(`[webpack-cli] ${this.colors.cyan(`ℹ ${val}`)}`), success: (val) => console.log(`[webpack-cli] ${this.colors.green(`✔ ${val}`)}`), log: (val) => console.log(`[webpack-cli] ${val}`), raw: (val) => console.log(val), }; } async getDefaultPackageManager() { const { sync } = await import("cross-spawn"); try { await node_fs_1.default.promises.access(node_path_1.default.resolve(process.cwd(), "package-lock.json"), node_fs_1.default.constants.F_OK); return "npm"; } catch { // Nothing } try { await node_fs_1.default.promises.access(node_path_1.default.resolve(process.cwd(), "yarn.lock"), node_fs_1.default.constants.F_OK); return "yarn"; } catch { // Nothing } try { await node_fs_1.default.promises.access(node_path_1.default.resolve(process.cwd(), "pnpm-lock.yaml"), node_fs_1.default.constants.F_OK); return "pnpm"; } catch { // Nothing } try { // the sync function below will fail if npm is not installed, // an error will be thrown if (sync("npm", ["--version"])) { return "npm"; } } catch { // Nothing } try { // the sync function below will fail if yarn is not installed, // an error will be thrown if (sync("yarn", ["--version"])) { return "yarn"; } } catch { // Nothing } try { // the sync function below will fail if pnpm is not installed, // an error will be thrown if (sync("pnpm", ["--version"])) { return "pnpm"; } } catch { this.logger.error("No package manager found."); process.exit(2); } } async isPackageInstalled(packageName) { if (process.versions.pnp) { return true; } try { require.resolve(packageName); return true; } catch { // Nothing } // Fallback using fs let dir = __dirname; do { try { const stats = await node_fs_1.default.promises.stat(node_path_1.default.join(dir, "node_modules", packageName)); if (stats.isDirectory()) { return true; } } catch { // Nothing } } while (dir !== (dir = node_path_1.default.dirname(dir))); // Extra fallback using fs and hidden API // @ts-expect-error No types, private API const { globalPaths } = await import("node:module"); // https://github.com/nodejs/node/blob/v18.9.1/lib/internal/modules/cjs/loader.js#L1274 const results = await Promise.all(globalPaths.map(async (internalPath) => { try { const stats = await node_fs_1.default.promises.stat(node_path_1.default.join(internalPath, packageName)); if (stats.isDirectory()) { return true; } } catch { // Nothing } return false; })); if (results.includes(true)) { return true; } return false; } async installPackage(packageName, options = {}) { const packageManager = await this.getDefaultPackageManager(); if (!packageManager) { this.logger.error("Can't find package manager"); process.exit(2); } if (options.preMessage) { options.preMessage(); } const { createInterface } = await import("node:readline"); const prompt = ({ message, defaultResponse, stream, }) => { const rl = createInterface({ input: process.stdin, output: stream, }); return new Promise((resolve) => { rl.question(`${message} `, (answer) => { // Close the stream rl.close(); const response = (answer || defaultResponse).toLowerCase(); // Resolve with the input response if (response === "y" || response === "yes") { resolve(true); } else { resolve(false); } }); }); }; // yarn uses 'add' command, rest npm and pnpm both use 'install' const commandArguments = [packageManager === "yarn" ? "add" : "install", "-D", packageName]; const commandToBeRun = `${packageManager} ${commandArguments.join(" ")}`; let needInstall; try { needInstall = await prompt({ message: `[webpack-cli] Would you like to install '${this.colors.green(packageName)}' package? (That will run '${this.colors.green(commandToBeRun)}') (${this.colors.yellow("Y/n")})`, defaultResponse: "Y", stream: process.stderr, }); } catch (error) { this.logger.error(error); process.exit(error); } if (needInstall) { const { sync } = await import("cross-spawn"); try { sync(packageManager, commandArguments, { stdio: "inherit" }); } catch (error) { this.logger.error(error); process.exit(2); } return packageName; } process.exit(2); } async makeCommand(options) { const alreadyLoaded = this.program.commands.find((command) => command.name() === options.rawName); if (alreadyLoaded) { return alreadyLoaded; } const command = this.program.command(options.name, { hidden: options.hidden, isDefault: options.isDefault, }); if (options.description) { command.description(options.description); } if (options.usage) { command.usage(options.usage); } if (Array.isArray(options.alias)) { command.aliases(options.alias); } else { command.alias(options.alias); } command.pkg = options.pkg || "webpack-cli"; const { forHelp } = this.program; let allDependenciesInstalled = true; if (options.dependencies && options.dependencies.length > 0) { for (const dependency of options.dependencies) { if ( // Allow to use `./path/to/webpack.js` outside `node_modules` (dependency === WEBPACK_PACKAGE && WEBPACK_PACKAGE_IS_CUSTOM) || // 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.isPackageInstalled(dependency); if (isPkgExist) { continue; } allDependenciesInstalled = false; if (forHelp) { command.description(`${options.description} To see all available options you need to install ${options.dependencies .map((dependency) => `'${dependency}'`) .join(", ")}.`); continue; } await this.installPackage(dependency, { preMessage: () => { this.logger.error(`For using '${this.colors.green(options.rawName)}' command you need to install: '${this.colors.green(dependency)}' package.`); }, }); } } command.context = {}; if (typeof options.preload === "function") { let data; try { data = await options.preload(); } catch (err) { if (!forHelp) { throw err; } } command.context = { ...command.context, ...data }; } if (options.options) { // Register every option for help, otherwise only the ones present in argv. const neededOptions = forHelp ? undefined : this.#neededOptionNames(); // With no option flags in argv (e.g. a plain `webpack build`), nothing // needs to be registered and no unknown-option suggestions are possible, // so skip building the (large) option list entirely. This avoids the // schema-to-arguments walk on the most common invocation. if (!neededOptions || neededOptions.size > 0) { let commandOptions; if (forHelp && !allDependenciesInstalled && options.dependencies && options.dependencies.length > 0) { commandOptions = []; } else if (typeof options.options === "function") { commandOptions = await options.options(command); } else { commandOptions = options.options; } // Keep all option names (including `no-` negated forms) for "did you mean" suggestions, since not every option is registered below. const allOptionNames = []; for (const option of commandOptions) { allOptionNames.push(option.name); if (this.#optionSupportsNegation(option)) { allOptionNames.push(`no-${option.name}`); } } command.allOptionNames = allOptionNames; for (const option of commandOptions) { if (neededOptions && !this.#isOptionNeeded(option, neededOptions)) { continue; } this.makeOption(command, option); } } } command.action(options.action); return command; } #neededOptionNames() { const argv = this.#argvForParsing; if (!argv) { return undefined; } const names = new Set(); for (const token of argv) { // Must start with `-` to name an option. if (token.length < 2 || token.charCodeAt(0) !== 45) { continue; } if (token.charCodeAt(1) === 45) { // Long option: `--name` or `--name=value`. let name = token.slice(2); const equalsIndex = name.indexOf("="); if (equalsIndex !== -1) { name = name.slice(0, equalsIndex); } if (!name) { continue; } names.add(name); // `--no-x` must register the `x` option (which provides the negation). if (name.startsWith("no-")) { names.add(name.slice(3)); } } else { // Register every letter of a short token to cover both attached values (`-d`) and combined flags (`-abc`); over-registering is harmless. for (const char of token.slice(1).split("=", 1)[0]) { names.add(char); } } } return names; } #isOptionNeeded(option, neededOptions) { if (neededOptions.has(option.name)) { return true; } // `makeOption` derives a single-character alias for these from the name. const alias = option.alias ?? (FLAGS_WITH_ALIAS.has(option.name) ? option.name[0] : undefined); return typeof alias === "string" && neededOptions.has(alias); } // Mirrors when `makeOption` registers a `--no-` negated option. #optionSupportsNegation(option) { if (option.configs) { return option.configs.some((config) => config.type === "boolean" || (config.type === "enum" && (config.values || []).includes(false))); } return Boolean(option.negative); } makeOption(command, option) { let mainOption; let negativeOption; if (FLAGS_WITH_ALIAS.has(option.name)) { [option.alias] = option.name; } if (option.configs) { let needNegativeOption = false; let negatedDescription; const mainOptionType = new Set(); for (const config of option.configs) { switch (config.type) { case "reset": mainOptionType.add(Boolean); break; case "boolean": if (!needNegativeOption) { needNegativeOption = true; negatedDescription = config.negatedDescription; } mainOptionType.add(Boolean); break; case "number": mainOptionType.add(Number); break; case "string": case "path": case "RegExp": mainOptionType.add(String); break; case "enum": { let hasFalseEnum = false; for (const value of config.values || []) { switch (typeof value) { case "string": mainOptionType.add(String); break; case "number": mainOptionType.add(Number); break; case "boolean": if (!hasFalseEnum && value === false) { hasFalseEnum = true; break; } mainOptionType.add(Boolean); break; } } if (!needNegativeOption) { needNegativeOption = hasFalseEnum; negatedDescription = config.negatedDescription; } } } } mainOption = { flags: option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`, valueName: option.valueName || "value", description: option.description || "", type: mainOptionType, multiple: option.multiple, defaultValue: option.defaultValue, configs: option.configs, }; if (needNegativeOption) { negativeOption = { flags: `--no-${option.name}`, description: negatedDescription || option.negatedDescription || `Negative '${option.name}' option.`, }; } } else { mainOption = { flags: option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`, valueName: option.valueName || "value", description: option.description || "", type: option.type ? new Set(Array.isArray(option.type) ? option.type : [option.type]) : new Set([Boolean]), multiple: option.multiple, defaultValue: option.defaultValue, }; if (option.negative) { negativeOption = { flags: `--no-${option.name}`, description: option.negatedDescription || `Negative '${option.name}' option.`, }; } } if (mainOption.type.size > 1 && mainOption.type.has(Boolean)) { mainOption.flags = `${mainOption.flags} [${mainOption.valueName}${mainOption.multiple ? "..." : ""}]`; } else if (mainOption.type.size > 0 && !mainOption.type.has(Boolean)) { mainOption.flags = `${mainOption.flags} <${mainOption.valueName}${mainOption.multiple ? "..." : ""}>`; } if (mainOption.type.size === 1) { if (mainOption.type.has(Number)) { let skipDefault = true; const optionForCommand = new commander_1.Option(mainOption.flags, mainOption.description) .argParser((value, prev = []) => { if (mainOption.defaultValue && mainOption.multiple && skipDefault) { prev = []; skipDefault = false; } return mainOption.multiple ? [...prev, Number(value)] : Number(value); }) .default(mainOption.defaultValue); optionForCommand.hidden = option.hidden || false; command.addOption(optionForCommand); } else if (mainOption.type.has(String)) { let skipDefault = true; const optionForCommand = new commander_1.Option(mainOption.flags, mainOption.description) .argParser((value, prev = []) => { if (mainOption.defaultValue && mainOption.multiple && skipDefault) { prev = []; skipDefault = false; } return mainOption.multiple ? [...prev, value] : value; }) .default(mainOption.defaultValue); optionForCommand.hidden = option.hidden || false; if (option.configs) { optionForCommand.configs = option.configs; } command.addOption(optionForCommand); } else if (mainOption.type.has(Boolean)) { const optionForCommand = new commander_1.Option(mainOption.flags, mainOption.description).default(mainOption.defaultValue); optionForCommand.hidden = option.hidden || false; command.addOption(optionForCommand); } else { const optionForCommand = new commander_1.Option(mainOption.flags, mainOption.description) .argParser([...mainOption.type][0]) .default(mainOption.defaultValue); optionForCommand.hidden = option.hidden || false; command.addOption(optionForCommand); } } else if (mainOption.type.size > 1) { let skipDefault = true; const optionForCommand = new commander_1.Option(mainOption.flags, mainOption.description) .argParser((value, prev = []) => { if (mainOption.defaultValue && mainOption.multiple && skipDefault) { prev = []; skipDefault = false; } if (mainOption.type.has(Number)) { const numberValue = Number(value); if (!Number.isNaN(numberValue)) { return mainOption.multiple ? [...prev, numberValue] : numberValue; } } if (mainOption.type.has(String)) { return mainOption.multiple ? [...prev, value] : value; } return value; }) .default(mainOption.defaultValue); optionForCommand.hidden = option.hidden || false; if (option.configs) { optionForCommand.configs = option.configs; } command.addOption(optionForCommand); } else if (mainOption.type.size === 0 && negativeOption) { const optionForCommand = new commander_1.Option(mainOption.flags, mainOption.description); // Hide stub option // TODO find a solution to hide such options in the new commander version, for example `--performance` and `--no-performance` because we don't have `--performance` at all optionForCommand.hidden = option.hidden || true; optionForCommand.internal = true; command.addOption(optionForCommand); } if (negativeOption) { const optionForCommand = new commander_1.Option(negativeOption.flags, negativeOption.description).default(false); optionForCommand.hidden = option.hidden || option.negativeHidden || false; command.addOption(optionForCommand); } } isMultipleConfiguration(config) { return Array.isArray(config); } isMultipleCompiler(compiler) { return compiler.compilers; } isValidationError(error) { return error.name === "ValidationError"; } // Cache the expensive schema-to-arguments walk per webpack module and schema, held via `WeakRef` so the GC can reclaim the ~1MB result after command setup (a miss simply rebuilds it). #argumentsCache = new WeakMap(); #getArguments(webpackMod, schema) { let perModuleCache = this.#argumentsCache.get(webpackMod); if (!perModuleCache) { perModuleCache = new Map(); this.#argumentsCache.set(webpackMod, perModuleCache); } let args = perModuleCache.get(schema)?.deref(); if (!args) { args = webpackMod.cli.getArguments(schema); perModuleCache.set(schema, new WeakRef(args)); } return args; } schemaToOptions(webpackMod, schema = undefined, additionalOptions = [], override = {}) { const args = this.#getArguments(webpackMod, schema); // Take memory const options = Array.from({ length: additionalOptions.length + Object.keys(args).length, }); let i = 0; // Adding own options for (; i < additionalOptions.length; i++) options[i] = additionalOptions[i]; // Adding core options for (const name in args) { const meta = args[name]; options[i++] = { ...meta, name, description: meta.description, hidden: !this.#minimumHelpOptions.has(name), negativeHidden: !this.#minimumNegativeHelpOptions.has(name), ...override, }; } return options; } #processArguments(webpackMod, args, configuration, values) { const problems = webpackMod.cli.processArguments(args, configuration, values); if (problems) { const groupBy = (xs, key) => xs.reduce((rv, problem) => { const path = problem[key]; (rv[path] ||= []).push(problem); return rv; }, {}); const problemsByPath = groupBy(problems, "path"); for (const path in problemsByPath) { const problems = problemsByPath[path]; for (const problem of problems) { this.logger.error(`${this.capitalizeFirstLetter(problem.type.replaceAll("-", " "))}${problem.value ? ` '${problem.value}'` : ""} for the '--${problem.argument.replaceAll(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}' option${problem.index ? ` by index '${problem.index}'` : ""}`); if (problem.expected) { if (problem.expected === "true | false") { this.logger.error("Expected: without value or negative option"); } else { this.logger.error(`Expected: '${problem.expected}'`); } } } } process.exit(2); } } async #outputHelp(options, isVerbose, isHelpCommandSyntax, program) { const isOption = (value) => value.startsWith("-"); const isGlobalOption = (value) => value === "--color" || value === "--no-color" || value === "-v" || value === "--version" || value === "-h" || value === "--help"; const { bold } = this.colors; // Shared "chrome" used to render the help screens. These are purely visual: // colors collapse to plain strings when colors are disabled (e.g. piped output // or `--no-color`), so the textual structure stays stable for scripts. const INDENT = UI_INDENT; const divider = this.#uiDivider(); const header = (title) => this.#uiHeader(title); // Section headings ("Options", "Commands", …) get a colored title and an underline. const sectionTitle = (title) => this.#uiSectionTitle(title); const outputIncorrectUsageOfHelp = () => { this.logger.error("Incorrect use of help"); this.logger.error("Please use: 'webpack help [command] [option]' | 'webpack [command] --help'"); this.logger.error("Run 'webpack --help' to see available commands and options"); process.exit(2); }; const isGlobalHelp = options.length === 0; const isCommandHelp = options.length === 1 && !isOption(options[0]); if (isGlobalHelp || isCommandHelp) { program.configureHelp({ helpWidth: typeof process.env.WEBPACK_CLI_HELP_WIDTH !== "undefined" ? Number.parseInt(process.env.WEBPACK_CLI_HELP_WIDTH, 10) : 40, sortSubcommands: true, // Support multiple aliases commandUsage: (command) => { let parentCmdNames = ""; for (let parentCmd = command.parent; parentCmd; parentCmd = parentCmd.parent) { parentCmdNames = `${parentCmd.name()} ${parentCmdNames}`; } if (isGlobalHelp) { // Show both supported invocation syntaxes on a single line. return `${parentCmdNames}${command.usage()} | ${parentCmdNames}[command] [options]`; } return `${parentCmdNames}${command.name()}|${command .aliases() .join("|")} ${command.usage()}`; }, // Support multiple aliases subcommandTerm: (command) => { const usage = command.usage(); return `${command.name()}|${command.aliases().join("|")}${usage.length > 0 ? ` ${usage}` : ""}`; }, visibleOptions: function visibleOptions(command) { return command.options.filter((option) => { if (option.internal) { return false; } // Hide `--watch` option when developer use `webpack watch --help` if ((options[0] === "w" || options[0] === "watch") && (option.name() === "watch" || option.name() === "no-watch")) { return false; } if (option.hidden) { return isVerbose; } return true; }); }, padWidth(command, helper) { return Math.max(helper.longestArgumentTermLength(command, helper), helper.longestOptionTermLength(command, helper), // For global options helper.longestOptionTermLength(program, helper), helper.longestSubcommandTermLength(isGlobalHelp ? program : command, helper)); }, formatHelp: (command, helper) => { // `helper.formatItem` is ANSI-aware (it pads using the visible width), so we // can colorize the term and keep every description column aligned. const formatItem = (term, description) => { if (description) { return helper.formatItem(bold(term), helper.padWidth(command, helper), description, helper); } return `${INDENT}${bold(term)}`; }; const formatList = (textArray) => textArray.join("\n"); const addSection = (output, title, items) => items.length > 0 ? [...output, sectionTitle(title), formatList(items), ""] : output; // Header — a branded, divided title for the screen. let output = [ "", header(isGlobalHelp ? "webpack" : `webpack ${command.name()}`), divider, ]; // Description const commandDescription = isGlobalHelp ? "The build tool for modern web applications." : helper.commandDescription(command); if (commandDescription.length > 0) { output = [...output, commandDescription, ""]; } // Usage output = [...output, `${bold("Usage:")} ${helper.commandUsage(command)}`, ""]; // Arguments output = addSection(output, "Arguments", helper .visibleArguments(command) .map((argument) => formatItem(argument.name(), argument.description))); // Options output = addSection(output, "Options", helper .visibleOptions(command) .map((option) => formatItem(helper.optionTerm(option), helper.optionDescription(option)))); // Global options output = addSection(output, "Global options", program.options.map((option) => formatItem(helper.optionTerm(option), helper.optionDescription(option)))); // Commands output = addSection(output, "Commands", helper .visibleCommands(isGlobalHelp ? program : command) .map((subcommand) => formatItem(helper.subcommandTerm(subcommand), helper.subcommandDescription(subcommand)))); return [...output, divider].join("\n"); }, }); if (isGlobalHelp) { await Promise.all(Object.values(this.#commands).map((knownCommand) => this.#loadCommandByName(knownCommand.rawName))); const buildCommand = this.#findCommandByName(this.#commands.build.rawName); if (buildCommand) { this.logger.raw(buildCommand.helpInformation()); } } else { const [name] = options; const command = await this.#loadCommandByName(name); if (!command) { this.logger.error(`Can't find and load command '${name}'`); this.logger.error("Run 'webpack --help' to see available commands and options."); process.exit(2); } this.logger.raw(command.helpInformation()); } } else if (isHelpCommandSyntax) { let isCommandSpecified = false; let commandName = this.#commands.build.rawName; let optionName = ""; if (options.length === 1) { [optionName] = options; } else if (options.length === 2) { isCommandSpecified = true; [commandName, optionName] = options; if (isOption(commandName)) { outputIncorrectUsageOfHelp(); } } else { outputIncorrectUsageOfHelp(); } const command = isGlobalOption(optionName) ? program : await this.#loadCommandByName(commandName); if (!command) { this.logger.error(`Can't find and load command '${commandName}'`); this.logger.error("Run 'webpack --help' to see available commands and options"); process.exit(2); } const option = command.options.find((option) => option.short === optionName || option.long === optionName); if (!option) { this.logger.error(`Unknown option '${optionName}'`); this.logger.error("Run 'webpack --help' to see available commands and options"); process.exit(2); return; } const nameOutput = option.flags.replace(/^.+[[<]/, "").replace(/(\.\.\.)?[\]>].*$/, "") + (option.variadic === true ? "..." : ""); const value = option.required ? `<${nameOutput}>` : option.optional ? `[${nameOutput}]` : ""; const scope = isCommandSpecified ? ` ${commandName}` : ""; this.logger.raw(""); this.logger.raw(header(option.long ?? optionName)); this.logger.raw(divider); this.logger.raw(`${INDENT}${bold("Usage:")} webpack${scope} ${option.long}${value ? ` ${value}` : ""}`); if (option.short) { this.logger.raw(`${INDENT}${bold("Short:")} webpack${scope} ${option.short}${value ? ` ${value}` : ""}`); } if (option.description) { this.logger.raw(`${INDENT}${bold("Description:")} ${option.description}`); } const { configs } = option; if (configs) { const possibleValues = configs.reduce((accumulator, currentValue) => { if (currentValue.values) { return [...accumulator, ...currentValue.values]; } return accumulator; }, []); if (possibleValues.length > 0) { // Convert the possible values to a union type string // ['mode', 'development', 'production'] => "'mode' | 'development' | 'production'" // [false, 'eval'] => "false | 'eval'" const possibleValuesUnionTypeString = possibleValues .map((value) => (typeof value === "string" ? `'${value}'` : value)) .join(" | "); this.logger.raw(`${INDENT}${bold("Possible values:")} ${possibleValuesUnionTypeString}`); } } this.logger.raw(divider); this.logger.raw(""); // TODO implement this after refactor cli arguments // logger.raw('Documentation: https://webpack.js.org/option/name/'); } else { outputIncorrectUsageOfHelp(); } // Footer — shared across every help screen and framed command. this.logger.raw(this.#uiFooter({ verbose: isVerbose })); process.exit(0); } async #renderVersion(options = {}) { const info = await this.#getInfoOutput({ ...options, information: { npmPackages: `{${DEFAULT_WEBPACK_PACKAGES.map((item) => `*${item}*`).join(",")}}`, }, }); // `--output json|markdown` must stay machine-readable, so only the default // plain-text output gets the visual treatment. return typeof options.output === "undefined" ? this.#renderEnvinfo(info) : info; } // ─── Shared UI "chrome" ────────────────────────────────────────────────── // Small, pure(-ish) renderers for the CLI's visual structure. They read // `this.colors`, which collapses to plain strings when colors are disabled, // so the textual layout is identical with or without ANSI. /** A horizontal rule used to separate sections. */ #uiDivider() { // Separators should recede behind content, so render them faint rather than // in a saturated color (default-blue is the least readable hue on dark terminals). return this.colors.dim("─".repeat(UI_DIVIDER_WIDTH)); } /** A branded title, e.g. `⬡ webpack build`. */ #uiHeader(title) { const { bold, cyan } = this.colors; return `${bold(cyan("⬡"))} ${bold(cyan(title))}`; } /** A section heading with an underline, e.g. `Options` followed by a divider. */ #uiSectionTitle(title) { const { bold, cyan } = this.colors; return `${bold(cyan(title))}\n${this.#uiDivider()}`; } /** A single status message prefixed with an icon (`✔`/`✖`/`⚠`/`ℹ`). */ #uiStatusLine(kind, message) { const { green, red, yellow, cyan } = this.colors; const color = { success: green, error: red, warning: yellow, info: cyan }[kind]; return `${color(UI_STATUS_ICONS[kind])} ${message}`; } /** The header block printed at the top of a framed command (`info`, `version`, …). */ #uiCommandHeader(name, description) { const lines = ["", this.#uiHeader(`webpack ${name}`), this.#uiDivider()]; if (description) { lines.push(description, ""); } return lines.join("\n"); } /** The documentation footer shared by every help and framed-command screen. */ #uiFooter(options = {}) { const { bold, cyan, red } = this.colors; const lines = []; if (!options.verbose) { lines.push(`${cyan("ℹ")} Run ${bold("'webpack --help=verbose'")} to see all available commands and options.`); } lines.push("", `${bold("Webpack documentation:")} ${cyan("https://webpack.js.org/")}`, `${bold("CLI documentation:")} ${cyan("https://webpack.js.org/api/cli/")}`, `${bold("Made with")} ${red("♥")} ${bold("by the webpack team")}`); return lines.join("\n"); } /** * Renders the plain-text output of `envinfo` (used by `info` and `version`) * into branded sections with aligned rows. Section titles keep their trailing * colon so existing tooling/assertions that grep for e.g. "System:" keep * working, and "requested => resolved" version pairs are shown with a "→" * arrow. * @param {string} raw The raw multi-line text produced by `envinfo`. * @returns {string} The styled, section-divided output. */ #renderEnvinfo(raw) { const { bold, cyan, green } = this.colors; const lines = []; const sections = []; let section; for (const line of raw.split("\n")) { const sectionMatch = /^ {2}(\S[^:]*):\s*$/.exec(line); if (sectionMatch) { section = { title: sectionMatch[1].trim(), rows: [] }; sections.push(section); continue; } const rowMatch = /^ {4}([^:]+):\s*(.*)$/.exec(line); if (rowMatch && section) { section.rows.push({ label: rowMatch[1].trim(), value: rowMatch[2].trim() || "N/A" }); } } for (const { title, rows } of sections) { if (rows.length === 0) { continue; } const labelWidth = Math.max(...rows.map((row) => row.label.length)) + 1; lines.push("", this.#uiHeader(`${title}:`), this.#uiDivider()); for (const { label, value } of rows) { const arrowIndex = value.indexOf("=>"); const renderedValue = arrowIndex === -1 ? green(value) : `${green(value.slice(0, arrowIndex).trim())} ${cyan("→")} ${bold(green(value.slice(arrowIndex + 2).trim()))}`; lines.push(`${UI_INDENT}${bold(`${label}:`.padEnd(labelWidth))} ${renderedValue}`); } } return [...lines, this.#uiDivider()].join("\n"); } /** * Prints a framed command screen: a branded header, the command body, and the * shared documentation footer. Used by `info`, `version` and `configtest`. * @param {object} meta The command name and a one-line description. * @param {Function} body A callback that prints the command's main output. */ async #frame(meta, body) { this.logger.raw(this.#uiCommandHeader(meta.name, meta.description)); await body(); this.logger.raw(this.#uiFooter()); } /** Prints a status message with an icon; errors go to stderr, the rest to stdout. */ #status(kind, message) { if (kind === "error") { process.stderr.write(`${this.#uiStatusLine(kind, message)}\n`); return; } this.logger.raw(this.#uiStatusLine(kind, message)); } async #getInfoOutput(options) { let { output } = options; const envinfoConfig = {}; if (output) { // Remove quotes if exist output = output.replaceAll(/['"]+/g, ""); switch (output) { case "markdown": envinfoConfig.markdown = true; break; case "json": envinfoConfig.json = true; break; default: this.logger.error(`'${output}' is not a valid value for output`); process.exit(2); } } let envinfoOptions; if (options.information) { envinfoOptions = options.information; } else { const defaultInformation = { Binaries: ["Node", "Yarn", "npm", "pnpm"], Browsers: [ "Brave Browser", "Chrome", "Chrome Canary", "Edge", "Firefox", "Firefox Developer Edition", "Firefox Nightly", "Internet Explorer", "Safari", "Safari Technology Preview", ], // @ts-expect-error No in types Monorepos: ["Yarn Workspaces", "Lerna"], System: ["OS", "CPU", "Memory"], npmGlobalPackages: ["webpack", "webpack-cli", "webpack-dev-server"], }; const npmPackages = [...DEFAULT_WEBPACK_PACKAGES, ...(options.additionalPackage || [])]; defaultInformation.npmPackages = `{${npmPackages.map((item) => `*${item}*`).join(",")}}`; envinfoOptions = defaultInformation; } const envinfo = (await import("envinfo")).default; let info = await envinfo.run(envinfoOptions, envinfoConfig); info = info.replace("npmPackages", "Packages"); info = info.replace("npmGlobalPackages", "Global Packages"); return info; } async #loadPackage(pkg, isCustom) { const importTarget = isCustom && /^(?:[A-Za-z]:(\\|\/)|\\\\|\/)/.test(pkg) ? (0, node_url_1.pathToFileURL)(pkg).toString() : pkg; return (await import(importTarget)).default; } async loadWebpack() { return this.#loadPackage(WEBPACK_PACKAGE, WEBPACK_PACKAGE_IS_CUSTOM); } async loadWebpackDevServer() { return this.#loadPackage(WEBPACK_DEV_SERVER_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM); } #minimumHelpOptions = new Set([ "mode", "watch", "watch-options-stdin", "stats", "devtool", "entry", "target", "name", "output-path", "extends", ]); #minimumNegativeHelpOptions = new Set(["devtool"]); #CLIOptions = [ // For configs { name: "config", alias: "c", configs: [ { type: "string", }, ], multiple: true, valueName: "pathToConfigFile", description: 'Provide path to one or more webpack configuration files to process, e.g. "./webpack.config.js".', hidden: false, }, { name: "config-name", configs: [ { type: "string", }, ], multiple: true, valueName: "name", description: "Name(s) of particular configuration(s) to use if configuration file exports an array of multiple configurations.", hidden: false, }, { name: "merge", alias: "m", configs: [ { type: "enum", values: [true], }, ], description: "Merge two or more configurations using 'webpack-merge'.", hidden: false, }, // Complex configs { name: "env", type: (value, previous = {}) => { // This ensures we're only splitting by the first `=` const [allKeys, val] = value.split(/[=](.+)/, 2); const splitKeys = allKeys.split(/\.(?!$)/); let prevRef = previous; for (let [index, someKey] of splitKeys.entries()) { // https://github.com/webpack/webpack-cli/issues/3284 if (someKey.endsWith("=")) { // remove '=' from key someKey = someKey.slice(0, -1); // @ts-expect-error we explicitly want to set it to undefined prevRef[someKey] = undefined; continue; } if (!prevRef[someKey]) { prevRef[someKey] = {}; } if (typeof prevRef[someKey] === "string") { prevRef[someKey] = {}; } if (index === splitKeys.length - 1) { prevRef[someKey] = typeof val === "string" ? val : true; } prevRef = prevRef[someKey]; } return previous; }, multiple: true, description: 'Environment variables passed to the configuration when it is a function, e.g. "myvar" or "myvar=myval".', hidden: false, }, { name: "config-node-env", configs: [ { type: "string", }, ], multiple: false, description: "Sets process.env.NODE_ENV to the specified value for access within the configuration.", hidden: false, }, // Adding more plugins { name: "analyze", configs: [ { type: "enum", values: [true], }, ], multiple: false, description: "It invokes webpack-bundle-analyzer plugin to get bundle information.", hidden: false, }, { name: "progress", configs: [ { type: "string", }, { type: "enum", values: [true], }, ], description: "Print compilation progress during build.", hidden: false, }, // Output options { name: "json", configs: [ { type: "string", }, { type: "enum", values: [true], }, ], alias: "j", valueName: "pathToJsonFile", description: "Prints result as JSON or store it in a file.", hidden: false, }, { name: "fail-on-warnings", configs: [ { type: "enum", values: [true], }, ], description: "Stop webpack-cli process with non-zero exit code on warnings from webpack.", hidden: false, }, { name: "disable-interpret", configs: [ { type: "enum", values: [true], }, ], description: "Disable interpret for loading the config file.", hidden: false, }, ]; #commands = { build: { rawName: "build", name: "build [entries...]", alias: ["bundle", "b"], description: "Run webpack (default command, can be omitted).", usage: "[entries...] [options]", dependencies: [WEBPACK_PACKAGE], preload: async () => { const webpack = await this.loadWebpack(); return { webpack }; }, options: async (cmd) => this.schemaToOptions(cmd.context.webpack, undefined, this.#CLIOptions), action: async (entries, options, cmd) => { const { webpack } = cmd.context; if (entries.length > 0) { options.entry = [...entries, ...(options.entry || [])]; } options.webpack = webpack; await this.runWebpack(options, false); }, }, watch: { rawName: "watch", name: "watch [entries...]", alias: "w", description: "Run webpack and watch for files changes.", usage: "[entries...] [options]", dependencies: [WEBPACK_PACKAGE], preload: async () => { const webpack = await this.loadWebpack(); return { webpack }; }, options: async (cmd) => this.schemaToOptions(cmd.context.webpack, undefined, this.#CLIOptions), action: async (entries, options, cmd) => { const { webpack } = cmd.context; if (entries.length > 0) { options.entry = [...entries, ...(options.entry || [])]; } options.webpack = webpack; await this.runWebpack(options, true); }, }, serve: { rawName: "serve", name: "serve [entries...]", alias: ["server", "s"], description: "Run the webpack dev server and watch for source file changes while serving.", usage: "[entries...] [options]", dependencies: [WEBPACK_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE], preload: async () => { const webpack = await this.loadWebpack(); const devServer = await this.loadWebpackDevServer(); return { webpack, devServer }; }, options: (cmd) => { const { webpack, devServer } = cmd.context; const webpackOptions = this.schemaToOptions(webpack, undefined, this.#CLIOptions); // @ts-expect-error different versions of the `Schema` type const devServerOptions = this.schemaToOptions(webpack, devServer.schema, undefined, { hidden: false, negativeHidden: false, }); return [...webpackOptions, ...devServerOptions]; }, action: async (entries, options, cmd) => { const { webpack, devServer } = cmd.context; const webpackCLIOptions = { webpack, isWatchingLikeCommand: true }; const devServerCLIOptions = {}; // Derive the built-in option names from the cached argument metadata // instead of retaining the full option arrays for the whole session. const webpackOptionNames = new Set([ ...this.#CLIOptions.map((option) => option.name), ...Object.keys(this.#getArguments(webpack, undefined)), ]); for (const optionName in options) { const kebabedOption = this.toKebabCase(optionName); const isBuiltInOption = webpackOptionNames.has(kebabedOption); if (isBuiltInOption) { webpackCLIOptions[optionName] = options[optionName]; } else { devServerCLIOptions[optionName] = options[optionName]; } } if (entries.length > 0) { webpackCLIOptions.entry = [...entries, ...(options.entry || [])]; } webpackCLIOptions.argv = { ...options, env: { WEBPACK_SERVE: true, ...options.env }, }; const compiler = await this.createCompiler(webpackCLIOptions); if (!compiler) { return; } const DevServer = cmd.context.devServer; const servers = []; if (this.#needWatchStdin(compiler)) { process.stdin.on("end", () => { Promise.all(servers.map((server) => server.stop())).then(() => { process.exit(0); }); }); process.stdin.resume(); } const compilers = this.isMultipleCompiler(compiler) ? compiler.compilers : [compiler]; const possibleCompilers = compilers.filter((compiler) => compiler.options.devServer); const compilersForDevServer = possibleCompilers.length > 0 ? possibleCompilers : [compilers[0]]; const usedPorts = []; // @ts-expect-error different versions of the `Schema` type const devServerArgs = this.#getArguments(webpack, devServer.schema); for (const compilerForDevServer of compilersForDevServer) { if (compilerForDevServer.options.devServer === false) { continue; } const devServerConfiguration = compilerForDevServer.options.devServer || {}; const args = {}; const values = {}; for (const name of Object.keys(options)) { if (name === "argv") continue; const kebabName = this.toKebabCase(name); const arg = devServerArgs[kebabName]; if (arg) { args[name] = arg; // We really don't know what the value is // eslint-disable-next-line @typescript-eslint/no-explicit-any values[name] = options[name]; } } if (Object.keys(values).length > 0) { this.#processArguments(webpack, args, devServerConfiguration, values); } if (devServerConfiguration.port) { const portNumber = Number(devServerConfiguration.port); if (usedPorts.includes(portNumber)) { throw new Error("Unique ports must be specified for each devServer option in your webpack configuration. Alternatively, run only 1 devServer config using the --config-name flag to specify your desired config."); } usedPorts.push(portNumber); } try { const server = new DevServer(devServerConfiguration, compiler); await server.start(); servers.push(server); } catch (error) { if (this.isValidationError(error)) { this.logger.error(error.message); } else { this.logger.error(error); } process.exit(2); } } if (servers.length === 0) { this.logger.error("No dev server configurations to run"); process.exit(2); } }, }, help: { rawName: "help", name: "help [command] [option]", alias: "h", description: "Display help for commands and options.", action: () => { // Nothing, just stub }, }, version: { rawName: "version", name: "version", alias: "v", usage: "[options]", description: "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and other packages.", options: [ { name: "output", alias: "o", configs: [ { type: "string", }, ], description: "To get the output in a specified format (accept json or markdown)", hidden: false, }, ], action: async (options) => { // `--output json|markdown` stays machine-readable and unframed. if (options.output) { this.logger.raw(await this.#renderVersion(options)); return; } await this.#frame({ name: "version", description: "Installed package versions." }, async () => this.logger.raw(await this.#renderVersion(options))); }, }, info: { rawName: "info", name: "info", alias: "i", usage: "[options]", description: "Outputs information about your system.", options: [ { name: "output", alias: "o", configs: [ { type: "string", }, ], description: "To get the output in a specified format (accept json or markdown)", hidden: false, }, { name: "additional-package", alias: "a", configs: [{ type: "string" }], multiple: true, description: "Adds additional packages to the output", hidden: false, }, ], action: async (options) => { const info = await this.#getInfoOutput(options); // `--output json|markdown` stays machine-readable and unframed. if (options.output) { this.logger.raw(info); return; } await this.#frame({ name: "info", description: "System and environment information." }, () => this.logger.raw(this.#renderEnvinfo(info))); }, }, configtest: { rawName: "configtest", name: "configtest [config-path]", alias: "t", description: "Validate a webpack configuration.", dependencies: [WEBPACK_PACKAGE], options: [], preload: async () => { const webpack = await this.loadWebpack(); return { webpack }; }, action: async (configPath, _options, cmd) => { const { webpack } = cmd.context; const env = {}; const argv = { env }; const config = await this.loadConfig(configPath ? { env, argv, webpack, config: [configPath] } : { env, argv, webpack }); const configPaths = new Set(); if (Array.isArray(config.options)) { for (const options of config.options) { const loadedConfigPaths = config.path.get(options); if (loadedConfigPaths) { for (const path of loadedConfigPaths) configPaths.add(path); } } } else if (config.path.get(config.options)) { const loadedConfigPaths = config.path.get(config.options); if (loadedConfigPaths) { for (const path of loadedConfigPaths) configPaths.add(path); } } await this.#frame({ name: "configtest", description: "Validating your webpack configuration." }, () => { if (configPaths.size === 0) { this.#status("error", "No configuration found."); process.exit(2); } this.#status("info", `Validating: ${[...configPaths].join(", ")}`); try { cmd.context.webpack.validate(config.options); } catch (error) { this.#status("error", this.isValidationError(error) ? error.message : String(error instanceof Error ? error.message : error)); process.exit(2); } this.#status("success", "No validation errors found."); }); }, }, }; #isCommand(input, commandOptions) { const longName = commandOptions.rawName; if (input === longName) { return true; } if (commandOptions.alias) { if (Array.isArray(commandOptions.alias)) { return commandOptions.alias.includes(input); } return commandOptions.alias === input; } return false; } #findCommandByName(name) { return this.program.commands.find((command) => name === command.name() || command.aliases().includes(name)); } async #loadCommandByName(commandName) { if (this.#isCommand(commandName, this.#commands.build)) { return await this.makeCommand(this.#commands.build); } else if (this.#isCommand(commandName, this.#commands.serve)) { return await this.makeCommand(this.#commands.serve); } else if (this.#isCommand(commandName, this.#commands.watch)) { return await this.makeCommand(this.#commands.watch); } else if (this.#isCommand(commandName, this.#commands.help)) { // Stub for the `help` command return await this.makeCommand(this.#commands.help); } else if (this.#isCommand(commandName, this.#commands.version)) { return await this.makeCommand(this.#commands.version); } else if (this.#isCommand(commandName, this.#commands.info)) { return await this.makeCommand(this.#commands.info); } else if (this.#isCommand(commandName, this.#commands.configtest)) { return await this.makeCommand(this.#commands.configtest); } const pkg = commandName; let LoadedCommand; try { LoadedCommand = (await import(pkg)).default; } catch (error) { if (error.code !== "ERR_MODULE_NOT_FOUND") { this.logger.error(`Unable to load '${pkg}' command`); this.logger.error(error); process.exit(2); } return; } let command; let externalCommand; try { command = new LoadedCommand(); externalCommand = await command.apply(this); } catch (error) { this.logger.error(`Unable to load '${pkg}' command`); this.logger.error(error); process.exit(2); } return externalCommand; } async run(args, parseOptions) { // Default `--color` and `--no-color` options const self = this; // Register own exit this.program.exitOverride((error) => { if (error.exitCode === 0) { process.exit(0); return; } if (error.code === "commander.unknownOption") { let name = error.message.match(/'(.+)'/); 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] : this.#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 { allOptionNames } = command; const candidateNames = allOptionNames ?? command.options .filter((option) => !option.internal) .map((option) => option.long?.slice(2)); for (const candidate of candidateNames) { if (candidate && WebpackCLI.#distance(name, candidate) < 3) { this.logger.error(`Did you mean '--${candidate}'?`); } } } } } this.logger.error("Run 'webpack --help' to see available commands and options"); process.exit(2); }); this.program.option("--color", "Enable colors on console."); this.program.on("option:color", function color() { 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() { const { color } = this.opts(); self.#isColorSupportChanged = color; self.colors = self.#createColors(color); }); this.program.option("-v, --version", "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and other packages."); // webpack-cli has it's own logic for showing suggestions this.program.showSuggestionAfterError(false); // Suppress the default help option this.program.helpOption(false); // Suppress the default help command this.program.helpCommand(false); this.program.option("-h, --help [verbose]", "Display help for commands and options."); // Basic command for lazy loading other commands // By default we don't load any commands and options, commands and options registration takes a lot of time instead we load them lazily // That is why we need to set `allowUnknownOption` to `true`, otherwise commander will not work this.program.allowUnknownOption(true); // For lazy loading other commands too this.program.allowExcessArguments(true); this.program.action(async (options) => { const { operands, unknown } = this.program.parseOptions(this.program.args); // Remember the flag tokens so command setup only registers options that // are actually used (see `#neededOptionNames`). this.#argvForParsing = unknown; const defaultCommandNameToRun = this.#commands.build.rawName; const hasOperand = typeof operands[0] !== "undefined"; const operand = hasOperand ? operands[0] : defaultCommandNameToRun; const isHelpOption = typeof options.help !== "undefined"; const isHelpCommandSyntax = this.#isCommand(operand, this.#commands.help); if (isHelpOption || isHelpCommandSyntax) { let isVerbose = false; if (isHelpOption && typeof options.help === "string") { if (options.help !== "verbose") { this.logger.error("Unknown value for '--help' option, please use '--help=verbose'"); process.exit(2); } isVerbose = true; } this.program.forHelp = true; const optionsForHelp = [ ...(isHelpOption && hasOperand ? [operand] : []), ...operands.slice(1), ...unknown, ...(isHelpCommandSyntax && typeof options.color !== "undefined" ? [options.color ? "--color" : "--no-color"] : []), ...(isHelpCommandSyntax && typeof options.version !== "undefined" ? ["--version"] : []), ]; await this.#outputHelp(optionsForHelp, isVerbose, isHelpCommandSyntax, this.program); } const isVersionOption = typeof options.version !== "undefined"; if (isVersionOption) { const info = await this.#renderVersion(); this.logger.raw(info); process.exit(0); } let isKnownCommand = false; for (const command of Object.values(this.#commands)) { if (command.rawName === operand || (Array.isArray(command.alias) ? command.alias.includes(operand) : command.alias === operand)) { isKnownCommand = true; break; } } let command; let commandOperands = operands.slice(1); if (isKnownCommand) { command = await this.#loadCommandByName(operand); } else { let isEntrySyntax; try { await node_fs_1.default.promises.access(operand, node_fs_1.default.constants.F_OK); isEntrySyntax = true; } catch { isEntrySyntax = false; } if (isEntrySyntax) { commandOperands = operands; command = await this.#loadCommandByName(defaultCommandNameToRun); } else { // Try to load external command try { command = await this.#loadCommandByName(operand); } catch { // Nothing } if (!command) { this.logger.error(`Unknown command or entry '${operand}'`); const found = Object.values(this.#commands).find((commandOptions) => WebpackCLI.#distance(operand, commandOptions.rawName) < 3); if (found) { this.logger.error(`Did you mean '${found.rawName}' (alias '${Array.isArray(found.alias) ? found.alias.join(", ") : found.alias}')?`); } this.logger.error("Run 'webpack --help' to see available commands and options"); process.exit(2); } } } if (!command) { throw new Error(`Internal error: Registered command "${operand}" is missing an action handler.`); } await command.parseAsync([...commandOperands, ...unknown], { from: "user" }); }); await this.program.parseAsync(args, parseOptions); } // Finds the highest-priority default config file by reading each candidate directory once (case-insensitively) and confirming with `access`, instead of probing every `` combination separately. async #findDefaultConfigFile() { // Popular extensions, tried first. The common case (e.g. `webpack.config.js`) // matches here, so `interpret` is never loaded — see `getExoticExtensions`. const commonExtensions = [".js", ".mjs", ".cjs", ".ts", ".cts", ".mts"]; // `interpret`'s extra extensions (e.g. `.coffee`) are only needed when no // common-extension config exists, so defer importing it until then. let exoticExtensions; const getExoticExtensions = async () => { if (typeof exoticExtensions === "undefined") { const interpret = await import("interpret"); const common = new Set(commonExtensions); exoticExtensions = Object.keys(interpret.extensions).filter((ext) => !common.has(ext)); } return exoticExtensions; }; const directoryEntriesCache = new Map(); const readDirectoryEntries = async (directory) => { let entries = directoryEntriesCache.get(directory); if (typeof entries === "undefined") { try { entries = new Set((await node_fs_1.default.promises.readdir(directory)).map((entry) => entry.toLowerCase())); } catch { entries = null; } directoryEntriesCache.set(directory, entries); } return entries; }; const findInExtensions = async (resolvedBase, basename, entries, extensions) => { for (const ext of extensions) { // Skip candidates absent from the listing, but when the directory can't be listed (`entries` is `null`) probe every candidate directly. if (entries && !entries.has((basename + ext).toLowerCase())) { continue; } const candidate = resolvedBase + ext; // Confirm with `access` to preserve exact existence semantics (e.g. // broken symlinks are listed by `readdir` but fail `access`). try { await node_fs_1.default.promises.access(candidate, node_fs_1.default.constants.F_OK); return candidate; } catch { // Listed but not accessible, keep looking } } return undefined; }; // Order defines the priority, in decreasing order. Within each filename, // common extensions take priority over the exotic ones (matching the // previous combined ordering). for (const filename of DEFAULT_CONFIGURATION_FILES) { const resolvedBase = node_path_1.default.resolve(filename); const entries = await readDirectoryEntries(node_path_1.default.dirname(resolvedBase)); const basename = node_path_1.default.basename(resolvedBase); const common = await findInExtensions(resolvedBase, basename, entries, commonExtensions); if (common) { return common; } const exotic = await findInExtensions(resolvedBase, basename, entries, await getExoticExtensions()); if (exotic) { return exotic; } } return undefined; } async loadConfig(options) { const disableInterpret = typeof options.disableInterpret !== "undefined" && options.disableInterpret; const loadConfigByPath = async (configPath, argv = { env: {} }) => { let options; const isFileURL = configPath.startsWith("file://"); try { const ext = node_path_1.default.extname(configPath).toLowerCase(); const dataFormatLoader = DATA_FORMAT_LOADERS[ext]; if (dataFormatLoader) { // Non-JavaScript formats cannot be `import(...)`ed, so parse them // directly and skip the doomed dynamic import attempt entirely. const configFilePath = isFileURL ? (0, node_url_1.fileURLToPath)(configPath) : node_path_1.default.resolve(configPath); let parser; try { // Resolve the parser from the configuration file's location (so it // is picked up from the user's project rather than from webpack-cli), // then load it asynchronously via `import(...)`. const parserPath = (0, node_module_1.createRequire)(configFilePath).resolve(dataFormatLoader.package); const parserModule = await import((0, node_url_1.pathToFileURL)(parserPath).href); parser = (parserModule.default ?? parserModule); } catch { this.logger.error(`Unable to load the '${configFilePath}' configuration file.`); this.logger.error(`Loading '${ext}' configuration files requires the '${dataFormatLoader.package}' package, which is not installed. Please install it, e.g. \`npm install --save-dev ${dataFormatLoader.package}\`.`); process.exit(2); } // Reading/parsing errors propagate to the handler below, which // reports `Failed to load '' config`. const source = await node_fs_1.default.promises.readFile(configFilePath, "utf8"); options = parser[dataFormatLoader.method](source); } else { let loadingError; try { options = // eslint-disable-next-line no-eval (await eval(`import("${isFileURL ? configPath : (0, node_url_1.pathToFileURL)(configPath)}")`)) .default; } catch (err) { if (this.isValidationError(err) || process.env?.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) { throw err; } loadingError = err; } // Fallback logic when we can't use `import(...)`. `interpret`/`rechoir` // still handle the JavaScript variants (`.ts`, `.coffee`, // `.babel.js`, ...). This path is planned to be removed once those // formats are loaded natively as well. if (loadingError) { const configFilePath = isFileURL ? (0, node_url_1.fileURLToPath)(configPath) : node_path_1.default.resolve(configPath); const { jsVariants, extensions } = await import("interpret"); let interpreted = Object.keys(jsVariants).find((variant) => variant === ext); if (!interpreted && ext.endsWith(".cts")) { interpreted = jsVariants[".ts"]; } // `tsx` is missing from `interpret`'s loader tables, and it cannot // be added there: its `tsx/cjs` entry point exists only in the // package's `exports` map, which `rechoir`'s resolver does not // support. Register it manually instead, resolved from the // configuration file's location so the user's copy is picked up. // Requiring `tsx/cjs` installs its require hook as a side effect. const registerTsxLoader = () => { try { (0, node_module_1.createRequire)(configFilePath)("tsx/cjs"); return undefined; } catch (err) { // `throw` may raise non-`Error` values; normalize so the // error-reporting path can rely on `.message`. return err instanceof Error ? err : new Error(String(err)); } }; if (interpreted && !disableInterpret) { const rechoir = (await import("rechoir")).default; try { rechoir.prepare(extensions, configPath); } catch (error) { // The loaders known to `interpret` take priority; `tsx` is // only tried once all of them have failed. const isTsxLoadable = TSX_LOADABLE_EXTENSIONS.has(ext); const tsxFailure = isTsxLoadable ? registerTsxLoader() : undefined; if (!isTsxLoadable || tsxFailure) { if (error?.failures) { this.logger.error(`Unable to load '${configPath}'`); this.logger.error(error.message); for (const failure of error.failures) { this.logger.error(failure.error.message); } if (tsxFailure) { // Only the first line — `require` errors append a noisy // "Require stack" to the message. const [tsxFailureReason] = tsxFailure.message.split("\n"); this.logger.error(tsxFailureReason); } this.logger.error("Please install one of them"); process.exit(2); } this.logger.error(error); process.exit(2); } } } else if (TSX_LOADABLE_EXTENSIONS.has(ext) && !disableInterpret) { // `.mts` has no entry in `interpret`'s tables, so `tsx` is the // only loader we can offer. Best effort: when it is missing, the // `require()` below fails and the original errors are reported, // exactly as before. registerTsxLoader(); } try { options = require(configFilePath); } catch (err) { if (this.isValidationError(err)) { throw err; } throw new ConfigurationLoadingError([loadingError, err]); } } } // To handle `babel`/`module.exports.default = {};` if (options && typeof options === "object" && "default" in options) { options = options.default; } if (!options) { this.logger.warn(`Default export is missing or nullish at (from ${configPath}). Webpack will run with an empty configuration. Please double-check that this is what you want. If you want to run webpack with an empty config, \`export {}\`/\`module.exports = {};\` to remove this warning.`); options = {}; } } catch (error) { if (error instanceof ConfigurationLoadingError) { this.logger.error(`Failed to load '${configPath}' config\n${error.message}`); } else { this.logger.error(`Failed to load '${configPath}' config`); this.logger.error(error); } process.exit(2); } if (Array.isArray(options)) { const optionsArray = options; await Promise.all(optionsArray.map(async (_, i) => { if (this.isPromise(optionsArray[i])) { optionsArray[i] = await optionsArray[i]; } // `Promise` may return `Function` if (this.isFunction(optionsArray[i])) { // when config is a function, pass the env from args to the config function optionsArray[i] = await optionsArray[i](argv.env, argv); } })); options = optionsArray; } else { if (this.isPromise(options)) { options = await options; } // `Promise` may return `Function` if (this.isFunction(options)) { // when config is a function, pass the env from args to the config function options = await options(argv.env, argv); } } const isObject = (value) => typeof value === "object" && value !== null; if (!isObject(options) && !Array.isArray(options)) { this.logger.error(`Invalid configuration in '${configPath}'`); process.exit(2); } return { options: options, path: configPath, }; }; const config = { options: {}, path: new WeakMap(), }; if (options.config && options.config.length > 0) { const loadedConfigs = await Promise.all(options.config.map((configPath) => loadConfigByPath(configPath, options.argv))); if (loadedConfigs.length === 1) { config.options = loadedConfigs[0].options; config.path.set(loadedConfigs[0].options, [loadedConfigs[0].path]); } else { config.options = []; for (const loadedConfig of loadedConfigs) { if (Array.isArray(loadedConfig.options)) { for (const item of loadedConfig.options) { config.options.push(item); config.path.set(options, [loadedConfig.path]); } } else { config.options.push(loadedConfig.options); config.path.set(loadedConfig.options, [loadedConfig.path]); } } } } else { const foundDefaultConfigFile = await this.#findDefaultConfigFile(); if (foundDefaultConfigFile) { const loadedConfig = await loadConfigByPath(foundDefaultConfigFile, options.argv); config.options = loadedConfig.options; if (this.isMultipleConfiguration(config.options)) { for (const item of config.options) { config.path.set(item, [loadedConfig.path]); } } else { config.path.set(loadedConfig.options, [loadedConfig.path]); } } } if (options.configName) { const notFoundConfigNames = []; config.options = options.configName.map((configName) => { let found; if (this.isMultipleConfiguration(config.options)) { found = config.options.find((options) => options.name === configName); } else { found = config.options.name === configName ? config.options : undefined; } if (!found) { notFoundConfigNames.push(configName); } return found; }); if (notFoundConfigNames.length > 0) { this.logger.error(notFoundConfigNames .map((configName) => `Configuration with the name "${configName}" was not found.`) .join(" ")); process.exit(2); } } const resolveExtends = async (config, configPaths, extendsPaths) => { delete config.extends; const loadedConfigs = await Promise.all(extendsPaths.map((extendsPath) => loadConfigByPath(extendsPath, options.argv))); const { merge } = await import("webpack-merge"); const loadedOptions = loadedConfigs.flatMap((config) => config.options); if (loadedOptions.length > 0) { const prevPaths = configPaths.get(config); const loadedPaths = loadedConfigs.flatMap((config) => config.path); if (prevPaths) { const intersection = loadedPaths.filter((element) => prevPaths.includes(element)); if (intersection.length > 0) { this.logger.error("Recursive configuration detected, exiting."); process.exit(2); } } config = merge(...loadedOptions, config); if (prevPaths) { configPaths.set(config, [...prevPaths, ...loadedPaths]); } } if (config.extends) { const extendsPaths = typeof config.extends === "string" ? [config.extends] : config.extends; config = await resolveExtends(config, configPaths, extendsPaths); } return config; }; // The `extends` param in CLI gets priority over extends in config file if (options.extends && options.extends.length > 0) { const extendsPaths = options.extends; if (this.isMultipleConfiguration(config.options)) { config.options = await Promise.all(config.options.map((options) => resolveExtends(options, config.path, extendsPaths))); } else { // load the config from the extends option config.options = await resolveExtends(config.options, config.path, extendsPaths); } } // if no extends option is passed, check if the config file has extends else if (this.isMultipleConfiguration(config.options) && config.options.some((options) => options.extends)) { config.options = await Promise.all(config.options.map((options) => { if (options.extends) { return resolveExtends(options, config.path, typeof options.extends === "string" ? [options.extends] : options.extends); } return options; })); } else if (!this.isMultipleConfiguration(config.options) && config.options.extends) { config.options = await resolveExtends(config.options, config.path, typeof config.options.extends === "string" ? [config.options.extends] : config.options.extends); } if (options.merge) { const { merge } = await import("webpack-merge"); // we can only merge when there are multiple configurations // either by passing multiple configs by flags or passing a // single config exporting an array if (!this.isMultipleConfiguration(config.options) || config.options.length <= 1) { this.logger.error("At least two configurations are required for merge."); process.exit(2); } const mergedConfigPaths = []; config.options = config.options.reduce((accumulator, options) => { const configPath = config.path.get(options); const mergedOptions = merge(accumulator, options); if (configPath) { mergedConfigPaths.push(...configPath); } return mergedOptions; }, {}); config.path.set(config.options, mergedConfigPaths); } if (options.analyze && !(await this.isPackageInstalled("webpack-bundle-analyzer"))) { await this.installPackage("webpack-bundle-analyzer", { preMessage: () => { this.logger.error(`It looks like ${this.colors.yellow("webpack-bundle-analyzer")} is not installed.`); }, }); this.logger.success(`${this.colors.yellow("webpack-bundle-analyzer")} was installed successfully.`); } if (typeof options.progress === "string" && options.progress !== "profile") { this.logger.error(`'${options.progress}' is an invalid value for the --progress option. Only 'profile' is allowed.`); process.exit(2); } // Node's CJS interop nests the class under `.default.default`; Bun honors // `__esModule` and unwraps once already, leaving the class one level up. const cliPluginModule = (await import("./plugins/cli-plugin.js")).default; const CLIPlugin = cliPluginModule.default ?? cliPluginModule; // `getArguments()` already returns a name-keyed map of exactly the argument // metadata `processArguments` consumes, so use it directly (cached) instead // of rebuilding a `schemaToOptions` array and a lookup map on every run. // Computed lazily: a plain `webpack build` only has internal option keys, so // it skips the schema-to-arguments walk entirely. let builtInArgs; const internalBuildConfig = (configuration) => { const originalWatchValue = configuration.watch; // Apply options const args = {}; const values = {}; for (const name of Object.keys(options)) { if (INTERNAL_OPTION_KEYS.has(name)) continue; const kebabName = this.toKebabCase(name); const arg = (builtInArgs ??= this.#getArguments(options.webpack, undefined))[kebabName]; if (arg) { args[name] = arg; // We really don't know what the value is // eslint-disable-next-line @typescript-eslint/no-explicit-any values[name] = options[name]; } } if (Object.keys(values).length > 0) { this.#processArguments(options.webpack, args, configuration, values); } // Output warnings if (!Object.isExtensible(configuration)) { return; } if (options.isWatchingLikeCommand && options.argv?.env && (typeof originalWatchValue !== "undefined" || typeof options.argv?.watch !== "undefined")) { this.logger.warn(`No need to use the '${options.argv.env.WEBPACK_WATCH ? "watch" : "serve"}' command together with '{ watch: true | false }' or '--watch'/'--no-watch' configuration, it does not make sense.`); if (options.argv.env.WEBPACK_SERVE) { configuration.watch = false; } } const isFileSystemCacheOptions = (config) => typeof config.cache !== "undefined" && typeof config.cache !== "boolean" && config.cache.type === "filesystem"; // Setup default cache options if (isFileSystemCacheOptions(configuration) && Object.isExtensible(configuration.cache)) { const configPath = config.path.get(configuration); if (configPath) { if (!configuration.cache.buildDependencies) { configuration.cache.buildDependencies = {}; } if (!configuration.cache.buildDependencies.defaultConfig) { configuration.cache.buildDependencies.defaultConfig = []; } const normalizeConfigPath = (configPath) => configPath.startsWith("file://") ? (0, node_url_1.fileURLToPath)(configPath) : node_path_1.default.resolve(configPath); if (Array.isArray(configPath)) { for (const oneOfConfigPath of configPath) { configuration.cache.buildDependencies.defaultConfig.push(normalizeConfigPath(oneOfConfigPath)); } } else { configuration.cache.buildDependencies.defaultConfig.push( // TODO fix `file:` support on webpack side and remove it in the next major release normalizeConfigPath(configPath)); } } } // Respect `process.env.NODE_ENV` if (!configuration.mode && process.env?.NODE_ENV && (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production" || process.env.NODE_ENV === "none")) { configuration.mode = process.env.NODE_ENV; } // Setup stats if (typeof configuration.stats === "undefined") { configuration.stats = { preset: "normal" }; } else if (typeof configuration.stats === "boolean") { configuration.stats = configuration.stats ? { preset: "normal" } : { preset: "none" }; } else if (typeof configuration.stats === "string") { configuration.stats = { preset: configuration.stats }; } let colors; // From arguments if (typeof this.#isColorSupportChanged !== "undefined") { colors = Boolean(this.#isColorSupportChanged); } // From stats else if (typeof configuration.stats.colors !== "undefined") { colors = configuration.stats.colors; } // Default else { colors = Boolean(this.colors.isColorSupported); } if (Object.isExtensible(configuration.stats)) { configuration.stats.colors = colors; } // Apply CLI plugin if (!configuration.plugins) { configuration.plugins = []; } if (Object.isExtensible(configuration.plugins)) { configuration.plugins.unshift(new CLIPlugin({ configPath: config.path.get(configuration), helpfulOutput: !options.json, progress: options.progress, analyze: options.analyze, isMultiCompiler: this.isMultipleConfiguration(config.options), })); } }; if (this.isMultipleConfiguration(config.options)) { for (const item of config.options) { internalBuildConfig(item); } } else { internalBuildConfig(config.options); } return config; } async createCompiler(options, callback) { const { webpack } = options; if (typeof options.configNodeEnv === "string") { process.env.NODE_ENV = options.configNodeEnv; } const config = await this.loadConfig(options); let compiler; try { compiler = callback ? webpack(config.options, (error, stats) => { if (error && this.isValidationError(error)) { this.logger.error(error.message); process.exit(2); } callback(error, stats); }) : webpack(config.options); } catch (error) { if (this.isValidationError(error)) { this.logger.error(error.message); } else { this.logger.error(error); } process.exit(2); } return compiler; } #needWatchStdin(compiler) { if (this.isMultipleCompiler(compiler)) { return Boolean(compiler.compilers.some((compiler) => compiler.options.watchOptions?.stdin)); } return Boolean(compiler.options.watchOptions?.stdin); } async runWebpack(options, isWatchCommand) { let compiler; let stringifyChunked; let Readable; if (options.json) { ({ stringifyChunked } = await import("@discoveryjs/json-ext")); ({ Readable } = await import("node:stream")); } const callback = (error, stats) => { if (error) { this.logger.error(error); process.exit(2); } if (stats && (stats.hasErrors() || (options.failOnWarnings && stats.hasWarnings()))) { process.exitCode = 1; } if (!compiler || !stats) { return; } const statsOptions = this.isMultipleCompiler(compiler) ? { children: compiler.compilers.map((compiler) => compiler.options.stats), } : compiler.options.stats; if (options.json) { const handleWriteError = (error) => { this.logger.error(error); process.exit(2); }; if (options.json === true) { Readable.from(stringifyChunked(stats.toJson(statsOptions))) .on("error", handleWriteError) .pipe(process.stdout) .on("error", handleWriteError) .on("close", () => process.stdout.write("\n")); } else { Readable.from(stringifyChunked(stats.toJson(statsOptions))) .on("error", handleWriteError) .pipe(node_fs_1.default.createWriteStream(options.json)) .on("error", handleWriteError) // Use stderr to logging .on("close", () => { process.stderr.write(`[webpack-cli] ${this.colors.green(`stats are successfully stored as json to ${options.json}`)}\n`); }); } } else { const printedStats = stats.toString(statsOptions); // Avoid extra empty line when `stats: 'none'` if (printedStats) { this.logger.raw(printedStats); } } }; const env = isWatchCommand || options.watch ? { WEBPACK_WATCH: true, ...options.env } : { WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, ...options.env }; options.argv = { ...options, env }; if (isWatchCommand) { options.watch = true; options.isWatchingLikeCommand = true; } compiler = await this.createCompiler(options, callback); if (!compiler) { return; } const needGracefulShutdown = (compiler) => Boolean(this.isMultipleCompiler(compiler) ? compiler.compilers.some((compiler) => compiler.options.watch || (compiler.options.cache && compiler.options.cache.type === "filesystem")) : compiler.options.watch || (compiler.options.cache && compiler.options.cache.type === "filesystem")); if (needGracefulShutdown(compiler)) { let needForceShutdown = false; for (const signal of EXIT_SIGNALS) { // eslint-disable-next-line @typescript-eslint/no-loop-func const listener = () => { if (needForceShutdown) { process.exit(0); } // Output message after delay to avoid extra logging const timeout = setTimeout(() => { this.logger.info("Gracefully shutting down. To force exit, press ^C again. Please wait..."); }, 2000); needForceShutdown = true; compiler.close(() => { clearTimeout(timeout); process.exit(0); }); }; process.on(signal, listener); } if (this.#needWatchStdin(compiler)) { process.stdin.on("end", () => { process.exit(0); }); process.stdin.resume(); } } } } exports.default = WebpackCLI;