Skip to content

Commit 095d282

Browse files
fix(complete): resolve command aliases and make the parser optional
Completing after an alias returned only the global options, because `@bomb.sh/tab` registers each command under `command.name()` and has no alias concept: `webpack b <TAB>` offered 4 candidates where `webpack build <TAB>` offers 21, and `webpack s <TAB>` 4 against `serve`'s 94. The alias being completed is now registered against the command it stands for, so every alias resolves. Only the requested one is registered, which keeps `webpack <TAB>` listing the eight commands rather than all thirteen names. `@bomb.sh/tab` also moves to an optional peer dependency, next to the other packages the CLI loads on demand. It is imported only to serve a completion request, so installing it is the user's choice: the shell runs `complete -- <words>` on every keypress and now gets silence when the package is absent, while `complete <shell>` says what to install. Also updates the dev-server 6 help snapshots, which were added after this work branched.
1 parent 313f96b commit 095d282

7 files changed

Lines changed: 112 additions & 12 deletions

File tree

.changeset/tab-completions.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack-cli": minor
3+
---
4+
5+
feat: add shell completions for zsh, bash, fish and powershell through the `complete` command

package-lock.json

Lines changed: 8 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"prepare": "husky"
4949
},
5050
"devDependencies": {
51+
"@bomb.sh/tab": "^0.0.22",
5152
"@babel/core": "^8.0.1",
5253
"@babel/preset-env": "^8.0.2",
5354
"@babel/register": "^8.0.1",

packages/webpack-cli/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
"!**/*__tests__"
3232
],
3333
"dependencies": {
34-
"@bomb.sh/tab": "^0.0.22",
3534
"@discoveryjs/json-ext": "^1.1.0",
3635
"commander": "^14.0.3",
3736
"cross-spawn": "^7.0.6",
@@ -45,6 +44,7 @@
4544
"@types/envinfo": "^7.8.4"
4645
},
4746
"peerDependencies": {
47+
"@bomb.sh/tab": "^0.0.22",
4848
"js-yaml": "^4.0.0 || ^5.0.0",
4949
"json5": "^2.2.3",
5050
"toml": "^3.0.0 || ^4.0.0 || ^5.0.0",
@@ -53,6 +53,9 @@
5353
"webpack-dev-server": "^5.0.0 || ^6.0.0"
5454
},
5555
"peerDependenciesMeta": {
56+
"@bomb.sh/tab": {
57+
"optional": true
58+
},
5659
"js-yaml": {
5760
"optional": true
5861
},

packages/webpack-cli/src/webpack-cli.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM = Boolean(process.env.WEBPACK_DEV_SER
4141
const WEBPACK_DEV_SERVER_PACKAGE = WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM
4242
? (process.env.WEBPACK_DEV_SERVER_PACKAGE as string)
4343
: "webpack-dev-server";
44+
const COMPLETION_PACKAGE = "@bomb.sh/tab";
4445

4546
// `webpack-dev-server` v6 is ESM and exposes the server as its `default` export,
4647
// while v5 exports it directly
@@ -2545,13 +2546,24 @@ class WebpackCLI {
25452546
return externalCommand;
25462547
}
25472548

2548-
#isCompletionRequest(args: readonly string[], parseOptions?: ParseOptions): boolean {
2549+
#completionRequest(
2550+
args: readonly string[],
2551+
parseOptions?: ParseOptions,
2552+
): { words: string[] } | undefined {
25492553
const userArgs = parseOptions?.from === "user" ? args : args.slice(2);
25502554

2551-
return userArgs.find((arg) => arg !== "--" && !arg.startsWith("-")) === "complete";
2555+
if (userArgs.find((arg) => arg !== "--" && !arg.startsWith("-")) !== "complete") {
2556+
return;
2557+
}
2558+
2559+
// `complete -- <words>` asks what to suggest for `<words>`; `complete <shell>`
2560+
// only prints a script and has no words to resolve.
2561+
const separator = userArgs.indexOf("--");
2562+
2563+
return { words: separator === -1 ? [] : userArgs.slice(separator + 1) };
25522564
}
25532565

2554-
async #prepareCompletions(): Promise<void> {
2566+
async #prepareCompletions(words: string[]): Promise<void> {
25552567
// Register every option (same as `--help`) so tab can inspect the full tree.
25562568
this.program.forHelp = true;
25572569

@@ -2561,12 +2573,31 @@ class WebpackCLI {
25612573
.map((command) => this.#loadCommandByName(command.rawName)),
25622574
);
25632575

2564-
const { default: tab } = await import("@bomb.sh/tab/commander");
2576+
let tab: typeof import("@bomb.sh/tab/commander").default;
2577+
2578+
try {
2579+
({ default: tab } = await import("@bomb.sh/tab/commander"));
2580+
} catch (error) {
2581+
if ((error as NodeJS.ErrnoException).code !== "ERR_MODULE_NOT_FOUND") {
2582+
throw error;
2583+
}
2584+
2585+
// The shell runs `complete -- <words>` on every keypress, so a missing
2586+
// optional package stays silent there and only reports when asked for a script.
2587+
if (words.length === 0) {
2588+
this.logger.error(
2589+
`For using '${this.colors.green("complete")}' command you need to install: '${this.colors.green(COMPLETION_PACKAGE)}' package.`,
2590+
);
2591+
process.exit(2);
2592+
}
2593+
2594+
process.exit(0);
2595+
}
25652596

2566-
this.#attachCompletionHandlers(tab(this.program));
2597+
this.#attachCompletionHandlers(tab(this.program), words);
25672598
}
25682599

2569-
#attachCompletionHandlers(completion: RootCommand): void {
2600+
#attachCompletionHandlers(completion: RootCommand, words: string[]): void {
25702601
const helpOption = completion.options.get("help");
25712602

25722603
if (helpOption) {
@@ -2612,13 +2643,22 @@ class WebpackCLI {
26122643
};
26132644
}
26142645

2646+
// The command being completed, e.g. `b` in `complete -- b --mode=`.
2647+
const requested = words.find((word) => word !== "--" && !word.startsWith("-"));
2648+
26152649
for (const command of this.program.commands) {
26162650
const tabCommand = completion.commands.get(command.name());
26172651

26182652
if (!tabCommand) {
26192653
continue;
26202654
}
26212655

2656+
// Commands are registered under their name, so an alias resolves to nothing.
2657+
// Only the requested one is registered, to keep aliases out of the suggestions.
2658+
if (requested && requested !== command.name() && command.aliases().includes(requested)) {
2659+
completion.commands.set(requested, tabCommand);
2660+
}
2661+
26222662
for (const option of command.options) {
26232663
const longName = option.long?.slice(2);
26242664

@@ -2871,8 +2911,10 @@ class WebpackCLI {
28712911
await command.parseAsync([...commandOperands, ...unknown], { from: "user" });
28722912
});
28732913

2874-
if (this.#isCompletionRequest(args, parseOptions)) {
2875-
await this.#prepareCompletions();
2914+
const completionRequest = this.#completionRequest(args, parseOptions);
2915+
2916+
if (completionRequest) {
2917+
await this.#prepareCompletions(completionRequest.words);
28762918
}
28772919

28782920
await this.program.parseAsync(args, parseOptions);

test/complete/complete.test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,43 @@ describe("complete", () => {
7070
expect(suggestions).toEqual(expect.arrayContaining(["json", "markdown"]));
7171
});
7272

73+
it("should suggest the same options for a command alias", async () => {
74+
const { stdout: canonical } = await run(__dirname, ["complete", "--", "build", "--"]);
75+
76+
for (const alias of ["b", "bundle"]) {
77+
const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "--", alias, "--"]);
78+
79+
expect(exitCode).toBe(0);
80+
expect(stderr).toBeFalsy();
81+
expect(parseCompletions(stdout)).toEqual(parseCompletions(canonical));
82+
}
83+
});
84+
85+
it("should suggest option values for a command alias", async () => {
86+
const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "--", "b", "--mode="]);
87+
88+
expect(exitCode).toBe(0);
89+
expect(stderr).toBeFalsy();
90+
expect(parseCompletions(stdout)).toEqual(
91+
expect.arrayContaining(["development", "production", "none"]),
92+
);
93+
});
94+
95+
it("should not suggest aliases as commands", async () => {
96+
const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "--", ""]);
97+
98+
expect(exitCode).toBe(0);
99+
expect(stderr).toBeFalsy();
100+
101+
const suggestions = parseCompletions(stdout);
102+
103+
expect(suggestions).toEqual(expect.arrayContaining(["build", "watch", "serve"]));
104+
105+
for (const alias of ["b", "bundle", "w", "s", "server"]) {
106+
expect(suggestions).not.toContain(alias);
107+
}
108+
});
109+
73110
it("should suggest flags for the build command", async () => {
74111
const { exitCode, stderr, stdout } = await run(__dirname, ["complete", "--", "build", "--"]);
75112

test/help/__snapshots__/help.test.js.snap.devServer6.webpack5

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ Global options
134134
Commands
135135
────────────────────────────────────────────────────────────────────────
136136
build|bundle|b [entries...] [options] Run webpack (default command, can be omitted).
137+
complete [shell] Generate shell completion scripts.
137138
configtest|t [config-path] Validate a webpack configuration.
138139
help|h [command] [option] Display help for commands and options.
139140
info|i [options] Outputs information about your system.
@@ -193,6 +194,7 @@ Global options
193194
Commands
194195
────────────────────────────────────────────────────────────────────────
195196
build|bundle|b [entries...] [options] Run webpack (default command, can be omitted).
197+
complete [shell] Generate shell completion scripts.
196198
configtest|t [config-path] Validate a webpack configuration.
197199
help|h [command] [option] Display help for commands and options.
198200
info|i [options] Outputs information about your system.
@@ -252,6 +254,7 @@ Global options
252254
Commands
253255
────────────────────────────────────────────────────────────────────────
254256
build|bundle|b [entries...] [options] Run webpack (default command, can be omitted).
257+
complete [shell] Generate shell completion scripts.
255258
configtest|t [config-path] Validate a webpack configuration.
256259
help|h [command] [option] Display help for commands and options.
257260
info|i [options] Outputs information about your system.
@@ -2643,6 +2646,7 @@ Global options
26432646
Commands
26442647
────────────────────────────────────────────────────────────────────────
26452648
build|bundle|b [entries...] [options] Run webpack (default command, can be omitted).
2649+
complete [shell] Generate shell completion scripts.
26462650
configtest|t [config-path] Validate a webpack configuration.
26472651
help|h [command] [option] Display help for commands and options.
26482652
info|i [options] Outputs information about your system.
@@ -2732,6 +2736,7 @@ Global options
27322736
Commands
27332737
────────────────────────────────────────────────────────────────────────
27342738
build|bundle|b [entries...] [options] Run webpack (default command, can be omitted).
2739+
complete [shell] Generate shell completion scripts.
27352740
configtest|t [config-path] Validate a webpack configuration.
27362741
help|h [command] [option] Display help for commands and options.
27372742
info|i [options] Outputs information about your system.
@@ -3089,6 +3094,7 @@ Global options
30893094
Commands
30903095
────────────────────────────────────────────────────────────────────────
30913096
build|bundle|b [entries...] [options] Run webpack (default command, can be omitted).
3097+
complete [shell] Generate shell completion scripts.
30923098
configtest|t [config-path] Validate a webpack configuration.
30933099
help|h [command] [option] Display help for commands and options.
30943100
info|i [options] Outputs information about your system.
@@ -3146,6 +3152,7 @@ Global options
31463152
Commands
31473153
────────────────────────────────────────────────────────────────────────
31483154
build|bundle|b [entries...] [options] Run webpack (default command, can be omitted).
3155+
complete [shell] Generate shell completion scripts.
31493156
configtest|t [config-path] Validate a webpack configuration.
31503157
help|h [command] [option] Display help for commands and options.
31513158
info|i [options] Outputs information about your system.

0 commit comments

Comments
 (0)