Skip to content

Commit

Permalink
feat: allow to use help command to show option information (#2353)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Jan 15, 2021
1 parent 7590f66 commit 15eb411
Show file tree
Hide file tree
Showing 4 changed files with 355 additions and 57 deletions.
4 changes: 2 additions & 2 deletions OPTIONS.md
Expand Up @@ -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.
Expand Down
178 changes: 127 additions & 51 deletions packages/webpack-cli/lib/webpack-cli.js
Expand Up @@ -144,8 +144,7 @@ class WebpackCLI {
flags = `${flags} <value${isMultiple ? '...' : ''}>`;
}

// 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;

Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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;
Expand All @@ -313,7 +320,7 @@ class WebpackCLI {
let allowToSearchCommand = true;

args.forEach((arg) => {
if (!arg.startsWith('-') && allowToSearchCommand) {
if (!isOption(arg) && allowToSearchCommand) {
commandName = arg;

allowToSearchCommand = false;
Expand Down Expand Up @@ -491,9 +498,7 @@ class WebpackCLI {
);

possibleCommandNames.forEach((possibleCommandName) => {
const isOption = possibleCommandName.startsWith('-');

if (!isOption) {
if (!isOption(possibleCommandName)) {
return;
}

Expand Down Expand Up @@ -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) => {
Expand All @@ -564,11 +567,40 @@ class WebpackCLI {
return true;
});
};
const outputGlobalOptions = () => {
const programHelpInformation = program.helpInformation();
const globalOptions = programHelpInformation.match(/Options:\n(?<globalOptions>.+)\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(?<globalCommands>.+)/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));
}),
);

Expand All @@ -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);

Expand Down Expand Up @@ -629,26 +652,71 @@ class WebpackCLI {
}

logger.raw(helpInformation);
}

const programHelpInformation = program.helpInformation();
const globalOptions = programHelpInformation.match(/Options:\n(?<globalOptions>.+)\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(?<globalCommands>.+)/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");
Expand Down Expand Up @@ -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) {
Expand All @@ -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)) {
Expand All @@ -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
}')?`,
);
}

Expand Down
10 changes: 10 additions & 0 deletions test/build/basic/basic.test.js
Expand Up @@ -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);

Expand Down

0 comments on commit 15eb411

Please sign in to comment.