Skip to content

Commit 15eb411

Browse files
feat: allow to use help command to show option information (#2353)
1 parent 7590f66 commit 15eb411

File tree

4 files changed

+355
-57
lines changed

4 files changed

+355
-57
lines changed

OPTIONS.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -700,8 +700,8 @@ Global options:
700700
701701
Commands:
702702
build|bundle|b [options] Run webpack (default command, can be omitted).
703-
version|v Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.
704-
help|h Display help for commands and options.
703+
version|v [commands...] Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.
704+
help|h [command] [option] Display help for commands and options.
705705
serve|s [options] Run the webpack dev server.
706706
info|i [options] Outputs information about your system.
707707
init|c [options] [scaffold...] Initialize a new webpack configuration.

packages/webpack-cli/lib/webpack-cli.js

+127-51
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,7 @@ class WebpackCLI {
144144
flags = `${flags} <value${isMultiple ? '...' : ''}>`;
145145
}
146146

147-
// TODO need to fix on webpack-dev-server side
148-
// `describe` used by `webpack-dev-server`
147+
// TODO `describe` used by `webpack-dev-server@3`
149148
const description = option.description || option.describe || '';
150149
const defaultValue = option.defaultValue;
151150

@@ -236,16 +235,14 @@ class WebpackCLI {
236235
usage: '[options]',
237236
};
238237
const versionCommandOptions = {
239-
name: 'version',
238+
name: 'version [commands...]',
240239
alias: 'v',
241240
description: "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.",
242-
usage: '[commands...]',
243241
};
244242
const helpCommandOptions = {
245-
name: 'help',
243+
name: 'help [command] [option]',
246244
alias: 'h',
247245
description: 'Display help for commands and options.',
248-
usage: '[command]',
249246
};
250247
// Built-in external commands
251248
const externalBuiltInCommandsInfo = [
@@ -287,24 +284,34 @@ class WebpackCLI {
287284
];
288285

289286
const knownCommands = [buildCommandOptions, versionCommandOptions, helpCommandOptions, ...externalBuiltInCommandsInfo];
287+
const getCommandName = (name) => name.split(' ')[0];
290288
const isKnownCommand = (name) =>
291289
knownCommands.find(
292290
(command) =>
293-
command.name === name || (Array.isArray(command.alias) ? command.alias.includes(name) : command.alias === name),
291+
getCommandName(command.name) === name ||
292+
(Array.isArray(command.alias) ? command.alias.includes(name) : command.alias === name),
294293
);
295294
const isBuildCommand = (name) =>
296-
buildCommandOptions.name === name ||
295+
getCommandName(buildCommandOptions.name) === name ||
297296
(Array.isArray(buildCommandOptions.alias) ? buildCommandOptions.alias.includes(name) : buildCommandOptions.alias === name);
298297
const isHelpCommand = (name) =>
299-
helpCommandOptions.name === name ||
298+
getCommandName(helpCommandOptions.name) === name ||
300299
(Array.isArray(helpCommandOptions.alias) ? helpCommandOptions.alias.includes(name) : helpCommandOptions.alias === name);
301300
const isVersionCommand = (name) =>
302-
versionCommandOptions.name === name ||
301+
getCommandName(versionCommandOptions.name) === name ||
303302
(Array.isArray(versionCommandOptions.alias)
304303
? versionCommandOptions.alias.includes(name)
305304
: versionCommandOptions.alias === name);
306305
const findCommandByName = (name) =>
307306
this.program.commands.find((command) => name === command.name() || command.alias().includes(name));
307+
const isOption = (value) => value.startsWith('-');
308+
const isGlobalOption = (value) =>
309+
value === '--color' ||
310+
value === '--no-color' ||
311+
value === '-v' ||
312+
value === '--version' ||
313+
value === '-h' ||
314+
value === '--help';
308315

309316
const getCommandNameAndOptions = (args) => {
310317
let commandName;
@@ -313,7 +320,7 @@ class WebpackCLI {
313320
let allowToSearchCommand = true;
314321

315322
args.forEach((arg) => {
316-
if (!arg.startsWith('-') && allowToSearchCommand) {
323+
if (!isOption(arg) && allowToSearchCommand) {
317324
commandName = arg;
318325

319326
allowToSearchCommand = false;
@@ -491,9 +498,7 @@ class WebpackCLI {
491498
);
492499

493500
possibleCommandNames.forEach((possibleCommandName) => {
494-
const isOption = possibleCommandName.startsWith('-');
495-
496-
if (!isOption) {
501+
if (!isOption(possibleCommandName)) {
497502
return;
498503
}
499504

@@ -544,9 +549,7 @@ class WebpackCLI {
544549
"Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.",
545550
);
546551

547-
// Default global `help` command
548-
const outputHelp = async (options, isVerbose, program) => {
549-
const isGlobal = options.length === 0;
552+
const outputHelp = async (options, isVerbose, isHelpCommandSyntax, program) => {
550553
const hideVerboseOptions = (command) => {
551554
command.options = command.options.filter((option) => {
552555
const foundOption = flags.find((flag) => {
@@ -564,11 +567,40 @@ class WebpackCLI {
564567
return true;
565568
});
566569
};
570+
const outputGlobalOptions = () => {
571+
const programHelpInformation = program.helpInformation();
572+
const globalOptions = programHelpInformation.match(/Options:\n(?<globalOptions>.+)\nCommands:\n/s);
573+
574+
if (globalOptions && globalOptions.groups.globalOptions) {
575+
logger.raw('\nGlobal options:');
576+
logger.raw(globalOptions.groups.globalOptions.trimRight());
577+
}
578+
};
579+
const outputGlobalCommands = () => {
580+
const programHelpInformation = program.helpInformation();
581+
const globalCommands = programHelpInformation.match(/Commands:\n(?<globalCommands>.+)/s);
582+
583+
if (globalCommands.groups.globalCommands) {
584+
logger.raw('\nCommands:');
585+
logger.raw(
586+
globalCommands.groups.globalCommands
587+
.trimRight()
588+
// `commander` doesn't support multiple alias in help
589+
.replace('build|bundle [options] ', 'build|bundle|b [options]'),
590+
);
591+
}
592+
};
593+
const outputIncorrectUsageOfHelp = () => {
594+
logger.error('Incorrect use of help');
595+
logger.error("Please use: 'webpack help [command] [option]' | 'webpack [command] --help'");
596+
logger.error("Run 'webpack --help' to see available commands and options");
597+
process.exit(2);
598+
};
567599

568-
if (isGlobal) {
600+
if (options.length === 0) {
569601
await Promise.all(
570602
knownCommands.map((knownCommand) => {
571-
return loadCommandByName(knownCommand.name);
603+
return loadCommandByName(getCommandName(knownCommand.name));
572604
}),
573605
);
574606

@@ -588,20 +620,11 @@ class WebpackCLI {
588620
);
589621

590622
logger.raw(helpInformation);
591-
} else {
592-
const [name, ...optionsWithoutCommandName] = options;
593623

594-
if (name.startsWith('-')) {
595-
logger.error(`Unknown option '${name}'`);
596-
logger.error("Run 'webpack --help' to see available commands and options");
597-
process.exit(2);
598-
}
599-
600-
optionsWithoutCommandName.forEach((option) => {
601-
logger.error(`Unknown option '${option}'`);
602-
logger.error("Run 'webpack --help' to see available commands and options");
603-
process.exit(2);
604-
});
624+
outputGlobalOptions();
625+
outputGlobalCommands();
626+
} else if (options.length === 1 && !isOption(options[0])) {
627+
const name = options[0];
605628

606629
await loadCommandByName(name);
607630

@@ -629,26 +652,71 @@ class WebpackCLI {
629652
}
630653

631654
logger.raw(helpInformation);
632-
}
633655

634-
const programHelpInformation = program.helpInformation();
635-
const globalOptions = programHelpInformation.match(/Options:\n(?<globalOptions>.+)\nCommands:\n/s);
656+
outputGlobalOptions();
657+
} else if (isHelpCommandSyntax) {
658+
let commandName;
659+
let optionName;
636660

637-
if (globalOptions && globalOptions.groups.globalOptions) {
638-
logger.raw('\nGlobal options:');
639-
logger.raw(globalOptions.groups.globalOptions.trimRight());
640-
}
661+
if (options.length === 1) {
662+
commandName = buildCommandOptions.name;
663+
optionName = options[0];
664+
} else if (options.length === 2) {
665+
commandName = options[0];
666+
optionName = options[1];
667+
668+
if (isOption(commandName)) {
669+
outputIncorrectUsageOfHelp();
670+
}
671+
} else {
672+
outputIncorrectUsageOfHelp();
673+
}
674+
675+
await loadCommandByName(commandName);
676+
677+
const command = isGlobalOption(optionName) ? this.program : findCommandByName(commandName);
678+
679+
if (!command) {
680+
logger.error(`Can't find and load command '${commandName}'`);
681+
logger.error("Run 'webpack --help' to see available commands and options");
682+
process.exit(2);
683+
}
684+
685+
const option = command.options.find((option) => option.short === optionName || option.long === optionName);
641686

642-
const globalCommands = programHelpInformation.match(/Commands:\n(?<globalCommands>.+)/s);
687+
if (!option) {
688+
logger.error(`Unknown option '${optionName}'`);
689+
logger.error("Run 'webpack --help' to see available commands and options");
690+
process.exit(2);
691+
}
692+
693+
const nameOutput =
694+
option.flags.replace(/^.+[[<]/, '').replace(/(\.\.\.)?[\]>].*$/, '') + (option.variadic === true ? '...' : '');
695+
const value = option.required ? '<' + nameOutput + '>' : option.optional ? '[' + nameOutput + ']' : '';
643696

644-
if (isGlobal && globalCommands.groups.globalCommands) {
645-
logger.raw('\nCommands:');
646697
logger.raw(
647-
globalCommands.groups.globalCommands
648-
.trimRight()
649-
// `commander` doesn't support multiple alias in help
650-
.replace('build|bundle [options] ', 'build|bundle|b [options]'),
698+
`Usage: webpack${isBuildCommand(commandName) ? '' : ` ${commandName}`} ${option.long}${value ? ` ${value}` : ''}`,
651699
);
700+
701+
if (option.short) {
702+
logger.raw(
703+
`Short: webpack${isBuildCommand(commandName) ? '' : ` ${commandName}`} ${option.short}${value ? ` ${value}` : ''}`,
704+
);
705+
}
706+
707+
if (option.description) {
708+
logger.raw(`Description: ${option.description}`);
709+
}
710+
711+
if (!option.negate && options.defaultValue) {
712+
logger.raw(`Default value: ${JSON.stringify(option.defaultValue)}`);
713+
}
714+
715+
// TODO implement this after refactor cli arguments
716+
// logger.raw('Possible values: foo | bar');
717+
// logger.raw('Documentation: https://webpack.js.org/option/name/');
718+
} else {
719+
outputIncorrectUsageOfHelp();
652720
}
653721

654722
logger.raw("\nTo see list of all supported commands and options run 'webpack --help=verbose'.\n");
@@ -678,7 +746,9 @@ class WebpackCLI {
678746

679747
const opts = program.opts();
680748

681-
if (opts.help || isHelpCommand(commandName)) {
749+
const isHelpCommandSyntax = isHelpCommand(commandName);
750+
751+
if (opts.help || isHelpCommandSyntax) {
682752
let isVerbose = false;
683753

684754
if (opts.help) {
@@ -694,9 +764,13 @@ class WebpackCLI {
694764

695765
this.program.forHelp = true;
696766

697-
const optionsForHelp = [].concat(opts.help && !isDefault ? [commandName] : []).concat(options);
767+
const optionsForHelp = []
768+
.concat(opts.help && !isDefault ? [commandName] : [])
769+
.concat(options)
770+
.concat(isHelpCommandSyntax && typeof opts.color !== 'undefined' ? [opts.color ? '--color' : '--no-color'] : [])
771+
.concat(isHelpCommandSyntax && typeof opts.version !== 'undefined' ? ['--version'] : []);
698772

699-
await outputHelp(optionsForHelp, isVerbose, program);
773+
await outputHelp(optionsForHelp, isVerbose, isHelpCommandSyntax, program);
700774
}
701775

702776
if (opts.version || isVersionCommand(commandName)) {
@@ -710,11 +784,13 @@ class WebpackCLI {
710784
} else {
711785
logger.error(`Unknown command '${commandName}'`);
712786

713-
const found = knownCommands.find((commandOptions) => distance(commandName, commandOptions.name) < 3);
787+
const found = knownCommands.find((commandOptions) => distance(commandName, getCommandName(commandOptions.name)) < 3);
714788

715789
if (found) {
716790
logger.error(
717-
`Did you mean '${found.name}' (alias '${Array.isArray(found.alias) ? found.alias.join(', ') : found.alias}')?`,
791+
`Did you mean '${getCommandName(found.name)}' (alias '${
792+
Array.isArray(found.alias) ? found.alias.join(', ') : found.alias
793+
}')?`,
718794
);
719795
}
720796

test/build/basic/basic.test.js

+10
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ describe('bundle command', () => {
2727
expect(stdout).toBeTruthy();
2828
});
2929

30+
it('should log error and suggest right name on the "buil" command', async () => {
31+
const { exitCode, stderr, stdout } = run(__dirname, ['buil'], false);
32+
33+
expect(exitCode).toBe(2);
34+
expect(stderr).toContain("Unknown command 'buil'");
35+
expect(stderr).toContain("Did you mean 'build' (alias 'bundle, b')?");
36+
expect(stderr).toContain("Run 'webpack --help' to see available commands and options");
37+
expect(stdout).toBeFalsy();
38+
});
39+
3040
it('should log error with multi commands', async () => {
3141
const { exitCode, stderr, stdout } = run(__dirname, ['bundle', 'info'], false);
3242

0 commit comments

Comments
 (0)