@@ -4,6 +4,7 @@ import path from "node:path";
44import { type Readable as ReadableType } from "node:stream" ;
55import { fileURLToPath , pathToFileURL } from "node:url" ;
66import util from "node:util" ;
7+ import { type RootCommand } from "@bomb.sh/tab" ;
78import { type stringifyChunked as stringifyChunkedType } from "@discoveryjs/json-ext" ;
89import {
910 type Command as CommanderCommand ,
@@ -157,6 +158,7 @@ interface KnownWebpackCLICommands {
157158 help : CommandOptions < void , CommanderArgs , Context > ;
158159 info : CommandOptions < void , CommanderArgs , Context > ;
159160 configtest : CommandOptions < string | undefined , CommanderArgs , WebpackContext & Context > ;
161+ complete : CommandOptions < void , CommanderArgs , Context > ;
160162}
161163
162164// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -585,6 +587,12 @@ class WebpackCLI {
585587 return str . length > 0 ? str . charAt ( 0 ) . toUpperCase ( ) + str . slice ( 1 ) : str ;
586588 }
587589
590+ #commandTerm( command : CommanderCommand ) : string {
591+ const aliases = command . aliases ( ) . filter ( Boolean ) ;
592+
593+ return aliases . length > 0 ? `${ command . name ( ) } |${ aliases . join ( "|" ) } ` : command . name ( ) ;
594+ }
595+
588596 toKebabCase ( str : string ) : string {
589597 return str . replaceAll ( / ( [ a - z 0 - 9 ] ) ( [ A - Z ] ) / g, "$1-$2" ) . toLowerCase ( ) ;
590598 }
@@ -1432,14 +1440,12 @@ class WebpackCLI {
14321440 return `${ parentCmdNames } ${ command . usage ( ) } | ${ parentCmdNames } [command] [options]` ;
14331441 }
14341442
1435- return `${ parentCmdNames } ${ command . name ( ) } |${ command
1436- . aliases ( )
1437- . join ( "|" ) } ${ command . usage ( ) } `;
1443+ return `${ parentCmdNames } ${ this . #commandTerm( command ) } ${ command . usage ( ) } ` ;
14381444 } ,
14391445 // Support multiple aliases
14401446 subcommandTerm : ( command ) => {
14411447 const usage = command . usage ( ) ;
1442- return `${ command . name ( ) } | ${ command . aliases ( ) . join ( "|" ) } ${ usage . length > 0 ? ` ${ usage } ` : "" } ` ;
1448+ return `${ this . #commandTerm ( command ) } ${ usage . length > 0 ? ` ${ usage } ` : "" } ` ;
14431449 } ,
14441450 visibleOptions : function visibleOptions ( command ) {
14451451 return command . options . filter ( ( option ) => {
@@ -2445,6 +2451,16 @@ class WebpackCLI {
24452451 ) ;
24462452 } ,
24472453 } ,
2454+ complete : {
2455+ rawName : "complete" ,
2456+ name : "complete [shell]" ,
2457+ alias : [ ] ,
2458+ description : "Generate shell completion scripts." ,
2459+ action : ( ) => {
2460+ // Display-only stub for `--help`. Completion requests are handled in `run()`
2461+ // before parse so the completion adapter can inspect the loaded command tree.
2462+ } ,
2463+ } ,
24482464 } ;
24492465
24502466 #isCommand< A = void , O extends CommanderArgs = CommanderArgs , C extends Context = Context > (
@@ -2489,6 +2505,8 @@ class WebpackCLI {
24892505 return await this . makeCommand ( this . #commands. info ) ;
24902506 } else if ( this . #isCommand( commandName , this . #commands. configtest ) ) {
24912507 return await this . makeCommand ( this . #commands. configtest ) ;
2508+ } else if ( this . #isCommand( commandName , this . #commands. complete ) ) {
2509+ return await this . makeCommand ( this . #commands. complete ) ;
24922510 }
24932511
24942512 const pkg : string = commandName ;
@@ -2527,7 +2545,125 @@ class WebpackCLI {
25272545 return externalCommand ;
25282546 }
25292547
2530- async run ( args : readonly string [ ] , parseOptions : ParseOptions ) {
2548+ #isCompletionRequest( args : readonly string [ ] , parseOptions ?: ParseOptions ) : boolean {
2549+ const userArgs = parseOptions ?. from === "user" ? args : args . slice ( 2 ) ;
2550+
2551+ return userArgs . find ( ( arg ) => arg !== "--" && ! arg . startsWith ( "-" ) ) === "complete" ;
2552+ }
2553+
2554+ async #prepareCompletions( ) : Promise < void > {
2555+ // Register every option (same as `--help`) so tab can inspect the full tree.
2556+ this . program . forHelp = true ;
2557+
2558+ await Promise . all (
2559+ Object . values ( this . #commands)
2560+ . filter ( ( command ) => command . rawName !== "complete" )
2561+ . map ( ( command ) => this . #loadCommandByName( command . rawName ) ) ,
2562+ ) ;
2563+
2564+ const { default : tab } = await import ( "@bomb.sh/tab/commander" ) ;
2565+
2566+ this . #attachCompletionHandlers( tab ( this . program ) ) ;
2567+ }
2568+
2569+ #attachCompletionHandlers( completion : RootCommand ) : void {
2570+ const helpOption = completion . options . get ( "help" ) ;
2571+
2572+ if ( helpOption ) {
2573+ helpOption . handler = ( complete ) => {
2574+ complete ( "verbose" , "Show all available commands and options" ) ;
2575+ } ;
2576+ }
2577+
2578+ const outputHandler = ( complete : ( value : string , description : string ) => void ) => {
2579+ complete ( "json" , "JSON output" ) ;
2580+ complete ( "markdown" , "Markdown output" ) ;
2581+ } ;
2582+
2583+ for ( const name of [ "info" , "version" ] ) {
2584+ const outputOption = completion . commands . get ( name ) ?. options . get ( "output" ) ;
2585+
2586+ if ( outputOption ) {
2587+ outputOption . handler = outputHandler ;
2588+ }
2589+ }
2590+
2591+ const progressHandler = ( complete : ( value : string , description : string ) => void ) => {
2592+ complete ( "profile" , "Capture profiling data" ) ;
2593+ } ;
2594+
2595+ for ( const name of [ "build" , "watch" , "serve" ] ) {
2596+ const progressOption = completion . commands . get ( name ) ?. options . get ( "progress" ) ;
2597+
2598+ if ( progressOption ) {
2599+ progressOption . handler = progressHandler ;
2600+ }
2601+ }
2602+
2603+ const completeCommand = completion . commands . get ( "complete" ) ;
2604+ const shellArg = completeCommand ?. arguments . get ( "shell" ) ;
2605+
2606+ if ( shellArg ) {
2607+ shellArg . handler = ( complete ) => {
2608+ complete ( "zsh" , "Zsh" ) ;
2609+ complete ( "bash" , "Bash" ) ;
2610+ complete ( "fish" , "Fish" ) ;
2611+ complete ( "powershell" , "PowerShell" ) ;
2612+ } ;
2613+ }
2614+
2615+ for ( const command of this . program . commands ) {
2616+ const tabCommand = completion . commands . get ( command . name ( ) ) ;
2617+
2618+ if ( ! tabCommand ) {
2619+ continue ;
2620+ }
2621+
2622+ for ( const option of command . options ) {
2623+ const longName = option . long ?. slice ( 2 ) ;
2624+
2625+ if ( ! longName ) {
2626+ continue ;
2627+ }
2628+
2629+ const { configs } = option as Option & { configs ?: ArgumentConfig [ ] } ;
2630+
2631+ if ( ! configs ) {
2632+ continue ;
2633+ }
2634+
2635+ const values : string [ ] = [ ] ;
2636+
2637+ for ( const config of configs ) {
2638+ if ( config . type !== "enum" || ! config . values ) {
2639+ continue ;
2640+ }
2641+
2642+ for ( const value of config . values ) {
2643+ if ( typeof value === "string" ) {
2644+ values . push ( value ) ;
2645+ }
2646+ }
2647+ }
2648+
2649+ if ( values . length === 0 ) {
2650+ continue ;
2651+ }
2652+
2653+ const tabOption = tabCommand . options . get ( longName ) ;
2654+
2655+ if ( tabOption ) {
2656+ tabOption . handler = ( complete ) => {
2657+ for ( const value of values ) {
2658+ complete ( value , "" ) ;
2659+ }
2660+ } ;
2661+ }
2662+ }
2663+ }
2664+ }
2665+
2666+ async run ( args : readonly string [ ] , parseOptions ?: ParseOptions ) {
25312667 // Default `--color` and `--no-color` options
25322668
25332669 const self : WebpackCLI = this ;
@@ -2735,6 +2871,10 @@ class WebpackCLI {
27352871 await command . parseAsync ( [ ...commandOperands , ...unknown ] , { from : "user" } ) ;
27362872 } ) ;
27372873
2874+ if ( this . #isCompletionRequest( args , parseOptions ) ) {
2875+ await this . #prepareCompletions( ) ;
2876+ }
2877+
27382878 await this . program . parseAsync ( args , parseOptions ) ;
27392879 }
27402880
0 commit comments