Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow to use help command to show option information #2353

Merged
merged 7 commits into from Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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' ||
alexander-akait marked this conversation as resolved.
Show resolved Hide resolved
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