@@ -59,7 +59,9 @@ A definition is checked at the moment `defineCommand` is called, not when the
5959command eventually runs. A misspelled field, a missing ` run ` , an option
6060declared with something other than the four helpers, an ` arguments ` value
6161outside ` "none" | "any" ` — each throws immediately, naming the command and the
62- accepted form:
62+ accepted form. The class form's meta is checked the same way at the
63+ ` Command({ ... }) ` call, which also rejects handlers passed there; only a
64+ missing ` run ` method waits until the definition is first read:
6365
6466```
6567Invalid command definition for 'widget|add': unknown field(s) 'handler'; a
@@ -419,73 +421,69 @@ resolves providers a child scope supplied — see
419421[ Registering a definition] ( #registering-a-definition ) . The same guidance, and
420422the reasoning behind it, is in ` dependency-injection.md ` .
421423
422- ` setup ` — hoisting work out of ` run `
423- ------------------------------------
424+ Where a handler gets its services
425+ ---------------------------------
424426
425- ` setup(ctx) ` runs once per invocation, before ` canExecute ` , and its return
426- value is handed to ` canExecute ` , ` run ` and ` postRun ` as their second argument:
427+ A handler resolves what it needs itself, at the top of its own body:
427428
428429``` ts
429430export default defineCommand ({
430431 name: " widget|add" ,
431432 arguments: " any" ,
432- setup() {
433+ async run(ctx ) {
434+ const widgets = inject (WidgetService );
433435 const projectData = inject (ProjectData );
436+
434437 projectData .initializeProjectData ();
435- return { projectData , widgets: inject (WidgetService ) };
436- },
437- canExecute(ctx , { projectData }) {
438- return !! projectData .projectDir ;
439- },
440- async run(ctx , { widgets }) {
441438 await widgets .add (ctx .args );
442439 },
443440});
444441```
445442
446- It exists for two reasons. It is the place to inject services before the first
447- ` await ` when several handlers need them, and it is where the work a command
448- class used to do in its constructor goes — most often
449- ` $projectData.initializeProjectData() ` .
450-
451- ` setup ` is sugar. A command may ignore it entirely and call ` inject() ` at the
452- top of ` run ` ; nothing else changes. "Once per invocation" means once across
453- ` canExecute ` , ` run ` and ` postRun ` together — whichever of them the CLI reaches
454- first triggers it, and the rest reuse the value.
455-
456- When several commands share a setup, or a helper outside the definition takes
457- the services as a parameter, lift it into a named function and derive the type
458- from it instead of writing the shape out by hand:
459-
460- ``` ts
461- export function setupWidgetAddCommand() {
462- const projectData = inject (ProjectData );
463- projectData .initializeProjectData ();
464- return { projectData , widgets: inject (WidgetService ) };
465- }
466- export type IWidgetAddCommandServices = ReturnType <
467- typeof setupWidgetAddCommand
468- >;
469-
470- export function canAddWidget(services : IWidgetAddCommandServices ): boolean {
471- return !! services .projectData .projectDir ;
472- }
443+ The injection context is synchronous, so the ` inject() ` calls belong ** above
444+ the first ` await ` ** — see [ Injection, and the first
445+ ` await ` ] ( #injection-and-the-first-await ) . Resolve everything the handler needs
446+ there and the rule never bites; for anything that genuinely has to wait —
447+ resolved after an ` await ` , or inside a helper called later — use
448+ ` ctx.injector.get(token) ` , which works at any point.
449+
450+ ** Services are never bundled.** There is no ` setupXCommand() ` returning an
451+ object of injected services for another command to spread, and no
452+ ` IXCommandServices ` type travelling between commands. A dependency is named
453+ where it is used, so reading a handler tells you exactly what it touches.
454+ Sharing is either of two things, and neither of them is a bag:
455+
456+ - ** Shared logic** — a plain function taking the typed ` ctx ` and plain values,
457+ resolving its own services through ` ctx.injector.get(...) ` :
458+
459+ ``` ts
460+ export async function canBuildFor(
461+ ctx : CommandContext <any >,
462+ platform : string ,
463+ ): Promise <boolean > {
464+ const validation = ctx .injector .get (PlatformValidationService );
465+ return validation .canBuild (platform );
466+ }
467+ ```
468+
469+ - ** A whole command's precondition** — ` canExecuteCommand(name, args) ` , which
470+ asks that command itself; see [ Asking another
471+ command] ( #asking-another-command ) .
472+
473+ ### ` setup ` , when a command has one
473474
474- export default defineCommand ({
475- name: " widget|add" ,
476- arguments: " any" ,
477- setup: setupWidgetAddCommand ,
478- canExecute : (ctx , services ) => canAddWidget (services ),
479- async run(ctx , { widgets }) {
480- await widgets .add (ctx .args );
481- },
482- });
483- ```
484-
485- Leave the setup function's return type off: the alias reads what the body
486- infers, so annotating the function with the alias makes the pair circular. Read
487- a setup curried over a parameter — ` setupX(platform) ` returning the setup
488- itself — through its inner function, ` ReturnType<ReturnType<typeof setupX>> ` .
475+ ` setup(ctx) ` runs once per invocation, before ` canExecute ` , and its return
476+ value is handed to ` canExecute ` , ` run ` and ` postRun ` as their second argument.
477+ "Once per invocation" means once across the three together — whichever the CLI
478+ reaches first triggers it, and the rest reuse the value.
479+
480+ It is optional sugar for ** one** command's own handlers, for the case where
481+ ` canExecute ` and ` run ` would otherwise repeat the same per-invocation
482+ derivation. It is never a place to assemble services for anything but the
483+ command it belongs to, and a command with a single handler does not need it at
484+ all. When a command has enough structure to want one, the
485+ [ class form] ( #class-form ) usually says the same thing better: the instance * is*
486+ the setup, and each dependency is a field.
489487
490488` run ` 's return value, and ` postRun `
491489-----------------------------------
@@ -563,18 +561,26 @@ export class PlatformCleanCommand extends Command({
563561` arguments ` , ` allowUnknownOptions ` , ` disableAnalytics ` and ` enableHooks ` . The
564562handlers are methods instead — ` run ` is required, and ` canExecute ` , ` postRun `
565563and ` shortcuts ` are optional, each with the same meaning and the same ordering
566- as the fields of the same name. ` postRun(result) ` receives what ` run ` returned;
564+ as the fields of the same name. The result type is inferred from ` run ` , and
565+ ` postRun(result) ` receives it, awaited, with no type argument to restate;
567566` shortcuts() ` returns the same table ` shortcuts(ctx, setup) ` does. A method the
568567class does not declare is left out of the definition entirely, so a class
569568without ` postRun ` gets no ` postCommandAction ` , exactly as an object without one
570569does.
571570
572- ** Which form to use.** The class form is for a single named command. When a
573- function generates variants of one command — the ` run|ios ` / ` run|vision `
574- family, one definition per platform — the object form is what fits, because
575- the thing being parameterized is a value and definitions are values.
576- Registering the same class twice under two names is not the equivalent: the
577- class is one definition.
571+ ** Which form to use.** The class form is for a single named command with
572+ internal structure: state shared between ` canExecute ` and ` run ` , values derived
573+ once per invocation, several private steps, or enough collaborators that
574+ ` this.$service ` reads better than a local in every handler. Everything simpler
575+ — a handful of services and a short handler — is an object definition with its
576+ handlers written inline, where ` ctx ` is typed by inference and there is nothing
577+ to name.
578+
579+ When a function generates variants of one command — the ` run|ios ` /
580+ ` run|vision ` family, one definition per platform — the object form is what
581+ fits, because the thing being parameterized is a value and definitions are
582+ values. Registering the same class twice under two names is not the
583+ equivalent: the class is one definition.
578584
579585** The class is the setup.** One instance is constructed per invocation, as that
580586invocation's ` setup ` , before ` canExecute ` runs. So field initializers and the
@@ -597,26 +603,37 @@ the base class reads it. A provider registered for one command — through the
597603` providers ` argument of ` registerCommand ` or ` registerLazyCommand ` — can inject
598604it too, and resolves nothing outside a running invocation.
599605
600- ** Share through functions, not base classes .** Two commands that need the same
601- services share an ` inject() ` -based helper, not a common ancestor :
606+ ** One field per dependency .** Each service the class uses is its own field,
607+ read as ` this.$x ` :
602608
603609``` ts
604- export function injectPlatformCommandServices() {
605- const projectData = inject (ProjectData );
606- projectData .initializeProjectData ();
607- return { projectData , platformHelper: inject (PlatformCommandHelper ) };
608- }
609-
610610export class PlatformAddCommand extends Command ({ name: " platform|add" }) {
611- private services = injectPlatformCommandServices ();
611+ private $projectData = inject <IProjectData >(" projectData" );
612+ private $platformHelper = inject <IPlatformCommandHelper >(
613+ " platformCommandHelper" ,
614+ );
615+
616+ constructor () {
617+ super ();
618+ this .$projectData .initializeProjectData ();
619+ }
612620 // ...
613621}
614622```
615623
616- A helper composes — a command can call two of them — and it stays readable
617- without the reader walking a chain of files. A base class between ` Command() `
618- and the command does not: it is the pattern the legacy ` ICommand ` hierarchy
619- used, and untangling it is most of why this API exists.
624+ Never a ` private services = injectSomething() ` holding a bag — the fields are
625+ the point, and a bag puts the dependency list back behind one more hop. Two
626+ commands needing the same four services restate those four lines; that
627+ duplication is cheaper than a shared shape neither of them owns.
628+
629+ ** Share logic, not base classes and not services.** What two commands genuinely
630+ have in common is a check or a step, so share a function that takes
631+ ` this.context ` and plain values and resolves its own services — see [ Where a
632+ handler gets its services] ( #where-a-handler-gets-its-services ) . To reuse
633+ another command's precondition whole, ask that command: [ Asking another
634+ command] ( #asking-another-command ) . A base class between ` Command() ` and the
635+ command is the pattern the legacy ` ICommand ` hierarchy used, and untangling it
636+ is most of why this API exists.
620637
621638Registration takes the class itself; see
622639[ Registering a definition] ( #registering-a-definition ) :
@@ -848,6 +865,40 @@ the injector of the current injection context, and the CLI's own outside one.
848865` runCommand ` is a thin call onto ` CommandsService.executeCommandInProcess ` ,
849866where the pipeline itself lives.
850867
868+ ### Asking another command
869+
870+ ` canExecuteCommand(name, args) ` asks a registered command whether it * could*
871+ run, without running it:
872+
873+ ``` ts
874+ import { canExecuteCommand } from " ../common/services/command-definition-adapter" ;
875+
876+ async canExecute (): Promise < boolean > {
877+ if (!(await canExecuteCommand (" prepare" , [this .args [0 ]]))) {
878+ return false ;
879+ }
880+
881+ return !!this.hostProjectPath;
882+ }
883+ ```
884+
885+ This is how one command builds on another's precondition. ` embed ` prepares the
886+ project, so "could ` embed ` run" starts with "could ` prepare ` run" — and the way
887+ to ask that is to ask ` prepare ` , not to import its ` canExecute ` and hand it
888+ services. The named command is resolved and its options primed exactly as
889+ ` runCommand ` does, then its own ` canExecute ` returns the verdict. It builds its
890+ own setup from its own services; nothing crosses between the two commands but
891+ the name and the arguments.
892+
893+ Pass only the arguments the child's own ` arguments ` policy accepts. The child
894+ enforces that policy before its ` canExecute ` , so forwarding a caller's whole
895+ argument list to a child that declares fewer is a rejection, not a wider check.
896+
897+ ` canExecuteCommand ` is a thin call onto
898+ ` CommandsService.canExecuteCommandInProcess ` , and follows ` runCommand ` in
899+ everything else: the same injector rule, the same option priming and
900+ restoration.
901+
851902### Key shortcuts
852903
853904The interactive keys ` ns start ` and ` ns run ` offer are the CLI's own caller. A
0 commit comments