webpack-cli
Version:
CLI for webpack & friends
1,156 lines (1,155 loc) • 90.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
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 fastest_levenshtein_1 = require("fastest-levenshtein");
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"];
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;
logger;
#isColorSupportChanged;
program;
constructor() {
this.colors = this.#createColors();
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();
}
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) {
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;
}
for (const option of commandOptions) {
this.makeOption(command, option);
}
}
command.action(options.action);
return command;
}
makeOption(command, option) {
let mainOption;
let negativeOption;
const flagsWithAlias = ["devtool", "output-path", "target", "watch", "extends"];
if (flagsWithAlias.includes(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";
}
schemaToOptions(webpackMod, schema = undefined, additionalOptions = [], override = {}) {
const args = webpackMod.cli.getArguments(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",
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 webpackOptions = this.schemaToOptions(webpack, undefined, this.#CLIOptions);
const devServer = await this.loadWebpackDevServer();
// @ts-expect-error different versions of the `Schema` type
const devServerOptions = this.schemaToOptions(webpack, devServer.schema, undefined, {
hidden: false,
negativeHidden: false,
});
return { webpack, webpackOptions, devServer, devServerOptions };
},
options: (cmd) => {
const { webpackOptions, devServerOptions } = cmd.context;
return [...webpackOptions, ...devServerOptions];
},
action: async (entries, options, cmd) => {
const { webpack, webpackOptions, devServerOptions } = cmd.context;
const webpackCLIOptions = { webpack, isWatchingLikeCommand: true };
const devServerCLIOptions = {};
for (const optionName in options) {
const kebabedOption = this.toKebabCase(optionName);
const isBuiltInOption = webpackOptions.find((builtInOption) => builtInOption.name === 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 = [];
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 = devServerOptions.find((item) => item.name === 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);