diff --git a/OPTIONS.md b/OPTIONS.md index cfdd161bd68..797fe3b1ce6 100644 --- a/OPTIONS.md +++ b/OPTIONS.md @@ -700,8 +700,8 @@ Global options: Commands: build|bundle|b [options] Run webpack (default command, can be omitted). - version|v Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands. - help|h Display help for commands and options. + version|v [commands...] Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands. + help|h [command] [option] Display help for commands and options. serve|s [options] Run the webpack dev server. info|i [options] Outputs information about your system. init|c [options] [scaffold...] Initialize a new webpack configuration. diff --git a/packages/webpack-cli/lib/webpack-cli.js b/packages/webpack-cli/lib/webpack-cli.js index 605311f7c7d..e3b1edb7ebe 100644 --- a/packages/webpack-cli/lib/webpack-cli.js +++ b/packages/webpack-cli/lib/webpack-cli.js @@ -144,8 +144,7 @@ class WebpackCLI { flags = `${flags} `; } - // TODO need to fix on webpack-dev-server side - // `describe` used by `webpack-dev-server` + // TODO `describe` used by `webpack-dev-server@3` const description = option.description || option.describe || ''; const defaultValue = option.defaultValue; @@ -236,16 +235,14 @@ class WebpackCLI { usage: '[options]', }; const versionCommandOptions = { - name: 'version', + name: 'version [commands...]', alias: 'v', description: "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.", - usage: '[commands...]', }; const helpCommandOptions = { - name: 'help', + name: 'help [command] [option]', alias: 'h', description: 'Display help for commands and options.', - usage: '[command]', }; // Built-in external commands const externalBuiltInCommandsInfo = [ @@ -287,24 +284,34 @@ class WebpackCLI { ]; const knownCommands = [buildCommandOptions, versionCommandOptions, helpCommandOptions, ...externalBuiltInCommandsInfo]; + const getCommandName = (name) => name.split(' ')[0]; const isKnownCommand = (name) => knownCommands.find( (command) => - command.name === name || (Array.isArray(command.alias) ? command.alias.includes(name) : command.alias === name), + getCommandName(command.name) === name || + (Array.isArray(command.alias) ? command.alias.includes(name) : command.alias === name), ); const isBuildCommand = (name) => - buildCommandOptions.name === name || + getCommandName(buildCommandOptions.name) === name || (Array.isArray(buildCommandOptions.alias) ? buildCommandOptions.alias.includes(name) : buildCommandOptions.alias === name); const isHelpCommand = (name) => - helpCommandOptions.name === name || + getCommandName(helpCommandOptions.name) === name || (Array.isArray(helpCommandOptions.alias) ? helpCommandOptions.alias.includes(name) : helpCommandOptions.alias === name); const isVersionCommand = (name) => - versionCommandOptions.name === name || + getCommandName(versionCommandOptions.name) === name || (Array.isArray(versionCommandOptions.alias) ? versionCommandOptions.alias.includes(name) : versionCommandOptions.alias === name); const findCommandByName = (name) => this.program.commands.find((command) => name === command.name() || command.alias().includes(name)); + const isOption = (value) => value.startsWith('-'); + const isGlobalOption = (value) => + value === '--color' || + value === '--no-color' || + value === '-v' || + value === '--version' || + value === '-h' || + value === '--help'; const getCommandNameAndOptions = (args) => { let commandName; @@ -313,7 +320,7 @@ class WebpackCLI { let allowToSearchCommand = true; args.forEach((arg) => { - if (!arg.startsWith('-') && allowToSearchCommand) { + if (!isOption(arg) && allowToSearchCommand) { commandName = arg; allowToSearchCommand = false; @@ -491,9 +498,7 @@ class WebpackCLI { ); possibleCommandNames.forEach((possibleCommandName) => { - const isOption = possibleCommandName.startsWith('-'); - - if (!isOption) { + if (!isOption(possibleCommandName)) { return; } @@ -544,9 +549,7 @@ class WebpackCLI { "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.", ); - // Default global `help` command - const outputHelp = async (options, isVerbose, program) => { - const isGlobal = options.length === 0; + const outputHelp = async (options, isVerbose, isHelpCommandSyntax, program) => { const hideVerboseOptions = (command) => { command.options = command.options.filter((option) => { const foundOption = flags.find((flag) => { @@ -564,11 +567,40 @@ class WebpackCLI { return true; }); }; + const outputGlobalOptions = () => { + const programHelpInformation = program.helpInformation(); + const globalOptions = programHelpInformation.match(/Options:\n(?.+)\nCommands:\n/s); + + if (globalOptions && globalOptions.groups.globalOptions) { + logger.raw('\nGlobal options:'); + logger.raw(globalOptions.groups.globalOptions.trimRight()); + } + }; + const outputGlobalCommands = () => { + const programHelpInformation = program.helpInformation(); + const globalCommands = programHelpInformation.match(/Commands:\n(?.+)/s); + + if (globalCommands.groups.globalCommands) { + logger.raw('\nCommands:'); + logger.raw( + globalCommands.groups.globalCommands + .trimRight() + // `commander` doesn't support multiple alias in help + .replace('build|bundle [options] ', 'build|bundle|b [options]'), + ); + } + }; + const outputIncorrectUsageOfHelp = () => { + logger.error('Incorrect use of help'); + logger.error("Please use: 'webpack help [command] [option]' | 'webpack [command] --help'"); + logger.error("Run 'webpack --help' to see available commands and options"); + process.exit(2); + }; - if (isGlobal) { + if (options.length === 0) { await Promise.all( knownCommands.map((knownCommand) => { - return loadCommandByName(knownCommand.name); + return loadCommandByName(getCommandName(knownCommand.name)); }), ); @@ -588,20 +620,11 @@ class WebpackCLI { ); logger.raw(helpInformation); - } else { - const [name, ...optionsWithoutCommandName] = options; - if (name.startsWith('-')) { - logger.error(`Unknown option '${name}'`); - logger.error("Run 'webpack --help' to see available commands and options"); - process.exit(2); - } - - optionsWithoutCommandName.forEach((option) => { - logger.error(`Unknown option '${option}'`); - logger.error("Run 'webpack --help' to see available commands and options"); - process.exit(2); - }); + outputGlobalOptions(); + outputGlobalCommands(); + } else if (options.length === 1 && !isOption(options[0])) { + const name = options[0]; await loadCommandByName(name); @@ -629,26 +652,71 @@ class WebpackCLI { } logger.raw(helpInformation); - } - const programHelpInformation = program.helpInformation(); - const globalOptions = programHelpInformation.match(/Options:\n(?.+)\nCommands:\n/s); + outputGlobalOptions(); + } else if (isHelpCommandSyntax) { + let commandName; + let optionName; - if (globalOptions && globalOptions.groups.globalOptions) { - logger.raw('\nGlobal options:'); - logger.raw(globalOptions.groups.globalOptions.trimRight()); - } + if (options.length === 1) { + commandName = buildCommandOptions.name; + optionName = options[0]; + } else if (options.length === 2) { + commandName = options[0]; + optionName = options[1]; + + if (isOption(commandName)) { + outputIncorrectUsageOfHelp(); + } + } else { + outputIncorrectUsageOfHelp(); + } + + await loadCommandByName(commandName); + + const command = isGlobalOption(optionName) ? this.program : findCommandByName(commandName); + + if (!command) { + logger.error(`Can't find and load command '${commandName}'`); + logger.error("Run 'webpack --help' to see available commands and options"); + process.exit(2); + } + + const option = command.options.find((option) => option.short === optionName || option.long === optionName); - const globalCommands = programHelpInformation.match(/Commands:\n(?.+)/s); + if (!option) { + logger.error(`Unknown option '${optionName}'`); + logger.error("Run 'webpack --help' to see available commands and options"); + process.exit(2); + } + + const nameOutput = + option.flags.replace(/^.+[[<]/, '').replace(/(\.\.\.)?[\]>].*$/, '') + (option.variadic === true ? '...' : ''); + const value = option.required ? '<' + nameOutput + '>' : option.optional ? '[' + nameOutput + ']' : ''; - if (isGlobal && globalCommands.groups.globalCommands) { - logger.raw('\nCommands:'); logger.raw( - globalCommands.groups.globalCommands - .trimRight() - // `commander` doesn't support multiple alias in help - .replace('build|bundle [options] ', 'build|bundle|b [options]'), + `Usage: webpack${isBuildCommand(commandName) ? '' : ` ${commandName}`} ${option.long}${value ? ` ${value}` : ''}`, ); + + if (option.short) { + logger.raw( + `Short: webpack${isBuildCommand(commandName) ? '' : ` ${commandName}`} ${option.short}${value ? ` ${value}` : ''}`, + ); + } + + if (option.description) { + logger.raw(`Description: ${option.description}`); + } + + if (!option.negate && options.defaultValue) { + logger.raw(`Default value: ${JSON.stringify(option.defaultValue)}`); + } + + // TODO implement this after refactor cli arguments + // logger.raw('Possible values: foo | bar'); + // logger.raw('Documentation: https://webpack.js.org/option/name/'); + } else { + outputIncorrectUsageOfHelp(); } logger.raw("\nTo see list of all supported commands and options run 'webpack --help=verbose'.\n"); @@ -678,7 +746,9 @@ class WebpackCLI { const opts = program.opts(); - if (opts.help || isHelpCommand(commandName)) { + const isHelpCommandSyntax = isHelpCommand(commandName); + + if (opts.help || isHelpCommandSyntax) { let isVerbose = false; if (opts.help) { @@ -694,9 +764,13 @@ class WebpackCLI { this.program.forHelp = true; - const optionsForHelp = [].concat(opts.help && !isDefault ? [commandName] : []).concat(options); + const optionsForHelp = [] + .concat(opts.help && !isDefault ? [commandName] : []) + .concat(options) + .concat(isHelpCommandSyntax && typeof opts.color !== 'undefined' ? [opts.color ? '--color' : '--no-color'] : []) + .concat(isHelpCommandSyntax && typeof opts.version !== 'undefined' ? ['--version'] : []); - await outputHelp(optionsForHelp, isVerbose, program); + await outputHelp(optionsForHelp, isVerbose, isHelpCommandSyntax, program); } if (opts.version || isVersionCommand(commandName)) { @@ -710,11 +784,13 @@ class WebpackCLI { } else { logger.error(`Unknown command '${commandName}'`); - const found = knownCommands.find((commandOptions) => distance(commandName, commandOptions.name) < 3); + const found = knownCommands.find((commandOptions) => distance(commandName, getCommandName(commandOptions.name)) < 3); if (found) { logger.error( - `Did you mean '${found.name}' (alias '${Array.isArray(found.alias) ? found.alias.join(', ') : found.alias}')?`, + `Did you mean '${getCommandName(found.name)}' (alias '${ + Array.isArray(found.alias) ? found.alias.join(', ') : found.alias + }')?`, ); } diff --git a/test/build/basic/basic.test.js b/test/build/basic/basic.test.js index cd5a3137ed1..a758c3b523c 100644 --- a/test/build/basic/basic.test.js +++ b/test/build/basic/basic.test.js @@ -27,6 +27,16 @@ describe('bundle command', () => { expect(stdout).toBeTruthy(); }); + it('should log error and suggest right name on the "buil" command', async () => { + const { exitCode, stderr, stdout } = run(__dirname, ['buil'], false); + + expect(exitCode).toBe(2); + expect(stderr).toContain("Unknown command 'buil'"); + expect(stderr).toContain("Did you mean 'build' (alias 'bundle, b')?"); + expect(stderr).toContain("Run 'webpack --help' to see available commands and options"); + expect(stdout).toBeFalsy(); + }); + it('should log error with multi commands', async () => { const { exitCode, stderr, stdout } = run(__dirname, ['bundle', 'info'], false); diff --git a/test/help/help.test.js b/test/help/help.test.js index 2e14e2bab9c..ad3a1089c4c 100644 --- a/test/help/help.test.js +++ b/test/help/help.test.js @@ -208,7 +208,7 @@ describe('help', () => { expect(stdout).toContain('Made with ♥ by the webpack team'); }); - it('should show help information and taking precedence when "--help" and "--verison" option using together', () => { + it('should show help information and taking precedence when "--help" and "--version" option using together', () => { const { exitCode, stderr, stdout } = run(__dirname, ['--help', '--version'], false); expect(exitCode).toBe(0); @@ -226,6 +226,150 @@ describe('help', () => { // expect(coloretteEnabled ? stripAnsi(stdout) : stdout).toContain('Made with ♥ by the webpack team.'); }); + it('should show help information using the "help --mode" option', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', '--mode'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain('Usage: webpack --mode '); + expect(stdout).toContain('Description: Defines the mode to pass to webpack.'); + expect(stdout).toContain("To see list of all supported commands and options run 'webpack --help=verbose'."); + expect(stdout).toContain('CLI documentation: https://webpack.js.org/api/cli/.'); + }); + + it('should show help information using the "help --target" option', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', '--target'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + + if (isWebpack5) { + expect(stdout).toContain('Usage: webpack --target '); + expect(stdout).toContain('Short: webpack -t '); + } else { + expect(stdout).toContain('Usage: webpack --target '); + expect(stdout).toContain('Short: webpack -t '); + } + + expect(stdout).toContain('Description: Sets the build target e.g. node.'); + expect(stdout).toContain("To see list of all supported commands and options run 'webpack --help=verbose'."); + expect(stdout).toContain('CLI documentation: https://webpack.js.org/api/cli/.'); + }); + + it('should show help information using the "help --stats" option', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', '--stats'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain('Usage: webpack --stats [value]'); + expect(stdout).toContain('Description: It instructs webpack on how to treat the stats e.g. verbose.'); + expect(stdout).toContain("To see list of all supported commands and options run 'webpack --help=verbose'."); + expect(stdout).toContain('CLI documentation: https://webpack.js.org/api/cli/.'); + }); + + it('should show help information using the "help --no-stats" option', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', '--no-stats'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain('Usage: webpack --no-stats'); + expect(stdout).toContain('Description: Disable stats output.'); + expect(stdout).toContain("To see list of all supported commands and options run 'webpack --help=verbose'."); + expect(stdout).toContain('CLI documentation: https://webpack.js.org/api/cli/.'); + }); + + it('should show help information using the "help --mode" option', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', '--mode'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain('Usage: webpack --mode '); + expect(stdout).toContain('Description: Defines the mode to pass to webpack.'); + expect(stdout).toContain("To see list of all supported commands and options run 'webpack --help=verbose'."); + expect(stdout).toContain('CLI documentation: https://webpack.js.org/api/cli/.'); + }); + + it('should show help information using the "help serve --mode" option', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', 'serve', '--mode'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain('Usage: webpack serve --mode '); + expect(stdout).toContain('Description: Defines the mode to pass to webpack.'); + expect(stdout).toContain("To see list of all supported commands and options run 'webpack --help=verbose'."); + expect(stdout).toContain('CLI documentation: https://webpack.js.org/api/cli/.'); + }); + + it('should show help information using the "help --color" option', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', '--color'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain('Usage: webpack --color'); + expect(stdout).toContain('Description: Enable colors on console.'); + expect(stdout).toContain("To see list of all supported commands and options run 'webpack --help=verbose'."); + expect(stdout).toContain('CLI documentation: https://webpack.js.org/api/cli/.'); + }); + + it('should show help information using the "help --no-color" option', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', '--no-color'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain('Usage: webpack --no-color'); + expect(stdout).toContain('Description: Disable colors on console.'); + expect(stdout).toContain("To see list of all supported commands and options run 'webpack --help=verbose'."); + expect(stdout).toContain('CLI documentation: https://webpack.js.org/api/cli/.'); + }); + + it('should show help information using the "help serve --color" option', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', 'serve', '--color'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain('Usage: webpack serve --color'); + expect(stdout).toContain('Description: Enable colors on console.'); + expect(stdout).toContain("To see list of all supported commands and options run 'webpack --help=verbose'."); + expect(stdout).toContain('CLI documentation: https://webpack.js.org/api/cli/.'); + }); + + it('should show help information using the "help serve --no-color" option', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', 'serve', '--no-color'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain('Usage: webpack serve --no-color'); + expect(stdout).toContain('Description: Disable colors on console.'); + expect(stdout).toContain("To see list of all supported commands and options run 'webpack --help=verbose'."); + expect(stdout).toContain('CLI documentation: https://webpack.js.org/api/cli/.'); + }); + + it('should show help information using the "help --version" option', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', '--version'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain('Usage: webpack --version'); + expect(stdout).toContain('Short: webpack -v'); + expect(stdout).toContain( + "Description: Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.", + ); + expect(stdout).toContain('CLI documentation: https://webpack.js.org/api/cli/.'); + }); + + it('should show help information using the "help -v" option', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', '-v'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain('Usage: webpack --version'); + expect(stdout).toContain('Short: webpack -v'); + expect(stdout).toContain( + "Description: Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.", + ); + expect(stdout).toContain('CLI documentation: https://webpack.js.org/api/cli/.'); + }); + it('should log error for invalid command using the "--help" option', () => { const { exitCode, stderr, stdout } = run(__dirname, ['--help', 'myCommand'], false); @@ -234,7 +378,27 @@ describe('help', () => { expect(stdout).toBeFalsy(); }); - it('should log error for invalid command using command syntax', () => { + it('should log error for invalid command using the "--help" option #2', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['--flag', '--help'], false); + + expect(exitCode).toBe(2); + expect(stderr).toContain('Incorrect use of help'); + expect(stderr).toContain("Please use: 'webpack help [command] [option]' | 'webpack [command] --help'"); + expect(stderr).toContain("Run 'webpack --help' to see available commands and options"); + expect(stdout).toBeFalsy(); + }); + + it('should log error for invalid command using the "--help" option #3', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['serve', '--flag', '--help'], false); + + expect(exitCode).toBe(2); + expect(stderr).toContain('Incorrect use of help'); + expect(stderr).toContain("Please use: 'webpack help [command] [option]' | 'webpack [command] --help'"); + expect(stderr).toContain("Run 'webpack --help' to see available commands and options"); + expect(stdout).toBeFalsy(); + }); + + it('should log error for unknown command using command syntax', () => { const { exitCode, stderr, stdout } = run(__dirname, ['help', 'myCommand'], false); expect(exitCode).toBe(2); @@ -243,7 +407,7 @@ describe('help', () => { expect(stdout).toBeFalsy(); }); - it('should log error for invalid command using command syntax #2', () => { + it('should log error for unknown command using command syntax #2', () => { const { exitCode, stderr, stdout } = run(__dirname, ['help', 'verbose'], false); expect(exitCode).toBe(2); @@ -252,11 +416,59 @@ describe('help', () => { expect(stdout).toBeFalsy(); }); + it('should log error for unknown option using command syntax #2', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', '--made'], false); + + expect(exitCode).toBe(2); + expect(stderr).toContain("Unknown option '--made'"); + expect(stderr).toContain("Run 'webpack --help' to see available commands and options"); + expect(stdout).toBeFalsy(); + }); + + it('should log error for unknown option using command syntax #3', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', 'serve', '--made'], false); + + expect(exitCode).toBe(2); + expect(stderr).toContain("Unknown option '--made'"); + expect(stderr).toContain("Run 'webpack --help' to see available commands and options"); + expect(stdout).toBeFalsy(); + }); + + it('should log error for unknown option using command syntax #4', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', 'bui', '--mode'], false); + + expect(exitCode).toBe(2); + expect(stderr).toContain("Can't find and load command 'bui'"); + expect(stderr).toContain("Run 'webpack --help' to see available commands and options"); + expect(stdout).toBeFalsy(); + }); + + it('should log error for invalid command using command syntax #3', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', '--mode', 'serve'], false); + + expect(exitCode).toBe(2); + expect(stderr).toContain('Incorrect use of help'); + expect(stderr).toContain("Please use: 'webpack help [command] [option]' | 'webpack [command] --help'"); + expect(stderr).toContain("Run 'webpack --help' to see available commands and options"); + expect(stdout).toBeFalsy(); + }); + + it('should log error for invalid command using command syntax #4', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['help', 'serve', '--mode', '--mode'], false); + + expect(exitCode).toBe(2); + expect(stderr).toContain('Incorrect use of help'); + expect(stderr).toContain("Please use: 'webpack help [command] [option]' | 'webpack [command] --help'"); + expect(stderr).toContain("Run 'webpack --help' to see available commands and options"); + expect(stdout).toBeFalsy(); + }); + it('should log error for invalid flag with the "--help" option', () => { const { exitCode, stderr, stdout } = run(__dirname, ['--help', '--my-flag'], false); expect(exitCode).toBe(2); - expect(stderr).toContain("Unknown option '--my-flag'"); + expect(stderr).toContain('Incorrect use of help'); + expect(stderr).toContain("Please use: 'webpack help [command] [option]' | 'webpack [command] --help'"); expect(stderr).toContain("Run 'webpack --help' to see available commands and options"); expect(stdout).toBeFalsy(); });