webpack-cli
Version:
CLI for webpack & friends
1,132 lines • 119 kB
JavaScript
"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<value>`) 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-<name>` 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