webpack-cli.js 116 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599
  1. "use strict";
  2. var __importDefault = (this && this.__importDefault) || function (mod) {
  3. return (mod && mod.__esModule) ? mod : { "default": mod };
  4. };
  5. Object.defineProperty(exports, "__esModule", { value: true });
  6. exports.distance = distance;
  7. const node_fs_1 = __importDefault(require("node:fs"));
  8. const node_module_1 = require("node:module");
  9. const node_path_1 = __importDefault(require("node:path"));
  10. const node_url_1 = require("node:url");
  11. const node_util_1 = __importDefault(require("node:util"));
  12. const commander_1 = require("commander");
  13. const WEBPACK_PACKAGE_IS_CUSTOM = Boolean(process.env.WEBPACK_PACKAGE);
  14. const WEBPACK_PACKAGE = WEBPACK_PACKAGE_IS_CUSTOM
  15. ? process.env.WEBPACK_PACKAGE
  16. : "webpack";
  17. const WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM = Boolean(process.env.WEBPACK_DEV_SERVER_PACKAGE);
  18. const WEBPACK_DEV_SERVER_PACKAGE = WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM
  19. ? process.env.WEBPACK_DEV_SERVER_PACKAGE
  20. : "webpack-dev-server";
  21. const EXIT_SIGNALS = ["SIGINT", "SIGTERM"];
  22. const DEFAULT_CONFIGURATION_FILES = [
  23. "webpack.config",
  24. ".webpack/webpack.config",
  25. ".webpack/webpackfile",
  26. ];
  27. // Non-JavaScript configuration formats that Node.js cannot `import()`.
  28. // webpack-cli reads and parses these itself instead of relying on `interpret`.
  29. // The parsers are intentionally not shipped: the relevant package is imported
  30. // on demand from the user's project, and a helpful error is shown when it is
  31. // missing. `method` is the parser's entry point (`json5`/`toml` expose `parse`,
  32. // `js-yaml` exposes `load`). Hoisted to module scope so it is built only once.
  33. const DATA_FORMAT_LOADERS = {
  34. ".json5": { package: "json5", method: "parse" },
  35. ".yaml": { package: "js-yaml", method: "load" },
  36. ".yml": { package: "js-yaml", method: "load" },
  37. ".toml": { package: "toml", method: "parse" },
  38. };
  39. // TypeScript and JSX configuration files that `tsx` can load through its
  40. // CommonJS require hook (`tsx/cjs`). Used as a fallback when the loaders
  41. // listed in `interpret` are unavailable (for `.mts` there is no `interpret`
  42. // entry at all, so `tsx` is the only fallback).
  43. const TSX_LOADABLE_EXTENSIONS = new Set([".ts", ".tsx", ".cts", ".mts", ".jsx"]);
  44. const DEFAULT_WEBPACK_PACKAGES = ["webpack", "loader"];
  45. // Shared visual constants for the help/info/version/configtest "chrome".
  46. // Colors collapse to plain strings when disabled (piped output or `--no-color`),
  47. // so the textual structure stays stable and script-friendly.
  48. const UI_INDENT = " ";
  49. const UI_DIVIDER_WIDTH = 72;
  50. const UI_STATUS_ICONS = { success: "✔", error: "✖", warning: "⚠", info: "ℹ" };
  51. // Options that get a single-character alias derived from their name.
  52. const FLAGS_WITH_ALIAS = new Set(["devtool", "output-path", "target", "watch", "extends"]);
  53. // Keys the CLI sets on the parsed options itself (never webpack arguments), so
  54. // they don't need to be forwarded to webpack's `processArguments`.
  55. const INTERNAL_OPTION_KEYS = new Set(["webpack", "argv", "isWatchingLikeCommand"]);
  56. // Levenshtein distance via Myers' bit-parallel algorithm, used only for "did you
  57. // mean" suggestions. Inspired by fastest-levenshtein (MIT,
  58. // https://github.com/ka-weihe/fastest-levenshtein).
  59. //
  60. // The 256 KB buffer is allocated lazily on first use: suggestions only run on
  61. // error paths, so a normal build never pays for it.
  62. let levenshteinPeq;
  63. function myers32(a, b, peq) {
  64. const n = a.length;
  65. const m = b.length;
  66. const lst = 1 << (n - 1);
  67. let pv = -1;
  68. let mv = 0;
  69. let sc = n;
  70. let i = n;
  71. while (i--) {
  72. peq[a.charCodeAt(i)] |= 1 << i;
  73. }
  74. for (i = 0; i < m; i++) {
  75. let eq = peq[b.charCodeAt(i)];
  76. const xv = eq | mv;
  77. eq |= ((eq & pv) + pv) ^ pv;
  78. mv |= ~(eq | pv);
  79. pv &= eq;
  80. if (mv & lst) {
  81. sc++;
  82. }
  83. if (pv & lst) {
  84. sc--;
  85. }
  86. mv = (mv << 1) | 1;
  87. pv = (pv << 1) | ~(xv | mv);
  88. mv &= xv;
  89. }
  90. i = n;
  91. while (i--) {
  92. peq[a.charCodeAt(i)] = 0;
  93. }
  94. return sc;
  95. }
  96. function myersX(longer, shorter, peq) {
  97. const n = shorter.length;
  98. const m = longer.length;
  99. const mhc = [];
  100. const phc = [];
  101. const horizontalSize = Math.ceil(n / 32);
  102. const verticalSize = Math.ceil(m / 32);
  103. for (let i = 0; i < horizontalSize; i++) {
  104. phc[i] = -1;
  105. mhc[i] = 0;
  106. }
  107. let j = 0;
  108. for (; j < verticalSize - 1; j++) {
  109. let mv = 0;
  110. let pv = -1;
  111. const start = j * 32;
  112. const verticalLen = Math.min(32, m) + start;
  113. for (let k = start; k < verticalLen; k++) {
  114. peq[longer.charCodeAt(k)] |= 1 << k;
  115. }
  116. for (let i = 0; i < n; i++) {
  117. const eq = peq[shorter.charCodeAt(i)];
  118. const pb = (phc[(i / 32) | 0] >>> i) & 1;
  119. const mb = (mhc[(i / 32) | 0] >>> i) & 1;
  120. const xv = eq | mv;
  121. const xh = ((((eq | mb) & pv) + pv) ^ pv) | eq | mb;
  122. let ph = mv | ~(xh | pv);
  123. let mh = pv & xh;
  124. if ((ph >>> 31) ^ pb) {
  125. phc[(i / 32) | 0] ^= 1 << i;
  126. }
  127. if ((mh >>> 31) ^ mb) {
  128. mhc[(i / 32) | 0] ^= 1 << i;
  129. }
  130. ph = (ph << 1) | pb;
  131. mh = (mh << 1) | mb;
  132. pv = mh | ~(xv | ph);
  133. mv = ph & xv;
  134. }
  135. for (let k = start; k < verticalLen; k++) {
  136. peq[longer.charCodeAt(k)] = 0;
  137. }
  138. }
  139. let mv = 0;
  140. let pv = -1;
  141. const start = j * 32;
  142. const verticalLen = Math.min(32, m - start) + start;
  143. for (let k = start; k < verticalLen; k++) {
  144. peq[longer.charCodeAt(k)] |= 1 << k;
  145. }
  146. let score = m;
  147. for (let i = 0; i < n; i++) {
  148. const eq = peq[shorter.charCodeAt(i)];
  149. const pb = (phc[(i / 32) | 0] >>> i) & 1;
  150. const mb = (mhc[(i / 32) | 0] >>> i) & 1;
  151. const xv = eq | mv;
  152. const xh = ((((eq | mb) & pv) + pv) ^ pv) | eq | mb;
  153. let ph = mv | ~(xh | pv);
  154. let mh = pv & xh;
  155. score += (ph >>> (m - 1)) & 1;
  156. score -= (mh >>> (m - 1)) & 1;
  157. if ((ph >>> 31) ^ pb) {
  158. phc[(i / 32) | 0] ^= 1 << i;
  159. }
  160. if ((mh >>> 31) ^ mb) {
  161. mhc[(i / 32) | 0] ^= 1 << i;
  162. }
  163. ph = (ph << 1) | pb;
  164. mh = (mh << 1) | mb;
  165. pv = mh | ~(xv | ph);
  166. mv = ph & xv;
  167. }
  168. for (let k = start; k < verticalLen; k++) {
  169. peq[longer.charCodeAt(k)] = 0;
  170. }
  171. return score;
  172. }
  173. // Levenshtein edit distance between two strings, used for "did you mean"
  174. // suggestions. Exported only so it can be unit-tested directly; the CLI uses it
  175. // through the private `WebpackCLI.#distance`.
  176. function distance(first, second) {
  177. let a = first;
  178. let b = second;
  179. if (a.length < b.length) {
  180. const tmp = b;
  181. b = a;
  182. a = tmp;
  183. }
  184. if (b.length === 0) {
  185. return a.length;
  186. }
  187. levenshteinPeq ??= new Uint32Array(0x10000);
  188. return a.length <= 32 ? myers32(a, b, levenshteinPeq) : myersX(a, b, levenshteinPeq);
  189. }
  190. class ConfigurationLoadingError extends Error {
  191. name = "ConfigurationLoadingError";
  192. constructor(errors) {
  193. const message = `▶ ESM (\`import\`) failed:\n${ConfigurationLoadingError.format(errors[0])}\n\n▶ CJS (\`require\`) failed:\n${ConfigurationLoadingError.format(errors[1])}`.trim();
  194. super(message);
  195. this.stack = "";
  196. }
  197. // Format a single underlying loading error as verbosely as possible. For
  198. // module-loading errors (e.g. a `SyntaxError`) `error.stack` begins with a
  199. // code frame (file, line, source and a caret) that points at the exact
  200. // location of the problem — far more useful than `error.message` alone. We
  201. // keep the full stack and surface the Node.js error `code` (e.g.
  202. // `MODULE_NOT_FOUND`) when present.
  203. static format(error) {
  204. return ConfigurationLoadingError.indent(ConfigurationLoadingError.describe(error));
  205. }
  206. // Build the full, un-indented description of an error, walking the `cause`
  207. // chain. ES2022 error causes aren't reflected in `error.stack`, so they'd
  208. // otherwise be lost. A `seen` set guards against cyclic causes.
  209. static describe(error, seen = new Set()) {
  210. if (!(error instanceof Error)) {
  211. return node_util_1.default.stripVTControlCharacters(String(error));
  212. }
  213. seen.add(error);
  214. let details = (error.stack ?? `${error.name}: ${error.message}`).trimEnd();
  215. const { code } = error;
  216. if (code) {
  217. details += `\ncode: ${code}`;
  218. }
  219. details = node_util_1.default.stripVTControlCharacters(details);
  220. const { cause } = error;
  221. if (cause !== null && cause !== undefined && !seen.has(cause)) {
  222. details += `\n\nCaused by: ${ConfigurationLoadingError.describe(cause, seen)}`;
  223. }
  224. return details;
  225. }
  226. static indent(text) {
  227. return text
  228. .split("\n")
  229. .map((line) => (line ? ` ${line}` : line))
  230. .join("\n");
  231. }
  232. }
  233. class WebpackCLI {
  234. #colors;
  235. // Created lazily because `#createColors` loads the (large) webpack package,
  236. // which commands like `version`/`info` don't otherwise need.
  237. get colors() {
  238. return (this.#colors ??= this.#createColors());
  239. }
  240. set colors(value) {
  241. this.#colors = value;
  242. }
  243. logger;
  244. #isColorSupportChanged;
  245. // Flag tokens of the current invocation, used to register only the options
  246. // actually present (instead of all ~850) when setting up a command.
  247. #argvForParsing;
  248. program;
  249. constructor() {
  250. this.logger = this.getLogger();
  251. // Initialize program
  252. this.program = commander_1.program;
  253. this.program.name("webpack");
  254. this.program.configureOutput({
  255. writeErr: (str) => {
  256. this.logger.error(str);
  257. },
  258. outputError: (str, write) => {
  259. write(`Error: ${this.capitalizeFirstLetter(str.replace(/^error:/, "").trim())}`);
  260. },
  261. });
  262. }
  263. #createColors(useColor) {
  264. let pkg;
  265. try {
  266. pkg = require(WEBPACK_PACKAGE);
  267. }
  268. catch {
  269. // Nothing
  270. }
  271. // Some big repos can have a problem with update webpack everywhere, so let's create a simple proxy for colors
  272. if (!pkg || !pkg.cli || typeof pkg.cli.createColors !== "function") {
  273. // webpack provides the color helpers; when it isn't available, fall back to
  274. // identity functions so output is plain (uncolored) text. Returning the
  275. // string unchanged (rather than an array) is important because the value is
  276. // passed to consumers — e.g. commander's `formatItem` — that expect a string.
  277. return new Proxy({}, {
  278. get(_target, property) {
  279. return property === "isColorSupported" ? false : (value) => value;
  280. },
  281. });
  282. }
  283. const { createColors, isColorSupported } = pkg.cli;
  284. const shouldUseColor = useColor || isColorSupported();
  285. return { ...createColors({ useColor: shouldUseColor }), isColorSupported: shouldUseColor };
  286. }
  287. isPromise(value) {
  288. return typeof value.then === "function";
  289. }
  290. isFunction(value) {
  291. return typeof value === "function";
  292. }
  293. capitalizeFirstLetter(str) {
  294. return str.length > 0 ? str.charAt(0).toUpperCase() + str.slice(1) : str;
  295. }
  296. toKebabCase(str) {
  297. return str.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
  298. }
  299. // Levenshtein edit distance between two strings, for "did you mean" suggestions.
  300. static #distance(first, second) {
  301. return distance(first, second);
  302. }
  303. getLogger() {
  304. // Status icons brand every webpack-cli message (`✔`/`✖`/`⚠`/`ℹ`). They sit
  305. // between the `[webpack-cli]` prefix and the unchanged message text, so
  306. // tooling that greps the prefix or the message keeps working, and they
  307. // collapse to plain glyphs when colors are disabled.
  308. return {
  309. error: (val) => console.error(`[webpack-cli] ${this.colors.red(`✖ ${node_util_1.default.format(val)}`)}`),
  310. warn: (val) => console.warn(`[webpack-cli] ${this.colors.yellow(`⚠ ${val}`)}`),
  311. info: (val) => console.info(`[webpack-cli] ${this.colors.cyan(`ℹ ${val}`)}`),
  312. success: (val) => console.log(`[webpack-cli] ${this.colors.green(`✔ ${val}`)}`),
  313. log: (val) => console.log(`[webpack-cli] ${val}`),
  314. raw: (val) => console.log(val),
  315. };
  316. }
  317. async getDefaultPackageManager() {
  318. const { sync } = await import("cross-spawn");
  319. try {
  320. await node_fs_1.default.promises.access(node_path_1.default.resolve(process.cwd(), "package-lock.json"), node_fs_1.default.constants.F_OK);
  321. return "npm";
  322. }
  323. catch {
  324. // Nothing
  325. }
  326. try {
  327. await node_fs_1.default.promises.access(node_path_1.default.resolve(process.cwd(), "yarn.lock"), node_fs_1.default.constants.F_OK);
  328. return "yarn";
  329. }
  330. catch {
  331. // Nothing
  332. }
  333. try {
  334. await node_fs_1.default.promises.access(node_path_1.default.resolve(process.cwd(), "pnpm-lock.yaml"), node_fs_1.default.constants.F_OK);
  335. return "pnpm";
  336. }
  337. catch {
  338. // Nothing
  339. }
  340. try {
  341. // the sync function below will fail if npm is not installed,
  342. // an error will be thrown
  343. if (sync("npm", ["--version"])) {
  344. return "npm";
  345. }
  346. }
  347. catch {
  348. // Nothing
  349. }
  350. try {
  351. // the sync function below will fail if yarn is not installed,
  352. // an error will be thrown
  353. if (sync("yarn", ["--version"])) {
  354. return "yarn";
  355. }
  356. }
  357. catch {
  358. // Nothing
  359. }
  360. try {
  361. // the sync function below will fail if pnpm is not installed,
  362. // an error will be thrown
  363. if (sync("pnpm", ["--version"])) {
  364. return "pnpm";
  365. }
  366. }
  367. catch {
  368. this.logger.error("No package manager found.");
  369. process.exit(2);
  370. }
  371. }
  372. async isPackageInstalled(packageName) {
  373. if (process.versions.pnp) {
  374. return true;
  375. }
  376. try {
  377. require.resolve(packageName);
  378. return true;
  379. }
  380. catch {
  381. // Nothing
  382. }
  383. // Fallback using fs
  384. let dir = __dirname;
  385. do {
  386. try {
  387. const stats = await node_fs_1.default.promises.stat(node_path_1.default.join(dir, "node_modules", packageName));
  388. if (stats.isDirectory()) {
  389. return true;
  390. }
  391. }
  392. catch {
  393. // Nothing
  394. }
  395. } while (dir !== (dir = node_path_1.default.dirname(dir)));
  396. // Extra fallback using fs and hidden API
  397. // @ts-expect-error No types, private API
  398. const { globalPaths } = await import("node:module");
  399. // https://github.com/nodejs/node/blob/v18.9.1/lib/internal/modules/cjs/loader.js#L1274
  400. const results = await Promise.all(globalPaths.map(async (internalPath) => {
  401. try {
  402. const stats = await node_fs_1.default.promises.stat(node_path_1.default.join(internalPath, packageName));
  403. if (stats.isDirectory()) {
  404. return true;
  405. }
  406. }
  407. catch {
  408. // Nothing
  409. }
  410. return false;
  411. }));
  412. if (results.includes(true)) {
  413. return true;
  414. }
  415. return false;
  416. }
  417. async installPackage(packageName, options = {}) {
  418. const packageManager = await this.getDefaultPackageManager();
  419. if (!packageManager) {
  420. this.logger.error("Can't find package manager");
  421. process.exit(2);
  422. }
  423. if (options.preMessage) {
  424. options.preMessage();
  425. }
  426. const { createInterface } = await import("node:readline");
  427. const prompt = ({ message, defaultResponse, stream, }) => {
  428. const rl = createInterface({
  429. input: process.stdin,
  430. output: stream,
  431. });
  432. return new Promise((resolve) => {
  433. rl.question(`${message} `, (answer) => {
  434. // Close the stream
  435. rl.close();
  436. const response = (answer || defaultResponse).toLowerCase();
  437. // Resolve with the input response
  438. if (response === "y" || response === "yes") {
  439. resolve(true);
  440. }
  441. else {
  442. resolve(false);
  443. }
  444. });
  445. });
  446. };
  447. // yarn uses 'add' command, rest npm and pnpm both use 'install'
  448. const commandArguments = [packageManager === "yarn" ? "add" : "install", "-D", packageName];
  449. const commandToBeRun = `${packageManager} ${commandArguments.join(" ")}`;
  450. let needInstall;
  451. try {
  452. needInstall = await prompt({
  453. 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")})`,
  454. defaultResponse: "Y",
  455. stream: process.stderr,
  456. });
  457. }
  458. catch (error) {
  459. this.logger.error(error);
  460. process.exit(error);
  461. }
  462. if (needInstall) {
  463. const { sync } = await import("cross-spawn");
  464. try {
  465. sync(packageManager, commandArguments, { stdio: "inherit" });
  466. }
  467. catch (error) {
  468. this.logger.error(error);
  469. process.exit(2);
  470. }
  471. return packageName;
  472. }
  473. process.exit(2);
  474. }
  475. async makeCommand(options) {
  476. const alreadyLoaded = this.program.commands.find((command) => command.name() === options.rawName);
  477. if (alreadyLoaded) {
  478. return alreadyLoaded;
  479. }
  480. const command = this.program.command(options.name, {
  481. hidden: options.hidden,
  482. isDefault: options.isDefault,
  483. });
  484. if (options.description) {
  485. command.description(options.description);
  486. }
  487. if (options.usage) {
  488. command.usage(options.usage);
  489. }
  490. if (Array.isArray(options.alias)) {
  491. command.aliases(options.alias);
  492. }
  493. else {
  494. command.alias(options.alias);
  495. }
  496. command.pkg = options.pkg || "webpack-cli";
  497. const { forHelp } = this.program;
  498. let allDependenciesInstalled = true;
  499. if (options.dependencies && options.dependencies.length > 0) {
  500. for (const dependency of options.dependencies) {
  501. if (
  502. // Allow to use `./path/to/webpack.js` outside `node_modules`
  503. (dependency === WEBPACK_PACKAGE && WEBPACK_PACKAGE_IS_CUSTOM) ||
  504. // Allow to use `./path/to/webpack-dev-server.js` outside `node_modules`
  505. (dependency === WEBPACK_DEV_SERVER_PACKAGE && WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM)) {
  506. continue;
  507. }
  508. const isPkgExist = await this.isPackageInstalled(dependency);
  509. if (isPkgExist) {
  510. continue;
  511. }
  512. allDependenciesInstalled = false;
  513. if (forHelp) {
  514. command.description(`${options.description} To see all available options you need to install ${options.dependencies
  515. .map((dependency) => `'${dependency}'`)
  516. .join(", ")}.`);
  517. continue;
  518. }
  519. await this.installPackage(dependency, {
  520. preMessage: () => {
  521. this.logger.error(`For using '${this.colors.green(options.rawName)}' command you need to install: '${this.colors.green(dependency)}' package.`);
  522. },
  523. });
  524. }
  525. }
  526. command.context = {};
  527. if (typeof options.preload === "function") {
  528. let data;
  529. try {
  530. data = await options.preload();
  531. }
  532. catch (err) {
  533. if (!forHelp) {
  534. throw err;
  535. }
  536. }
  537. command.context = { ...command.context, ...data };
  538. }
  539. if (options.options) {
  540. // Register every option for help, otherwise only the ones present in argv.
  541. const neededOptions = forHelp ? undefined : this.#neededOptionNames();
  542. // With no option flags in argv (e.g. a plain `webpack build`), nothing
  543. // needs to be registered and no unknown-option suggestions are possible,
  544. // so skip building the (large) option list entirely. This avoids the
  545. // schema-to-arguments walk on the most common invocation.
  546. if (!neededOptions || neededOptions.size > 0) {
  547. let commandOptions;
  548. if (forHelp &&
  549. !allDependenciesInstalled &&
  550. options.dependencies &&
  551. options.dependencies.length > 0) {
  552. commandOptions = [];
  553. }
  554. else if (typeof options.options === "function") {
  555. commandOptions = await options.options(command);
  556. }
  557. else {
  558. commandOptions = options.options;
  559. }
  560. // Keep all option names (including `no-` negated forms) for "did you mean" suggestions, since not every option is registered below.
  561. const allOptionNames = [];
  562. for (const option of commandOptions) {
  563. allOptionNames.push(option.name);
  564. if (this.#optionSupportsNegation(option)) {
  565. allOptionNames.push(`no-${option.name}`);
  566. }
  567. }
  568. command.allOptionNames = allOptionNames;
  569. for (const option of commandOptions) {
  570. if (neededOptions && !this.#isOptionNeeded(option, neededOptions)) {
  571. continue;
  572. }
  573. this.makeOption(command, option);
  574. }
  575. }
  576. }
  577. command.action(options.action);
  578. return command;
  579. }
  580. #neededOptionNames() {
  581. const argv = this.#argvForParsing;
  582. if (!argv) {
  583. return undefined;
  584. }
  585. const names = new Set();
  586. for (const token of argv) {
  587. // Must start with `-` to name an option.
  588. if (token.length < 2 || token.charCodeAt(0) !== 45) {
  589. continue;
  590. }
  591. if (token.charCodeAt(1) === 45) {
  592. // Long option: `--name` or `--name=value`.
  593. let name = token.slice(2);
  594. const equalsIndex = name.indexOf("=");
  595. if (equalsIndex !== -1) {
  596. name = name.slice(0, equalsIndex);
  597. }
  598. if (!name) {
  599. continue;
  600. }
  601. names.add(name);
  602. // `--no-x` must register the `x` option (which provides the negation).
  603. if (name.startsWith("no-")) {
  604. names.add(name.slice(3));
  605. }
  606. }
  607. else {
  608. // Register every letter of a short token to cover both attached values (`-d<value>`) and combined flags (`-abc`); over-registering is harmless.
  609. for (const char of token.slice(1).split("=", 1)[0]) {
  610. names.add(char);
  611. }
  612. }
  613. }
  614. return names;
  615. }
  616. #isOptionNeeded(option, neededOptions) {
  617. if (neededOptions.has(option.name)) {
  618. return true;
  619. }
  620. // `makeOption` derives a single-character alias for these from the name.
  621. const alias = option.alias ?? (FLAGS_WITH_ALIAS.has(option.name) ? option.name[0] : undefined);
  622. return typeof alias === "string" && neededOptions.has(alias);
  623. }
  624. // Mirrors when `makeOption` registers a `--no-<name>` negated option.
  625. #optionSupportsNegation(option) {
  626. if (option.configs) {
  627. return option.configs.some((config) => config.type === "boolean" ||
  628. (config.type === "enum" && (config.values || []).includes(false)));
  629. }
  630. return Boolean(option.negative);
  631. }
  632. makeOption(command, option) {
  633. let mainOption;
  634. let negativeOption;
  635. if (FLAGS_WITH_ALIAS.has(option.name)) {
  636. [option.alias] = option.name;
  637. }
  638. if (option.configs) {
  639. let needNegativeOption = false;
  640. let negatedDescription;
  641. const mainOptionType = new Set();
  642. for (const config of option.configs) {
  643. switch (config.type) {
  644. case "reset":
  645. mainOptionType.add(Boolean);
  646. break;
  647. case "boolean":
  648. if (!needNegativeOption) {
  649. needNegativeOption = true;
  650. negatedDescription = config.negatedDescription;
  651. }
  652. mainOptionType.add(Boolean);
  653. break;
  654. case "number":
  655. mainOptionType.add(Number);
  656. break;
  657. case "string":
  658. case "path":
  659. case "RegExp":
  660. mainOptionType.add(String);
  661. break;
  662. case "enum": {
  663. let hasFalseEnum = false;
  664. for (const value of config.values || []) {
  665. switch (typeof value) {
  666. case "string":
  667. mainOptionType.add(String);
  668. break;
  669. case "number":
  670. mainOptionType.add(Number);
  671. break;
  672. case "boolean":
  673. if (!hasFalseEnum && value === false) {
  674. hasFalseEnum = true;
  675. break;
  676. }
  677. mainOptionType.add(Boolean);
  678. break;
  679. }
  680. }
  681. if (!needNegativeOption) {
  682. needNegativeOption = hasFalseEnum;
  683. negatedDescription = config.negatedDescription;
  684. }
  685. }
  686. }
  687. }
  688. mainOption = {
  689. flags: option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`,
  690. valueName: option.valueName || "value",
  691. description: option.description || "",
  692. type: mainOptionType,
  693. multiple: option.multiple,
  694. defaultValue: option.defaultValue,
  695. configs: option.configs,
  696. };
  697. if (needNegativeOption) {
  698. negativeOption = {
  699. flags: `--no-${option.name}`,
  700. description: negatedDescription || option.negatedDescription || `Negative '${option.name}' option.`,
  701. };
  702. }
  703. }
  704. else {
  705. mainOption = {
  706. flags: option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`,
  707. valueName: option.valueName || "value",
  708. description: option.description || "",
  709. type: option.type
  710. ? new Set(Array.isArray(option.type) ? option.type : [option.type])
  711. : new Set([Boolean]),
  712. multiple: option.multiple,
  713. defaultValue: option.defaultValue,
  714. };
  715. if (option.negative) {
  716. negativeOption = {
  717. flags: `--no-${option.name}`,
  718. description: option.negatedDescription || `Negative '${option.name}' option.`,
  719. };
  720. }
  721. }
  722. if (mainOption.type.size > 1 && mainOption.type.has(Boolean)) {
  723. mainOption.flags = `${mainOption.flags} [${mainOption.valueName}${mainOption.multiple ? "..." : ""}]`;
  724. }
  725. else if (mainOption.type.size > 0 && !mainOption.type.has(Boolean)) {
  726. mainOption.flags = `${mainOption.flags} <${mainOption.valueName}${mainOption.multiple ? "..." : ""}>`;
  727. }
  728. if (mainOption.type.size === 1) {
  729. if (mainOption.type.has(Number)) {
  730. let skipDefault = true;
  731. const optionForCommand = new commander_1.Option(mainOption.flags, mainOption.description)
  732. .argParser((value, prev = []) => {
  733. if (mainOption.defaultValue && mainOption.multiple && skipDefault) {
  734. prev = [];
  735. skipDefault = false;
  736. }
  737. return mainOption.multiple ? [...prev, Number(value)] : Number(value);
  738. })
  739. .default(mainOption.defaultValue);
  740. optionForCommand.hidden = option.hidden || false;
  741. command.addOption(optionForCommand);
  742. }
  743. else if (mainOption.type.has(String)) {
  744. let skipDefault = true;
  745. const optionForCommand = new commander_1.Option(mainOption.flags, mainOption.description)
  746. .argParser((value, prev = []) => {
  747. if (mainOption.defaultValue && mainOption.multiple && skipDefault) {
  748. prev = [];
  749. skipDefault = false;
  750. }
  751. return mainOption.multiple ? [...prev, value] : value;
  752. })
  753. .default(mainOption.defaultValue);
  754. optionForCommand.hidden = option.hidden || false;
  755. if (option.configs) {
  756. optionForCommand.configs = option.configs;
  757. }
  758. command.addOption(optionForCommand);
  759. }
  760. else if (mainOption.type.has(Boolean)) {
  761. const optionForCommand = new commander_1.Option(mainOption.flags, mainOption.description).default(mainOption.defaultValue);
  762. optionForCommand.hidden = option.hidden || false;
  763. command.addOption(optionForCommand);
  764. }
  765. else {
  766. const optionForCommand = new commander_1.Option(mainOption.flags, mainOption.description)
  767. .argParser([...mainOption.type][0])
  768. .default(mainOption.defaultValue);
  769. optionForCommand.hidden = option.hidden || false;
  770. command.addOption(optionForCommand);
  771. }
  772. }
  773. else if (mainOption.type.size > 1) {
  774. let skipDefault = true;
  775. const optionForCommand = new commander_1.Option(mainOption.flags, mainOption.description)
  776. .argParser((value, prev = []) => {
  777. if (mainOption.defaultValue && mainOption.multiple && skipDefault) {
  778. prev = [];
  779. skipDefault = false;
  780. }
  781. if (mainOption.type.has(Number)) {
  782. const numberValue = Number(value);
  783. if (!Number.isNaN(numberValue)) {
  784. return mainOption.multiple ? [...prev, numberValue] : numberValue;
  785. }
  786. }
  787. if (mainOption.type.has(String)) {
  788. return mainOption.multiple ? [...prev, value] : value;
  789. }
  790. return value;
  791. })
  792. .default(mainOption.defaultValue);
  793. optionForCommand.hidden = option.hidden || false;
  794. if (option.configs) {
  795. optionForCommand.configs = option.configs;
  796. }
  797. command.addOption(optionForCommand);
  798. }
  799. else if (mainOption.type.size === 0 && negativeOption) {
  800. const optionForCommand = new commander_1.Option(mainOption.flags, mainOption.description);
  801. // Hide stub option
  802. // 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
  803. optionForCommand.hidden = option.hidden || true;
  804. optionForCommand.internal = true;
  805. command.addOption(optionForCommand);
  806. }
  807. if (negativeOption) {
  808. const optionForCommand = new commander_1.Option(negativeOption.flags, negativeOption.description).default(false);
  809. optionForCommand.hidden = option.hidden || option.negativeHidden || false;
  810. command.addOption(optionForCommand);
  811. }
  812. }
  813. isMultipleConfiguration(config) {
  814. return Array.isArray(config);
  815. }
  816. isMultipleCompiler(compiler) {
  817. return compiler.compilers;
  818. }
  819. isValidationError(error) {
  820. return error.name === "ValidationError";
  821. }
  822. // 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).
  823. #argumentsCache = new WeakMap();
  824. #getArguments(webpackMod, schema) {
  825. let perModuleCache = this.#argumentsCache.get(webpackMod);
  826. if (!perModuleCache) {
  827. perModuleCache = new Map();
  828. this.#argumentsCache.set(webpackMod, perModuleCache);
  829. }
  830. let args = perModuleCache.get(schema)?.deref();
  831. if (!args) {
  832. args = webpackMod.cli.getArguments(schema);
  833. perModuleCache.set(schema, new WeakRef(args));
  834. }
  835. return args;
  836. }
  837. schemaToOptions(webpackMod, schema = undefined, additionalOptions = [], override = {}) {
  838. const args = this.#getArguments(webpackMod, schema);
  839. // Take memory
  840. const options = Array.from({
  841. length: additionalOptions.length + Object.keys(args).length,
  842. });
  843. let i = 0;
  844. // Adding own options
  845. for (; i < additionalOptions.length; i++)
  846. options[i] = additionalOptions[i];
  847. // Adding core options
  848. for (const name in args) {
  849. const meta = args[name];
  850. options[i++] = {
  851. ...meta,
  852. name,
  853. description: meta.description,
  854. hidden: !this.#minimumHelpOptions.has(name),
  855. negativeHidden: !this.#minimumNegativeHelpOptions.has(name),
  856. ...override,
  857. };
  858. }
  859. return options;
  860. }
  861. #processArguments(webpackMod, args, configuration, values) {
  862. const problems = webpackMod.cli.processArguments(args, configuration, values);
  863. if (problems) {
  864. const groupBy = (xs, key) => xs.reduce((rv, problem) => {
  865. const path = problem[key];
  866. (rv[path] ||= []).push(problem);
  867. return rv;
  868. }, {});
  869. const problemsByPath = groupBy(problems, "path");
  870. for (const path in problemsByPath) {
  871. const problems = problemsByPath[path];
  872. for (const problem of problems) {
  873. 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}'` : ""}`);
  874. if (problem.expected) {
  875. if (problem.expected === "true | false") {
  876. this.logger.error("Expected: without value or negative option");
  877. }
  878. else {
  879. this.logger.error(`Expected: '${problem.expected}'`);
  880. }
  881. }
  882. }
  883. }
  884. process.exit(2);
  885. }
  886. }
  887. async #outputHelp(options, isVerbose, isHelpCommandSyntax, program) {
  888. const isOption = (value) => value.startsWith("-");
  889. const isGlobalOption = (value) => value === "--color" ||
  890. value === "--no-color" ||
  891. value === "-v" ||
  892. value === "--version" ||
  893. value === "-h" ||
  894. value === "--help";
  895. const { bold } = this.colors;
  896. // Shared "chrome" used to render the help screens. These are purely visual:
  897. // colors collapse to plain strings when colors are disabled (e.g. piped output
  898. // or `--no-color`), so the textual structure stays stable for scripts.
  899. const INDENT = UI_INDENT;
  900. const divider = this.#uiDivider();
  901. const header = (title) => this.#uiHeader(title);
  902. // Section headings ("Options", "Commands", …) get a colored title and an underline.
  903. const sectionTitle = (title) => this.#uiSectionTitle(title);
  904. const outputIncorrectUsageOfHelp = () => {
  905. this.logger.error("Incorrect use of help");
  906. this.logger.error("Please use: 'webpack help [command] [option]' | 'webpack [command] --help'");
  907. this.logger.error("Run 'webpack --help' to see available commands and options");
  908. process.exit(2);
  909. };
  910. const isGlobalHelp = options.length === 0;
  911. const isCommandHelp = options.length === 1 && !isOption(options[0]);
  912. if (isGlobalHelp || isCommandHelp) {
  913. program.configureHelp({
  914. helpWidth: typeof process.env.WEBPACK_CLI_HELP_WIDTH !== "undefined"
  915. ? Number.parseInt(process.env.WEBPACK_CLI_HELP_WIDTH, 10)
  916. : 40,
  917. sortSubcommands: true,
  918. // Support multiple aliases
  919. commandUsage: (command) => {
  920. let parentCmdNames = "";
  921. for (let parentCmd = command.parent; parentCmd; parentCmd = parentCmd.parent) {
  922. parentCmdNames = `${parentCmd.name()} ${parentCmdNames}`;
  923. }
  924. if (isGlobalHelp) {
  925. // Show both supported invocation syntaxes on a single line.
  926. return `${parentCmdNames}${command.usage()} | ${parentCmdNames}[command] [options]`;
  927. }
  928. return `${parentCmdNames}${command.name()}|${command
  929. .aliases()
  930. .join("|")} ${command.usage()}`;
  931. },
  932. // Support multiple aliases
  933. subcommandTerm: (command) => {
  934. const usage = command.usage();
  935. return `${command.name()}|${command.aliases().join("|")}${usage.length > 0 ? ` ${usage}` : ""}`;
  936. },
  937. visibleOptions: function visibleOptions(command) {
  938. return command.options.filter((option) => {
  939. if (option.internal) {
  940. return false;
  941. }
  942. // Hide `--watch` option when developer use `webpack watch --help`
  943. if ((options[0] === "w" || options[0] === "watch") &&
  944. (option.name() === "watch" || option.name() === "no-watch")) {
  945. return false;
  946. }
  947. if (option.hidden) {
  948. return isVerbose;
  949. }
  950. return true;
  951. });
  952. },
  953. padWidth(command, helper) {
  954. return Math.max(helper.longestArgumentTermLength(command, helper), helper.longestOptionTermLength(command, helper),
  955. // For global options
  956. helper.longestOptionTermLength(program, helper), helper.longestSubcommandTermLength(isGlobalHelp ? program : command, helper));
  957. },
  958. formatHelp: (command, helper) => {
  959. // `helper.formatItem` is ANSI-aware (it pads using the visible width), so we
  960. // can colorize the term and keep every description column aligned.
  961. const formatItem = (term, description) => {
  962. if (description) {
  963. return helper.formatItem(bold(term), helper.padWidth(command, helper), description, helper);
  964. }
  965. return `${INDENT}${bold(term)}`;
  966. };
  967. const formatList = (textArray) => textArray.join("\n");
  968. const addSection = (output, title, items) => items.length > 0 ? [...output, sectionTitle(title), formatList(items), ""] : output;
  969. // Header — a branded, divided title for the screen.
  970. let output = [
  971. "",
  972. header(isGlobalHelp ? "webpack" : `webpack ${command.name()}`),
  973. divider,
  974. ];
  975. // Description
  976. const commandDescription = isGlobalHelp
  977. ? "The build tool for modern web applications."
  978. : helper.commandDescription(command);
  979. if (commandDescription.length > 0) {
  980. output = [...output, commandDescription, ""];
  981. }
  982. // Usage
  983. output = [...output, `${bold("Usage:")} ${helper.commandUsage(command)}`, ""];
  984. // Arguments
  985. output = addSection(output, "Arguments", helper
  986. .visibleArguments(command)
  987. .map((argument) => formatItem(argument.name(), argument.description)));
  988. // Options
  989. output = addSection(output, "Options", helper
  990. .visibleOptions(command)
  991. .map((option) => formatItem(helper.optionTerm(option), helper.optionDescription(option))));
  992. // Global options
  993. output = addSection(output, "Global options", program.options.map((option) => formatItem(helper.optionTerm(option), helper.optionDescription(option))));
  994. // Commands
  995. output = addSection(output, "Commands", helper
  996. .visibleCommands(isGlobalHelp ? program : command)
  997. .map((subcommand) => formatItem(helper.subcommandTerm(subcommand), helper.subcommandDescription(subcommand))));
  998. return [...output, divider].join("\n");
  999. },
  1000. });
  1001. if (isGlobalHelp) {
  1002. await Promise.all(Object.values(this.#commands).map((knownCommand) => this.#loadCommandByName(knownCommand.rawName)));
  1003. const buildCommand = this.#findCommandByName(this.#commands.build.rawName);
  1004. if (buildCommand) {
  1005. this.logger.raw(buildCommand.helpInformation());
  1006. }
  1007. }
  1008. else {
  1009. const [name] = options;
  1010. const command = await this.#loadCommandByName(name);
  1011. if (!command) {
  1012. this.logger.error(`Can't find and load command '${name}'`);
  1013. this.logger.error("Run 'webpack --help' to see available commands and options.");
  1014. process.exit(2);
  1015. }
  1016. this.logger.raw(command.helpInformation());
  1017. }
  1018. }
  1019. else if (isHelpCommandSyntax) {
  1020. let isCommandSpecified = false;
  1021. let commandName = this.#commands.build.rawName;
  1022. let optionName = "";
  1023. if (options.length === 1) {
  1024. [optionName] = options;
  1025. }
  1026. else if (options.length === 2) {
  1027. isCommandSpecified = true;
  1028. [commandName, optionName] = options;
  1029. if (isOption(commandName)) {
  1030. outputIncorrectUsageOfHelp();
  1031. }
  1032. }
  1033. else {
  1034. outputIncorrectUsageOfHelp();
  1035. }
  1036. const command = isGlobalOption(optionName)
  1037. ? program
  1038. : await this.#loadCommandByName(commandName);
  1039. if (!command) {
  1040. this.logger.error(`Can't find and load command '${commandName}'`);
  1041. this.logger.error("Run 'webpack --help' to see available commands and options");
  1042. process.exit(2);
  1043. }
  1044. const option = command.options.find((option) => option.short === optionName || option.long === optionName);
  1045. if (!option) {
  1046. this.logger.error(`Unknown option '${optionName}'`);
  1047. this.logger.error("Run 'webpack --help' to see available commands and options");
  1048. process.exit(2);
  1049. return;
  1050. }
  1051. const nameOutput = option.flags.replace(/^.+[[<]/, "").replace(/(\.\.\.)?[\]>].*$/, "") +
  1052. (option.variadic === true ? "..." : "");
  1053. const value = option.required ? `<${nameOutput}>` : option.optional ? `[${nameOutput}]` : "";
  1054. const scope = isCommandSpecified ? ` ${commandName}` : "";
  1055. this.logger.raw("");
  1056. this.logger.raw(header(option.long ?? optionName));
  1057. this.logger.raw(divider);
  1058. this.logger.raw(`${INDENT}${bold("Usage:")} webpack${scope} ${option.long}${value ? ` ${value}` : ""}`);
  1059. if (option.short) {
  1060. this.logger.raw(`${INDENT}${bold("Short:")} webpack${scope} ${option.short}${value ? ` ${value}` : ""}`);
  1061. }
  1062. if (option.description) {
  1063. this.logger.raw(`${INDENT}${bold("Description:")} ${option.description}`);
  1064. }
  1065. const { configs } = option;
  1066. if (configs) {
  1067. const possibleValues = configs.reduce((accumulator, currentValue) => {
  1068. if (currentValue.values) {
  1069. return [...accumulator, ...currentValue.values];
  1070. }
  1071. return accumulator;
  1072. }, []);
  1073. if (possibleValues.length > 0) {
  1074. // Convert the possible values to a union type string
  1075. // ['mode', 'development', 'production'] => "'mode' | 'development' | 'production'"
  1076. // [false, 'eval'] => "false | 'eval'"
  1077. const possibleValuesUnionTypeString = possibleValues
  1078. .map((value) => (typeof value === "string" ? `'${value}'` : value))
  1079. .join(" | ");
  1080. this.logger.raw(`${INDENT}${bold("Possible values:")} ${possibleValuesUnionTypeString}`);
  1081. }
  1082. }
  1083. this.logger.raw(divider);
  1084. this.logger.raw("");
  1085. // TODO implement this after refactor cli arguments
  1086. // logger.raw('Documentation: https://webpack.js.org/option/name/');
  1087. }
  1088. else {
  1089. outputIncorrectUsageOfHelp();
  1090. }
  1091. // Footer — shared across every help screen and framed command.
  1092. this.logger.raw(this.#uiFooter({ verbose: isVerbose }));
  1093. process.exit(0);
  1094. }
  1095. async #renderVersion(options = {}) {
  1096. const info = await this.#getInfoOutput({
  1097. ...options,
  1098. information: {
  1099. npmPackages: `{${DEFAULT_WEBPACK_PACKAGES.map((item) => `*${item}*`).join(",")}}`,
  1100. },
  1101. });
  1102. // `--output json|markdown` must stay machine-readable, so only the default
  1103. // plain-text output gets the visual treatment.
  1104. return typeof options.output === "undefined" ? this.#renderEnvinfo(info) : info;
  1105. }
  1106. // ─── Shared UI "chrome" ──────────────────────────────────────────────────
  1107. // Small, pure(-ish) renderers for the CLI's visual structure. They read
  1108. // `this.colors`, which collapses to plain strings when colors are disabled,
  1109. // so the textual layout is identical with or without ANSI.
  1110. /** A horizontal rule used to separate sections. */
  1111. #uiDivider() {
  1112. // Separators should recede behind content, so render them faint rather than
  1113. // in a saturated color (default-blue is the least readable hue on dark terminals).
  1114. return this.colors.dim("─".repeat(UI_DIVIDER_WIDTH));
  1115. }
  1116. /** A branded title, e.g. `⬡ webpack build`. */
  1117. #uiHeader(title) {
  1118. const { bold, cyan } = this.colors;
  1119. return `${bold(cyan("⬡"))} ${bold(cyan(title))}`;
  1120. }
  1121. /** A section heading with an underline, e.g. `Options` followed by a divider. */
  1122. #uiSectionTitle(title) {
  1123. const { bold, cyan } = this.colors;
  1124. return `${bold(cyan(title))}\n${this.#uiDivider()}`;
  1125. }
  1126. /** A single status message prefixed with an icon (`✔`/`✖`/`⚠`/`ℹ`). */
  1127. #uiStatusLine(kind, message) {
  1128. const { green, red, yellow, cyan } = this.colors;
  1129. const color = { success: green, error: red, warning: yellow, info: cyan }[kind];
  1130. return `${color(UI_STATUS_ICONS[kind])} ${message}`;
  1131. }
  1132. /** The header block printed at the top of a framed command (`info`, `version`, …). */
  1133. #uiCommandHeader(name, description) {
  1134. const lines = ["", this.#uiHeader(`webpack ${name}`), this.#uiDivider()];
  1135. if (description) {
  1136. lines.push(description, "");
  1137. }
  1138. return lines.join("\n");
  1139. }
  1140. /** The documentation footer shared by every help and framed-command screen. */
  1141. #uiFooter(options = {}) {
  1142. const { bold, cyan, red } = this.colors;
  1143. const lines = [];
  1144. if (!options.verbose) {
  1145. lines.push(`${cyan("ℹ")} Run ${bold("'webpack --help=verbose'")} to see all available commands and options.`);
  1146. }
  1147. 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")}`);
  1148. return lines.join("\n");
  1149. }
  1150. /**
  1151. * Renders the plain-text output of `envinfo` (used by `info` and `version`)
  1152. * into branded sections with aligned rows. Section titles keep their trailing
  1153. * colon so existing tooling/assertions that grep for e.g. "System:" keep
  1154. * working, and "requested => resolved" version pairs are shown with a "→"
  1155. * arrow.
  1156. * @param {string} raw The raw multi-line text produced by `envinfo`.
  1157. * @returns {string} The styled, section-divided output.
  1158. */
  1159. #renderEnvinfo(raw) {
  1160. const { bold, cyan, green } = this.colors;
  1161. const lines = [];
  1162. const sections = [];
  1163. let section;
  1164. for (const line of raw.split("\n")) {
  1165. const sectionMatch = /^ {2}(\S[^:]*):\s*$/.exec(line);
  1166. if (sectionMatch) {
  1167. section = { title: sectionMatch[1].trim(), rows: [] };
  1168. sections.push(section);
  1169. continue;
  1170. }
  1171. const rowMatch = /^ {4}([^:]+):\s*(.*)$/.exec(line);
  1172. if (rowMatch && section) {
  1173. section.rows.push({ label: rowMatch[1].trim(), value: rowMatch[2].trim() || "N/A" });
  1174. }
  1175. }
  1176. for (const { title, rows } of sections) {
  1177. if (rows.length === 0) {
  1178. continue;
  1179. }
  1180. const labelWidth = Math.max(...rows.map((row) => row.label.length)) + 1;
  1181. lines.push("", this.#uiHeader(`${title}:`), this.#uiDivider());
  1182. for (const { label, value } of rows) {
  1183. const arrowIndex = value.indexOf("=>");
  1184. const renderedValue = arrowIndex === -1
  1185. ? green(value)
  1186. : `${green(value.slice(0, arrowIndex).trim())} ${cyan("→")} ${bold(green(value.slice(arrowIndex + 2).trim()))}`;
  1187. lines.push(`${UI_INDENT}${bold(`${label}:`.padEnd(labelWidth))} ${renderedValue}`);
  1188. }
  1189. }
  1190. return [...lines, this.#uiDivider()].join("\n");
  1191. }
  1192. /**
  1193. * Prints a framed command screen: a branded header, the command body, and the
  1194. * shared documentation footer. Used by `info`, `version` and `configtest`.
  1195. * @param {object} meta The command name and a one-line description.
  1196. * @param {Function} body A callback that prints the command's main output.
  1197. */
  1198. async #frame(meta, body) {
  1199. this.logger.raw(this.#uiCommandHeader(meta.name, meta.description));
  1200. await body();
  1201. this.logger.raw(this.#uiFooter());
  1202. }
  1203. /** Prints a status message with an icon; errors go to stderr, the rest to stdout. */
  1204. #status(kind, message) {
  1205. if (kind === "error") {
  1206. process.stderr.write(`${this.#uiStatusLine(kind, message)}\n`);
  1207. return;
  1208. }
  1209. this.logger.raw(this.#uiStatusLine(kind, message));
  1210. }
  1211. async #getInfoOutput(options) {
  1212. let { output } = options;
  1213. const envinfoConfig = {};
  1214. if (output) {
  1215. // Remove quotes if exist
  1216. output = output.replaceAll(/['"]+/g, "");
  1217. switch (output) {
  1218. case "markdown":
  1219. envinfoConfig.markdown = true;
  1220. break;
  1221. case "json":
  1222. envinfoConfig.json = true;
  1223. break;
  1224. default:
  1225. this.logger.error(`'${output}' is not a valid value for output`);
  1226. process.exit(2);
  1227. }
  1228. }
  1229. let envinfoOptions;
  1230. if (options.information) {
  1231. envinfoOptions = options.information;
  1232. }
  1233. else {
  1234. const defaultInformation = {
  1235. Binaries: ["Node", "Yarn", "npm", "pnpm"],
  1236. Browsers: [
  1237. "Brave Browser",
  1238. "Chrome",
  1239. "Chrome Canary",
  1240. "Edge",
  1241. "Firefox",
  1242. "Firefox Developer Edition",
  1243. "Firefox Nightly",
  1244. "Internet Explorer",
  1245. "Safari",
  1246. "Safari Technology Preview",
  1247. ],
  1248. // @ts-expect-error No in types
  1249. Monorepos: ["Yarn Workspaces", "Lerna"],
  1250. System: ["OS", "CPU", "Memory"],
  1251. npmGlobalPackages: ["webpack", "webpack-cli", "webpack-dev-server"],
  1252. };
  1253. const npmPackages = [...DEFAULT_WEBPACK_PACKAGES, ...(options.additionalPackage || [])];
  1254. defaultInformation.npmPackages = `{${npmPackages.map((item) => `*${item}*`).join(",")}}`;
  1255. envinfoOptions = defaultInformation;
  1256. }
  1257. const envinfo = (await import("envinfo")).default;
  1258. let info = await envinfo.run(envinfoOptions, envinfoConfig);
  1259. info = info.replace("npmPackages", "Packages");
  1260. info = info.replace("npmGlobalPackages", "Global Packages");
  1261. return info;
  1262. }
  1263. async #loadPackage(pkg, isCustom) {
  1264. const importTarget = isCustom && /^(?:[A-Za-z]:(\\|\/)|\\\\|\/)/.test(pkg) ? (0, node_url_1.pathToFileURL)(pkg).toString() : pkg;
  1265. return (await import(importTarget)).default;
  1266. }
  1267. async loadWebpack() {
  1268. return this.#loadPackage(WEBPACK_PACKAGE, WEBPACK_PACKAGE_IS_CUSTOM);
  1269. }
  1270. async loadWebpackDevServer() {
  1271. return this.#loadPackage(WEBPACK_DEV_SERVER_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM);
  1272. }
  1273. #minimumHelpOptions = new Set([
  1274. "mode",
  1275. "watch",
  1276. "watch-options-stdin",
  1277. "stats",
  1278. "devtool",
  1279. "entry",
  1280. "target",
  1281. "name",
  1282. "output-path",
  1283. "extends",
  1284. ]);
  1285. #minimumNegativeHelpOptions = new Set(["devtool"]);
  1286. #CLIOptions = [
  1287. // For configs
  1288. {
  1289. name: "config",
  1290. alias: "c",
  1291. configs: [
  1292. {
  1293. type: "string",
  1294. },
  1295. ],
  1296. multiple: true,
  1297. valueName: "pathToConfigFile",
  1298. description: 'Provide path to one or more webpack configuration files to process, e.g. "./webpack.config.js".',
  1299. hidden: false,
  1300. },
  1301. {
  1302. name: "config-name",
  1303. configs: [
  1304. {
  1305. type: "string",
  1306. },
  1307. ],
  1308. multiple: true,
  1309. valueName: "name",
  1310. description: "Name(s) of particular configuration(s) to use if configuration file exports an array of multiple configurations.",
  1311. hidden: false,
  1312. },
  1313. {
  1314. name: "merge",
  1315. alias: "m",
  1316. configs: [
  1317. {
  1318. type: "enum",
  1319. values: [true],
  1320. },
  1321. ],
  1322. description: "Merge two or more configurations using 'webpack-merge'.",
  1323. hidden: false,
  1324. },
  1325. // Complex configs
  1326. {
  1327. name: "env",
  1328. type: (value, previous = {}) => {
  1329. // This ensures we're only splitting by the first `=`
  1330. const [allKeys, val] = value.split(/[=](.+)/, 2);
  1331. const splitKeys = allKeys.split(/\.(?!$)/);
  1332. let prevRef = previous;
  1333. for (let [index, someKey] of splitKeys.entries()) {
  1334. // https://github.com/webpack/webpack-cli/issues/3284
  1335. if (someKey.endsWith("=")) {
  1336. // remove '=' from key
  1337. someKey = someKey.slice(0, -1);
  1338. // @ts-expect-error we explicitly want to set it to undefined
  1339. prevRef[someKey] = undefined;
  1340. continue;
  1341. }
  1342. if (!prevRef[someKey]) {
  1343. prevRef[someKey] = {};
  1344. }
  1345. if (typeof prevRef[someKey] === "string") {
  1346. prevRef[someKey] = {};
  1347. }
  1348. if (index === splitKeys.length - 1) {
  1349. prevRef[someKey] = typeof val === "string" ? val : true;
  1350. }
  1351. prevRef = prevRef[someKey];
  1352. }
  1353. return previous;
  1354. },
  1355. multiple: true,
  1356. description: 'Environment variables passed to the configuration when it is a function, e.g. "myvar" or "myvar=myval".',
  1357. hidden: false,
  1358. },
  1359. {
  1360. name: "config-node-env",
  1361. configs: [
  1362. {
  1363. type: "string",
  1364. },
  1365. ],
  1366. multiple: false,
  1367. description: "Sets process.env.NODE_ENV to the specified value for access within the configuration.",
  1368. hidden: false,
  1369. },
  1370. // Adding more plugins
  1371. {
  1372. name: "analyze",
  1373. configs: [
  1374. {
  1375. type: "enum",
  1376. values: [true],
  1377. },
  1378. ],
  1379. multiple: false,
  1380. description: "It invokes webpack-bundle-analyzer plugin to get bundle information.",
  1381. hidden: false,
  1382. },
  1383. {
  1384. name: "progress",
  1385. configs: [
  1386. {
  1387. type: "string",
  1388. },
  1389. {
  1390. type: "enum",
  1391. values: [true],
  1392. },
  1393. ],
  1394. description: "Print compilation progress during build.",
  1395. hidden: false,
  1396. },
  1397. // Output options
  1398. {
  1399. name: "json",
  1400. configs: [
  1401. {
  1402. type: "string",
  1403. },
  1404. {
  1405. type: "enum",
  1406. values: [true],
  1407. },
  1408. ],
  1409. alias: "j",
  1410. valueName: "pathToJsonFile",
  1411. description: "Prints result as JSON or store it in a file.",
  1412. hidden: false,
  1413. },
  1414. {
  1415. name: "fail-on-warnings",
  1416. configs: [
  1417. {
  1418. type: "enum",
  1419. values: [true],
  1420. },
  1421. ],
  1422. description: "Stop webpack-cli process with non-zero exit code on warnings from webpack.",
  1423. hidden: false,
  1424. },
  1425. {
  1426. name: "disable-interpret",
  1427. configs: [
  1428. {
  1429. type: "enum",
  1430. values: [true],
  1431. },
  1432. ],
  1433. description: "Disable interpret for loading the config file.",
  1434. hidden: false,
  1435. },
  1436. ];
  1437. #commands = {
  1438. build: {
  1439. rawName: "build",
  1440. name: "build [entries...]",
  1441. alias: ["bundle", "b"],
  1442. description: "Run webpack (default command, can be omitted).",
  1443. usage: "[entries...] [options]",
  1444. dependencies: [WEBPACK_PACKAGE],
  1445. preload: async () => {
  1446. const webpack = await this.loadWebpack();
  1447. return { webpack };
  1448. },
  1449. options: async (cmd) => this.schemaToOptions(cmd.context.webpack, undefined, this.#CLIOptions),
  1450. action: async (entries, options, cmd) => {
  1451. const { webpack } = cmd.context;
  1452. if (entries.length > 0) {
  1453. options.entry = [...entries, ...(options.entry || [])];
  1454. }
  1455. options.webpack = webpack;
  1456. await this.runWebpack(options, false);
  1457. },
  1458. },
  1459. watch: {
  1460. rawName: "watch",
  1461. name: "watch [entries...]",
  1462. alias: "w",
  1463. description: "Run webpack and watch for files changes.",
  1464. usage: "[entries...] [options]",
  1465. dependencies: [WEBPACK_PACKAGE],
  1466. preload: async () => {
  1467. const webpack = await this.loadWebpack();
  1468. return { webpack };
  1469. },
  1470. options: async (cmd) => this.schemaToOptions(cmd.context.webpack, undefined, this.#CLIOptions),
  1471. action: async (entries, options, cmd) => {
  1472. const { webpack } = cmd.context;
  1473. if (entries.length > 0) {
  1474. options.entry = [...entries, ...(options.entry || [])];
  1475. }
  1476. options.webpack = webpack;
  1477. await this.runWebpack(options, true);
  1478. },
  1479. },
  1480. serve: {
  1481. rawName: "serve",
  1482. name: "serve [entries...]",
  1483. alias: ["server", "s"],
  1484. description: "Run the webpack dev server and watch for source file changes while serving.",
  1485. usage: "[entries...] [options]",
  1486. dependencies: [WEBPACK_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE],
  1487. preload: async () => {
  1488. const webpack = await this.loadWebpack();
  1489. const devServer = await this.loadWebpackDevServer();
  1490. return { webpack, devServer };
  1491. },
  1492. options: (cmd) => {
  1493. const { webpack, devServer } = cmd.context;
  1494. const webpackOptions = this.schemaToOptions(webpack, undefined, this.#CLIOptions);
  1495. // @ts-expect-error different versions of the `Schema` type
  1496. const devServerOptions = this.schemaToOptions(webpack, devServer.schema, undefined, {
  1497. hidden: false,
  1498. negativeHidden: false,
  1499. });
  1500. return [...webpackOptions, ...devServerOptions];
  1501. },
  1502. action: async (entries, options, cmd) => {
  1503. const { webpack, devServer } = cmd.context;
  1504. const webpackCLIOptions = { webpack, isWatchingLikeCommand: true };
  1505. const devServerCLIOptions = {};
  1506. // Derive the built-in option names from the cached argument metadata
  1507. // instead of retaining the full option arrays for the whole session.
  1508. const webpackOptionNames = new Set([
  1509. ...this.#CLIOptions.map((option) => option.name),
  1510. ...Object.keys(this.#getArguments(webpack, undefined)),
  1511. ]);
  1512. for (const optionName in options) {
  1513. const kebabedOption = this.toKebabCase(optionName);
  1514. const isBuiltInOption = webpackOptionNames.has(kebabedOption);
  1515. if (isBuiltInOption) {
  1516. webpackCLIOptions[optionName] = options[optionName];
  1517. }
  1518. else {
  1519. devServerCLIOptions[optionName] = options[optionName];
  1520. }
  1521. }
  1522. if (entries.length > 0) {
  1523. webpackCLIOptions.entry = [...entries, ...(options.entry || [])];
  1524. }
  1525. webpackCLIOptions.argv = {
  1526. ...options,
  1527. env: { WEBPACK_SERVE: true, ...options.env },
  1528. };
  1529. const compiler = await this.createCompiler(webpackCLIOptions);
  1530. if (!compiler) {
  1531. return;
  1532. }
  1533. const DevServer = cmd.context.devServer;
  1534. const servers = [];
  1535. if (this.#needWatchStdin(compiler)) {
  1536. process.stdin.on("end", () => {
  1537. Promise.all(servers.map((server) => server.stop())).then(() => {
  1538. process.exit(0);
  1539. });
  1540. });
  1541. process.stdin.resume();
  1542. }
  1543. const compilers = this.isMultipleCompiler(compiler) ? compiler.compilers : [compiler];
  1544. const possibleCompilers = compilers.filter((compiler) => compiler.options.devServer);
  1545. const compilersForDevServer = possibleCompilers.length > 0 ? possibleCompilers : [compilers[0]];
  1546. const usedPorts = [];
  1547. // @ts-expect-error different versions of the `Schema` type
  1548. const devServerArgs = this.#getArguments(webpack, devServer.schema);
  1549. for (const compilerForDevServer of compilersForDevServer) {
  1550. if (compilerForDevServer.options.devServer === false) {
  1551. continue;
  1552. }
  1553. const devServerConfiguration = compilerForDevServer.options.devServer || {};
  1554. const args = {};
  1555. const values = {};
  1556. for (const name of Object.keys(options)) {
  1557. if (name === "argv")
  1558. continue;
  1559. const kebabName = this.toKebabCase(name);
  1560. const arg = devServerArgs[kebabName];
  1561. if (arg) {
  1562. args[name] = arg;
  1563. // We really don't know what the value is
  1564. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  1565. values[name] = options[name];
  1566. }
  1567. }
  1568. if (Object.keys(values).length > 0) {
  1569. this.#processArguments(webpack, args, devServerConfiguration, values);
  1570. }
  1571. if (devServerConfiguration.port) {
  1572. const portNumber = Number(devServerConfiguration.port);
  1573. if (usedPorts.includes(portNumber)) {
  1574. 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.");
  1575. }
  1576. usedPorts.push(portNumber);
  1577. }
  1578. try {
  1579. const server = new DevServer(devServerConfiguration, compiler);
  1580. await server.start();
  1581. servers.push(server);
  1582. }
  1583. catch (error) {
  1584. if (this.isValidationError(error)) {
  1585. this.logger.error(error.message);
  1586. }
  1587. else {
  1588. this.logger.error(error);
  1589. }
  1590. process.exit(2);
  1591. }
  1592. }
  1593. if (servers.length === 0) {
  1594. this.logger.error("No dev server configurations to run");
  1595. process.exit(2);
  1596. }
  1597. },
  1598. },
  1599. help: {
  1600. rawName: "help",
  1601. name: "help [command] [option]",
  1602. alias: "h",
  1603. description: "Display help for commands and options.",
  1604. action: () => {
  1605. // Nothing, just stub
  1606. },
  1607. },
  1608. version: {
  1609. rawName: "version",
  1610. name: "version",
  1611. alias: "v",
  1612. usage: "[options]",
  1613. description: "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and other packages.",
  1614. options: [
  1615. {
  1616. name: "output",
  1617. alias: "o",
  1618. configs: [
  1619. {
  1620. type: "string",
  1621. },
  1622. ],
  1623. description: "To get the output in a specified format (accept json or markdown)",
  1624. hidden: false,
  1625. },
  1626. ],
  1627. action: async (options) => {
  1628. // `--output json|markdown` stays machine-readable and unframed.
  1629. if (options.output) {
  1630. this.logger.raw(await this.#renderVersion(options));
  1631. return;
  1632. }
  1633. await this.#frame({ name: "version", description: "Installed package versions." }, async () => this.logger.raw(await this.#renderVersion(options)));
  1634. },
  1635. },
  1636. info: {
  1637. rawName: "info",
  1638. name: "info",
  1639. alias: "i",
  1640. usage: "[options]",
  1641. description: "Outputs information about your system.",
  1642. options: [
  1643. {
  1644. name: "output",
  1645. alias: "o",
  1646. configs: [
  1647. {
  1648. type: "string",
  1649. },
  1650. ],
  1651. description: "To get the output in a specified format (accept json or markdown)",
  1652. hidden: false,
  1653. },
  1654. {
  1655. name: "additional-package",
  1656. alias: "a",
  1657. configs: [{ type: "string" }],
  1658. multiple: true,
  1659. description: "Adds additional packages to the output",
  1660. hidden: false,
  1661. },
  1662. ],
  1663. action: async (options) => {
  1664. const info = await this.#getInfoOutput(options);
  1665. // `--output json|markdown` stays machine-readable and unframed.
  1666. if (options.output) {
  1667. this.logger.raw(info);
  1668. return;
  1669. }
  1670. await this.#frame({ name: "info", description: "System and environment information." }, () => this.logger.raw(this.#renderEnvinfo(info)));
  1671. },
  1672. },
  1673. configtest: {
  1674. rawName: "configtest",
  1675. name: "configtest [config-path]",
  1676. alias: "t",
  1677. description: "Validate a webpack configuration.",
  1678. dependencies: [WEBPACK_PACKAGE],
  1679. options: [],
  1680. preload: async () => {
  1681. const webpack = await this.loadWebpack();
  1682. return { webpack };
  1683. },
  1684. action: async (configPath, _options, cmd) => {
  1685. const { webpack } = cmd.context;
  1686. const env = {};
  1687. const argv = { env };
  1688. const config = await this.loadConfig(configPath ? { env, argv, webpack, config: [configPath] } : { env, argv, webpack });
  1689. const configPaths = new Set();
  1690. if (Array.isArray(config.options)) {
  1691. for (const options of config.options) {
  1692. const loadedConfigPaths = config.path.get(options);
  1693. if (loadedConfigPaths) {
  1694. for (const path of loadedConfigPaths)
  1695. configPaths.add(path);
  1696. }
  1697. }
  1698. }
  1699. else if (config.path.get(config.options)) {
  1700. const loadedConfigPaths = config.path.get(config.options);
  1701. if (loadedConfigPaths) {
  1702. for (const path of loadedConfigPaths)
  1703. configPaths.add(path);
  1704. }
  1705. }
  1706. await this.#frame({ name: "configtest", description: "Validating your webpack configuration." }, () => {
  1707. if (configPaths.size === 0) {
  1708. this.#status("error", "No configuration found.");
  1709. process.exit(2);
  1710. }
  1711. this.#status("info", `Validating: ${[...configPaths].join(", ")}`);
  1712. try {
  1713. cmd.context.webpack.validate(config.options);
  1714. }
  1715. catch (error) {
  1716. this.#status("error", this.isValidationError(error)
  1717. ? error.message
  1718. : String(error instanceof Error ? error.message : error));
  1719. process.exit(2);
  1720. }
  1721. this.#status("success", "No validation errors found.");
  1722. });
  1723. },
  1724. },
  1725. };
  1726. #isCommand(input, commandOptions) {
  1727. const longName = commandOptions.rawName;
  1728. if (input === longName) {
  1729. return true;
  1730. }
  1731. if (commandOptions.alias) {
  1732. if (Array.isArray(commandOptions.alias)) {
  1733. return commandOptions.alias.includes(input);
  1734. }
  1735. return commandOptions.alias === input;
  1736. }
  1737. return false;
  1738. }
  1739. #findCommandByName(name) {
  1740. return this.program.commands.find((command) => name === command.name() || command.aliases().includes(name));
  1741. }
  1742. async #loadCommandByName(commandName) {
  1743. if (this.#isCommand(commandName, this.#commands.build)) {
  1744. return await this.makeCommand(this.#commands.build);
  1745. }
  1746. else if (this.#isCommand(commandName, this.#commands.serve)) {
  1747. return await this.makeCommand(this.#commands.serve);
  1748. }
  1749. else if (this.#isCommand(commandName, this.#commands.watch)) {
  1750. return await this.makeCommand(this.#commands.watch);
  1751. }
  1752. else if (this.#isCommand(commandName, this.#commands.help)) {
  1753. // Stub for the `help` command
  1754. return await this.makeCommand(this.#commands.help);
  1755. }
  1756. else if (this.#isCommand(commandName, this.#commands.version)) {
  1757. return await this.makeCommand(this.#commands.version);
  1758. }
  1759. else if (this.#isCommand(commandName, this.#commands.info)) {
  1760. return await this.makeCommand(this.#commands.info);
  1761. }
  1762. else if (this.#isCommand(commandName, this.#commands.configtest)) {
  1763. return await this.makeCommand(this.#commands.configtest);
  1764. }
  1765. const pkg = commandName;
  1766. let LoadedCommand;
  1767. try {
  1768. LoadedCommand = (await import(pkg)).default;
  1769. }
  1770. catch (error) {
  1771. if (error.code !== "ERR_MODULE_NOT_FOUND") {
  1772. this.logger.error(`Unable to load '${pkg}' command`);
  1773. this.logger.error(error);
  1774. process.exit(2);
  1775. }
  1776. return;
  1777. }
  1778. let command;
  1779. let externalCommand;
  1780. try {
  1781. command = new LoadedCommand();
  1782. externalCommand = await command.apply(this);
  1783. }
  1784. catch (error) {
  1785. this.logger.error(`Unable to load '${pkg}' command`);
  1786. this.logger.error(error);
  1787. process.exit(2);
  1788. }
  1789. return externalCommand;
  1790. }
  1791. async run(args, parseOptions) {
  1792. // Default `--color` and `--no-color` options
  1793. const self = this;
  1794. // Register own exit
  1795. this.program.exitOverride((error) => {
  1796. if (error.exitCode === 0) {
  1797. process.exit(0);
  1798. return;
  1799. }
  1800. if (error.code === "commander.unknownOption") {
  1801. let name = error.message.match(/'(.+)'/);
  1802. if (name) {
  1803. name = name[1].slice(2);
  1804. if (name.includes("=")) {
  1805. [name] = name.split("=");
  1806. }
  1807. const { operands } = this.program.parseOptions(this.program.args);
  1808. const operand = typeof operands[0] !== "undefined" ? operands[0] : this.#commands.build.rawName;
  1809. if (operand) {
  1810. const command = this.#findCommandByName(operand);
  1811. if (!command) {
  1812. this.logger.error(`Can't find and load command '${operand}'`);
  1813. this.logger.error("Run 'webpack --help' to see available commands and options");
  1814. process.exit(2);
  1815. }
  1816. const { allOptionNames } = command;
  1817. const candidateNames = allOptionNames ??
  1818. command.options
  1819. .filter((option) => !option.internal)
  1820. .map((option) => option.long?.slice(2));
  1821. for (const candidate of candidateNames) {
  1822. if (candidate && WebpackCLI.#distance(name, candidate) < 3) {
  1823. this.logger.error(`Did you mean '--${candidate}'?`);
  1824. }
  1825. }
  1826. }
  1827. }
  1828. }
  1829. this.logger.error("Run 'webpack --help' to see available commands and options");
  1830. process.exit(2);
  1831. });
  1832. this.program.option("--color", "Enable colors on console.");
  1833. this.program.on("option:color", function color() {
  1834. const { color } = this.opts();
  1835. self.#isColorSupportChanged = color;
  1836. self.colors = self.#createColors(color);
  1837. });
  1838. this.program.option("--no-color", "Disable colors on console.");
  1839. this.program.on("option:no-color", function noColor() {
  1840. const { color } = this.opts();
  1841. self.#isColorSupportChanged = color;
  1842. self.colors = self.#createColors(color);
  1843. });
  1844. this.program.option("-v, --version", "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and other packages.");
  1845. // webpack-cli has it's own logic for showing suggestions
  1846. this.program.showSuggestionAfterError(false);
  1847. // Suppress the default help option
  1848. this.program.helpOption(false);
  1849. // Suppress the default help command
  1850. this.program.helpCommand(false);
  1851. this.program.option("-h, --help [verbose]", "Display help for commands and options.");
  1852. // Basic command for lazy loading other commands
  1853. // By default we don't load any commands and options, commands and options registration takes a lot of time instead we load them lazily
  1854. // That is why we need to set `allowUnknownOption` to `true`, otherwise commander will not work
  1855. this.program.allowUnknownOption(true);
  1856. // For lazy loading other commands too
  1857. this.program.allowExcessArguments(true);
  1858. this.program.action(async (options) => {
  1859. const { operands, unknown } = this.program.parseOptions(this.program.args);
  1860. // Remember the flag tokens so command setup only registers options that
  1861. // are actually used (see `#neededOptionNames`).
  1862. this.#argvForParsing = unknown;
  1863. const defaultCommandNameToRun = this.#commands.build.rawName;
  1864. const hasOperand = typeof operands[0] !== "undefined";
  1865. const operand = hasOperand ? operands[0] : defaultCommandNameToRun;
  1866. const isHelpOption = typeof options.help !== "undefined";
  1867. const isHelpCommandSyntax = this.#isCommand(operand, this.#commands.help);
  1868. if (isHelpOption || isHelpCommandSyntax) {
  1869. let isVerbose = false;
  1870. if (isHelpOption && typeof options.help === "string") {
  1871. if (options.help !== "verbose") {
  1872. this.logger.error("Unknown value for '--help' option, please use '--help=verbose'");
  1873. process.exit(2);
  1874. }
  1875. isVerbose = true;
  1876. }
  1877. this.program.forHelp = true;
  1878. const optionsForHelp = [
  1879. ...(isHelpOption && hasOperand ? [operand] : []),
  1880. ...operands.slice(1),
  1881. ...unknown,
  1882. ...(isHelpCommandSyntax && typeof options.color !== "undefined"
  1883. ? [options.color ? "--color" : "--no-color"]
  1884. : []),
  1885. ...(isHelpCommandSyntax && typeof options.version !== "undefined" ? ["--version"] : []),
  1886. ];
  1887. await this.#outputHelp(optionsForHelp, isVerbose, isHelpCommandSyntax, this.program);
  1888. }
  1889. const isVersionOption = typeof options.version !== "undefined";
  1890. if (isVersionOption) {
  1891. const info = await this.#renderVersion();
  1892. this.logger.raw(info);
  1893. process.exit(0);
  1894. }
  1895. let isKnownCommand = false;
  1896. for (const command of Object.values(this.#commands)) {
  1897. if (command.rawName === operand ||
  1898. (Array.isArray(command.alias)
  1899. ? command.alias.includes(operand)
  1900. : command.alias === operand)) {
  1901. isKnownCommand = true;
  1902. break;
  1903. }
  1904. }
  1905. let command;
  1906. let commandOperands = operands.slice(1);
  1907. if (isKnownCommand) {
  1908. command = await this.#loadCommandByName(operand);
  1909. }
  1910. else {
  1911. let isEntrySyntax;
  1912. try {
  1913. await node_fs_1.default.promises.access(operand, node_fs_1.default.constants.F_OK);
  1914. isEntrySyntax = true;
  1915. }
  1916. catch {
  1917. isEntrySyntax = false;
  1918. }
  1919. if (isEntrySyntax) {
  1920. commandOperands = operands;
  1921. command = await this.#loadCommandByName(defaultCommandNameToRun);
  1922. }
  1923. else {
  1924. // Try to load external command
  1925. try {
  1926. command = await this.#loadCommandByName(operand);
  1927. }
  1928. catch {
  1929. // Nothing
  1930. }
  1931. if (!command) {
  1932. this.logger.error(`Unknown command or entry '${operand}'`);
  1933. const found = Object.values(this.#commands).find((commandOptions) => WebpackCLI.#distance(operand, commandOptions.rawName) < 3);
  1934. if (found) {
  1935. this.logger.error(`Did you mean '${found.rawName}' (alias '${Array.isArray(found.alias) ? found.alias.join(", ") : found.alias}')?`);
  1936. }
  1937. this.logger.error("Run 'webpack --help' to see available commands and options");
  1938. process.exit(2);
  1939. }
  1940. }
  1941. }
  1942. if (!command) {
  1943. throw new Error(`Internal error: Registered command "${operand}" is missing an action handler.`);
  1944. }
  1945. await command.parseAsync([...commandOperands, ...unknown], { from: "user" });
  1946. });
  1947. await this.program.parseAsync(args, parseOptions);
  1948. }
  1949. // 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.
  1950. async #findDefaultConfigFile() {
  1951. // Popular extensions, tried first. The common case (e.g. `webpack.config.js`)
  1952. // matches here, so `interpret` is never loaded — see `getExoticExtensions`.
  1953. const commonExtensions = [".js", ".mjs", ".cjs", ".ts", ".cts", ".mts"];
  1954. // `interpret`'s extra extensions (e.g. `.coffee`) are only needed when no
  1955. // common-extension config exists, so defer importing it until then.
  1956. let exoticExtensions;
  1957. const getExoticExtensions = async () => {
  1958. if (typeof exoticExtensions === "undefined") {
  1959. const interpret = await import("interpret");
  1960. const common = new Set(commonExtensions);
  1961. exoticExtensions = Object.keys(interpret.extensions).filter((ext) => !common.has(ext));
  1962. }
  1963. return exoticExtensions;
  1964. };
  1965. const directoryEntriesCache = new Map();
  1966. const readDirectoryEntries = async (directory) => {
  1967. let entries = directoryEntriesCache.get(directory);
  1968. if (typeof entries === "undefined") {
  1969. try {
  1970. entries = new Set((await node_fs_1.default.promises.readdir(directory)).map((entry) => entry.toLowerCase()));
  1971. }
  1972. catch {
  1973. entries = null;
  1974. }
  1975. directoryEntriesCache.set(directory, entries);
  1976. }
  1977. return entries;
  1978. };
  1979. const findInExtensions = async (resolvedBase, basename, entries, extensions) => {
  1980. for (const ext of extensions) {
  1981. // Skip candidates absent from the listing, but when the directory can't be listed (`entries` is `null`) probe every candidate directly.
  1982. if (entries && !entries.has((basename + ext).toLowerCase())) {
  1983. continue;
  1984. }
  1985. const candidate = resolvedBase + ext;
  1986. // Confirm with `access` to preserve exact existence semantics (e.g.
  1987. // broken symlinks are listed by `readdir` but fail `access`).
  1988. try {
  1989. await node_fs_1.default.promises.access(candidate, node_fs_1.default.constants.F_OK);
  1990. return candidate;
  1991. }
  1992. catch {
  1993. // Listed but not accessible, keep looking
  1994. }
  1995. }
  1996. return undefined;
  1997. };
  1998. // Order defines the priority, in decreasing order. Within each filename,
  1999. // common extensions take priority over the exotic ones (matching the
  2000. // previous combined ordering).
  2001. for (const filename of DEFAULT_CONFIGURATION_FILES) {
  2002. const resolvedBase = node_path_1.default.resolve(filename);
  2003. const entries = await readDirectoryEntries(node_path_1.default.dirname(resolvedBase));
  2004. const basename = node_path_1.default.basename(resolvedBase);
  2005. const common = await findInExtensions(resolvedBase, basename, entries, commonExtensions);
  2006. if (common) {
  2007. return common;
  2008. }
  2009. const exotic = await findInExtensions(resolvedBase, basename, entries, await getExoticExtensions());
  2010. if (exotic) {
  2011. return exotic;
  2012. }
  2013. }
  2014. return undefined;
  2015. }
  2016. async loadConfig(options) {
  2017. const disableInterpret = typeof options.disableInterpret !== "undefined" && options.disableInterpret;
  2018. const loadConfigByPath = async (configPath, argv = { env: {} }) => {
  2019. let options;
  2020. const isFileURL = configPath.startsWith("file://");
  2021. try {
  2022. const ext = node_path_1.default.extname(configPath).toLowerCase();
  2023. const dataFormatLoader = DATA_FORMAT_LOADERS[ext];
  2024. if (dataFormatLoader) {
  2025. // Non-JavaScript formats cannot be `import(...)`ed, so parse them
  2026. // directly and skip the doomed dynamic import attempt entirely.
  2027. const configFilePath = isFileURL ? (0, node_url_1.fileURLToPath)(configPath) : node_path_1.default.resolve(configPath);
  2028. let parser;
  2029. try {
  2030. // Resolve the parser from the configuration file's location (so it
  2031. // is picked up from the user's project rather than from webpack-cli),
  2032. // then load it asynchronously via `import(...)`.
  2033. const parserPath = (0, node_module_1.createRequire)(configFilePath).resolve(dataFormatLoader.package);
  2034. const parserModule = await import((0, node_url_1.pathToFileURL)(parserPath).href);
  2035. parser = (parserModule.default ?? parserModule);
  2036. }
  2037. catch {
  2038. this.logger.error(`Unable to load the '${configFilePath}' configuration file.`);
  2039. 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}\`.`);
  2040. process.exit(2);
  2041. }
  2042. // Reading/parsing errors propagate to the handler below, which
  2043. // reports `Failed to load '<config>' config`.
  2044. const source = await node_fs_1.default.promises.readFile(configFilePath, "utf8");
  2045. options = parser[dataFormatLoader.method](source);
  2046. }
  2047. else {
  2048. let loadingError;
  2049. try {
  2050. options = // eslint-disable-next-line no-eval
  2051. (await eval(`import("${isFileURL ? configPath : (0, node_url_1.pathToFileURL)(configPath)}")`))
  2052. .default;
  2053. }
  2054. catch (err) {
  2055. if (this.isValidationError(err) || process.env?.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) {
  2056. throw err;
  2057. }
  2058. loadingError = err;
  2059. }
  2060. // Fallback logic when we can't use `import(...)`. `interpret`/`rechoir`
  2061. // still handle the JavaScript variants (`.ts`, `.coffee`,
  2062. // `.babel.js`, ...). This path is planned to be removed once those
  2063. // formats are loaded natively as well.
  2064. if (loadingError) {
  2065. const configFilePath = isFileURL ? (0, node_url_1.fileURLToPath)(configPath) : node_path_1.default.resolve(configPath);
  2066. const { jsVariants, extensions } = await import("interpret");
  2067. let interpreted = Object.keys(jsVariants).find((variant) => variant === ext);
  2068. if (!interpreted && ext.endsWith(".cts")) {
  2069. interpreted = jsVariants[".ts"];
  2070. }
  2071. // `tsx` is missing from `interpret`'s loader tables, and it cannot
  2072. // be added there: its `tsx/cjs` entry point exists only in the
  2073. // package's `exports` map, which `rechoir`'s resolver does not
  2074. // support. Register it manually instead, resolved from the
  2075. // configuration file's location so the user's copy is picked up.
  2076. // Requiring `tsx/cjs` installs its require hook as a side effect.
  2077. const registerTsxLoader = () => {
  2078. try {
  2079. (0, node_module_1.createRequire)(configFilePath)("tsx/cjs");
  2080. return undefined;
  2081. }
  2082. catch (err) {
  2083. // `throw` may raise non-`Error` values; normalize so the
  2084. // error-reporting path can rely on `.message`.
  2085. return err instanceof Error ? err : new Error(String(err));
  2086. }
  2087. };
  2088. if (interpreted && !disableInterpret) {
  2089. const rechoir = (await import("rechoir")).default;
  2090. try {
  2091. rechoir.prepare(extensions, configPath);
  2092. }
  2093. catch (error) {
  2094. // The loaders known to `interpret` take priority; `tsx` is
  2095. // only tried once all of them have failed.
  2096. const isTsxLoadable = TSX_LOADABLE_EXTENSIONS.has(ext);
  2097. const tsxFailure = isTsxLoadable ? registerTsxLoader() : undefined;
  2098. if (!isTsxLoadable || tsxFailure) {
  2099. if (error?.failures) {
  2100. this.logger.error(`Unable to load '${configPath}'`);
  2101. this.logger.error(error.message);
  2102. for (const failure of error.failures) {
  2103. this.logger.error(failure.error.message);
  2104. }
  2105. if (tsxFailure) {
  2106. // Only the first line — `require` errors append a noisy
  2107. // "Require stack" to the message.
  2108. const [tsxFailureReason] = tsxFailure.message.split("\n");
  2109. this.logger.error(tsxFailureReason);
  2110. }
  2111. this.logger.error("Please install one of them");
  2112. process.exit(2);
  2113. }
  2114. this.logger.error(error);
  2115. process.exit(2);
  2116. }
  2117. }
  2118. }
  2119. else if (TSX_LOADABLE_EXTENSIONS.has(ext) && !disableInterpret) {
  2120. // `.mts` has no entry in `interpret`'s tables, so `tsx` is the
  2121. // only loader we can offer. Best effort: when it is missing, the
  2122. // `require()` below fails and the original errors are reported,
  2123. // exactly as before.
  2124. registerTsxLoader();
  2125. }
  2126. try {
  2127. options = require(configFilePath);
  2128. }
  2129. catch (err) {
  2130. if (this.isValidationError(err)) {
  2131. throw err;
  2132. }
  2133. throw new ConfigurationLoadingError([loadingError, err]);
  2134. }
  2135. }
  2136. }
  2137. // To handle `babel`/`module.exports.default = {};`
  2138. if (options && typeof options === "object" && "default" in options) {
  2139. options = options.default;
  2140. }
  2141. if (!options) {
  2142. 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.`);
  2143. options = {};
  2144. }
  2145. }
  2146. catch (error) {
  2147. if (error instanceof ConfigurationLoadingError) {
  2148. this.logger.error(`Failed to load '${configPath}' config\n${error.message}`);
  2149. }
  2150. else {
  2151. this.logger.error(`Failed to load '${configPath}' config`);
  2152. this.logger.error(error);
  2153. }
  2154. process.exit(2);
  2155. }
  2156. if (Array.isArray(options)) {
  2157. const optionsArray = options;
  2158. await Promise.all(optionsArray.map(async (_, i) => {
  2159. if (this.isPromise(optionsArray[i])) {
  2160. optionsArray[i] = await optionsArray[i];
  2161. }
  2162. // `Promise` may return `Function`
  2163. if (this.isFunction(optionsArray[i])) {
  2164. // when config is a function, pass the env from args to the config function
  2165. optionsArray[i] = await optionsArray[i](argv.env, argv);
  2166. }
  2167. }));
  2168. options = optionsArray;
  2169. }
  2170. else {
  2171. if (this.isPromise(options)) {
  2172. options = await options;
  2173. }
  2174. // `Promise` may return `Function`
  2175. if (this.isFunction(options)) {
  2176. // when config is a function, pass the env from args to the config function
  2177. options = await options(argv.env, argv);
  2178. }
  2179. }
  2180. const isObject = (value) => typeof value === "object" && value !== null;
  2181. if (!isObject(options) && !Array.isArray(options)) {
  2182. this.logger.error(`Invalid configuration in '${configPath}'`);
  2183. process.exit(2);
  2184. }
  2185. return {
  2186. options: options,
  2187. path: configPath,
  2188. };
  2189. };
  2190. const config = {
  2191. options: {},
  2192. path: new WeakMap(),
  2193. };
  2194. if (options.config && options.config.length > 0) {
  2195. const loadedConfigs = await Promise.all(options.config.map((configPath) => loadConfigByPath(configPath, options.argv)));
  2196. if (loadedConfigs.length === 1) {
  2197. config.options = loadedConfigs[0].options;
  2198. config.path.set(loadedConfigs[0].options, [loadedConfigs[0].path]);
  2199. }
  2200. else {
  2201. config.options = [];
  2202. for (const loadedConfig of loadedConfigs) {
  2203. if (Array.isArray(loadedConfig.options)) {
  2204. for (const item of loadedConfig.options) {
  2205. config.options.push(item);
  2206. config.path.set(options, [loadedConfig.path]);
  2207. }
  2208. }
  2209. else {
  2210. config.options.push(loadedConfig.options);
  2211. config.path.set(loadedConfig.options, [loadedConfig.path]);
  2212. }
  2213. }
  2214. }
  2215. }
  2216. else {
  2217. const foundDefaultConfigFile = await this.#findDefaultConfigFile();
  2218. if (foundDefaultConfigFile) {
  2219. const loadedConfig = await loadConfigByPath(foundDefaultConfigFile, options.argv);
  2220. config.options = loadedConfig.options;
  2221. if (this.isMultipleConfiguration(config.options)) {
  2222. for (const item of config.options) {
  2223. config.path.set(item, [loadedConfig.path]);
  2224. }
  2225. }
  2226. else {
  2227. config.path.set(loadedConfig.options, [loadedConfig.path]);
  2228. }
  2229. }
  2230. }
  2231. if (options.configName) {
  2232. const notFoundConfigNames = [];
  2233. config.options = options.configName.map((configName) => {
  2234. let found;
  2235. if (this.isMultipleConfiguration(config.options)) {
  2236. found = config.options.find((options) => options.name === configName);
  2237. }
  2238. else {
  2239. found = config.options.name === configName ? config.options : undefined;
  2240. }
  2241. if (!found) {
  2242. notFoundConfigNames.push(configName);
  2243. }
  2244. return found;
  2245. });
  2246. if (notFoundConfigNames.length > 0) {
  2247. this.logger.error(notFoundConfigNames
  2248. .map((configName) => `Configuration with the name "${configName}" was not found.`)
  2249. .join(" "));
  2250. process.exit(2);
  2251. }
  2252. }
  2253. const resolveExtends = async (config, configPaths, extendsPaths) => {
  2254. delete config.extends;
  2255. const loadedConfigs = await Promise.all(extendsPaths.map((extendsPath) => loadConfigByPath(extendsPath, options.argv)));
  2256. const { merge } = await import("webpack-merge");
  2257. const loadedOptions = loadedConfigs.flatMap((config) => config.options);
  2258. if (loadedOptions.length > 0) {
  2259. const prevPaths = configPaths.get(config);
  2260. const loadedPaths = loadedConfigs.flatMap((config) => config.path);
  2261. if (prevPaths) {
  2262. const intersection = loadedPaths.filter((element) => prevPaths.includes(element));
  2263. if (intersection.length > 0) {
  2264. this.logger.error("Recursive configuration detected, exiting.");
  2265. process.exit(2);
  2266. }
  2267. }
  2268. config = merge(...loadedOptions, config);
  2269. if (prevPaths) {
  2270. configPaths.set(config, [...prevPaths, ...loadedPaths]);
  2271. }
  2272. }
  2273. if (config.extends) {
  2274. const extendsPaths = typeof config.extends === "string" ? [config.extends] : config.extends;
  2275. config = await resolveExtends(config, configPaths, extendsPaths);
  2276. }
  2277. return config;
  2278. };
  2279. // The `extends` param in CLI gets priority over extends in config file
  2280. if (options.extends && options.extends.length > 0) {
  2281. const extendsPaths = options.extends;
  2282. if (this.isMultipleConfiguration(config.options)) {
  2283. config.options = await Promise.all(config.options.map((options) => resolveExtends(options, config.path, extendsPaths)));
  2284. }
  2285. else {
  2286. // load the config from the extends option
  2287. config.options = await resolveExtends(config.options, config.path, extendsPaths);
  2288. }
  2289. }
  2290. // if no extends option is passed, check if the config file has extends
  2291. else if (this.isMultipleConfiguration(config.options) &&
  2292. config.options.some((options) => options.extends)) {
  2293. config.options = await Promise.all(config.options.map((options) => {
  2294. if (options.extends) {
  2295. return resolveExtends(options, config.path, typeof options.extends === "string" ? [options.extends] : options.extends);
  2296. }
  2297. return options;
  2298. }));
  2299. }
  2300. else if (!this.isMultipleConfiguration(config.options) && config.options.extends) {
  2301. config.options = await resolveExtends(config.options, config.path, typeof config.options.extends === "string"
  2302. ? [config.options.extends]
  2303. : config.options.extends);
  2304. }
  2305. if (options.merge) {
  2306. const { merge } = await import("webpack-merge");
  2307. // we can only merge when there are multiple configurations
  2308. // either by passing multiple configs by flags or passing a
  2309. // single config exporting an array
  2310. if (!this.isMultipleConfiguration(config.options) || config.options.length <= 1) {
  2311. this.logger.error("At least two configurations are required for merge.");
  2312. process.exit(2);
  2313. }
  2314. const mergedConfigPaths = [];
  2315. config.options = config.options.reduce((accumulator, options) => {
  2316. const configPath = config.path.get(options);
  2317. const mergedOptions = merge(accumulator, options);
  2318. if (configPath) {
  2319. mergedConfigPaths.push(...configPath);
  2320. }
  2321. return mergedOptions;
  2322. }, {});
  2323. config.path.set(config.options, mergedConfigPaths);
  2324. }
  2325. if (options.analyze && !(await this.isPackageInstalled("webpack-bundle-analyzer"))) {
  2326. await this.installPackage("webpack-bundle-analyzer", {
  2327. preMessage: () => {
  2328. this.logger.error(`It looks like ${this.colors.yellow("webpack-bundle-analyzer")} is not installed.`);
  2329. },
  2330. });
  2331. this.logger.success(`${this.colors.yellow("webpack-bundle-analyzer")} was installed successfully.`);
  2332. }
  2333. if (typeof options.progress === "string" && options.progress !== "profile") {
  2334. this.logger.error(`'${options.progress}' is an invalid value for the --progress option. Only 'profile' is allowed.`);
  2335. process.exit(2);
  2336. }
  2337. // Node's CJS interop nests the class under `.default.default`; Bun honors
  2338. // `__esModule` and unwraps once already, leaving the class one level up.
  2339. const cliPluginModule = (await import("./plugins/cli-plugin.js")).default;
  2340. const CLIPlugin = cliPluginModule.default ?? cliPluginModule;
  2341. // `getArguments()` already returns a name-keyed map of exactly the argument
  2342. // metadata `processArguments` consumes, so use it directly (cached) instead
  2343. // of rebuilding a `schemaToOptions` array and a lookup map on every run.
  2344. // Computed lazily: a plain `webpack build` only has internal option keys, so
  2345. // it skips the schema-to-arguments walk entirely.
  2346. let builtInArgs;
  2347. const internalBuildConfig = (configuration) => {
  2348. const originalWatchValue = configuration.watch;
  2349. // Apply options
  2350. const args = {};
  2351. const values = {};
  2352. for (const name of Object.keys(options)) {
  2353. if (INTERNAL_OPTION_KEYS.has(name))
  2354. continue;
  2355. const kebabName = this.toKebabCase(name);
  2356. const arg = (builtInArgs ??= this.#getArguments(options.webpack, undefined))[kebabName];
  2357. if (arg) {
  2358. args[name] = arg;
  2359. // We really don't know what the value is
  2360. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  2361. values[name] = options[name];
  2362. }
  2363. }
  2364. if (Object.keys(values).length > 0) {
  2365. this.#processArguments(options.webpack, args, configuration, values);
  2366. }
  2367. // Output warnings
  2368. if (!Object.isExtensible(configuration)) {
  2369. return;
  2370. }
  2371. if (options.isWatchingLikeCommand &&
  2372. options.argv?.env &&
  2373. (typeof originalWatchValue !== "undefined" || typeof options.argv?.watch !== "undefined")) {
  2374. 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.`);
  2375. if (options.argv.env.WEBPACK_SERVE) {
  2376. configuration.watch = false;
  2377. }
  2378. }
  2379. const isFileSystemCacheOptions = (config) => typeof config.cache !== "undefined" &&
  2380. typeof config.cache !== "boolean" &&
  2381. config.cache.type === "filesystem";
  2382. // Setup default cache options
  2383. if (isFileSystemCacheOptions(configuration) && Object.isExtensible(configuration.cache)) {
  2384. const configPath = config.path.get(configuration);
  2385. if (configPath) {
  2386. if (!configuration.cache.buildDependencies) {
  2387. configuration.cache.buildDependencies = {};
  2388. }
  2389. if (!configuration.cache.buildDependencies.defaultConfig) {
  2390. configuration.cache.buildDependencies.defaultConfig = [];
  2391. }
  2392. const normalizeConfigPath = (configPath) => configPath.startsWith("file://") ? (0, node_url_1.fileURLToPath)(configPath) : node_path_1.default.resolve(configPath);
  2393. if (Array.isArray(configPath)) {
  2394. for (const oneOfConfigPath of configPath) {
  2395. configuration.cache.buildDependencies.defaultConfig.push(normalizeConfigPath(oneOfConfigPath));
  2396. }
  2397. }
  2398. else {
  2399. configuration.cache.buildDependencies.defaultConfig.push(
  2400. // TODO fix `file:` support on webpack side and remove it in the next major release
  2401. normalizeConfigPath(configPath));
  2402. }
  2403. }
  2404. }
  2405. // Respect `process.env.NODE_ENV`
  2406. if (!configuration.mode &&
  2407. process.env?.NODE_ENV &&
  2408. (process.env.NODE_ENV === "development" ||
  2409. process.env.NODE_ENV === "production" ||
  2410. process.env.NODE_ENV === "none")) {
  2411. configuration.mode = process.env.NODE_ENV;
  2412. }
  2413. // Setup stats
  2414. if (typeof configuration.stats === "undefined") {
  2415. configuration.stats = { preset: "normal" };
  2416. }
  2417. else if (typeof configuration.stats === "boolean") {
  2418. configuration.stats = configuration.stats ? { preset: "normal" } : { preset: "none" };
  2419. }
  2420. else if (typeof configuration.stats === "string") {
  2421. configuration.stats = { preset: configuration.stats };
  2422. }
  2423. let colors;
  2424. // From arguments
  2425. if (typeof this.#isColorSupportChanged !== "undefined") {
  2426. colors = Boolean(this.#isColorSupportChanged);
  2427. }
  2428. // From stats
  2429. else if (typeof configuration.stats.colors !== "undefined") {
  2430. colors = configuration.stats.colors;
  2431. }
  2432. // Default
  2433. else {
  2434. colors = Boolean(this.colors.isColorSupported);
  2435. }
  2436. if (Object.isExtensible(configuration.stats)) {
  2437. configuration.stats.colors = colors;
  2438. }
  2439. // Apply CLI plugin
  2440. if (!configuration.plugins) {
  2441. configuration.plugins = [];
  2442. }
  2443. if (Object.isExtensible(configuration.plugins)) {
  2444. configuration.plugins.unshift(new CLIPlugin({
  2445. configPath: config.path.get(configuration),
  2446. helpfulOutput: !options.json,
  2447. progress: options.progress,
  2448. analyze: options.analyze,
  2449. isMultiCompiler: this.isMultipleConfiguration(config.options),
  2450. }));
  2451. }
  2452. };
  2453. if (this.isMultipleConfiguration(config.options)) {
  2454. for (const item of config.options) {
  2455. internalBuildConfig(item);
  2456. }
  2457. }
  2458. else {
  2459. internalBuildConfig(config.options);
  2460. }
  2461. return config;
  2462. }
  2463. async createCompiler(options, callback) {
  2464. const { webpack } = options;
  2465. if (typeof options.configNodeEnv === "string") {
  2466. process.env.NODE_ENV = options.configNodeEnv;
  2467. }
  2468. const config = await this.loadConfig(options);
  2469. let compiler;
  2470. try {
  2471. compiler = callback
  2472. ? webpack(config.options, (error, stats) => {
  2473. if (error && this.isValidationError(error)) {
  2474. this.logger.error(error.message);
  2475. process.exit(2);
  2476. }
  2477. callback(error, stats);
  2478. })
  2479. : webpack(config.options);
  2480. }
  2481. catch (error) {
  2482. if (this.isValidationError(error)) {
  2483. this.logger.error(error.message);
  2484. }
  2485. else {
  2486. this.logger.error(error);
  2487. }
  2488. process.exit(2);
  2489. }
  2490. return compiler;
  2491. }
  2492. #needWatchStdin(compiler) {
  2493. if (this.isMultipleCompiler(compiler)) {
  2494. return Boolean(compiler.compilers.some((compiler) => compiler.options.watchOptions?.stdin));
  2495. }
  2496. return Boolean(compiler.options.watchOptions?.stdin);
  2497. }
  2498. async runWebpack(options, isWatchCommand) {
  2499. let compiler;
  2500. let stringifyChunked;
  2501. let Readable;
  2502. if (options.json) {
  2503. ({ stringifyChunked } = await import("@discoveryjs/json-ext"));
  2504. ({ Readable } = await import("node:stream"));
  2505. }
  2506. const callback = (error, stats) => {
  2507. if (error) {
  2508. this.logger.error(error);
  2509. process.exit(2);
  2510. }
  2511. if (stats && (stats.hasErrors() || (options.failOnWarnings && stats.hasWarnings()))) {
  2512. process.exitCode = 1;
  2513. }
  2514. if (!compiler || !stats) {
  2515. return;
  2516. }
  2517. const statsOptions = this.isMultipleCompiler(compiler)
  2518. ? {
  2519. children: compiler.compilers.map((compiler) => compiler.options.stats),
  2520. }
  2521. : compiler.options.stats;
  2522. if (options.json) {
  2523. const handleWriteError = (error) => {
  2524. this.logger.error(error);
  2525. process.exit(2);
  2526. };
  2527. if (options.json === true) {
  2528. Readable.from(stringifyChunked(stats.toJson(statsOptions)))
  2529. .on("error", handleWriteError)
  2530. .pipe(process.stdout)
  2531. .on("error", handleWriteError)
  2532. .on("close", () => process.stdout.write("\n"));
  2533. }
  2534. else {
  2535. Readable.from(stringifyChunked(stats.toJson(statsOptions)))
  2536. .on("error", handleWriteError)
  2537. .pipe(node_fs_1.default.createWriteStream(options.json))
  2538. .on("error", handleWriteError)
  2539. // Use stderr to logging
  2540. .on("close", () => {
  2541. process.stderr.write(`[webpack-cli] ${this.colors.green(`stats are successfully stored as json to ${options.json}`)}\n`);
  2542. });
  2543. }
  2544. }
  2545. else {
  2546. const printedStats = stats.toString(statsOptions);
  2547. // Avoid extra empty line when `stats: 'none'`
  2548. if (printedStats) {
  2549. this.logger.raw(printedStats);
  2550. }
  2551. }
  2552. };
  2553. const env = isWatchCommand || options.watch
  2554. ? { WEBPACK_WATCH: true, ...options.env }
  2555. : { WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, ...options.env };
  2556. options.argv = { ...options, env };
  2557. if (isWatchCommand) {
  2558. options.watch = true;
  2559. options.isWatchingLikeCommand = true;
  2560. }
  2561. compiler = await this.createCompiler(options, callback);
  2562. if (!compiler) {
  2563. return;
  2564. }
  2565. const needGracefulShutdown = (compiler) => Boolean(this.isMultipleCompiler(compiler)
  2566. ? compiler.compilers.some((compiler) => compiler.options.watch ||
  2567. (compiler.options.cache && compiler.options.cache.type === "filesystem"))
  2568. : compiler.options.watch ||
  2569. (compiler.options.cache && compiler.options.cache.type === "filesystem"));
  2570. if (needGracefulShutdown(compiler)) {
  2571. let needForceShutdown = false;
  2572. for (const signal of EXIT_SIGNALS) {
  2573. // eslint-disable-next-line @typescript-eslint/no-loop-func
  2574. const listener = () => {
  2575. if (needForceShutdown) {
  2576. process.exit(0);
  2577. }
  2578. // Output message after delay to avoid extra logging
  2579. const timeout = setTimeout(() => {
  2580. this.logger.info("Gracefully shutting down. To force exit, press ^C again. Please wait...");
  2581. }, 2000);
  2582. needForceShutdown = true;
  2583. compiler.close(() => {
  2584. clearTimeout(timeout);
  2585. process.exit(0);
  2586. });
  2587. };
  2588. process.on(signal, listener);
  2589. }
  2590. if (this.#needWatchStdin(compiler)) {
  2591. process.stdin.on("end", () => {
  2592. process.exit(0);
  2593. });
  2594. process.stdin.resume();
  2595. }
  2596. }
  2597. }
  2598. }
  2599. exports.default = WebpackCLI;