| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599 |
- "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 (`info`, `version`, …). */
- #uiCommandHeader(name, description) {
- const lines = ["", this.#uiHeader(`webpack ${name}`), this.#uiDivider()];
- if (description) {
- lines.push(description, "");
- }
- return lines.join("\n");
- }
- /** The documentation footer shared by every help and framed-command screen. */
- #uiFooter(options = {}) {
- const { bold, cyan, red } = this.colors;
- const lines = [];
- if (!options.verbose) {
- lines.push(`${cyan("ℹ")} Run ${bold("'webpack --help=verbose'")} to see all available commands and options.`);
- }
- lines.push("", `${bold("Webpack documentation:")} ${cyan("https://webpack.js.org/")}`, `${bold("CLI documentation:")} ${cyan("https://webpack.js.org/api/cli/")}`, `${bold("Made with")} ${red("♥")} ${bold("by the webpack team")}`);
- return lines.join("\n");
- }
- /**
- * Renders the plain-text output of `envinfo` (used by `info` and `version`)
- * into branded sections with aligned rows. Section titles keep their trailing
- * colon so existing tooling/assertions that grep for e.g. "System:" keep
- * working, and "requested => resolved" version pairs are shown with a "→"
- * arrow.
- * @param {string} raw The raw multi-line text produced by `envinfo`.
- * @returns {string} The styled, section-divided output.
- */
- #renderEnvinfo(raw) {
- const { bold, cyan, green } = this.colors;
- const lines = [];
- const sections = [];
- let section;
- for (const line of raw.split("\n")) {
- const sectionMatch = /^ {2}(\S[^:]*):\s*$/.exec(line);
- if (sectionMatch) {
- section = { title: sectionMatch[1].trim(), rows: [] };
- sections.push(section);
- continue;
- }
- const rowMatch = /^ {4}([^:]+):\s*(.*)$/.exec(line);
- if (rowMatch && section) {
- section.rows.push({ label: rowMatch[1].trim(), value: rowMatch[2].trim() || "N/A" });
- }
- }
- for (const { title, rows } of sections) {
- if (rows.length === 0) {
- continue;
- }
- const labelWidth = Math.max(...rows.map((row) => row.label.length)) + 1;
- lines.push("", this.#uiHeader(`${title}:`), this.#uiDivider());
- for (const { label, value } of rows) {
- const arrowIndex = value.indexOf("=>");
- const renderedValue = arrowIndex === -1
- ? green(value)
- : `${green(value.slice(0, arrowIndex).trim())} ${cyan("→")} ${bold(green(value.slice(arrowIndex + 2).trim()))}`;
- lines.push(`${UI_INDENT}${bold(`${label}:`.padEnd(labelWidth))} ${renderedValue}`);
- }
- }
- return [...lines, this.#uiDivider()].join("\n");
- }
- /**
- * Prints a framed command screen: a branded header, the command body, and the
- * shared documentation footer. Used by `info`, `version` and `configtest`.
- * @param {object} meta The command name and a one-line description.
- * @param {Function} body A callback that prints the command's main output.
- */
- async #frame(meta, body) {
- this.logger.raw(this.#uiCommandHeader(meta.name, meta.description));
- await body();
- this.logger.raw(this.#uiFooter());
- }
- /** Prints a status message with an icon; errors go to stderr, the rest to stdout. */
- #status(kind, message) {
- if (kind === "error") {
- process.stderr.write(`${this.#uiStatusLine(kind, message)}\n`);
- return;
- }
- this.logger.raw(this.#uiStatusLine(kind, message));
- }
- async #getInfoOutput(options) {
- let { output } = options;
- const envinfoConfig = {};
- if (output) {
- // Remove quotes if exist
- output = output.replaceAll(/['"]+/g, "");
- switch (output) {
- case "markdown":
- envinfoConfig.markdown = true;
- break;
- case "json":
- envinfoConfig.json = true;
- break;
- default:
- this.logger.error(`'${output}' is not a valid value for output`);
- process.exit(2);
- }
- }
- let envinfoOptions;
- if (options.information) {
- envinfoOptions = options.information;
- }
- else {
- const defaultInformation = {
- Binaries: ["Node", "Yarn", "npm", "pnpm"],
- Browsers: [
- "Brave Browser",
- "Chrome",
- "Chrome Canary",
- "Edge",
- "Firefox",
- "Firefox Developer Edition",
- "Firefox Nightly",
- "Internet Explorer",
- "Safari",
- "Safari Technology Preview",
- ],
- // @ts-expect-error No in types
- Monorepos: ["Yarn Workspaces", "Lerna"],
- System: ["OS", "CPU", "Memory"],
- npmGlobalPackages: ["webpack", "webpack-cli", "webpack-dev-server"],
- };
- const npmPackages = [...DEFAULT_WEBPACK_PACKAGES, ...(options.additionalPackage || [])];
- defaultInformation.npmPackages = `{${npmPackages.map((item) => `*${item}*`).join(",")}}`;
- envinfoOptions = defaultInformation;
- }
- const envinfo = (await import("envinfo")).default;
- let info = await envinfo.run(envinfoOptions, envinfoConfig);
- info = info.replace("npmPackages", "Packages");
- info = info.replace("npmGlobalPackages", "Global Packages");
- return info;
- }
- async #loadPackage(pkg, isCustom) {
- const importTarget = isCustom && /^(?:[A-Za-z]:(\\|\/)|\\\\|\/)/.test(pkg) ? (0, node_url_1.pathToFileURL)(pkg).toString() : pkg;
- return (await import(importTarget)).default;
- }
- async loadWebpack() {
- return this.#loadPackage(WEBPACK_PACKAGE, WEBPACK_PACKAGE_IS_CUSTOM);
- }
- async loadWebpackDevServer() {
- return this.#loadPackage(WEBPACK_DEV_SERVER_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM);
- }
- #minimumHelpOptions = new Set([
- "mode",
- "watch",
- "watch-options-stdin",
- "stats",
- "devtool",
- "entry",
- "target",
- "name",
- "output-path",
- "extends",
- ]);
- #minimumNegativeHelpOptions = new Set(["devtool"]);
- #CLIOptions = [
- // For configs
- {
- name: "config",
- alias: "c",
- configs: [
- {
- type: "string",
- },
- ],
- multiple: true,
- valueName: "pathToConfigFile",
- description: 'Provide path to one or more webpack configuration files to process, e.g. "./webpack.config.js".',
- hidden: false,
- },
- {
- name: "config-name",
- configs: [
- {
- type: "string",
- },
- ],
- multiple: true,
- valueName: "name",
- description: "Name(s) of particular configuration(s) to use if configuration file exports an array of multiple configurations.",
- hidden: false,
- },
- {
- name: "merge",
- alias: "m",
- configs: [
- {
- type: "enum",
- values: [true],
- },
- ],
- description: "Merge two or more configurations using 'webpack-merge'.",
- hidden: false,
- },
- // Complex configs
- {
- name: "env",
- type: (value, previous = {}) => {
- // This ensures we're only splitting by the first `=`
- const [allKeys, val] = value.split(/[=](.+)/, 2);
- const splitKeys = allKeys.split(/\.(?!$)/);
- let prevRef = previous;
- for (let [index, someKey] of splitKeys.entries()) {
- // https://github.com/webpack/webpack-cli/issues/3284
- if (someKey.endsWith("=")) {
- // remove '=' from key
- someKey = someKey.slice(0, -1);
- // @ts-expect-error we explicitly want to set it to undefined
- prevRef[someKey] = undefined;
- continue;
- }
- if (!prevRef[someKey]) {
- prevRef[someKey] = {};
- }
- if (typeof prevRef[someKey] === "string") {
- prevRef[someKey] = {};
- }
- if (index === splitKeys.length - 1) {
- prevRef[someKey] = typeof val === "string" ? val : true;
- }
- prevRef = prevRef[someKey];
- }
- return previous;
- },
- multiple: true,
- description: 'Environment variables passed to the configuration when it is a function, e.g. "myvar" or "myvar=myval".',
- hidden: false,
- },
- {
- name: "config-node-env",
- configs: [
- {
- type: "string",
- },
- ],
- multiple: false,
- description: "Sets process.env.NODE_ENV to the specified value for access within the configuration.",
- hidden: false,
- },
- // Adding more plugins
- {
- name: "analyze",
- configs: [
- {
- type: "enum",
- values: [true],
- },
- ],
- multiple: false,
- description: "It invokes webpack-bundle-analyzer plugin to get bundle information.",
- hidden: false,
- },
- {
- name: "progress",
- configs: [
- {
- type: "string",
- },
- {
- type: "enum",
- values: [true],
- },
- ],
- description: "Print compilation progress during build.",
- hidden: false,
- },
- // Output options
- {
- name: "json",
- configs: [
- {
- type: "string",
- },
- {
- type: "enum",
- values: [true],
- },
- ],
- alias: "j",
- valueName: "pathToJsonFile",
- description: "Prints result as JSON or store it in a file.",
- hidden: false,
- },
- {
- name: "fail-on-warnings",
- configs: [
- {
- type: "enum",
- values: [true],
- },
- ],
- description: "Stop webpack-cli process with non-zero exit code on warnings from webpack.",
- hidden: false,
- },
- {
- name: "disable-interpret",
- configs: [
- {
- type: "enum",
- values: [true],
- },
- ],
- description: "Disable interpret for loading the config file.",
- hidden: false,
- },
- ];
- #commands = {
- build: {
- rawName: "build",
- name: "build [entries...]",
- alias: ["bundle", "b"],
- description: "Run webpack (default command, can be omitted).",
- usage: "[entries...] [options]",
- dependencies: [WEBPACK_PACKAGE],
- preload: async () => {
- const webpack = await this.loadWebpack();
- return { webpack };
- },
- options: async (cmd) => this.schemaToOptions(cmd.context.webpack, undefined, this.#CLIOptions),
- action: async (entries, options, cmd) => {
- const { webpack } = cmd.context;
- if (entries.length > 0) {
- options.entry = [...entries, ...(options.entry || [])];
- }
- options.webpack = webpack;
- await this.runWebpack(options, false);
- },
- },
- watch: {
- rawName: "watch",
- name: "watch [entries...]",
- alias: "w",
- description: "Run webpack and watch for files changes.",
- usage: "[entries...] [options]",
- dependencies: [WEBPACK_PACKAGE],
- preload: async () => {
- const webpack = await this.loadWebpack();
- return { webpack };
- },
- options: async (cmd) => this.schemaToOptions(cmd.context.webpack, undefined, this.#CLIOptions),
- action: async (entries, options, cmd) => {
- const { webpack } = cmd.context;
- if (entries.length > 0) {
- options.entry = [...entries, ...(options.entry || [])];
- }
- options.webpack = webpack;
- await this.runWebpack(options, true);
- },
- },
- serve: {
- rawName: "serve",
- name: "serve [entries...]",
- alias: ["server", "s"],
- description: "Run the webpack dev server and watch for source file changes while serving.",
- usage: "[entries...] [options]",
- dependencies: [WEBPACK_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE],
- preload: async () => {
- const webpack = await this.loadWebpack();
- const devServer = await this.loadWebpackDevServer();
- return { webpack, devServer };
- },
- options: (cmd) => {
- const { webpack, devServer } = cmd.context;
- const webpackOptions = this.schemaToOptions(webpack, undefined, this.#CLIOptions);
- // @ts-expect-error different versions of the `Schema` type
- const devServerOptions = this.schemaToOptions(webpack, devServer.schema, undefined, {
- hidden: false,
- negativeHidden: false,
- });
- return [...webpackOptions, ...devServerOptions];
- },
- action: async (entries, options, cmd) => {
- const { webpack, devServer } = cmd.context;
- const webpackCLIOptions = { webpack, isWatchingLikeCommand: true };
- const devServerCLIOptions = {};
- // Derive the built-in option names from the cached argument metadata
- // instead of retaining the full option arrays for the whole session.
- const webpackOptionNames = new Set([
- ...this.#CLIOptions.map((option) => option.name),
- ...Object.keys(this.#getArguments(webpack, undefined)),
- ]);
- for (const optionName in options) {
- const kebabedOption = this.toKebabCase(optionName);
- const isBuiltInOption = webpackOptionNames.has(kebabedOption);
- if (isBuiltInOption) {
- webpackCLIOptions[optionName] = options[optionName];
- }
- else {
- devServerCLIOptions[optionName] = options[optionName];
- }
- }
- if (entries.length > 0) {
- webpackCLIOptions.entry = [...entries, ...(options.entry || [])];
- }
- webpackCLIOptions.argv = {
- ...options,
- env: { WEBPACK_SERVE: true, ...options.env },
- };
- const compiler = await this.createCompiler(webpackCLIOptions);
- if (!compiler) {
- return;
- }
- const DevServer = cmd.context.devServer;
- const servers = [];
- if (this.#needWatchStdin(compiler)) {
- process.stdin.on("end", () => {
- Promise.all(servers.map((server) => server.stop())).then(() => {
- process.exit(0);
- });
- });
- process.stdin.resume();
- }
- const compilers = this.isMultipleCompiler(compiler) ? compiler.compilers : [compiler];
- const possibleCompilers = compilers.filter((compiler) => compiler.options.devServer);
- const compilersForDevServer = possibleCompilers.length > 0 ? possibleCompilers : [compilers[0]];
- const usedPorts = [];
- // @ts-expect-error different versions of the `Schema` type
- const devServerArgs = this.#getArguments(webpack, devServer.schema);
- for (const compilerForDevServer of compilersForDevServer) {
- if (compilerForDevServer.options.devServer === false) {
- continue;
- }
- const devServerConfiguration = compilerForDevServer.options.devServer || {};
- const args = {};
- const values = {};
- for (const name of Object.keys(options)) {
- if (name === "argv")
- continue;
- const kebabName = this.toKebabCase(name);
- const arg = devServerArgs[kebabName];
- if (arg) {
- args[name] = arg;
- // We really don't know what the value is
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- values[name] = options[name];
- }
- }
- if (Object.keys(values).length > 0) {
- this.#processArguments(webpack, args, devServerConfiguration, values);
- }
- if (devServerConfiguration.port) {
- const portNumber = Number(devServerConfiguration.port);
- if (usedPorts.includes(portNumber)) {
- throw new Error("Unique ports must be specified for each devServer option in your webpack configuration. Alternatively, run only 1 devServer config using the --config-name flag to specify your desired config.");
- }
- usedPorts.push(portNumber);
- }
- try {
- const server = new DevServer(devServerConfiguration, compiler);
- await server.start();
- servers.push(server);
- }
- catch (error) {
- if (this.isValidationError(error)) {
- this.logger.error(error.message);
- }
- else {
- this.logger.error(error);
- }
- process.exit(2);
- }
- }
- if (servers.length === 0) {
- this.logger.error("No dev server configurations to run");
- process.exit(2);
- }
- },
- },
- help: {
- rawName: "help",
- name: "help [command] [option]",
- alias: "h",
- description: "Display help for commands and options.",
- action: () => {
- // Nothing, just stub
- },
- },
- version: {
- rawName: "version",
- name: "version",
- alias: "v",
- usage: "[options]",
- description: "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and other packages.",
- options: [
- {
- name: "output",
- alias: "o",
- configs: [
- {
- type: "string",
- },
- ],
- description: "To get the output in a specified format (accept json or markdown)",
- hidden: false,
- },
- ],
- action: async (options) => {
- // `--output json|markdown` stays machine-readable and unframed.
- if (options.output) {
- this.logger.raw(await this.#renderVersion(options));
- return;
- }
- await this.#frame({ name: "version", description: "Installed package versions." }, async () => this.logger.raw(await this.#renderVersion(options)));
- },
- },
- info: {
- rawName: "info",
- name: "info",
- alias: "i",
- usage: "[options]",
- description: "Outputs information about your system.",
- options: [
- {
- name: "output",
- alias: "o",
- configs: [
- {
- type: "string",
- },
- ],
- description: "To get the output in a specified format (accept json or markdown)",
- hidden: false,
- },
- {
- name: "additional-package",
- alias: "a",
- configs: [{ type: "string" }],
- multiple: true,
- description: "Adds additional packages to the output",
- hidden: false,
- },
- ],
- action: async (options) => {
- const info = await this.#getInfoOutput(options);
- // `--output json|markdown` stays machine-readable and unframed.
- if (options.output) {
- this.logger.raw(info);
- return;
- }
- await this.#frame({ name: "info", description: "System and environment information." }, () => this.logger.raw(this.#renderEnvinfo(info)));
- },
- },
- configtest: {
- rawName: "configtest",
- name: "configtest [config-path]",
- alias: "t",
- description: "Validate a webpack configuration.",
- dependencies: [WEBPACK_PACKAGE],
- options: [],
- preload: async () => {
- const webpack = await this.loadWebpack();
- return { webpack };
- },
- action: async (configPath, _options, cmd) => {
- const { webpack } = cmd.context;
- const env = {};
- const argv = { env };
- const config = await this.loadConfig(configPath ? { env, argv, webpack, config: [configPath] } : { env, argv, webpack });
- const configPaths = new Set();
- if (Array.isArray(config.options)) {
- for (const options of config.options) {
- const loadedConfigPaths = config.path.get(options);
- if (loadedConfigPaths) {
- for (const path of loadedConfigPaths)
- configPaths.add(path);
- }
- }
- }
- else if (config.path.get(config.options)) {
- const loadedConfigPaths = config.path.get(config.options);
- if (loadedConfigPaths) {
- for (const path of loadedConfigPaths)
- configPaths.add(path);
- }
- }
- await this.#frame({ name: "configtest", description: "Validating your webpack configuration." }, () => {
- if (configPaths.size === 0) {
- this.#status("error", "No configuration found.");
- process.exit(2);
- }
- this.#status("info", `Validating: ${[...configPaths].join(", ")}`);
- try {
- cmd.context.webpack.validate(config.options);
- }
- catch (error) {
- this.#status("error", this.isValidationError(error)
- ? error.message
- : String(error instanceof Error ? error.message : error));
- process.exit(2);
- }
- this.#status("success", "No validation errors found.");
- });
- },
- },
- };
- #isCommand(input, commandOptions) {
- const longName = commandOptions.rawName;
- if (input === longName) {
- return true;
- }
- if (commandOptions.alias) {
- if (Array.isArray(commandOptions.alias)) {
- return commandOptions.alias.includes(input);
- }
- return commandOptions.alias === input;
- }
- return false;
- }
- #findCommandByName(name) {
- return this.program.commands.find((command) => name === command.name() || command.aliases().includes(name));
- }
- async #loadCommandByName(commandName) {
- if (this.#isCommand(commandName, this.#commands.build)) {
- return await this.makeCommand(this.#commands.build);
- }
- else if (this.#isCommand(commandName, this.#commands.serve)) {
- return await this.makeCommand(this.#commands.serve);
- }
- else if (this.#isCommand(commandName, this.#commands.watch)) {
- return await this.makeCommand(this.#commands.watch);
- }
- else if (this.#isCommand(commandName, this.#commands.help)) {
- // Stub for the `help` command
- return await this.makeCommand(this.#commands.help);
- }
- else if (this.#isCommand(commandName, this.#commands.version)) {
- return await this.makeCommand(this.#commands.version);
- }
- else if (this.#isCommand(commandName, this.#commands.info)) {
- return await this.makeCommand(this.#commands.info);
- }
- else if (this.#isCommand(commandName, this.#commands.configtest)) {
- return await this.makeCommand(this.#commands.configtest);
- }
- const pkg = commandName;
- let LoadedCommand;
- try {
- LoadedCommand = (await import(pkg)).default;
- }
- catch (error) {
- if (error.code !== "ERR_MODULE_NOT_FOUND") {
- this.logger.error(`Unable to load '${pkg}' command`);
- this.logger.error(error);
- process.exit(2);
- }
- return;
- }
- let command;
- let externalCommand;
- try {
- command = new LoadedCommand();
- externalCommand = await command.apply(this);
- }
- catch (error) {
- this.logger.error(`Unable to load '${pkg}' command`);
- this.logger.error(error);
- process.exit(2);
- }
- return externalCommand;
- }
- async run(args, parseOptions) {
- // Default `--color` and `--no-color` options
- const self = this;
- // Register own exit
- this.program.exitOverride((error) => {
- if (error.exitCode === 0) {
- process.exit(0);
- return;
- }
- if (error.code === "commander.unknownOption") {
- let name = error.message.match(/'(.+)'/);
- if (name) {
- name = name[1].slice(2);
- if (name.includes("=")) {
- [name] = name.split("=");
- }
- const { operands } = this.program.parseOptions(this.program.args);
- const operand = typeof operands[0] !== "undefined" ? operands[0] : this.#commands.build.rawName;
- if (operand) {
- const command = this.#findCommandByName(operand);
- if (!command) {
- this.logger.error(`Can't find and load command '${operand}'`);
- this.logger.error("Run 'webpack --help' to see available commands and options");
- process.exit(2);
- }
- const { allOptionNames } = command;
- const candidateNames = allOptionNames ??
- command.options
- .filter((option) => !option.internal)
- .map((option) => option.long?.slice(2));
- for (const candidate of candidateNames) {
- if (candidate && WebpackCLI.#distance(name, candidate) < 3) {
- this.logger.error(`Did you mean '--${candidate}'?`);
- }
- }
- }
- }
- }
- this.logger.error("Run 'webpack --help' to see available commands and options");
- process.exit(2);
- });
- this.program.option("--color", "Enable colors on console.");
- this.program.on("option:color", function color() {
- const { color } = this.opts();
- self.#isColorSupportChanged = color;
- self.colors = self.#createColors(color);
- });
- this.program.option("--no-color", "Disable colors on console.");
- this.program.on("option:no-color", function noColor() {
- const { color } = this.opts();
- self.#isColorSupportChanged = color;
- self.colors = self.#createColors(color);
- });
- this.program.option("-v, --version", "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and other packages.");
- // webpack-cli has it's own logic for showing suggestions
- this.program.showSuggestionAfterError(false);
- // Suppress the default help option
- this.program.helpOption(false);
- // Suppress the default help command
- this.program.helpCommand(false);
- this.program.option("-h, --help [verbose]", "Display help for commands and options.");
- // Basic command for lazy loading other commands
- // By default we don't load any commands and options, commands and options registration takes a lot of time instead we load them lazily
- // That is why we need to set `allowUnknownOption` to `true`, otherwise commander will not work
- this.program.allowUnknownOption(true);
- // For lazy loading other commands too
- this.program.allowExcessArguments(true);
- this.program.action(async (options) => {
- const { operands, unknown } = this.program.parseOptions(this.program.args);
- // Remember the flag tokens so command setup only registers options that
- // are actually used (see `#neededOptionNames`).
- this.#argvForParsing = unknown;
- const defaultCommandNameToRun = this.#commands.build.rawName;
- const hasOperand = typeof operands[0] !== "undefined";
- const operand = hasOperand ? operands[0] : defaultCommandNameToRun;
- const isHelpOption = typeof options.help !== "undefined";
- const isHelpCommandSyntax = this.#isCommand(operand, this.#commands.help);
- if (isHelpOption || isHelpCommandSyntax) {
- let isVerbose = false;
- if (isHelpOption && typeof options.help === "string") {
- if (options.help !== "verbose") {
- this.logger.error("Unknown value for '--help' option, please use '--help=verbose'");
- process.exit(2);
- }
- isVerbose = true;
- }
- this.program.forHelp = true;
- const optionsForHelp = [
- ...(isHelpOption && hasOperand ? [operand] : []),
- ...operands.slice(1),
- ...unknown,
- ...(isHelpCommandSyntax && typeof options.color !== "undefined"
- ? [options.color ? "--color" : "--no-color"]
- : []),
- ...(isHelpCommandSyntax && typeof options.version !== "undefined" ? ["--version"] : []),
- ];
- await this.#outputHelp(optionsForHelp, isVerbose, isHelpCommandSyntax, this.program);
- }
- const isVersionOption = typeof options.version !== "undefined";
- if (isVersionOption) {
- const info = await this.#renderVersion();
- this.logger.raw(info);
- process.exit(0);
- }
- let isKnownCommand = false;
- for (const command of Object.values(this.#commands)) {
- if (command.rawName === operand ||
- (Array.isArray(command.alias)
- ? command.alias.includes(operand)
- : command.alias === operand)) {
- isKnownCommand = true;
- break;
- }
- }
- let command;
- let commandOperands = operands.slice(1);
- if (isKnownCommand) {
- command = await this.#loadCommandByName(operand);
- }
- else {
- let isEntrySyntax;
- try {
- await node_fs_1.default.promises.access(operand, node_fs_1.default.constants.F_OK);
- isEntrySyntax = true;
- }
- catch {
- isEntrySyntax = false;
- }
- if (isEntrySyntax) {
- commandOperands = operands;
- command = await this.#loadCommandByName(defaultCommandNameToRun);
- }
- else {
- // Try to load external command
- try {
- command = await this.#loadCommandByName(operand);
- }
- catch {
- // Nothing
- }
- if (!command) {
- this.logger.error(`Unknown command or entry '${operand}'`);
- const found = Object.values(this.#commands).find((commandOptions) => WebpackCLI.#distance(operand, commandOptions.rawName) < 3);
- if (found) {
- this.logger.error(`Did you mean '${found.rawName}' (alias '${Array.isArray(found.alias) ? found.alias.join(", ") : found.alias}')?`);
- }
- this.logger.error("Run 'webpack --help' to see available commands and options");
- process.exit(2);
- }
- }
- }
- if (!command) {
- throw new Error(`Internal error: Registered command "${operand}" is missing an action handler.`);
- }
- await command.parseAsync([...commandOperands, ...unknown], { from: "user" });
- });
- await this.program.parseAsync(args, parseOptions);
- }
- // Finds the highest-priority default config file by reading each candidate directory once (case-insensitively) and confirming with `access`, instead of probing every `<name><ext>` combination separately.
- async #findDefaultConfigFile() {
- // Popular extensions, tried first. The common case (e.g. `webpack.config.js`)
- // matches here, so `interpret` is never loaded — see `getExoticExtensions`.
- const commonExtensions = [".js", ".mjs", ".cjs", ".ts", ".cts", ".mts"];
- // `interpret`'s extra extensions (e.g. `.coffee`) are only needed when no
- // common-extension config exists, so defer importing it until then.
- let exoticExtensions;
- const getExoticExtensions = async () => {
- if (typeof exoticExtensions === "undefined") {
- const interpret = await import("interpret");
- const common = new Set(commonExtensions);
- exoticExtensions = Object.keys(interpret.extensions).filter((ext) => !common.has(ext));
- }
- return exoticExtensions;
- };
- const directoryEntriesCache = new Map();
- const readDirectoryEntries = async (directory) => {
- let entries = directoryEntriesCache.get(directory);
- if (typeof entries === "undefined") {
- try {
- entries = new Set((await node_fs_1.default.promises.readdir(directory)).map((entry) => entry.toLowerCase()));
- }
- catch {
- entries = null;
- }
- directoryEntriesCache.set(directory, entries);
- }
- return entries;
- };
- const findInExtensions = async (resolvedBase, basename, entries, extensions) => {
- for (const ext of extensions) {
- // Skip candidates absent from the listing, but when the directory can't be listed (`entries` is `null`) probe every candidate directly.
- if (entries && !entries.has((basename + ext).toLowerCase())) {
- continue;
- }
- const candidate = resolvedBase + ext;
- // Confirm with `access` to preserve exact existence semantics (e.g.
- // broken symlinks are listed by `readdir` but fail `access`).
- try {
- await node_fs_1.default.promises.access(candidate, node_fs_1.default.constants.F_OK);
- return candidate;
- }
- catch {
- // Listed but not accessible, keep looking
- }
- }
- return undefined;
- };
- // Order defines the priority, in decreasing order. Within each filename,
- // common extensions take priority over the exotic ones (matching the
- // previous combined ordering).
- for (const filename of DEFAULT_CONFIGURATION_FILES) {
- const resolvedBase = node_path_1.default.resolve(filename);
- const entries = await readDirectoryEntries(node_path_1.default.dirname(resolvedBase));
- const basename = node_path_1.default.basename(resolvedBase);
- const common = await findInExtensions(resolvedBase, basename, entries, commonExtensions);
- if (common) {
- return common;
- }
- const exotic = await findInExtensions(resolvedBase, basename, entries, await getExoticExtensions());
- if (exotic) {
- return exotic;
- }
- }
- return undefined;
- }
- async loadConfig(options) {
- const disableInterpret = typeof options.disableInterpret !== "undefined" && options.disableInterpret;
- const loadConfigByPath = async (configPath, argv = { env: {} }) => {
- let options;
- const isFileURL = configPath.startsWith("file://");
- try {
- const ext = node_path_1.default.extname(configPath).toLowerCase();
- const dataFormatLoader = DATA_FORMAT_LOADERS[ext];
- if (dataFormatLoader) {
- // Non-JavaScript formats cannot be `import(...)`ed, so parse them
- // directly and skip the doomed dynamic import attempt entirely.
- const configFilePath = isFileURL ? (0, node_url_1.fileURLToPath)(configPath) : node_path_1.default.resolve(configPath);
- let parser;
- try {
- // Resolve the parser from the configuration file's location (so it
- // is picked up from the user's project rather than from webpack-cli),
- // then load it asynchronously via `import(...)`.
- const parserPath = (0, node_module_1.createRequire)(configFilePath).resolve(dataFormatLoader.package);
- const parserModule = await import((0, node_url_1.pathToFileURL)(parserPath).href);
- parser = (parserModule.default ?? parserModule);
- }
- catch {
- this.logger.error(`Unable to load the '${configFilePath}' configuration file.`);
- this.logger.error(`Loading '${ext}' configuration files requires the '${dataFormatLoader.package}' package, which is not installed. Please install it, e.g. \`npm install --save-dev ${dataFormatLoader.package}\`.`);
- process.exit(2);
- }
- // Reading/parsing errors propagate to the handler below, which
- // reports `Failed to load '<config>' config`.
- const source = await node_fs_1.default.promises.readFile(configFilePath, "utf8");
- options = parser[dataFormatLoader.method](source);
- }
- else {
- let loadingError;
- try {
- options = // eslint-disable-next-line no-eval
- (await eval(`import("${isFileURL ? configPath : (0, node_url_1.pathToFileURL)(configPath)}")`))
- .default;
- }
- catch (err) {
- if (this.isValidationError(err) || process.env?.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) {
- throw err;
- }
- loadingError = err;
- }
- // Fallback logic when we can't use `import(...)`. `interpret`/`rechoir`
- // still handle the JavaScript variants (`.ts`, `.coffee`,
- // `.babel.js`, ...). This path is planned to be removed once those
- // formats are loaded natively as well.
- if (loadingError) {
- const configFilePath = isFileURL ? (0, node_url_1.fileURLToPath)(configPath) : node_path_1.default.resolve(configPath);
- const { jsVariants, extensions } = await import("interpret");
- let interpreted = Object.keys(jsVariants).find((variant) => variant === ext);
- if (!interpreted && ext.endsWith(".cts")) {
- interpreted = jsVariants[".ts"];
- }
- // `tsx` is missing from `interpret`'s loader tables, and it cannot
- // be added there: its `tsx/cjs` entry point exists only in the
- // package's `exports` map, which `rechoir`'s resolver does not
- // support. Register it manually instead, resolved from the
- // configuration file's location so the user's copy is picked up.
- // Requiring `tsx/cjs` installs its require hook as a side effect.
- const registerTsxLoader = () => {
- try {
- (0, node_module_1.createRequire)(configFilePath)("tsx/cjs");
- return undefined;
- }
- catch (err) {
- // `throw` may raise non-`Error` values; normalize so the
- // error-reporting path can rely on `.message`.
- return err instanceof Error ? err : new Error(String(err));
- }
- };
- if (interpreted && !disableInterpret) {
- const rechoir = (await import("rechoir")).default;
- try {
- rechoir.prepare(extensions, configPath);
- }
- catch (error) {
- // The loaders known to `interpret` take priority; `tsx` is
- // only tried once all of them have failed.
- const isTsxLoadable = TSX_LOADABLE_EXTENSIONS.has(ext);
- const tsxFailure = isTsxLoadable ? registerTsxLoader() : undefined;
- if (!isTsxLoadable || tsxFailure) {
- if (error?.failures) {
- this.logger.error(`Unable to load '${configPath}'`);
- this.logger.error(error.message);
- for (const failure of error.failures) {
- this.logger.error(failure.error.message);
- }
- if (tsxFailure) {
- // Only the first line — `require` errors append a noisy
- // "Require stack" to the message.
- const [tsxFailureReason] = tsxFailure.message.split("\n");
- this.logger.error(tsxFailureReason);
- }
- this.logger.error("Please install one of them");
- process.exit(2);
- }
- this.logger.error(error);
- process.exit(2);
- }
- }
- }
- else if (TSX_LOADABLE_EXTENSIONS.has(ext) && !disableInterpret) {
- // `.mts` has no entry in `interpret`'s tables, so `tsx` is the
- // only loader we can offer. Best effort: when it is missing, the
- // `require()` below fails and the original errors are reported,
- // exactly as before.
- registerTsxLoader();
- }
- try {
- options = require(configFilePath);
- }
- catch (err) {
- if (this.isValidationError(err)) {
- throw err;
- }
- throw new ConfigurationLoadingError([loadingError, err]);
- }
- }
- }
- // To handle `babel`/`module.exports.default = {};`
- if (options && typeof options === "object" && "default" in options) {
- options = options.default;
- }
- if (!options) {
- this.logger.warn(`Default export is missing or nullish at (from ${configPath}). Webpack will run with an empty configuration. Please double-check that this is what you want. If you want to run webpack with an empty config, \`export {}\`/\`module.exports = {};\` to remove this warning.`);
- options = {};
- }
- }
- catch (error) {
- if (error instanceof ConfigurationLoadingError) {
- this.logger.error(`Failed to load '${configPath}' config\n${error.message}`);
- }
- else {
- this.logger.error(`Failed to load '${configPath}' config`);
- this.logger.error(error);
- }
- process.exit(2);
- }
- if (Array.isArray(options)) {
- const optionsArray = options;
- await Promise.all(optionsArray.map(async (_, i) => {
- if (this.isPromise(optionsArray[i])) {
- optionsArray[i] = await optionsArray[i];
- }
- // `Promise` may return `Function`
- if (this.isFunction(optionsArray[i])) {
- // when config is a function, pass the env from args to the config function
- optionsArray[i] = await optionsArray[i](argv.env, argv);
- }
- }));
- options = optionsArray;
- }
- else {
- if (this.isPromise(options)) {
- options = await options;
- }
- // `Promise` may return `Function`
- if (this.isFunction(options)) {
- // when config is a function, pass the env from args to the config function
- options = await options(argv.env, argv);
- }
- }
- const isObject = (value) => typeof value === "object" && value !== null;
- if (!isObject(options) && !Array.isArray(options)) {
- this.logger.error(`Invalid configuration in '${configPath}'`);
- process.exit(2);
- }
- return {
- options: options,
- path: configPath,
- };
- };
- const config = {
- options: {},
- path: new WeakMap(),
- };
- if (options.config && options.config.length > 0) {
- const loadedConfigs = await Promise.all(options.config.map((configPath) => loadConfigByPath(configPath, options.argv)));
- if (loadedConfigs.length === 1) {
- config.options = loadedConfigs[0].options;
- config.path.set(loadedConfigs[0].options, [loadedConfigs[0].path]);
- }
- else {
- config.options = [];
- for (const loadedConfig of loadedConfigs) {
- if (Array.isArray(loadedConfig.options)) {
- for (const item of loadedConfig.options) {
- config.options.push(item);
- config.path.set(options, [loadedConfig.path]);
- }
- }
- else {
- config.options.push(loadedConfig.options);
- config.path.set(loadedConfig.options, [loadedConfig.path]);
- }
- }
- }
- }
- else {
- const foundDefaultConfigFile = await this.#findDefaultConfigFile();
- if (foundDefaultConfigFile) {
- const loadedConfig = await loadConfigByPath(foundDefaultConfigFile, options.argv);
- config.options = loadedConfig.options;
- if (this.isMultipleConfiguration(config.options)) {
- for (const item of config.options) {
- config.path.set(item, [loadedConfig.path]);
- }
- }
- else {
- config.path.set(loadedConfig.options, [loadedConfig.path]);
- }
- }
- }
- if (options.configName) {
- const notFoundConfigNames = [];
- config.options = options.configName.map((configName) => {
- let found;
- if (this.isMultipleConfiguration(config.options)) {
- found = config.options.find((options) => options.name === configName);
- }
- else {
- found = config.options.name === configName ? config.options : undefined;
- }
- if (!found) {
- notFoundConfigNames.push(configName);
- }
- return found;
- });
- if (notFoundConfigNames.length > 0) {
- this.logger.error(notFoundConfigNames
- .map((configName) => `Configuration with the name "${configName}" was not found.`)
- .join(" "));
- process.exit(2);
- }
- }
- const resolveExtends = async (config, configPaths, extendsPaths) => {
- delete config.extends;
- const loadedConfigs = await Promise.all(extendsPaths.map((extendsPath) => loadConfigByPath(extendsPath, options.argv)));
- const { merge } = await import("webpack-merge");
- const loadedOptions = loadedConfigs.flatMap((config) => config.options);
- if (loadedOptions.length > 0) {
- const prevPaths = configPaths.get(config);
- const loadedPaths = loadedConfigs.flatMap((config) => config.path);
- if (prevPaths) {
- const intersection = loadedPaths.filter((element) => prevPaths.includes(element));
- if (intersection.length > 0) {
- this.logger.error("Recursive configuration detected, exiting.");
- process.exit(2);
- }
- }
- config = merge(...loadedOptions, config);
- if (prevPaths) {
- configPaths.set(config, [...prevPaths, ...loadedPaths]);
- }
- }
- if (config.extends) {
- const extendsPaths = typeof config.extends === "string" ? [config.extends] : config.extends;
- config = await resolveExtends(config, configPaths, extendsPaths);
- }
- return config;
- };
- // The `extends` param in CLI gets priority over extends in config file
- if (options.extends && options.extends.length > 0) {
- const extendsPaths = options.extends;
- if (this.isMultipleConfiguration(config.options)) {
- config.options = await Promise.all(config.options.map((options) => resolveExtends(options, config.path, extendsPaths)));
- }
- else {
- // load the config from the extends option
- config.options = await resolveExtends(config.options, config.path, extendsPaths);
- }
- }
- // if no extends option is passed, check if the config file has extends
- else if (this.isMultipleConfiguration(config.options) &&
- config.options.some((options) => options.extends)) {
- config.options = await Promise.all(config.options.map((options) => {
- if (options.extends) {
- return resolveExtends(options, config.path, typeof options.extends === "string" ? [options.extends] : options.extends);
- }
- return options;
- }));
- }
- else if (!this.isMultipleConfiguration(config.options) && config.options.extends) {
- config.options = await resolveExtends(config.options, config.path, typeof config.options.extends === "string"
- ? [config.options.extends]
- : config.options.extends);
- }
- if (options.merge) {
- const { merge } = await import("webpack-merge");
- // we can only merge when there are multiple configurations
- // either by passing multiple configs by flags or passing a
- // single config exporting an array
- if (!this.isMultipleConfiguration(config.options) || config.options.length <= 1) {
- this.logger.error("At least two configurations are required for merge.");
- process.exit(2);
- }
- const mergedConfigPaths = [];
- config.options = config.options.reduce((accumulator, options) => {
- const configPath = config.path.get(options);
- const mergedOptions = merge(accumulator, options);
- if (configPath) {
- mergedConfigPaths.push(...configPath);
- }
- return mergedOptions;
- }, {});
- config.path.set(config.options, mergedConfigPaths);
- }
- if (options.analyze && !(await this.isPackageInstalled("webpack-bundle-analyzer"))) {
- await this.installPackage("webpack-bundle-analyzer", {
- preMessage: () => {
- this.logger.error(`It looks like ${this.colors.yellow("webpack-bundle-analyzer")} is not installed.`);
- },
- });
- this.logger.success(`${this.colors.yellow("webpack-bundle-analyzer")} was installed successfully.`);
- }
- if (typeof options.progress === "string" && options.progress !== "profile") {
- this.logger.error(`'${options.progress}' is an invalid value for the --progress option. Only 'profile' is allowed.`);
- process.exit(2);
- }
- // Node's CJS interop nests the class under `.default.default`; Bun honors
- // `__esModule` and unwraps once already, leaving the class one level up.
- const cliPluginModule = (await import("./plugins/cli-plugin.js")).default;
- const CLIPlugin = cliPluginModule.default ?? cliPluginModule;
- // `getArguments()` already returns a name-keyed map of exactly the argument
- // metadata `processArguments` consumes, so use it directly (cached) instead
- // of rebuilding a `schemaToOptions` array and a lookup map on every run.
- // Computed lazily: a plain `webpack build` only has internal option keys, so
- // it skips the schema-to-arguments walk entirely.
- let builtInArgs;
- const internalBuildConfig = (configuration) => {
- const originalWatchValue = configuration.watch;
- // Apply options
- const args = {};
- const values = {};
- for (const name of Object.keys(options)) {
- if (INTERNAL_OPTION_KEYS.has(name))
- continue;
- const kebabName = this.toKebabCase(name);
- const arg = (builtInArgs ??= this.#getArguments(options.webpack, undefined))[kebabName];
- if (arg) {
- args[name] = arg;
- // We really don't know what the value is
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- values[name] = options[name];
- }
- }
- if (Object.keys(values).length > 0) {
- this.#processArguments(options.webpack, args, configuration, values);
- }
- // Output warnings
- if (!Object.isExtensible(configuration)) {
- return;
- }
- if (options.isWatchingLikeCommand &&
- options.argv?.env &&
- (typeof originalWatchValue !== "undefined" || typeof options.argv?.watch !== "undefined")) {
- this.logger.warn(`No need to use the '${options.argv.env.WEBPACK_WATCH ? "watch" : "serve"}' command together with '{ watch: true | false }' or '--watch'/'--no-watch' configuration, it does not make sense.`);
- if (options.argv.env.WEBPACK_SERVE) {
- configuration.watch = false;
- }
- }
- const isFileSystemCacheOptions = (config) => typeof config.cache !== "undefined" &&
- typeof config.cache !== "boolean" &&
- config.cache.type === "filesystem";
- // Setup default cache options
- if (isFileSystemCacheOptions(configuration) && Object.isExtensible(configuration.cache)) {
- const configPath = config.path.get(configuration);
- if (configPath) {
- if (!configuration.cache.buildDependencies) {
- configuration.cache.buildDependencies = {};
- }
- if (!configuration.cache.buildDependencies.defaultConfig) {
- configuration.cache.buildDependencies.defaultConfig = [];
- }
- const normalizeConfigPath = (configPath) => configPath.startsWith("file://") ? (0, node_url_1.fileURLToPath)(configPath) : node_path_1.default.resolve(configPath);
- if (Array.isArray(configPath)) {
- for (const oneOfConfigPath of configPath) {
- configuration.cache.buildDependencies.defaultConfig.push(normalizeConfigPath(oneOfConfigPath));
- }
- }
- else {
- configuration.cache.buildDependencies.defaultConfig.push(
- // TODO fix `file:` support on webpack side and remove it in the next major release
- normalizeConfigPath(configPath));
- }
- }
- }
- // Respect `process.env.NODE_ENV`
- if (!configuration.mode &&
- process.env?.NODE_ENV &&
- (process.env.NODE_ENV === "development" ||
- process.env.NODE_ENV === "production" ||
- process.env.NODE_ENV === "none")) {
- configuration.mode = process.env.NODE_ENV;
- }
- // Setup stats
- if (typeof configuration.stats === "undefined") {
- configuration.stats = { preset: "normal" };
- }
- else if (typeof configuration.stats === "boolean") {
- configuration.stats = configuration.stats ? { preset: "normal" } : { preset: "none" };
- }
- else if (typeof configuration.stats === "string") {
- configuration.stats = { preset: configuration.stats };
- }
- let colors;
- // From arguments
- if (typeof this.#isColorSupportChanged !== "undefined") {
- colors = Boolean(this.#isColorSupportChanged);
- }
- // From stats
- else if (typeof configuration.stats.colors !== "undefined") {
- colors = configuration.stats.colors;
- }
- // Default
- else {
- colors = Boolean(this.colors.isColorSupported);
- }
- if (Object.isExtensible(configuration.stats)) {
- configuration.stats.colors = colors;
- }
- // Apply CLI plugin
- if (!configuration.plugins) {
- configuration.plugins = [];
- }
- if (Object.isExtensible(configuration.plugins)) {
- configuration.plugins.unshift(new CLIPlugin({
- configPath: config.path.get(configuration),
- helpfulOutput: !options.json,
- progress: options.progress,
- analyze: options.analyze,
- isMultiCompiler: this.isMultipleConfiguration(config.options),
- }));
- }
- };
- if (this.isMultipleConfiguration(config.options)) {
- for (const item of config.options) {
- internalBuildConfig(item);
- }
- }
- else {
- internalBuildConfig(config.options);
- }
- return config;
- }
- async createCompiler(options, callback) {
- const { webpack } = options;
- if (typeof options.configNodeEnv === "string") {
- process.env.NODE_ENV = options.configNodeEnv;
- }
- const config = await this.loadConfig(options);
- let compiler;
- try {
- compiler = callback
- ? webpack(config.options, (error, stats) => {
- if (error && this.isValidationError(error)) {
- this.logger.error(error.message);
- process.exit(2);
- }
- callback(error, stats);
- })
- : webpack(config.options);
- }
- catch (error) {
- if (this.isValidationError(error)) {
- this.logger.error(error.message);
- }
- else {
- this.logger.error(error);
- }
- process.exit(2);
- }
- return compiler;
- }
- #needWatchStdin(compiler) {
- if (this.isMultipleCompiler(compiler)) {
- return Boolean(compiler.compilers.some((compiler) => compiler.options.watchOptions?.stdin));
- }
- return Boolean(compiler.options.watchOptions?.stdin);
- }
- async runWebpack(options, isWatchCommand) {
- let compiler;
- let stringifyChunked;
- let Readable;
- if (options.json) {
- ({ stringifyChunked } = await import("@discoveryjs/json-ext"));
- ({ Readable } = await import("node:stream"));
- }
- const callback = (error, stats) => {
- if (error) {
- this.logger.error(error);
- process.exit(2);
- }
- if (stats && (stats.hasErrors() || (options.failOnWarnings && stats.hasWarnings()))) {
- process.exitCode = 1;
- }
- if (!compiler || !stats) {
- return;
- }
- const statsOptions = this.isMultipleCompiler(compiler)
- ? {
- children: compiler.compilers.map((compiler) => compiler.options.stats),
- }
- : compiler.options.stats;
- if (options.json) {
- const handleWriteError = (error) => {
- this.logger.error(error);
- process.exit(2);
- };
- if (options.json === true) {
- Readable.from(stringifyChunked(stats.toJson(statsOptions)))
- .on("error", handleWriteError)
- .pipe(process.stdout)
- .on("error", handleWriteError)
- .on("close", () => process.stdout.write("\n"));
- }
- else {
- Readable.from(stringifyChunked(stats.toJson(statsOptions)))
- .on("error", handleWriteError)
- .pipe(node_fs_1.default.createWriteStream(options.json))
- .on("error", handleWriteError)
- // Use stderr to logging
- .on("close", () => {
- process.stderr.write(`[webpack-cli] ${this.colors.green(`stats are successfully stored as json to ${options.json}`)}\n`);
- });
- }
- }
- else {
- const printedStats = stats.toString(statsOptions);
- // Avoid extra empty line when `stats: 'none'`
- if (printedStats) {
- this.logger.raw(printedStats);
- }
- }
- };
- const env = isWatchCommand || options.watch
- ? { WEBPACK_WATCH: true, ...options.env }
- : { WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, ...options.env };
- options.argv = { ...options, env };
- if (isWatchCommand) {
- options.watch = true;
- options.isWatchingLikeCommand = true;
- }
- compiler = await this.createCompiler(options, callback);
- if (!compiler) {
- return;
- }
- const needGracefulShutdown = (compiler) => Boolean(this.isMultipleCompiler(compiler)
- ? compiler.compilers.some((compiler) => compiler.options.watch ||
- (compiler.options.cache && compiler.options.cache.type === "filesystem"))
- : compiler.options.watch ||
- (compiler.options.cache && compiler.options.cache.type === "filesystem"));
- if (needGracefulShutdown(compiler)) {
- let needForceShutdown = false;
- for (const signal of EXIT_SIGNALS) {
- // eslint-disable-next-line @typescript-eslint/no-loop-func
- const listener = () => {
- if (needForceShutdown) {
- process.exit(0);
- }
- // Output message after delay to avoid extra logging
- const timeout = setTimeout(() => {
- this.logger.info("Gracefully shutting down. To force exit, press ^C again. Please wait...");
- }, 2000);
- needForceShutdown = true;
- compiler.close(() => {
- clearTimeout(timeout);
- process.exit(0);
- });
- };
- process.on(signal, listener);
- }
- if (this.#needWatchStdin(compiler)) {
- process.stdin.on("end", () => {
- process.exit(0);
- });
- process.stdin.resume();
- }
- }
- }
- }
- exports.default = WebpackCLI;
|