diff --git a/.changeset/tab-completions.md b/.changeset/tab-completions.md new file mode 100644 index 00000000000..a02d23abd15 --- /dev/null +++ b/.changeset/tab-completions.md @@ -0,0 +1,5 @@ +--- +"webpack-cli": minor +--- + +feat: add shell completions for zsh, bash, fish and powershell through the `complete` command diff --git a/.cspell.json b/.cspell.json index c79593fbea6..565018f91da 100644 --- a/.cspell.json +++ b/.cspell.json @@ -106,7 +106,8 @@ "Zenitsu", "quickstart", "pinia", - "watchpack" + "watchpack", + "zsh" ], "dictionaries": ["npm", "software-terms"], "ignorePaths": [ diff --git a/README.md b/README.md index 0885c87a284..0c50d6efbcb 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ - [Packages](#packages) - [Commands](#commands) - [Getting started](#getting-started) +- [Shell completion](#shell-completion) - [Exit codes and their meanings](#exit-codes-and-their-meanings) - [Contributing and Internal Documentation](#contributing-and-internal-documentation) - [Funding](#funding) @@ -58,6 +59,7 @@ Supporting developers is an important task for webpack CLI. Thus, webpack CLI provides different commands for many common tasks. - [`build|bundle|b [entries...] [options]`](https://webpack.js.org/api/cli/#build) - Run webpack (default command, can be omitted). +- `complete [shell]` - Print a shell completion script, see [Shell completion](#shell-completion). - [`configtest|t [config-path]`](https://webpack.js.org/api/cli/#configtest) - Validate a webpack configuration. - [`help|h [command] [option]`](https://webpack.js.org/api/cli/#help) - Display help for commands and options. - [`info|i [options]`](https://webpack.js.org/api/cli/#info) - Returns information related to the local environment. @@ -79,6 +81,41 @@ npx create-webpack-app init You will then be prompted for some questions about which features you want to use, such as `scss`, `typescript`, `PWA` support or other features. +## Shell completion + +Commands, their options and the values those options accept can be completed in `zsh`, `bash`, `fish` and `powershell`. The completions are read from webpack's own schema, so they cover whatever your webpack and `webpack-dev-server` versions support. + +Completion is served by [`@bomb.sh/tab`](https://www.npmjs.com/package/@bomb.sh/tab), which is optional and not installed with `webpack-cli`: + +```sh +npm install --save-dev @bomb.sh/tab +``` + +Then load the script for your shell: + +```sh +# zsh +echo 'source <(webpack complete zsh)' >> ~/.zshrc + +# bash +echo 'source <(webpack complete bash)' >> ~/.bashrc + +# fish +webpack complete fish > ~/.config/fish/completions/webpack.fish + +# powershell +webpack complete powershell > ~/.webpack-completion.ps1 +echo '. ~/.webpack-completion.ps1' >> $PROFILE +``` + +Once the shell is restarted, `Tab` completes commands, their aliases, options and option values: + +```sh +webpack b # webpack build +webpack build --mode= # development, production, none +webpack serve --his # webpack serve --history-api-fallback +``` + ## Exit codes and their meanings | Exit Code | Description | diff --git a/package-lock.json b/package-lock.json index 3d9eeff34e8..8ddebfa27b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@babel/core": "^8.0.1", "@babel/preset-env": "^8.0.2", "@babel/register": "^8.0.1", + "@bomb.sh/tab": "^0.0.22", "@changesets/cli": "^3.0.1", "@changesets/get-github-info": "^1.0.0", "@swc/core": "^1.16.1", @@ -1557,6 +1558,32 @@ "node": ">=18" } }, + "node_modules/@bomb.sh/tab": { + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/@bomb.sh/tab/-/tab-0.0.22.tgz", + "integrity": "sha512-l5IuWpV7szqG4MM6A7I+qPMmP2qPMq2TD4veIXaOH0sD53dV0Tv8WZPARp5O5Kp4+y4fnVCl3VL1HZKNghO1yg==", + "devOptional": true, + "license": "MIT", + "bin": { + "tab": "dist/bin/cli.mjs" + }, + "peerDependencies": { + "cac": "^6.7.14 || ^7.0.0", + "citty": "^0.1.6 || ^0.2.0", + "commander": "^13.1.0 || ^14.0.0 || ^15.0.0" + }, + "peerDependenciesMeta": { + "cac": { + "optional": true + }, + "citty": { + "optional": true + }, + "commander": { + "optional": true + } + } + }, "node_modules/@changesets/apply-release-plan": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-8.0.0.tgz", @@ -21268,7 +21295,7 @@ } }, "packages/create-webpack-app": { - "version": "2.1.0", + "version": "2.2.0", "license": "MIT", "dependencies": { "@inquirer/expand": "^5.1.3", @@ -21304,7 +21331,7 @@ } }, "packages/webpack-cli": { - "version": "7.2.2", + "version": "7.2.3", "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "^1.1.0", @@ -21330,6 +21357,7 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@bomb.sh/tab": "^0.0.22", "js-yaml": "^4.0.0 || ^5.0.0", "json5": "^2.2.3", "toml": "^3.0.0 || ^4.0.0 || ^5.0.0", @@ -21338,6 +21366,9 @@ "webpack-dev-server": "^5.0.0 || ^6.0.0" }, "peerDependenciesMeta": { + "@bomb.sh/tab": { + "optional": true + }, "js-yaml": { "optional": true }, diff --git a/package.json b/package.json index b51ce36f861..3d515e67e01 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "prepare": "husky" }, "devDependencies": { + "@bomb.sh/tab": "^0.0.22", "@babel/core": "^8.0.1", "@babel/preset-env": "^8.0.2", "@babel/register": "^8.0.1", diff --git a/packages/webpack-cli/README.md b/packages/webpack-cli/README.md index b32696c8224..064f9def5c4 100644 --- a/packages/webpack-cli/README.md +++ b/packages/webpack-cli/README.md @@ -64,6 +64,7 @@ npx webpack-cli --help verbose ``` build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system. diff --git a/packages/webpack-cli/package.json b/packages/webpack-cli/package.json index ad6dba55219..457588ec62a 100644 --- a/packages/webpack-cli/package.json +++ b/packages/webpack-cli/package.json @@ -44,6 +44,7 @@ "@types/envinfo": "^7.8.4" }, "peerDependencies": { + "@bomb.sh/tab": "^0.0.22", "js-yaml": "^4.0.0 || ^5.0.0", "json5": "^2.2.3", "toml": "^3.0.0 || ^4.0.0 || ^5.0.0", @@ -52,6 +53,9 @@ "webpack-dev-server": "^5.0.0 || ^6.0.0" }, "peerDependenciesMeta": { + "@bomb.sh/tab": { + "optional": true + }, "js-yaml": { "optional": true }, diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index f43b06b0879..8a324a51054 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { type Readable as ReadableType } from "node:stream"; import { fileURLToPath, pathToFileURL } from "node:url"; import util from "node:util"; +import { type RootCommand } from "@bomb.sh/tab"; import { type stringifyChunked as stringifyChunkedType } from "@discoveryjs/json-ext"; import { type Command as CommanderCommand, @@ -40,6 +41,7 @@ const WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM = Boolean(process.env.WEBPACK_DEV_SER const WEBPACK_DEV_SERVER_PACKAGE = WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM ? (process.env.WEBPACK_DEV_SERVER_PACKAGE as string) : "webpack-dev-server"; +const COMPLETION_PACKAGE = "@bomb.sh/tab"; // `webpack-dev-server` v6 is ESM and exposes the server as its `default` export, // while v5 exports it directly @@ -157,6 +159,7 @@ interface KnownWebpackCLICommands { help: CommandOptions; info: CommandOptions; configtest: CommandOptions; + complete: CommandOptions; } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -585,6 +588,12 @@ class WebpackCLI { return str.length > 0 ? str.charAt(0).toUpperCase() + str.slice(1) : str; } + #commandTerm(command: CommanderCommand): string { + const aliases = command.aliases().filter(Boolean); + + return aliases.length > 0 ? `${command.name()}|${aliases.join("|")}` : command.name(); + } + toKebabCase(str: string): string { return str.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); } @@ -1432,14 +1441,12 @@ class WebpackCLI { return `${parentCmdNames}${command.usage()} | ${parentCmdNames}[command] [options]`; } - return `${parentCmdNames}${command.name()}|${command - .aliases() - .join("|")} ${command.usage()}`; + return `${parentCmdNames}${this.#commandTerm(command)} ${command.usage()}`; }, // Support multiple aliases subcommandTerm: (command) => { const usage = command.usage(); - return `${command.name()}|${command.aliases().join("|")}${usage.length > 0 ? ` ${usage}` : ""}`; + return `${this.#commandTerm(command)}${usage.length > 0 ? ` ${usage}` : ""}`; }, visibleOptions: function visibleOptions(command) { return command.options.filter((option) => { @@ -2445,6 +2452,16 @@ class WebpackCLI { ); }, }, + complete: { + rawName: "complete", + name: "complete [shell]", + alias: [], + description: "Generate shell completion scripts.", + action: () => { + // Display-only stub for `--help`. Completion requests are handled in `run()` + // before parse so the completion adapter can inspect the loaded command tree. + }, + }, }; #isCommand( @@ -2489,6 +2506,8 @@ class WebpackCLI { return await this.makeCommand(this.#commands.info); } else if (this.#isCommand(commandName, this.#commands.configtest)) { return await this.makeCommand(this.#commands.configtest); + } else if (this.#isCommand(commandName, this.#commands.complete)) { + return await this.makeCommand(this.#commands.complete); } const pkg: string = commandName; @@ -2527,7 +2546,174 @@ class WebpackCLI { return externalCommand; } - async run(args: readonly string[], parseOptions: ParseOptions) { + #completionRequest( + args: readonly string[], + parseOptions?: ParseOptions, + ): { words: string[] } | undefined { + const userArgs = parseOptions?.from === "user" ? args : args.slice(2); + + if (userArgs.find((arg) => arg !== "--" && !arg.startsWith("-")) !== "complete") { + return; + } + + // `complete -- ` asks what to suggest for ``; `complete ` + // only prints a script and has no words to resolve. + const separator = userArgs.indexOf("--"); + + return { words: separator === -1 ? [] : userArgs.slice(separator + 1) }; + } + + async #prepareCompletions(words: string[]): Promise { + // Register every option (same as `--help`) so tab can inspect the full tree. + this.program.forHelp = true; + + await Promise.all( + Object.values(this.#commands) + .filter((command) => command.rawName !== "complete") + .map((command) => this.#loadCommandByName(command.rawName)), + ); + + let tab: typeof import("@bomb.sh/tab/commander").default; + + try { + ({ default: tab } = await import("@bomb.sh/tab/commander")); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ERR_MODULE_NOT_FOUND") { + throw error; + } + + // The shell runs `complete -- ` on every keypress, so a missing + // optional package stays silent there and only reports when asked for a script. + if (words.length === 0) { + this.logger.error( + `For using '${this.colors.green("complete")}' command you need to install: '${this.colors.green(COMPLETION_PACKAGE)}' package.`, + ); + process.exit(2); + } + + process.exit(0); + } + + this.#attachCompletionHandlers(tab(this.program), words); + } + + #attachCompletionHandlers(completion: RootCommand, words: string[]): void { + const helpOption = completion.options.get("help"); + + if (helpOption) { + helpOption.handler = (complete) => { + complete("verbose", "Show all available commands and options"); + }; + } + + const outputHandler = (complete: (value: string, description: string) => void) => { + complete("json", "JSON output"); + complete("markdown", "Markdown output"); + }; + + for (const name of ["info", "version"]) { + const outputOption = completion.commands.get(name)?.options.get("output"); + + if (outputOption) { + outputOption.handler = outputHandler; + } + } + + const progressHandler = (complete: (value: string, description: string) => void) => { + complete("profile", "Capture profiling data"); + }; + + for (const name of ["build", "watch", "serve"]) { + const progressOption = completion.commands.get(name)?.options.get("progress"); + + if (progressOption) { + progressOption.handler = progressHandler; + } + } + + const completeCommand = completion.commands.get("complete"); + const shellArg = completeCommand?.arguments.get("shell"); + + if (shellArg) { + shellArg.handler = (complete) => { + complete("zsh", "Zsh"); + complete("bash", "Bash"); + complete("fish", "Fish"); + complete("powershell", "PowerShell"); + }; + } + + // The command already typed, e.g. `b` in `complete -- b --mode=`. The last word + // is still being written — `complete -- b` is completing `b` itself, which must + // stay a prefix of `build` so the shell expands it instead of matching an alias. + const requested = words.slice(0, -1).find((word) => word !== "--" && !word.startsWith("-")); + + for (const command of this.program.commands) { + const tabCommand = completion.commands.get(command.name()); + + if (!tabCommand) { + continue; + } + + // Commands are registered under their name, so an alias resolves to nothing. + // Only the requested one is registered, to keep aliases out of the suggestions. + if (requested && requested !== command.name() && command.aliases().includes(requested)) { + completion.commands.set(requested, tabCommand); + } + + // Global options stay on the root command, yet they are accepted after a + // command too, so offer them there as well (e.g. `webpack build --col`). + for (const [name, option] of completion.options) { + if (!tabCommand.options.has(name)) { + tabCommand.options.set(name, option); + } + } + + for (const option of command.options) { + const longName = option.long?.slice(2); + + if (!longName) { + continue; + } + + const { configs } = option as Option & { configs?: ArgumentConfig[] }; + + if (!configs) { + continue; + } + + const values: string[] = []; + + for (const config of configs) { + if (config.type !== "enum" || !config.values) { + continue; + } + + for (const value of config.values) { + if (typeof value === "string") { + values.push(value); + } + } + } + + if (values.length === 0) { + continue; + } + + const tabOption = tabCommand.options.get(longName); + + if (tabOption) { + tabOption.handler = (complete) => { + for (const value of values) { + complete(value, ""); + } + }; + } + } + } + } + + async run(args: readonly string[], parseOptions?: ParseOptions) { // Default `--color` and `--no-color` options const self: WebpackCLI = this; @@ -2735,6 +2921,12 @@ class WebpackCLI { await command.parseAsync([...commandOperands, ...unknown], { from: "user" }); }); + const completionRequest = this.#completionRequest(args, parseOptions); + + if (completionRequest) { + await this.#prepareCompletions(completionRequest.words); + } + await this.program.parseAsync(args, parseOptions); } diff --git a/test/complete/complete.test.js b/test/complete/complete.test.js new file mode 100644 index 00000000000..457982ee7ca --- /dev/null +++ b/test/complete/complete.test.js @@ -0,0 +1,223 @@ +"use strict"; + +const path = require("node:path"); +const { pathToFileURL } = require("node:url"); +const { run } = require("../utils/test-utils"); + +// Hides `@bomb.sh/tab` from the CLI, the way a project that never installed the +// optional package sees it. +const withoutParser = { + nodeOptions: [ + `--import=${pathToFileURL(path.resolve(__dirname, "no-completion-parser.mjs")).href}`, + ], +}; + +const parseCompletions = (stdout) => + stdout + .split("\n") + .map((line) => line.trimEnd()) + .filter((line) => line && !line.startsWith(":")) + .map((line) => line.split("\t")[0]); + +describe("complete", () => { + it("should generate a zsh completion script", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "zsh"]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("webpack"); + expect(stdout).toContain("complete"); + }); + + it("should generate a bash completion script", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "bash"]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("webpack"); + }); + + it("should suggest commands", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "--", ""]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + + const suggestions = parseCompletions(stdout); + + expect(suggestions).toEqual(expect.arrayContaining(["build", "watch", "serve", "info"])); + }); + + it("should suggest option values for --mode", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "complete", + "--", + "build", + "--mode=", + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + + const suggestions = parseCompletions(stdout); + + expect(suggestions).toEqual(expect.arrayContaining(["development", "production", "none"])); + }); + + it("should suggest option values for info --output", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "complete", + "--", + "info", + "--output=", + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + + const suggestions = parseCompletions(stdout); + + expect(suggestions).toEqual(expect.arrayContaining(["json", "markdown"])); + }); + + it("should expand a partial command to a single suggestion", async () => { + // `webpack b` has to offer `build` alone, so the shell writes it out; + // suggesting the alias `b` next to it would leave the word as typed. + for (const [typed, command] of [ + ["b", "build"], + ["w", "watch"], + ["s", "serve"], + ["i", "info"], + ]) { + const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "--", typed]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(parseCompletions(stdout)).toEqual([command]); + } + }); + + it("should suggest the same options for a command alias", async () => { + const { stdout: canonical } = await run(__dirname, ["complete", "--", "build", "--"]); + + for (const alias of ["b", "bundle"]) { + const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "--", alias, "--"]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(parseCompletions(stdout)).toEqual(parseCompletions(canonical)); + } + }); + + it("should suggest option values for a command alias", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "--", "b", "--mode="]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(parseCompletions(stdout)).toEqual( + expect.arrayContaining(["development", "production", "none"]), + ); + }); + + it("should stay silent for a completion request without the parser", async () => { + const { exitCode, stderr, stdout } = await run( + __dirname, + ["complete", "--", "build", "--"], + withoutParser, + ); + + // A shell asks for this on every keypress, so it must print nothing at all. + expect(exitCode).toBe(0); + expect(stdout).toBeFalsy(); + expect(stderr).toBeFalsy(); + }); + + it("should ask to install the parser when generating a script", async () => { + const { exitCode, stderr } = await run(__dirname, ["complete", "zsh"], withoutParser); + + expect(exitCode).toBe(2); + expect(stderr).toContain("@bomb.sh/tab"); + }); + + it("should suggest option values for build --progress", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "complete", + "--", + "build", + "--progress=", + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(parseCompletions(stdout)).toEqual(expect.arrayContaining(["profile"])); + }); + + it("should suggest option values for version --output", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "complete", + "--", + "version", + "--output=", + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(parseCompletions(stdout)).toEqual(expect.arrayContaining(["json", "markdown"])); + }); + + it("should suggest option values for --help", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "--", "--help="]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(parseCompletions(stdout)).toEqual(expect.arrayContaining(["verbose"])); + }); + + it("should suggest shells for the complete command", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "--", "complete", ""]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(parseCompletions(stdout)).toEqual( + expect.arrayContaining(["zsh", "bash", "fish", "powershell"]), + ); + }); + + it("should suggest global options after a command", async () => { + for (const command of ["build", "serve", "help"]) { + const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "--", command, "--"]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(parseCompletions(stdout)).toEqual( + expect.arrayContaining(["--color", "--no-color", "--help"]), + ); + } + }); + + it("should not suggest aliases as commands", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "--", ""]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + + const suggestions = parseCompletions(stdout); + + expect(suggestions).toEqual(expect.arrayContaining(["build", "watch", "serve"])); + + for (const alias of ["b", "bundle", "w", "s", "server"]) { + expect(suggestions).not.toContain(alias); + } + }); + + it("should suggest flags for the build command", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "--", "build", "--"]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + + const suggestions = parseCompletions(stdout); + + expect(suggestions).toEqual(expect.arrayContaining(["--mode", "--config", "--entry"])); + }); +}); diff --git a/test/complete/no-completion-parser.mjs b/test/complete/no-completion-parser.mjs new file mode 100644 index 00000000000..1bf55e94ff9 --- /dev/null +++ b/test/complete/no-completion-parser.mjs @@ -0,0 +1,18 @@ +// Makes `@bomb.sh/tab` unresolvable, so the CLI takes its missing-package path. +import { register } from "node:module"; + +register( + `data:text/javascript,${encodeURIComponent(` + export async function resolve(specifier, context, nextResolve) { + if (specifier.startsWith("@bomb.sh/tab")) { + const error = new Error("Cannot find package '" + specifier + "'"); + + error.code = "ERR_MODULE_NOT_FOUND"; + + throw error; + } + + return nextResolve(specifier, context); + } + `)}`, +); diff --git a/test/help/__snapshots__/help.test.js.snap.devServer5.webpack5 b/test/help/__snapshots__/help.test.js.snap.devServer5.webpack5 index 46552514d82..e64cdf6d4b0 100644 --- a/test/help/__snapshots__/help.test.js.snap.devServer5.webpack5 +++ b/test/help/__snapshots__/help.test.js.snap.devServer5.webpack5 @@ -134,6 +134,7 @@ Global options Commands ──────────────────────────────────────────────────────────────────────── build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system. @@ -193,6 +194,7 @@ Global options Commands ──────────────────────────────────────────────────────────────────────── build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system. @@ -252,6 +254,7 @@ Global options Commands ──────────────────────────────────────────────────────────────────────── build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system. @@ -2643,6 +2646,7 @@ Global options Commands ──────────────────────────────────────────────────────────────────────── build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system. @@ -2732,6 +2736,7 @@ Global options Commands ──────────────────────────────────────────────────────────────────────── build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system. @@ -3089,6 +3094,7 @@ Global options Commands ──────────────────────────────────────────────────────────────────────── build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system. @@ -3146,6 +3152,7 @@ Global options Commands ──────────────────────────────────────────────────────────────────────── build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system. diff --git a/test/help/__snapshots__/help.test.js.snap.devServer6.webpack5 b/test/help/__snapshots__/help.test.js.snap.devServer6.webpack5 index 5d230226777..72507ea3f5c 100644 --- a/test/help/__snapshots__/help.test.js.snap.devServer6.webpack5 +++ b/test/help/__snapshots__/help.test.js.snap.devServer6.webpack5 @@ -134,6 +134,7 @@ Global options Commands ──────────────────────────────────────────────────────────────────────── build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system. @@ -193,6 +194,7 @@ Global options Commands ──────────────────────────────────────────────────────────────────────── build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system. @@ -252,6 +254,7 @@ Global options Commands ──────────────────────────────────────────────────────────────────────── build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system. @@ -2643,6 +2646,7 @@ Global options Commands ──────────────────────────────────────────────────────────────────────── build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system. @@ -2732,6 +2736,7 @@ Global options Commands ──────────────────────────────────────────────────────────────────────── build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system. @@ -3089,6 +3094,7 @@ Global options Commands ──────────────────────────────────────────────────────────────────────── build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system. @@ -3146,6 +3152,7 @@ Global options Commands ──────────────────────────────────────────────────────────────────────── build|bundle|b [entries...] [options] Run webpack (default command, can be omitted). + complete [shell] Generate shell completion scripts. configtest|t [config-path] Validate a webpack configuration. help|h [command] [option] Display help for commands and options. info|i [options] Outputs information about your system.