11const jaroWinklerDistance = require ( "../vendor/jaro-winkler_distance" ) ;
22import * as helpers from "../helpers" ;
3- import { CommandsDelimiters } from "../constants" ;
3+ import {
4+ CommandsDelimiters ,
5+ ERROR_NO_VALID_SUBCOMMAND_FORMAT ,
6+ } from "../constants" ;
47import { EOL } from "os" ;
58import * as _ from "lodash" ;
69import { IOptions , IOptionsTracker } from "../../declarations" ;
@@ -121,9 +124,7 @@ export class CommandsService
121124 await this . $optionsTracker . trackOptions ( this . $options ) ;
122125 }
123126
124- const shouldExecuteHooks =
125- ! this . $staticConfig . disableCommandHooks &&
126- ( command . enableHooks === undefined || command . enableHooks === true ) ;
127+ const shouldExecuteHooks = this . shouldExecuteHooks ( command ) ;
127128 let hookCommandName = commandName ;
128129 if ( shouldExecuteHooks ) {
129130 // Handle correctly hierarchical commands
@@ -132,15 +133,8 @@ export class CommandsService
132133 commandArguments ,
133134 ) ;
134135 if ( hierarchicalCommandName ) {
135- hookCommandName = helpers . stringReplaceAll (
136+ hookCommandName = this . toHookCommandName (
136137 hierarchicalCommandName . commandName ,
137- CommandsDelimiters . DefaultHierarchicalCommand ,
138- CommandsDelimiters . HooksCommand ,
139- ) ;
140- hookCommandName = helpers . stringReplaceAll (
141- hookCommandName ,
142- CommandsDelimiters . HierarchicalCommand ,
143- CommandsDelimiters . HooksCommand ,
144138 ) ;
145139 }
146140
@@ -157,6 +151,57 @@ export class CommandsService
157151 }
158152 }
159153
154+ private toHookCommandName ( commandName : string ) : string {
155+ const hookCommandName = helpers . stringReplaceAll (
156+ commandName ,
157+ CommandsDelimiters . DefaultHierarchicalCommand ,
158+ CommandsDelimiters . HooksCommand ,
159+ ) ;
160+ return helpers . stringReplaceAll (
161+ hookCommandName ,
162+ CommandsDelimiters . HierarchicalCommand ,
163+ CommandsDelimiters . HooksCommand ,
164+ ) ;
165+ }
166+
167+ /**
168+ * The command line reaches a subcommand through its parent's dispatcher,
169+ * which fires the subcommand's full hook name (`before-open-ios`) around
170+ * the subcommand's own dispatch, whose name the hooks service truncates at
171+ * the `|` (`before-open`). An in-process dispatch goes straight to the
172+ * subcommand, so it fires the outer pair itself.
173+ */
174+ private async runResolvedCommandInProcess (
175+ command : ICommand ,
176+ commandName : string ,
177+ commandArguments : string [ ] ,
178+ ) : Promise < void > {
179+ const subcommandHookName =
180+ commandName . includes ( CommandsDelimiters . HierarchicalCommand ) &&
181+ this . shouldExecuteHooks ( command )
182+ ? this . toHookCommandName ( commandName )
183+ : undefined ;
184+
185+ if ( subcommandHookName ) {
186+ await this . $hooksService . executeBeforeHooks ( subcommandHookName ) ;
187+ }
188+
189+ await this . runResolvedCommand ( command , commandName , commandArguments , {
190+ trackAnalytics : false ,
191+ } ) ;
192+
193+ if ( subcommandHookName ) {
194+ await this . $hooksService . executeAfterHooks ( subcommandHookName ) ;
195+ }
196+ }
197+
198+ private shouldExecuteHooks ( command : ICommand ) : boolean {
199+ return (
200+ ! this . $staticConfig . disableCommandHooks &&
201+ ( command . enableHooks === undefined || command . enableHooks === true )
202+ ) ;
203+ }
204+
160205 private printHelpSuggestion ( commandName ?: string ) : Promise < void > {
161206 const command = commandName
162207 ? helpers . stringReplaceAll (
@@ -256,9 +301,10 @@ export class CommandsService
256301 let commandName = typeof reference === "string" ? reference : undefined ;
257302 this . inProcessDepth ++ ;
258303 try {
259- const resolved = this . resolveReference ( reference ) ;
304+ const resolved = this . resolveReference ( reference , commandArguments ) ;
260305 const command = resolved . command ;
261306 commandName = resolved . commandName ;
307+ commandArguments = resolved . commandArguments ;
262308
263309 this . commands . push ( { commandName, commandArguments } ) ;
264310 const restoreOptions = this . primeOptions ( command ) ;
@@ -280,9 +326,11 @@ export class CommandsService
280326 ) ;
281327 }
282328
283- await this . runResolvedCommand ( command , commandName , commandArguments , {
284- trackAnalytics : false ,
285- } ) ;
329+ await this . runResolvedCommandInProcess (
330+ command ,
331+ commandName ,
332+ commandArguments ,
333+ ) ;
286334 } finally {
287335 restoreOptions ( ) ;
288336 this . commands . pop ( ) ;
@@ -301,7 +349,7 @@ export class CommandsService
301349 /**
302350 * The `canExecute` half of {@link runCommand}: the named command is resolved
303351 * and its options are primed the same way, and its own `canExecute` returns
304- * the verdict. The child builds its own setup from its own services —
352+ * its verdict or throws . The child builds its own setup from its own services —
305353 * nothing is threaded in from the caller — which is what lets one command
306354 * reuse another's precondition without importing its handlers.
307355 */
@@ -311,7 +359,9 @@ export class CommandsService
311359 ) : Promise < boolean > {
312360 this . inProcessDepth ++ ;
313361 try {
314- const { commandName, command } = this . resolveReference ( reference ) ;
362+ const resolved = this . resolveReference ( reference , commandArguments ) ;
363+ const { commandName, command } = resolved ;
364+ commandArguments = resolved . commandArguments ;
315365
316366 this . commands . push ( { commandName, commandArguments } ) ;
317367 const restoreOptions = this . primeOptions ( command ) ;
@@ -347,20 +397,22 @@ export class CommandsService
347397 return this . canExecuteCommand ( commandName , commandArguments ) ;
348398 }
349399
350- /**
351- * Merging a command's options into the parser rewrites the values the host
352- * process is still running on: a declared default replaces the CLI-wide one
353- * and the host keeps reading the replacement long after the command is
354- * done. An in-process dispatch has to put the parser back where it found it.
355- */
356400 /**
357401 * A name is looked up in the registry; a definition or class is run as the
358402 * caller holds it, registered or not, so what runs is what was referenced.
359403 * Its first name still identifies it for hooks and reporting.
404+ *
405+ * A parent name is routed to its subcommand here rather than run: the
406+ * parent's synthesized dispatcher re-enters through `tryExecuteCommand`,
407+ * which exits the process on failure and tracks analytics.
360408 */
361- private resolveReference ( reference : CommandReference ) : {
409+ private resolveReference (
410+ reference : CommandReference ,
411+ commandArguments : string [ ] ,
412+ ) : {
362413 commandName : string ;
363414 command : ICommand ;
415+ commandArguments : string [ ] ;
364416 } {
365417 if ( typeof reference === "string" ) {
366418 const command = this . $injector . resolveCommand ( reference ) ;
@@ -370,7 +422,28 @@ export class CommandsService
370422 ) ;
371423 }
372424
373- return { commandName : reference , command } ;
425+ if ( command . isHierarchicalCommand ) {
426+ const subcommand = this . $injector . buildHierarchicalCommand (
427+ reference ,
428+ commandArguments ,
429+ ) ;
430+ const subcommandInstance =
431+ subcommand && this . $injector . resolveCommand ( subcommand . commandName ) ;
432+ if ( ! subcommandInstance ) {
433+ this . $errors . failWithHelp (
434+ ERROR_NO_VALID_SUBCOMMAND_FORMAT ,
435+ reference ,
436+ ) ;
437+ }
438+
439+ return {
440+ commandName : subcommand . commandName ,
441+ command : subcommandInstance ,
442+ commandArguments : subcommand . remainingArguments ,
443+ } ;
444+ }
445+
446+ return { commandName : reference , command, commandArguments } ;
374447 }
375448
376449 const definition = toCommandDefinition ( reference ) ;
@@ -386,9 +459,16 @@ export class CommandsService
386459 ? definition . name [ 0 ]
387460 : definition . name ,
388461 command : createCommandFromDefinition ( definition , < any > this . $injector ) ,
462+ commandArguments,
389463 } ;
390464 }
391465
466+ /**
467+ * Merging a command's options into the parser rewrites the values the host
468+ * process is still running on: a declared default replaces the CLI-wide one
469+ * and the host keeps reading the replacement long after the command is
470+ * done. An in-process dispatch has to put the parser back where it found it.
471+ */
392472 private primeOptions ( command : ICommand ) : ( ) => void {
393473 if ( command . isHierarchicalCommand ) {
394474 return ( ) => undefined ;
0 commit comments