webpack-cli
Version:
CLI for webpack & friends
1,184 lines • 103 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_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",
];
const DEFAULT_WEBPACK_PACKAGES = ["webpack", "loader"];
// 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 message1 = errors[0] instanceof Error ? errors[0].message : String(errors[0]);
const message2 = node_util_1.default.stripVTControlCharacters(errors[1] instanceof Error ? errors[1].message : String(errors[1]));
const message = `▶ ESM (\`import\`) failed:\n ${message1.split("\n").join("\n ")}\n\n▶ CJS (\`require\`) failed:\n ${message2.split("\n").join("\n ")}`.trim();
super(message);
this.stack = "";
}
}
class WebpackCLI {
#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") {
return new Proxy({}, {
get() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (...args) => [...args];
},
});
}
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() {
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;
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) {
return `${parentCmdNames}${command.usage()}\n${bold("Alternative usage to run commands:")} ${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) => {
const formatItem = (term, description) => {
if (description) {
return helper.formatItem(term, helper.padWidth(command, helper), description, helper);
}
return term;
};
const formatList = (textArray) => textArray.join("\n").replaceAll(/^/gm, "");
// Usage
let output = [`${bold("Usage:")} ${helper.commandUsage(command)}`, ""];
// Description
const commandDescription = isGlobalHelp
? "The build tool for modern web applications."
: helper.commandDescription(command);
if (commandDescription.length > 0) {
output = [...output, commandDescription, ""];
}
// Arguments
const argumentList = helper
.visibleArguments(command)
.map((argument) => formatItem(argument.name(), argument.description));
if (argumentList.length > 0) {
output = [...output, bold("Arguments:"), formatList(argumentList), ""];
}
// Options
const optionList = helper
.visibleOptions(command)
.map((option) => formatItem(helper.optionTerm(option), helper.optionDescription(option)));
if (optionList.length > 0) {
output = [...output, bold("Options:"), formatList(optionList), ""];
}
// Global options
const globalOptionList = program.options.map((option) => formatItem(helper.optionTerm(option), helper.optionDescription(option)));
if (globalOptionList.length > 0) {
output = [...output, bold("Global options:"), formatList(globalOptionList), ""];
}
// Commands
const commandList = helper
.visibleCommands(isGlobalHelp ? program : command)
.map((command) => formatItem(helper.subcommandTerm(command), helper.subcommandDescription(command)));
if (commandList.length > 0) {
output = [...output, bold("Commands:"), formatList(commandList), ""];
}
return output.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}]` : "";
this.logger.raw(`${bold("Usage")}: webpack${isCommandSpecified ? ` ${commandName}` : ""} ${option.long}${value ? ` ${value}` : ""}`);
if (option.short) {
this.logger.raw(`${bold("Short:")} webpack${isCommandSpecified ? ` ${commandName}` : ""} ${option.short}${value ? ` ${value}` : ""}`);
}
if (option.description) {
this.logger.raw(`${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(`${bold("Possible values:")} ${possibleValuesUnionTypeString}`);
}
}
this.logger.raw("");
// TODO implement this after refactor cli arguments
// logger.raw('Documentation: https://webpack.js.org/option/name/');
}
else {
outputIncorrectUsageOfHelp();
}
this.logger.raw("To see list of all supported commands and options run 'webpack --help=verbose'.\n");
this.logger.raw(`${bold("Webpack documentation:")} https://webpack.js.org/.`);
this.logger.raw(`${bold("CLI documentation:")} https://webpack.js.org/api/cli/.`);
this.logger.raw(`${bold("Made with ♥ by the webpack team")}.`);
process.exit(0);
}
async #renderVersion(options = {}) {
let info = await this.#getInfoOutput({
...options,
information: {
npmPackages: `{${DEFAULT_WEBPACK_PACKAGES.map((item) => `*${item}*`).join(",")}}`,
},
});
if (typeof options.output === "undefined") {
info = info.replace("Packages:", "").replaceAll(/^\s+/gm, "").trim();
}
return info;
}
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",
config