webpack-cli.js (69136B)
1 const fs = require("fs"); 2 const path = require("path"); 3 const { pathToFileURL } = require("url"); 4 const util = require("util"); 5 6 const { program, Option } = require("commander"); 7 8 const WEBPACK_PACKAGE = process.env.WEBPACK_PACKAGE || "webpack"; 9 const WEBPACK_DEV_SERVER_PACKAGE = process.env.WEBPACK_DEV_SERVER_PACKAGE || "webpack-dev-server"; 10 11 class WebpackCLI { 12 constructor() { 13 this.colors = this.createColors(); 14 this.logger = this.getLogger(); 15 16 // Initialize program 17 this.program = program; 18 this.program.name("webpack"); 19 this.program.configureOutput({ 20 writeErr: this.logger.error, 21 outputError: (str, write) => 22 write(`Error: ${this.capitalizeFirstLetter(str.replace(/^error:/, "").trim())}`), 23 }); 24 } 25 26 capitalizeFirstLetter(str) { 27 if (typeof str !== "string") { 28 return ""; 29 } 30 31 return str.charAt(0).toUpperCase() + str.slice(1); 32 } 33 34 toKebabCase(str) { 35 return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); 36 } 37 38 createColors(useColor) { 39 const { createColors, isColorSupported } = require("colorette"); 40 41 let shouldUseColor; 42 43 if (useColor) { 44 shouldUseColor = useColor; 45 } else { 46 shouldUseColor = isColorSupported; 47 } 48 49 return { ...createColors({ useColor: shouldUseColor }), isColorSupported: shouldUseColor }; 50 } 51 52 getLogger() { 53 return { 54 error: (val) => console.error(`[webpack-cli] ${this.colors.red(util.format(val))}`), 55 warn: (val) => console.warn(`[webpack-cli] ${this.colors.yellow(val)}`), 56 info: (val) => console.info(`[webpack-cli] ${this.colors.cyan(val)}`), 57 success: (val) => console.log(`[webpack-cli] ${this.colors.green(val)}`), 58 log: (val) => console.log(`[webpack-cli] ${val}`), 59 raw: (val) => console.log(val), 60 }; 61 } 62 63 checkPackageExists(packageName) { 64 if (process.versions.pnp) { 65 return true; 66 } 67 68 let dir = __dirname; 69 70 do { 71 try { 72 if (fs.statSync(path.join(dir, "node_modules", packageName)).isDirectory()) { 73 return true; 74 } 75 } catch (_error) { 76 // Nothing 77 } 78 } while (dir !== (dir = path.dirname(dir))); 79 80 return false; 81 } 82 83 getAvailablePackageManagers() { 84 const { sync } = require("execa"); 85 const installers = ["npm", "yarn", "pnpm"]; 86 const hasPackageManagerInstalled = (packageManager) => { 87 try { 88 sync(packageManager, ["--version"]); 89 90 return packageManager; 91 } catch (err) { 92 return false; 93 } 94 }; 95 const availableInstallers = installers.filter((installer) => 96 hasPackageManagerInstalled(installer), 97 ); 98 99 if (!availableInstallers.length) { 100 this.logger.error("No package manager found."); 101 102 process.exit(2); 103 } 104 105 return availableInstallers; 106 } 107 108 getDefaultPackageManager() { 109 const { sync } = require("execa"); 110 const hasLocalNpm = fs.existsSync(path.resolve(process.cwd(), "package-lock.json")); 111 112 if (hasLocalNpm) { 113 return "npm"; 114 } 115 116 const hasLocalYarn = fs.existsSync(path.resolve(process.cwd(), "yarn.lock")); 117 118 if (hasLocalYarn) { 119 return "yarn"; 120 } 121 122 const hasLocalPnpm = fs.existsSync(path.resolve(process.cwd(), "pnpm-lock.yaml")); 123 124 if (hasLocalPnpm) { 125 return "pnpm"; 126 } 127 128 try { 129 // the sync function below will fail if npm is not installed, 130 // an error will be thrown 131 if (sync("npm", ["--version"])) { 132 return "npm"; 133 } 134 } catch (e) { 135 // Nothing 136 } 137 138 try { 139 // the sync function below will fail if yarn is not installed, 140 // an error will be thrown 141 if (sync("yarn", ["--version"])) { 142 return "yarn"; 143 } 144 } catch (e) { 145 // Nothing 146 } 147 148 try { 149 // the sync function below will fail if pnpm is not installed, 150 // an error will be thrown 151 if (sync("pnpm", ["--version"])) { 152 return "pnpm"; 153 } 154 } catch (e) { 155 this.logger.error("No package manager found."); 156 157 process.exit(2); 158 } 159 } 160 161 async doInstall(packageName, options = {}) { 162 const packageManager = this.getDefaultPackageManager(); 163 164 if (!packageManager) { 165 this.logger.error("Can't find package manager"); 166 167 process.exit(2); 168 } 169 170 if (options.preMessage) { 171 options.preMessage(); 172 } 173 174 // yarn uses 'add' command, rest npm and pnpm both use 'install' 175 const commandToBeRun = `${packageManager} ${[ 176 packageManager === "yarn" ? "add" : "install", 177 "-D", 178 packageName, 179 ].join(" ")}`; 180 181 const prompt = ({ message, defaultResponse, stream }) => { 182 const readline = require("readline"); 183 const rl = readline.createInterface({ 184 input: process.stdin, 185 output: stream, 186 }); 187 188 return new Promise((resolve) => { 189 rl.question(`${message} `, (answer) => { 190 // Close the stream 191 rl.close(); 192 193 const response = (answer || defaultResponse).toLowerCase(); 194 195 // Resolve with the input response 196 if (response === "y" || response === "yes") { 197 resolve(true); 198 } else { 199 resolve(false); 200 } 201 }); 202 }); 203 }; 204 205 let needInstall; 206 207 try { 208 needInstall = await prompt({ 209 message: `[webpack-cli] Would you like to install '${this.colors.green( 210 packageName, 211 )}' package? (That will run '${this.colors.green(commandToBeRun)}') (${this.colors.yellow( 212 "Y/n", 213 )})`, 214 defaultResponse: "Y", 215 stream: process.stderr, 216 }); 217 } catch (error) { 218 this.logger.error(error); 219 220 process.exit(error); 221 } 222 223 if (needInstall) { 224 const execa = require("execa"); 225 226 try { 227 await execa(commandToBeRun, [], { stdio: "inherit", shell: true }); 228 } catch (error) { 229 this.logger.error(error); 230 231 process.exit(2); 232 } 233 234 return packageName; 235 } 236 237 process.exit(2); 238 } 239 240 async tryRequireThenImport(module, handleError = true) { 241 let result; 242 243 try { 244 result = require(module); 245 } catch (error) { 246 const dynamicImportLoader = require("./utils/dynamic-import-loader")(); 247 if ( 248 (error.code === "ERR_REQUIRE_ESM" || process.env.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) && 249 pathToFileURL && 250 dynamicImportLoader 251 ) { 252 const urlForConfig = pathToFileURL(module); 253 254 result = await dynamicImportLoader(urlForConfig); 255 result = result.default; 256 257 return result; 258 } 259 260 if (handleError) { 261 this.logger.error(error); 262 process.exit(2); 263 } else { 264 throw error; 265 } 266 } 267 268 // For babel/typescript 269 if (result && typeof result === "object" && "default" in result) { 270 result = result.default || {}; 271 } 272 273 return result || {}; 274 } 275 276 loadJSONFile(pathToFile, handleError = true) { 277 let result; 278 279 try { 280 result = require(pathToFile); 281 } catch (error) { 282 if (handleError) { 283 this.logger.error(error); 284 process.exit(2); 285 } else { 286 throw error; 287 } 288 } 289 290 return result; 291 } 292 293 async makeCommand(commandOptions, options, action) { 294 const alreadyLoaded = this.program.commands.find( 295 (command) => 296 command.name() === commandOptions.name.split(" ")[0] || 297 command.aliases().includes(commandOptions.alias), 298 ); 299 300 if (alreadyLoaded) { 301 return; 302 } 303 304 const command = this.program.command(commandOptions.name, { 305 noHelp: commandOptions.noHelp, 306 hidden: commandOptions.hidden, 307 isDefault: commandOptions.isDefault, 308 }); 309 310 if (commandOptions.description) { 311 command.description(commandOptions.description, commandOptions.argsDescription); 312 } 313 314 if (commandOptions.usage) { 315 command.usage(commandOptions.usage); 316 } 317 318 if (Array.isArray(commandOptions.alias)) { 319 command.aliases(commandOptions.alias); 320 } else { 321 command.alias(commandOptions.alias); 322 } 323 324 if (commandOptions.pkg) { 325 command.pkg = commandOptions.pkg; 326 } else { 327 command.pkg = "webpack-cli"; 328 } 329 330 const { forHelp } = this.program; 331 332 let allDependenciesInstalled = true; 333 334 if (commandOptions.dependencies && commandOptions.dependencies.length > 0) { 335 for (const dependency of commandOptions.dependencies) { 336 const isPkgExist = this.checkPackageExists(dependency); 337 338 if (isPkgExist) { 339 continue; 340 } else if (!isPkgExist && forHelp) { 341 allDependenciesInstalled = false; 342 continue; 343 } 344 345 let skipInstallation = false; 346 347 // Allow to use `./path/to/webpack.js` outside `node_modules` 348 if (dependency === WEBPACK_PACKAGE && fs.existsSync(WEBPACK_PACKAGE)) { 349 skipInstallation = true; 350 } 351 352 // Allow to use `./path/to/webpack-dev-server.js` outside `node_modules` 353 if (dependency === WEBPACK_DEV_SERVER_PACKAGE && fs.existsSync(WEBPACK_PACKAGE)) { 354 skipInstallation = true; 355 } 356 357 if (skipInstallation) { 358 continue; 359 } 360 361 await this.doInstall(dependency, { 362 preMessage: () => { 363 this.logger.error( 364 `For using '${this.colors.green( 365 commandOptions.name.split(" ")[0], 366 )}' command you need to install: '${this.colors.green(dependency)}' package.`, 367 ); 368 }, 369 }); 370 } 371 } 372 373 if (options) { 374 if (typeof options === "function") { 375 if (forHelp && !allDependenciesInstalled) { 376 command.description( 377 `${ 378 commandOptions.description 379 } To see all available options you need to install ${commandOptions.dependencies 380 .map((dependency) => `'${dependency}'`) 381 .join(", ")}.`, 382 ); 383 options = []; 384 } else { 385 options = await options(); 386 } 387 } 388 389 options.forEach((optionForCommand) => { 390 this.makeOption(command, optionForCommand); 391 }); 392 } 393 394 command.action(action); 395 396 return command; 397 } 398 399 makeOption(command, option) { 400 let mainOption; 401 let negativeOption; 402 403 if (option.configs) { 404 let needNegativeOption = false; 405 let negatedDescription; 406 const mainOptionType = new Set(); 407 408 option.configs.forEach((config) => { 409 // Possible value: "enum" | "string" | "path" | "number" | "boolean" | "RegExp" | "reset" 410 switch (config.type) { 411 case "reset": 412 mainOptionType.add(Boolean); 413 break; 414 case "boolean": 415 if (!needNegativeOption) { 416 needNegativeOption = true; 417 negatedDescription = config.negatedDescription; 418 } 419 420 mainOptionType.add(Boolean); 421 break; 422 case "number": 423 mainOptionType.add(Number); 424 break; 425 case "string": 426 case "path": 427 case "RegExp": 428 mainOptionType.add(String); 429 break; 430 case "enum": { 431 let hasFalseEnum = false; 432 433 const enumTypes = config.values.map((value) => { 434 switch (typeof value) { 435 case "string": 436 mainOptionType.add(String); 437 break; 438 case "number": 439 mainOptionType.add(Number); 440 break; 441 case "boolean": 442 if (!hasFalseEnum && value === false) { 443 hasFalseEnum = true; 444 break; 445 } 446 447 mainOptionType.add(Boolean); 448 break; 449 } 450 }); 451 452 if (!needNegativeOption) { 453 needNegativeOption = hasFalseEnum; 454 negatedDescription = config.negatedDescription; 455 } 456 457 return enumTypes; 458 } 459 } 460 }); 461 462 mainOption = { 463 flags: option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`, 464 description: option.description || "", 465 type: mainOptionType, 466 multiple: option.multiple, 467 defaultValue: option.defaultValue, 468 }; 469 470 if (needNegativeOption) { 471 negativeOption = { 472 flags: `--no-${option.name}`, 473 description: 474 negatedDescription || option.negatedDescription || `Negative '${option.name}' option.`, 475 }; 476 } 477 } else { 478 mainOption = { 479 flags: option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`, 480 // TODO `describe` used by `webpack-dev-server@3` 481 description: option.description || option.describe || "", 482 type: option.type 483 ? new Set(Array.isArray(option.type) ? option.type : [option.type]) 484 : new Set([Boolean]), 485 multiple: option.multiple, 486 defaultValue: option.defaultValue, 487 }; 488 489 if (option.negative) { 490 negativeOption = { 491 flags: `--no-${option.name}`, 492 description: option.negatedDescription 493 ? option.negatedDescription 494 : `Negative '${option.name}' option.`, 495 }; 496 } 497 } 498 499 if (mainOption.type.size > 1 && mainOption.type.has(Boolean)) { 500 mainOption.flags = `${mainOption.flags} [value${mainOption.multiple ? "..." : ""}]`; 501 } else if (mainOption.type.size > 0 && !mainOption.type.has(Boolean)) { 502 mainOption.flags = `${mainOption.flags} <value${mainOption.multiple ? "..." : ""}>`; 503 } 504 505 if (mainOption.type.size === 1) { 506 if (mainOption.type.has(Number)) { 507 let skipDefault = true; 508 509 const optionForCommand = new Option(mainOption.flags, mainOption.description) 510 .argParser((value, prev = []) => { 511 if (mainOption.defaultValue && mainOption.multiple && skipDefault) { 512 prev = []; 513 skipDefault = false; 514 } 515 516 return mainOption.multiple ? [].concat(prev).concat(Number(value)) : Number(value); 517 }) 518 .default(mainOption.defaultValue); 519 520 optionForCommand.helpLevel = option.helpLevel; 521 522 command.addOption(optionForCommand); 523 } else if (mainOption.type.has(String)) { 524 let skipDefault = true; 525 526 const optionForCommand = new Option(mainOption.flags, mainOption.description) 527 .argParser((value, prev = []) => { 528 if (mainOption.defaultValue && mainOption.multiple && skipDefault) { 529 prev = []; 530 skipDefault = false; 531 } 532 533 return mainOption.multiple ? [].concat(prev).concat(value) : value; 534 }) 535 .default(mainOption.defaultValue); 536 537 optionForCommand.helpLevel = option.helpLevel; 538 539 command.addOption(optionForCommand); 540 } else if (mainOption.type.has(Boolean)) { 541 const optionForCommand = new Option(mainOption.flags, mainOption.description).default( 542 mainOption.defaultValue, 543 ); 544 545 optionForCommand.helpLevel = option.helpLevel; 546 547 command.addOption(optionForCommand); 548 } else { 549 const optionForCommand = new Option(mainOption.flags, mainOption.description) 550 .argParser(Array.from(mainOption.type)[0]) 551 .default(mainOption.defaultValue); 552 553 optionForCommand.helpLevel = option.helpLevel; 554 555 command.addOption(optionForCommand); 556 } 557 } else if (mainOption.type.size > 1) { 558 let skipDefault = true; 559 560 const optionForCommand = new Option( 561 mainOption.flags, 562 mainOption.description, 563 mainOption.defaultValue, 564 ) 565 .argParser((value, prev = []) => { 566 if (mainOption.defaultValue && mainOption.multiple && skipDefault) { 567 prev = []; 568 skipDefault = false; 569 } 570 571 if (mainOption.type.has(Number)) { 572 const numberValue = Number(value); 573 574 if (!isNaN(numberValue)) { 575 return mainOption.multiple ? [].concat(prev).concat(numberValue) : numberValue; 576 } 577 } 578 579 if (mainOption.type.has(String)) { 580 return mainOption.multiple ? [].concat(prev).concat(value) : value; 581 } 582 583 return value; 584 }) 585 .default(mainOption.defaultValue); 586 587 optionForCommand.helpLevel = option.helpLevel; 588 589 command.addOption(optionForCommand); 590 } else if (mainOption.type.size === 0 && negativeOption) { 591 const optionForCommand = new Option(mainOption.flags, mainOption.description); 592 593 // Hide stub option 594 optionForCommand.hideHelp(); 595 optionForCommand.helpLevel = option.helpLevel; 596 597 command.addOption(optionForCommand); 598 } 599 600 if (negativeOption) { 601 const optionForCommand = new Option(negativeOption.flags, negativeOption.description); 602 603 optionForCommand.helpLevel = option.helpLevel; 604 605 command.addOption(optionForCommand); 606 } 607 } 608 609 getBuiltInOptions() { 610 if (this.builtInOptionsCache) { 611 return this.builtInOptionsCache; 612 } 613 614 const minimumHelpFlags = [ 615 "config", 616 "config-name", 617 "merge", 618 "env", 619 "mode", 620 "watch", 621 "watch-options-stdin", 622 "stats", 623 "devtool", 624 "entry", 625 "target", 626 "progress", 627 "json", 628 "name", 629 "output-path", 630 "node-env", 631 ]; 632 633 const builtInFlags = [ 634 // For configs 635 { 636 name: "config", 637 alias: "c", 638 configs: [ 639 { 640 type: "string", 641 }, 642 ], 643 multiple: true, 644 description: "Provide path to a webpack configuration file e.g. ./webpack.config.js.", 645 }, 646 { 647 name: "config-name", 648 configs: [ 649 { 650 type: "string", 651 }, 652 ], 653 multiple: true, 654 description: "Name of the configuration to use.", 655 }, 656 { 657 name: "merge", 658 alias: "m", 659 configs: [ 660 { 661 type: "enum", 662 values: [true], 663 }, 664 ], 665 description: "Merge two or more configurations using 'webpack-merge'.", 666 }, 667 // Complex configs 668 { 669 name: "env", 670 type: (value, previous = {}) => { 671 // for https://github.com/webpack/webpack-cli/issues/2642 672 if (value.endsWith("=")) { 673 value.concat('""'); 674 } 675 676 // This ensures we're only splitting by the first `=` 677 const [allKeys, val] = value.split(/=(.+)/, 2); 678 const splitKeys = allKeys.split(/\.(?!$)/); 679 680 let prevRef = previous; 681 682 splitKeys.forEach((someKey, index) => { 683 if (!prevRef[someKey]) { 684 prevRef[someKey] = {}; 685 } 686 687 if (typeof prevRef[someKey] === "string") { 688 prevRef[someKey] = {}; 689 } 690 691 if (index === splitKeys.length - 1) { 692 if (typeof val === "string") { 693 prevRef[someKey] = val; 694 } else { 695 prevRef[someKey] = true; 696 } 697 } 698 699 prevRef = prevRef[someKey]; 700 }); 701 702 return previous; 703 }, 704 multiple: true, 705 description: "Environment passed to the configuration when it is a function.", 706 }, 707 { 708 name: "node-env", 709 configs: [ 710 { 711 type: "string", 712 }, 713 ], 714 multiple: false, 715 description: "Sets process.env.NODE_ENV to the specified value.", 716 }, 717 718 // Adding more plugins 719 { 720 name: "hot", 721 alias: "h", 722 configs: [ 723 { 724 type: "string", 725 }, 726 { 727 type: "boolean", 728 }, 729 ], 730 negative: true, 731 description: "Enables Hot Module Replacement", 732 negatedDescription: "Disables Hot Module Replacement.", 733 }, 734 { 735 name: "analyze", 736 configs: [ 737 { 738 type: "enum", 739 values: [true], 740 }, 741 ], 742 multiple: false, 743 description: "It invokes webpack-bundle-analyzer plugin to get bundle information.", 744 }, 745 { 746 name: "progress", 747 configs: [ 748 { 749 type: "string", 750 }, 751 { 752 type: "enum", 753 values: [true], 754 }, 755 ], 756 description: "Print compilation progress during build.", 757 }, 758 { 759 name: "prefetch", 760 configs: [ 761 { 762 type: "string", 763 }, 764 ], 765 description: "Prefetch this request.", 766 }, 767 768 // Output options 769 { 770 name: "json", 771 configs: [ 772 { 773 type: "string", 774 }, 775 { 776 type: "enum", 777 values: [true], 778 }, 779 ], 780 alias: "j", 781 description: "Prints result as JSON or store it in a file.", 782 }, 783 784 // For webpack@4 785 { 786 name: "entry", 787 configs: [ 788 { 789 type: "string", 790 }, 791 ], 792 multiple: true, 793 description: "The entry point(s) of your application e.g. ./src/main.js.", 794 }, 795 { 796 name: "output-path", 797 alias: "o", 798 configs: [ 799 { 800 type: "string", 801 }, 802 ], 803 description: "Output location of the file generated by webpack e.g. ./dist/.", 804 }, 805 { 806 name: "target", 807 alias: "t", 808 configs: [ 809 { 810 type: "string", 811 }, 812 ], 813 multiple: this.webpack.cli !== undefined, 814 description: "Sets the build target e.g. node.", 815 }, 816 { 817 name: "devtool", 818 configs: [ 819 { 820 type: "string", 821 }, 822 { 823 type: "enum", 824 values: [false], 825 }, 826 ], 827 negative: true, 828 alias: "d", 829 description: "Determine source maps to use.", 830 negatedDescription: "Do not generate source maps.", 831 }, 832 { 833 name: "mode", 834 configs: [ 835 { 836 type: "string", 837 }, 838 ], 839 description: "Defines the mode to pass to webpack.", 840 }, 841 { 842 name: "name", 843 configs: [ 844 { 845 type: "string", 846 }, 847 ], 848 description: "Name of the configuration. Used when loading multiple configurations.", 849 }, 850 { 851 name: "stats", 852 configs: [ 853 { 854 type: "string", 855 }, 856 { 857 type: "boolean", 858 }, 859 ], 860 negative: true, 861 description: "It instructs webpack on how to treat the stats e.g. verbose.", 862 negatedDescription: "Disable stats output.", 863 }, 864 { 865 name: "watch", 866 configs: [ 867 { 868 type: "boolean", 869 }, 870 ], 871 negative: true, 872 alias: "w", 873 description: "Watch for files changes.", 874 negatedDescription: "Do not watch for file changes.", 875 }, 876 { 877 name: "watch-options-stdin", 878 configs: [ 879 { 880 type: "boolean", 881 }, 882 ], 883 negative: true, 884 description: "Stop watching when stdin stream has ended.", 885 negatedDescription: "Do not stop watching when stdin stream has ended.", 886 }, 887 ]; 888 889 // Extract all the flags being exported from core. 890 // A list of cli flags generated by core can be found here https://github.com/webpack/webpack/blob/master/test/__snapshots__/Cli.test.js.snap 891 const coreFlags = this.webpack.cli 892 ? Object.entries(this.webpack.cli.getArguments()).map(([flag, meta]) => { 893 const inBuiltIn = builtInFlags.find((builtInFlag) => builtInFlag.name === flag); 894 895 if (inBuiltIn) { 896 return { 897 ...meta, 898 name: flag, 899 group: "core", 900 ...inBuiltIn, 901 configs: meta.configs || [], 902 }; 903 } 904 905 return { ...meta, name: flag, group: "core" }; 906 }) 907 : []; 908 909 const options = [] 910 .concat( 911 builtInFlags.filter( 912 (builtInFlag) => !coreFlags.find((coreFlag) => builtInFlag.name === coreFlag.name), 913 ), 914 ) 915 .concat(coreFlags) 916 .map((option) => { 917 option.helpLevel = minimumHelpFlags.includes(option.name) ? "minimum" : "verbose"; 918 919 return option; 920 }); 921 922 this.builtInOptionsCache = options; 923 924 return options; 925 } 926 927 async loadWebpack(handleError = true) { 928 return this.tryRequireThenImport(WEBPACK_PACKAGE, handleError); 929 } 930 931 async run(args, parseOptions) { 932 // Built-in internal commands 933 const buildCommandOptions = { 934 name: "build [entries...]", 935 alias: ["bundle", "b"], 936 description: "Run webpack (default command, can be omitted).", 937 usage: "[entries...] [options]", 938 dependencies: [WEBPACK_PACKAGE], 939 }; 940 const watchCommandOptions = { 941 name: "watch [entries...]", 942 alias: "w", 943 description: "Run webpack and watch for files changes.", 944 usage: "[entries...] [options]", 945 dependencies: [WEBPACK_PACKAGE], 946 }; 947 const versionCommandOptions = { 948 name: "version [commands...]", 949 alias: "v", 950 description: 951 "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.", 952 }; 953 const helpCommandOptions = { 954 name: "help [command] [option]", 955 alias: "h", 956 description: "Display help for commands and options.", 957 }; 958 // Built-in external commands 959 const externalBuiltInCommandsInfo = [ 960 { 961 name: "serve [entries...]", 962 alias: ["server", "s"], 963 pkg: "@webpack-cli/serve", 964 }, 965 { 966 name: "info", 967 alias: "i", 968 pkg: "@webpack-cli/info", 969 }, 970 { 971 name: "init", 972 alias: ["create", "new", "c", "n"], 973 pkg: "@webpack-cli/generators", 974 }, 975 { 976 name: "loader", 977 alias: "l", 978 pkg: "@webpack-cli/generators", 979 }, 980 { 981 name: "plugin", 982 alias: "p", 983 pkg: "@webpack-cli/generators", 984 }, 985 { 986 name: "migrate", 987 alias: "m", 988 pkg: "@webpack-cli/migrate", 989 }, 990 { 991 name: "configtest [config-path]", 992 alias: "t", 993 pkg: "@webpack-cli/configtest", 994 }, 995 ]; 996 997 const knownCommands = [ 998 buildCommandOptions, 999 watchCommandOptions, 1000 versionCommandOptions, 1001 helpCommandOptions, 1002 ...externalBuiltInCommandsInfo, 1003 ]; 1004 const getCommandName = (name) => name.split(" ")[0]; 1005 const isKnownCommand = (name) => 1006 knownCommands.find( 1007 (command) => 1008 getCommandName(command.name) === name || 1009 (Array.isArray(command.alias) ? command.alias.includes(name) : command.alias === name), 1010 ); 1011 const isCommand = (input, commandOptions) => { 1012 const longName = getCommandName(commandOptions.name); 1013 1014 if (input === longName) { 1015 return true; 1016 } 1017 1018 if (commandOptions.alias) { 1019 if (Array.isArray(commandOptions.alias)) { 1020 return commandOptions.alias.includes(input); 1021 } else { 1022 return commandOptions.alias === input; 1023 } 1024 } 1025 1026 return false; 1027 }; 1028 const findCommandByName = (name) => 1029 this.program.commands.find( 1030 (command) => name === command.name() || command.aliases().includes(name), 1031 ); 1032 const isOption = (value) => value.startsWith("-"); 1033 const isGlobalOption = (value) => 1034 value === "--color" || 1035 value === "--no-color" || 1036 value === "-v" || 1037 value === "--version" || 1038 value === "-h" || 1039 value === "--help"; 1040 1041 const loadCommandByName = async (commandName, allowToInstall = false) => { 1042 const isBuildCommandUsed = isCommand(commandName, buildCommandOptions); 1043 const isWatchCommandUsed = isCommand(commandName, watchCommandOptions); 1044 1045 if (isBuildCommandUsed || isWatchCommandUsed) { 1046 await this.makeCommand( 1047 isBuildCommandUsed ? buildCommandOptions : watchCommandOptions, 1048 async () => { 1049 this.webpack = await this.loadWebpack(); 1050 1051 return isWatchCommandUsed 1052 ? this.getBuiltInOptions().filter((option) => option.name !== "watch") 1053 : this.getBuiltInOptions(); 1054 }, 1055 async (entries, options) => { 1056 if (entries.length > 0) { 1057 options.entry = [...entries, ...(options.entry || [])]; 1058 } 1059 1060 await this.runWebpack(options, isWatchCommandUsed); 1061 }, 1062 ); 1063 } else if (isCommand(commandName, helpCommandOptions)) { 1064 // Stub for the `help` command 1065 this.makeCommand(helpCommandOptions, [], () => {}); 1066 } else if (isCommand(commandName, versionCommandOptions)) { 1067 // Stub for the `version` command 1068 this.makeCommand(versionCommandOptions, [], () => {}); 1069 } else { 1070 const builtInExternalCommandInfo = externalBuiltInCommandsInfo.find( 1071 (externalBuiltInCommandInfo) => 1072 getCommandName(externalBuiltInCommandInfo.name) === commandName || 1073 (Array.isArray(externalBuiltInCommandInfo.alias) 1074 ? externalBuiltInCommandInfo.alias.includes(commandName) 1075 : externalBuiltInCommandInfo.alias === commandName), 1076 ); 1077 1078 let pkg; 1079 1080 if (builtInExternalCommandInfo) { 1081 ({ pkg } = builtInExternalCommandInfo); 1082 } else { 1083 pkg = commandName; 1084 } 1085 1086 if (pkg !== "webpack-cli" && !this.checkPackageExists(pkg)) { 1087 if (!allowToInstall) { 1088 return; 1089 } 1090 1091 pkg = await this.doInstall(pkg, { 1092 preMessage: () => { 1093 this.logger.error( 1094 `For using this command you need to install: '${this.colors.green(pkg)}' package.`, 1095 ); 1096 }, 1097 }); 1098 } 1099 1100 let loadedCommand; 1101 1102 try { 1103 loadedCommand = await this.tryRequireThenImport(pkg, false); 1104 } catch (error) { 1105 // Ignore, command is not installed 1106 1107 return; 1108 } 1109 1110 let command; 1111 1112 try { 1113 command = new loadedCommand(); 1114 1115 await command.apply(this); 1116 } catch (error) { 1117 this.logger.error(`Unable to load '${pkg}' command`); 1118 this.logger.error(error); 1119 process.exit(2); 1120 } 1121 } 1122 }; 1123 1124 // Register own exit 1125 this.program.exitOverride(async (error) => { 1126 if (error.exitCode === 0) { 1127 process.exit(0); 1128 } 1129 1130 if (error.code === "executeSubCommandAsync") { 1131 process.exit(2); 1132 } 1133 1134 if (error.code === "commander.help") { 1135 process.exit(0); 1136 } 1137 1138 if (error.code === "commander.unknownOption") { 1139 let name = error.message.match(/'(.+)'/); 1140 1141 if (name) { 1142 name = name[1].substr(2); 1143 1144 if (name.includes("=")) { 1145 name = name.split("=")[0]; 1146 } 1147 1148 const { operands } = this.program.parseOptions(this.program.args); 1149 const operand = 1150 typeof operands[0] !== "undefined" 1151 ? operands[0] 1152 : getCommandName(buildCommandOptions.name); 1153 1154 if (operand) { 1155 const command = findCommandByName(operand); 1156 1157 if (!command) { 1158 this.logger.error(`Can't find and load command '${operand}'`); 1159 this.logger.error("Run 'webpack --help' to see available commands and options"); 1160 process.exit(2); 1161 } 1162 1163 const levenshtein = require("fastest-levenshtein"); 1164 1165 command.options.forEach((option) => { 1166 if (!option.hidden && levenshtein.distance(name, option.long.slice(2)) < 3) { 1167 this.logger.error(`Did you mean '--${option.name()}'?`); 1168 } 1169 }); 1170 } 1171 } 1172 } 1173 1174 // Codes: 1175 // - commander.unknownCommand 1176 // - commander.missingArgument 1177 // - commander.missingMandatoryOptionValue 1178 // - commander.optionMissingArgument 1179 1180 this.logger.error("Run 'webpack --help' to see available commands and options"); 1181 process.exit(2); 1182 }); 1183 1184 // Default `--color` and `--no-color` options 1185 const cli = this; 1186 this.program.option("--color", "Enable colors on console."); 1187 this.program.on("option:color", function () { 1188 const { color } = this.opts(); 1189 1190 cli.isColorSupportChanged = color; 1191 cli.colors = cli.createColors(color); 1192 }); 1193 this.program.option("--no-color", "Disable colors on console."); 1194 this.program.on("option:no-color", function () { 1195 const { color } = this.opts(); 1196 1197 cli.isColorSupportChanged = color; 1198 cli.colors = cli.createColors(color); 1199 }); 1200 1201 // Make `-v, --version` options 1202 // Make `version|v [commands...]` command 1203 const outputVersion = async (options) => { 1204 // Filter `bundle`, `watch`, `version` and `help` commands 1205 const possibleCommandNames = options.filter( 1206 (option) => 1207 !isCommand(option, buildCommandOptions) && 1208 !isCommand(option, watchCommandOptions) && 1209 !isCommand(option, versionCommandOptions) && 1210 !isCommand(option, helpCommandOptions), 1211 ); 1212 1213 possibleCommandNames.forEach((possibleCommandName) => { 1214 if (!isOption(possibleCommandName)) { 1215 return; 1216 } 1217 1218 this.logger.error(`Unknown option '${possibleCommandName}'`); 1219 this.logger.error("Run 'webpack --help' to see available commands and options"); 1220 process.exit(2); 1221 }); 1222 1223 if (possibleCommandNames.length > 0) { 1224 await Promise.all( 1225 possibleCommandNames.map((possibleCommand) => loadCommandByName(possibleCommand)), 1226 ); 1227 1228 for (const possibleCommandName of possibleCommandNames) { 1229 const foundCommand = findCommandByName(possibleCommandName); 1230 1231 if (!foundCommand) { 1232 this.logger.error(`Unknown command '${possibleCommandName}'`); 1233 this.logger.error("Run 'webpack --help' to see available commands and options"); 1234 process.exit(2); 1235 } 1236 1237 try { 1238 const { name, version } = this.loadJSONFile(`${foundCommand.pkg}/package.json`); 1239 1240 this.logger.raw(`${name} ${version}`); 1241 } catch (e) { 1242 this.logger.error(`Error: External package '${foundCommand.pkg}' not found`); 1243 process.exit(2); 1244 } 1245 } 1246 } 1247 1248 let webpack; 1249 1250 try { 1251 webpack = await this.loadWebpack(false); 1252 } catch (_error) { 1253 // Nothing 1254 } 1255 1256 this.logger.raw(`webpack: ${webpack ? webpack.version : "not installed"}`); 1257 1258 const pkgJSON = this.loadJSONFile("../package.json"); 1259 1260 this.logger.raw(`webpack-cli: ${pkgJSON.version}`); 1261 1262 let devServer; 1263 1264 try { 1265 devServer = await this.loadJSONFile("webpack-dev-server/package.json", false); 1266 } catch (_error) { 1267 // Nothing 1268 } 1269 1270 this.logger.raw(`webpack-dev-server ${devServer ? devServer.version : "not installed"}`); 1271 1272 process.exit(0); 1273 }; 1274 this.program.option( 1275 "-v, --version", 1276 "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.", 1277 ); 1278 1279 const outputHelp = async (options, isVerbose, isHelpCommandSyntax, program) => { 1280 const { bold } = this.colors; 1281 const outputIncorrectUsageOfHelp = () => { 1282 this.logger.error("Incorrect use of help"); 1283 this.logger.error( 1284 "Please use: 'webpack help [command] [option]' | 'webpack [command] --help'", 1285 ); 1286 this.logger.error("Run 'webpack --help' to see available commands and options"); 1287 process.exit(2); 1288 }; 1289 1290 const isGlobalHelp = options.length === 0; 1291 const isCommandHelp = options.length === 1 && !isOption(options[0]); 1292 1293 if (isGlobalHelp || isCommandHelp) { 1294 program.configureHelp({ 1295 sortSubcommands: true, 1296 // Support multiple aliases 1297 commandUsage: (command) => { 1298 let parentCmdNames = ""; 1299 1300 for (let parentCmd = command.parent; parentCmd; parentCmd = parentCmd.parent) { 1301 parentCmdNames = `${parentCmd.name()} ${parentCmdNames}`; 1302 } 1303 1304 if (isGlobalHelp) { 1305 return `${parentCmdNames}${command.usage()}\n${bold( 1306 "Alternative usage to run commands:", 1307 )} ${parentCmdNames}[command] [options]`; 1308 } 1309 1310 return `${parentCmdNames}${command.name()}|${command 1311 .aliases() 1312 .join("|")} ${command.usage()}`; 1313 }, 1314 // Support multiple aliases 1315 subcommandTerm: (command) => { 1316 const humanReadableArgumentName = (argument) => { 1317 const nameOutput = argument.name + (argument.variadic === true ? "..." : ""); 1318 1319 return argument.required ? "<" + nameOutput + ">" : "[" + nameOutput + "]"; 1320 }; 1321 const args = command._args.map((arg) => humanReadableArgumentName(arg)).join(" "); 1322 1323 return `${command.name()}|${command.aliases().join("|")}${args ? ` ${args}` : ""}${ 1324 command.options.length > 0 ? " [options]" : "" 1325 }`; 1326 }, 1327 visibleOptions: function visibleOptions(command) { 1328 return command.options.filter((option) => { 1329 if (option.hidden) { 1330 return false; 1331 } 1332 1333 switch (option.helpLevel) { 1334 case "verbose": 1335 return isVerbose; 1336 case "minimum": 1337 default: 1338 return true; 1339 } 1340 }); 1341 }, 1342 padWidth(command, helper) { 1343 return Math.max( 1344 helper.longestArgumentTermLength(command, helper), 1345 helper.longestOptionTermLength(command, helper), 1346 // For global options 1347 helper.longestOptionTermLength(program, helper), 1348 helper.longestSubcommandTermLength(isGlobalHelp ? program : command, helper), 1349 ); 1350 }, 1351 formatHelp: (command, helper) => { 1352 const termWidth = helper.padWidth(command, helper); 1353 const helpWidth = helper.helpWidth || process.env.WEBPACK_CLI_HELP_WIDTH || 80; 1354 const itemIndentWidth = 2; 1355 const itemSeparatorWidth = 2; // between term and description 1356 1357 const formatItem = (term, description) => { 1358 if (description) { 1359 const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`; 1360 1361 return helper.wrap( 1362 fullText, 1363 helpWidth - itemIndentWidth, 1364 termWidth + itemSeparatorWidth, 1365 ); 1366 } 1367 1368 return term; 1369 }; 1370 1371 const formatList = (textArray) => 1372 textArray.join("\n").replace(/^/gm, " ".repeat(itemIndentWidth)); 1373 1374 // Usage 1375 let output = [`${bold("Usage:")} ${helper.commandUsage(command)}`, ""]; 1376 1377 // Description 1378 const commandDescription = isGlobalHelp 1379 ? "The build tool for modern web applications." 1380 : helper.commandDescription(command); 1381 1382 if (commandDescription.length > 0) { 1383 output = output.concat([commandDescription, ""]); 1384 } 1385 1386 // Arguments 1387 const argumentList = helper 1388 .visibleArguments(command) 1389 .map((argument) => formatItem(argument.term, argument.description)); 1390 1391 if (argumentList.length > 0) { 1392 output = output.concat([bold("Arguments:"), formatList(argumentList), ""]); 1393 } 1394 1395 // Options 1396 const optionList = helper 1397 .visibleOptions(command) 1398 .map((option) => 1399 formatItem(helper.optionTerm(option), helper.optionDescription(option)), 1400 ); 1401 1402 if (optionList.length > 0) { 1403 output = output.concat([bold("Options:"), formatList(optionList), ""]); 1404 } 1405 1406 // Global options 1407 const globalOptionList = program.options.map((option) => 1408 formatItem(helper.optionTerm(option), helper.optionDescription(option)), 1409 ); 1410 1411 if (globalOptionList.length > 0) { 1412 output = output.concat([bold("Global options:"), formatList(globalOptionList), ""]); 1413 } 1414 1415 // Commands 1416 const commandList = helper 1417 .visibleCommands(isGlobalHelp ? program : command) 1418 .map((command) => 1419 formatItem(helper.subcommandTerm(command), helper.subcommandDescription(command)), 1420 ); 1421 1422 if (commandList.length > 0) { 1423 output = output.concat([bold("Commands:"), formatList(commandList), ""]); 1424 } 1425 1426 return output.join("\n"); 1427 }, 1428 }); 1429 1430 if (isGlobalHelp) { 1431 await Promise.all( 1432 knownCommands.map((knownCommand) => { 1433 return loadCommandByName(getCommandName(knownCommand.name)); 1434 }), 1435 ); 1436 1437 const buildCommand = findCommandByName(getCommandName(buildCommandOptions.name)); 1438 1439 this.logger.raw(buildCommand.helpInformation()); 1440 } else { 1441 const name = options[0]; 1442 1443 await loadCommandByName(name); 1444 1445 const command = findCommandByName(name); 1446 1447 if (!command) { 1448 const builtInCommandUsed = externalBuiltInCommandsInfo.find( 1449 (command) => command.name.includes(name) || name === command.alias, 1450 ); 1451 if (typeof builtInCommandUsed !== "undefined") { 1452 this.logger.error( 1453 `For using '${name}' command you need to install '${builtInCommandUsed.pkg}' package.`, 1454 ); 1455 } else { 1456 this.logger.error(`Can't find and load command '${name}'`); 1457 this.logger.error("Run 'webpack --help' to see available commands and options."); 1458 } 1459 process.exit(2); 1460 } 1461 1462 this.logger.raw(command.helpInformation()); 1463 } 1464 } else if (isHelpCommandSyntax) { 1465 let isCommandSpecified = false; 1466 let commandName = getCommandName(buildCommandOptions.name); 1467 let optionName; 1468 1469 if (options.length === 1) { 1470 optionName = options[0]; 1471 } else if (options.length === 2) { 1472 isCommandSpecified = true; 1473 commandName = options[0]; 1474 optionName = options[1]; 1475 1476 if (isOption(commandName)) { 1477 outputIncorrectUsageOfHelp(); 1478 } 1479 } else { 1480 outputIncorrectUsageOfHelp(); 1481 } 1482 1483 await loadCommandByName(commandName); 1484 1485 const command = isGlobalOption(optionName) ? program : findCommandByName(commandName); 1486 1487 if (!command) { 1488 this.logger.error(`Can't find and load command '${commandName}'`); 1489 this.logger.error("Run 'webpack --help' to see available commands and options"); 1490 process.exit(2); 1491 } 1492 1493 const option = command.options.find( 1494 (option) => option.short === optionName || option.long === optionName, 1495 ); 1496 1497 if (!option) { 1498 this.logger.error(`Unknown option '${optionName}'`); 1499 this.logger.error("Run 'webpack --help' to see available commands and options"); 1500 process.exit(2); 1501 } 1502 1503 const nameOutput = 1504 option.flags.replace(/^.+[[<]/, "").replace(/(\.\.\.)?[\]>].*$/, "") + 1505 (option.variadic === true ? "..." : ""); 1506 const value = option.required 1507 ? "<" + nameOutput + ">" 1508 : option.optional 1509 ? "[" + nameOutput + "]" 1510 : ""; 1511 1512 this.logger.raw( 1513 `${bold("Usage")}: webpack${isCommandSpecified ? ` ${commandName}` : ""} ${option.long}${ 1514 value ? ` ${value}` : "" 1515 }`, 1516 ); 1517 1518 if (option.short) { 1519 this.logger.raw( 1520 `${bold("Short:")} webpack${isCommandSpecified ? ` ${commandName}` : ""} ${ 1521 option.short 1522 }${value ? ` ${value}` : ""}`, 1523 ); 1524 } 1525 1526 if (option.description) { 1527 this.logger.raw(`${bold("Description:")} ${option.description}`); 1528 } 1529 1530 if (!option.negate && option.defaultValue) { 1531 this.logger.raw(`${bold("Default value:")} ${JSON.stringify(option.defaultValue)}`); 1532 } 1533 1534 const flag = this.getBuiltInOptions().find((flag) => option.long === `--${flag.name}`); 1535 1536 if (flag && flag.configs) { 1537 const possibleValues = flag.configs.reduce((accumulator, currentValue) => { 1538 if (currentValue.values) { 1539 return accumulator.concat(currentValue.values); 1540 } else { 1541 return accumulator; 1542 } 1543 }, []); 1544 1545 if (possibleValues.length > 0) { 1546 this.logger.raw( 1547 `${bold("Possible values:")} ${JSON.stringify(possibleValues.join(" | "))}`, 1548 ); 1549 } 1550 } 1551 1552 this.logger.raw(""); 1553 1554 // TODO implement this after refactor cli arguments 1555 // logger.raw('Documentation: https://webpack.js.org/option/name/'); 1556 } else { 1557 outputIncorrectUsageOfHelp(); 1558 } 1559 1560 this.logger.raw( 1561 "To see list of all supported commands and options run 'webpack --help=verbose'.\n", 1562 ); 1563 this.logger.raw(`${bold("Webpack documentation:")} https://webpack.js.org/.`); 1564 this.logger.raw(`${bold("CLI documentation:")} https://webpack.js.org/api/cli/.`); 1565 this.logger.raw(`${bold("Made with ♥ by the webpack team")}.`); 1566 process.exit(0); 1567 }; 1568 this.program.helpOption(false); 1569 this.program.addHelpCommand(false); 1570 this.program.option("-h, --help [verbose]", "Display help for commands and options."); 1571 1572 let isInternalActionCalled = false; 1573 1574 // Default action 1575 this.program.usage("[options]"); 1576 this.program.allowUnknownOption(true); 1577 this.program.action(async (options, program) => { 1578 if (!isInternalActionCalled) { 1579 isInternalActionCalled = true; 1580 } else { 1581 this.logger.error("No commands found to run"); 1582 process.exit(2); 1583 } 1584 1585 // Command and options 1586 const { operands, unknown } = this.program.parseOptions(program.args); 1587 const defaultCommandToRun = getCommandName(buildCommandOptions.name); 1588 const hasOperand = typeof operands[0] !== "undefined"; 1589 const operand = hasOperand ? operands[0] : defaultCommandToRun; 1590 const isHelpOption = typeof options.help !== "undefined"; 1591 const isHelpCommandSyntax = isCommand(operand, helpCommandOptions); 1592 1593 if (isHelpOption || isHelpCommandSyntax) { 1594 let isVerbose = false; 1595 1596 if (isHelpOption) { 1597 if (typeof options.help === "string") { 1598 if (options.help !== "verbose") { 1599 this.logger.error("Unknown value for '--help' option, please use '--help=verbose'"); 1600 process.exit(2); 1601 } 1602 1603 isVerbose = true; 1604 } 1605 } 1606 1607 this.program.forHelp = true; 1608 1609 const optionsForHelp = [] 1610 .concat(isHelpOption && hasOperand ? [operand] : []) 1611 // Syntax `webpack help [command]` 1612 .concat(operands.slice(1)) 1613 // Syntax `webpack help [option]` 1614 .concat(unknown) 1615 .concat( 1616 isHelpCommandSyntax && typeof options.color !== "undefined" 1617 ? [options.color ? "--color" : "--no-color"] 1618 : [], 1619 ) 1620 .concat( 1621 isHelpCommandSyntax && typeof options.version !== "undefined" ? ["--version"] : [], 1622 ); 1623 1624 await outputHelp(optionsForHelp, isVerbose, isHelpCommandSyntax, program); 1625 } 1626 1627 const isVersionOption = typeof options.version !== "undefined"; 1628 const isVersionCommandSyntax = isCommand(operand, versionCommandOptions); 1629 1630 if (isVersionOption || isVersionCommandSyntax) { 1631 const optionsForVersion = [] 1632 .concat(isVersionOption ? [operand] : []) 1633 .concat(operands.slice(1)) 1634 .concat(unknown); 1635 1636 await outputVersion(optionsForVersion, program); 1637 } 1638 1639 let commandToRun = operand; 1640 let commandOperands = operands.slice(1); 1641 1642 if (isKnownCommand(commandToRun)) { 1643 await loadCommandByName(commandToRun, true); 1644 } else { 1645 const isEntrySyntax = fs.existsSync(operand); 1646 1647 if (isEntrySyntax) { 1648 commandToRun = defaultCommandToRun; 1649 commandOperands = operands; 1650 1651 await loadCommandByName(commandToRun); 1652 } else { 1653 this.logger.error(`Unknown command or entry '${operand}'`); 1654 1655 const levenshtein = require("fastest-levenshtein"); 1656 const found = knownCommands.find( 1657 (commandOptions) => 1658 levenshtein.distance(operand, getCommandName(commandOptions.name)) < 3, 1659 ); 1660 1661 if (found) { 1662 this.logger.error( 1663 `Did you mean '${getCommandName(found.name)}' (alias '${ 1664 Array.isArray(found.alias) ? found.alias.join(", ") : found.alias 1665 }')?`, 1666 ); 1667 } 1668 1669 this.logger.error("Run 'webpack --help' to see available commands and options"); 1670 process.exit(2); 1671 } 1672 } 1673 1674 await this.program.parseAsync([commandToRun, ...commandOperands, ...unknown], { 1675 from: "user", 1676 }); 1677 }); 1678 1679 await this.program.parseAsync(args, parseOptions); 1680 } 1681 1682 async loadConfig(options) { 1683 const interpret = require("interpret"); 1684 const loadConfigByPath = async (configPath, argv = {}) => { 1685 const ext = path.extname(configPath); 1686 const interpreted = Object.keys(interpret.jsVariants).find((variant) => variant === ext); 1687 1688 if (interpreted) { 1689 const rechoir = require("rechoir"); 1690 1691 try { 1692 rechoir.prepare(interpret.extensions, configPath); 1693 } catch (error) { 1694 if (error.failures) { 1695 this.logger.error(`Unable load '${configPath}'`); 1696 this.logger.error(error.message); 1697 error.failures.forEach((failure) => { 1698 this.logger.error(failure.error.message); 1699 }); 1700 this.logger.error("Please install one of them"); 1701 process.exit(2); 1702 } 1703 1704 this.logger.error(error); 1705 process.exit(2); 1706 } 1707 } 1708 1709 let options; 1710 1711 try { 1712 options = await this.tryRequireThenImport(configPath, false); 1713 } catch (error) { 1714 this.logger.error(`Failed to load '${configPath}' config`); 1715 1716 if (this.isValidationError(error)) { 1717 this.logger.error(error.message); 1718 } else { 1719 this.logger.error(error); 1720 } 1721 1722 process.exit(2); 1723 } 1724 1725 if (Array.isArray(options)) { 1726 await Promise.all( 1727 options.map(async (_, i) => { 1728 if (typeof options[i].then === "function") { 1729 options[i] = await options[i]; 1730 } 1731 1732 // `Promise` may return `Function` 1733 if (typeof options[i] === "function") { 1734 // when config is a function, pass the env from args to the config function 1735 options[i] = await options[i](argv.env, argv); 1736 } 1737 }), 1738 ); 1739 } else { 1740 if (typeof options.then === "function") { 1741 options = await options; 1742 } 1743 1744 // `Promise` may return `Function` 1745 if (typeof options === "function") { 1746 // when config is a function, pass the env from args to the config function 1747 options = await options(argv.env, argv); 1748 } 1749 } 1750 1751 const isObject = (value) => typeof value === "object" && value !== null; 1752 1753 if (!isObject(options) && !Array.isArray(options)) { 1754 this.logger.error(`Invalid configuration in '${configPath}'`); 1755 1756 process.exit(2); 1757 } 1758 1759 return { options, path: configPath }; 1760 }; 1761 1762 const config = { options: {}, path: new WeakMap() }; 1763 1764 if (options.config && options.config.length > 0) { 1765 const loadedConfigs = await Promise.all( 1766 options.config.map((configPath) => 1767 loadConfigByPath(path.resolve(configPath), options.argv), 1768 ), 1769 ); 1770 1771 config.options = []; 1772 1773 loadedConfigs.forEach((loadedConfig) => { 1774 const isArray = Array.isArray(loadedConfig.options); 1775 1776 // TODO we should run webpack multiple times when the `--config` options have multiple values with `--merge`, need to solve for the next major release 1777 if (config.options.length === 0) { 1778 config.options = loadedConfig.options; 1779 } else { 1780 if (!Array.isArray(config.options)) { 1781 config.options = [config.options]; 1782 } 1783 1784 if (isArray) { 1785 loadedConfig.options.forEach((item) => { 1786 config.options.push(item); 1787 }); 1788 } else { 1789 config.options.push(loadedConfig.options); 1790 } 1791 } 1792 1793 if (isArray) { 1794 loadedConfig.options.forEach((options) => { 1795 config.path.set(options, loadedConfig.path); 1796 }); 1797 } else { 1798 config.path.set(loadedConfig.options, loadedConfig.path); 1799 } 1800 }); 1801 1802 config.options = config.options.length === 1 ? config.options[0] : config.options; 1803 } else { 1804 // Order defines the priority, in decreasing order 1805 const defaultConfigFiles = [ 1806 "webpack.config", 1807 ".webpack/webpack.config", 1808 ".webpack/webpackfile", 1809 ] 1810 .map((filename) => 1811 // Since .cjs is not available on interpret side add it manually to default config extension list 1812 [...Object.keys(interpret.extensions), ".cjs"].map((ext) => ({ 1813 path: path.resolve(filename + ext), 1814 ext: ext, 1815 module: interpret.extensions[ext], 1816 })), 1817 ) 1818 .reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); 1819 1820 let foundDefaultConfigFile; 1821 1822 for (const defaultConfigFile of defaultConfigFiles) { 1823 if (!fs.existsSync(defaultConfigFile.path)) { 1824 continue; 1825 } 1826 1827 foundDefaultConfigFile = defaultConfigFile; 1828 break; 1829 } 1830 1831 if (foundDefaultConfigFile) { 1832 const loadedConfig = await loadConfigByPath(foundDefaultConfigFile.path, options.argv); 1833 1834 config.options = loadedConfig.options; 1835 1836 if (Array.isArray(config.options)) { 1837 config.options.forEach((item) => { 1838 config.path.set(item, loadedConfig.path); 1839 }); 1840 } else { 1841 config.path.set(loadedConfig.options, loadedConfig.path); 1842 } 1843 } 1844 } 1845 1846 if (options.configName) { 1847 const notFoundConfigNames = []; 1848 1849 config.options = options.configName.map((configName) => { 1850 let found; 1851 1852 if (Array.isArray(config.options)) { 1853 found = config.options.find((options) => options.name === configName); 1854 } else { 1855 found = config.options.name === configName ? config.options : undefined; 1856 } 1857 1858 if (!found) { 1859 notFoundConfigNames.push(configName); 1860 } 1861 1862 return found; 1863 }); 1864 1865 if (notFoundConfigNames.length > 0) { 1866 this.logger.error( 1867 notFoundConfigNames 1868 .map((configName) => `Configuration with the name "${configName}" was not found.`) 1869 .join(" "), 1870 ); 1871 process.exit(2); 1872 } 1873 } 1874 1875 if (options.merge) { 1876 const merge = await this.tryRequireThenImport("webpack-merge"); 1877 1878 // we can only merge when there are multiple configurations 1879 // either by passing multiple configs by flags or passing a 1880 // single config exporting an array 1881 if (!Array.isArray(config.options) || config.options.length <= 1) { 1882 this.logger.error("At least two configurations are required for merge."); 1883 process.exit(2); 1884 } 1885 1886 const mergedConfigPaths = []; 1887 1888 config.options = config.options.reduce((accumulator, options) => { 1889 const configPath = config.path.get(options); 1890 const mergedOptions = merge(accumulator, options); 1891 1892 mergedConfigPaths.push(configPath); 1893 1894 return mergedOptions; 1895 }, {}); 1896 config.path.set(config.options, mergedConfigPaths); 1897 } 1898 1899 return config; 1900 } 1901 1902 async buildConfig(config, options) { 1903 const runFunctionOnEachConfig = (options, fn) => { 1904 if (Array.isArray(options)) { 1905 for (let item of options) { 1906 item = fn(item); 1907 } 1908 } else { 1909 options = fn(options); 1910 } 1911 1912 return options; 1913 }; 1914 1915 if (options.analyze) { 1916 if (!this.checkPackageExists("webpack-bundle-analyzer")) { 1917 await this.doInstall("webpack-bundle-analyzer", { 1918 preMessage: () => { 1919 this.logger.error( 1920 `It looks like ${this.colors.yellow("webpack-bundle-analyzer")} is not installed.`, 1921 ); 1922 }, 1923 }); 1924 1925 this.logger.success( 1926 `${this.colors.yellow("webpack-bundle-analyzer")} was installed successfully.`, 1927 ); 1928 } 1929 } 1930 1931 if (typeof options.progress === "string" && options.progress !== "profile") { 1932 this.logger.error( 1933 `'${options.progress}' is an invalid value for the --progress option. Only 'profile' is allowed.`, 1934 ); 1935 process.exit(2); 1936 } 1937 1938 if (typeof options.hot === "string" && options.hot !== "only") { 1939 this.logger.error( 1940 `'${options.hot}' is an invalid value for the --hot option. Use 'only' instead.`, 1941 ); 1942 process.exit(2); 1943 } 1944 1945 const CLIPlugin = await this.tryRequireThenImport("./plugins/CLIPlugin"); 1946 1947 const internalBuildConfig = (item) => { 1948 // Output warnings 1949 if ( 1950 item.watch && 1951 options.argv && 1952 options.argv.env && 1953 (options.argv.env["WEBPACK_WATCH"] || options.argv.env["WEBPACK_SERVE"]) 1954 ) { 1955 this.logger.warn( 1956 `No need to use the '${ 1957 options.argv.env["WEBPACK_WATCH"] ? "watch" : "serve" 1958 }' command together with '{ watch: true }' configuration, it does not make sense.`, 1959 ); 1960 1961 if (options.argv.env["WEBPACK_SERVE"]) { 1962 item.watch = false; 1963 } 1964 } 1965 1966 // Apply options 1967 if (this.webpack.cli) { 1968 const args = this.getBuiltInOptions() 1969 .filter((flag) => flag.group === "core") 1970 .reduce((accumulator, flag) => { 1971 accumulator[flag.name] = flag; 1972 1973 return accumulator; 1974 }, {}); 1975 1976 const values = Object.keys(options).reduce((accumulator, name) => { 1977 if (name === "argv") { 1978 return accumulator; 1979 } 1980 1981 const kebabName = this.toKebabCase(name); 1982 1983 if (args[kebabName]) { 1984 accumulator[kebabName] = options[name]; 1985 } 1986 1987 return accumulator; 1988 }, {}); 1989 1990 const problems = this.webpack.cli.processArguments(args, item, values); 1991 1992 if (problems) { 1993 const groupBy = (xs, key) => { 1994 return xs.reduce((rv, x) => { 1995 (rv[x[key]] = rv[x[key]] || []).push(x); 1996 1997 return rv; 1998 }, {}); 1999 }; 2000 const problemsByPath = groupBy(problems, "path"); 2001 2002 for (const path in problemsByPath) { 2003 const problems = problemsByPath[path]; 2004 2005 problems.forEach((problem) => { 2006 this.logger.error( 2007 `${this.capitalizeFirstLetter(problem.type.replace(/-/g, " "))}${ 2008 problem.value ? ` '${problem.value}'` : "" 2009 } for the '--${problem.argument}' option${ 2010 problem.index ? ` by index '${problem.index}'` : "" 2011 }`, 2012 ); 2013 2014 if (problem.expected) { 2015 this.logger.error(`Expected: '${problem.expected}'`); 2016 } 2017 }); 2018 } 2019 2020 process.exit(2); 2021 } 2022 2023 // Setup default cache options 2024 if (item.cache && item.cache.type === "filesystem") { 2025 const configPath = config.path.get(item); 2026 2027 if (configPath) { 2028 if (!item.cache.buildDependencies) { 2029 item.cache.buildDependencies = {}; 2030 } 2031 2032 if (!item.cache.buildDependencies.defaultConfig) { 2033 item.cache.buildDependencies.defaultConfig = []; 2034 } 2035 2036 if (Array.isArray(configPath)) { 2037 configPath.forEach((oneOfConfigPath) => { 2038 item.cache.buildDependencies.defaultConfig.push(oneOfConfigPath); 2039 }); 2040 } else { 2041 item.cache.buildDependencies.defaultConfig.push(configPath); 2042 } 2043 } 2044 } 2045 } 2046 2047 // Setup legacy logic for webpack@4 2048 // TODO respect `--entry-reset` in th next major release 2049 // TODO drop in the next major release 2050 if (options.entry) { 2051 item.entry = options.entry; 2052 } 2053 2054 if (options.outputPath) { 2055 item.output = { ...item.output, ...{ path: path.resolve(options.outputPath) } }; 2056 } 2057 2058 if (options.target) { 2059 item.target = options.target; 2060 } 2061 2062 if (typeof options.devtool !== "undefined") { 2063 item.devtool = options.devtool; 2064 } 2065 2066 if (options.name) { 2067 item.name = options.name; 2068 } 2069 2070 if (typeof options.stats !== "undefined") { 2071 item.stats = options.stats; 2072 } 2073 2074 if (typeof options.watch !== "undefined") { 2075 item.watch = options.watch; 2076 } 2077 2078 if (typeof options.watchOptionsStdin !== "undefined") { 2079 item.watchOptions = { ...item.watchOptions, ...{ stdin: options.watchOptionsStdin } }; 2080 } 2081 2082 if (options.mode) { 2083 item.mode = options.mode; 2084 } 2085 2086 // Respect `process.env.NODE_ENV` 2087 if ( 2088 !item.mode && 2089 process.env && 2090 process.env.NODE_ENV && 2091 (process.env.NODE_ENV === "development" || 2092 process.env.NODE_ENV === "production" || 2093 process.env.NODE_ENV === "none") 2094 ) { 2095 item.mode = process.env.NODE_ENV; 2096 } 2097 2098 // Setup stats 2099 // TODO remove after drop webpack@4 2100 const statsForWebpack4 = this.webpack.Stats && this.webpack.Stats.presetToOptions; 2101 2102 if (statsForWebpack4) { 2103 if (typeof item.stats === "undefined") { 2104 item.stats = {}; 2105 } else if (typeof item.stats === "boolean") { 2106 item.stats = this.webpack.Stats.presetToOptions(item.stats); 2107 } else if ( 2108 typeof item.stats === "string" && 2109 (item.stats === "none" || 2110 item.stats === "verbose" || 2111 item.stats === "detailed" || 2112 item.stats === "normal" || 2113 item.stats === "minimal" || 2114 item.stats === "errors-only" || 2115 item.stats === "errors-warnings") 2116 ) { 2117 item.stats = this.webpack.Stats.presetToOptions(item.stats); 2118 } 2119 } else { 2120 if (typeof item.stats === "undefined") { 2121 item.stats = { preset: "normal" }; 2122 } else if (typeof item.stats === "boolean") { 2123 item.stats = item.stats ? { preset: "normal" } : { preset: "none" }; 2124 } else if (typeof item.stats === "string") { 2125 item.stats = { preset: item.stats }; 2126 } 2127 } 2128 2129 let colors; 2130 2131 // From arguments 2132 if (typeof this.isColorSupportChanged !== "undefined") { 2133 colors = Boolean(this.isColorSupportChanged); 2134 } 2135 // From stats 2136 else if (typeof item.stats.colors !== "undefined") { 2137 colors = item.stats.colors; 2138 } 2139 // Default 2140 else { 2141 colors = Boolean(this.colors.isColorSupported); 2142 } 2143 2144 // TODO remove after drop webpack v4 2145 if (typeof item.stats === "object" && item.stats !== null) { 2146 item.stats.colors = colors; 2147 } 2148 2149 // Apply CLI plugin 2150 if (!item.plugins) { 2151 item.plugins = []; 2152 } 2153 2154 item.plugins.unshift( 2155 new CLIPlugin({ 2156 configPath: config.path.get(item), 2157 helpfulOutput: !options.json, 2158 hot: options.hot, 2159 progress: options.progress, 2160 prefetch: options.prefetch, 2161 analyze: options.analyze, 2162 }), 2163 ); 2164 2165 return options; 2166 }; 2167 2168 runFunctionOnEachConfig(config.options, internalBuildConfig); 2169 2170 return config; 2171 } 2172 2173 isValidationError(error) { 2174 // https://github.com/webpack/webpack/blob/master/lib/index.js#L267 2175 // https://github.com/webpack/webpack/blob/v4.44.2/lib/webpack.js#L90 2176 const ValidationError = 2177 this.webpack.ValidationError || this.webpack.WebpackOptionsValidationError; 2178 2179 return error instanceof ValidationError || error.name === "ValidationError"; 2180 } 2181 2182 async createCompiler(options, callback) { 2183 if (typeof options.nodeEnv === "string") { 2184 process.env.NODE_ENV = options.nodeEnv; 2185 } 2186 2187 let config = await this.loadConfig(options); 2188 config = await this.buildConfig(config, options); 2189 2190 let compiler; 2191 2192 try { 2193 compiler = this.webpack( 2194 config.options, 2195 callback 2196 ? (error, stats) => { 2197 if (error && this.isValidationError(error)) { 2198 this.logger.error(error.message); 2199 process.exit(2); 2200 } 2201 2202 callback(error, stats); 2203 } 2204 : callback, 2205 ); 2206 } catch (error) { 2207 if (this.isValidationError(error)) { 2208 this.logger.error(error.message); 2209 } else { 2210 this.logger.error(error); 2211 } 2212 2213 process.exit(2); 2214 } 2215 2216 // TODO webpack@4 return Watching and MultiWatching instead Compiler and MultiCompiler, remove this after drop webpack@4 2217 if (compiler && compiler.compiler) { 2218 compiler = compiler.compiler; 2219 } 2220 2221 return compiler; 2222 } 2223 2224 needWatchStdin(compiler) { 2225 if (compiler.compilers) { 2226 return compiler.compilers.some( 2227 (compiler) => compiler.options.watchOptions && compiler.options.watchOptions.stdin, 2228 ); 2229 } 2230 2231 return compiler.options.watchOptions && compiler.options.watchOptions.stdin; 2232 } 2233 2234 async runWebpack(options, isWatchCommand) { 2235 // eslint-disable-next-line prefer-const 2236 let compiler; 2237 let createJsonStringifyStream; 2238 2239 if (options.json) { 2240 const jsonExt = await this.tryRequireThenImport("@discoveryjs/json-ext"); 2241 2242 createJsonStringifyStream = jsonExt.stringifyStream; 2243 } 2244 2245 const callback = (error, stats) => { 2246 if (error) { 2247 this.logger.error(error); 2248 process.exit(2); 2249 } 2250 2251 if (stats.hasErrors()) { 2252 process.exitCode = 1; 2253 } 2254 2255 if (!compiler) { 2256 return; 2257 } 2258 2259 const statsOptions = compiler.compilers 2260 ? { 2261 children: compiler.compilers.map((compiler) => 2262 compiler.options ? compiler.options.stats : undefined, 2263 ), 2264 } 2265 : compiler.options 2266 ? compiler.options.stats 2267 : undefined; 2268 2269 // TODO webpack@4 doesn't support `{ children: [{ colors: true }, { colors: true }] }` for stats 2270 const statsForWebpack4 = this.webpack.Stats && this.webpack.Stats.presetToOptions; 2271 2272 if (compiler.compilers && statsForWebpack4) { 2273 statsOptions.colors = statsOptions.children.some((child) => child.colors); 2274 } 2275 2276 if (options.json && createJsonStringifyStream) { 2277 const handleWriteError = (error) => { 2278 this.logger.error(error); 2279 process.exit(2); 2280 }; 2281 2282 if (options.json === true) { 2283 createJsonStringifyStream(stats.toJson(statsOptions)) 2284 .on("error", handleWriteError) 2285 .pipe(process.stdout) 2286 .on("error", handleWriteError) 2287 .on("close", () => process.stdout.write("\n")); 2288 } else { 2289 createJsonStringifyStream(stats.toJson(statsOptions)) 2290 .on("error", handleWriteError) 2291 .pipe(fs.createWriteStream(options.json)) 2292 .on("error", handleWriteError) 2293 // Use stderr to logging 2294 .on("close", () => { 2295 process.stderr.write( 2296 `[webpack-cli] ${this.colors.green( 2297 `stats are successfully stored as json to ${options.json}`, 2298 )}\n`, 2299 ); 2300 }); 2301 } 2302 } else { 2303 const printedStats = stats.toString(statsOptions); 2304 2305 // Avoid extra empty line when `stats: 'none'` 2306 if (printedStats) { 2307 this.logger.raw(printedStats); 2308 } 2309 } 2310 }; 2311 2312 const env = 2313 isWatchCommand || options.watch 2314 ? { WEBPACK_WATCH: true, ...options.env } 2315 : { WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, ...options.env }; 2316 2317 options.argv = { ...options, env }; 2318 2319 if (isWatchCommand) { 2320 options.watch = true; 2321 } 2322 2323 compiler = await this.createCompiler(options, callback); 2324 2325 if (!compiler) { 2326 return; 2327 } 2328 2329 const isWatch = (compiler) => 2330 compiler.compilers 2331 ? compiler.compilers.some((compiler) => compiler.options.watch) 2332 : compiler.options.watch; 2333 2334 if (isWatch(compiler) && this.needWatchStdin(compiler)) { 2335 process.stdin.on("end", () => { 2336 process.exit(0); 2337 }); 2338 process.stdin.resume(); 2339 } 2340 } 2341 } 2342 2343 module.exports = WebpackCLI;