@@ -419,73 +419,69 @@ resolves providers a child scope supplied — see
419419[ Registering a definition] ( #registering-a-definition ) . The same guidance, and
420420the reasoning behind it, is in ` dependency-injection.md ` .
421421
422- ` setup ` — hoisting work out of ` run `
423- ------------------------------------
422+ Where a handler gets its services
423+ ---------------------------------
424424
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:
425+ A handler resolves what it needs itself, at the top of its own body:
427426
428427``` ts
429428export default defineCommand ({
430429 name: " widget|add" ,
431430 arguments: " any" ,
432- setup() {
431+ async run(ctx ) {
432+ const widgets = inject (WidgetService );
433433 const projectData = inject (ProjectData );
434+
434435 projectData .initializeProjectData ();
435- return { projectData , widgets: inject (WidgetService ) };
436- },
437- canExecute(ctx , { projectData }) {
438- return !! projectData .projectDir ;
439- },
440- async run(ctx , { widgets }) {
441436 await widgets .add (ctx .args );
442437 },
443438});
444439```
445440
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- }
441+ The injection context is synchronous, so the ` inject() ` calls belong ** above
442+ the first ` await ` ** — see [ Injection, and the first
443+ ` await ` ] ( #injection-and-the-first-await ) . Resolve everything the handler needs
444+ there and the rule never bites; for anything that genuinely has to wait —
445+ resolved after an ` await ` , or inside a helper called later — use
446+ ` ctx.injector.get(token) ` , which works at any point.
447+
448+ ** Services are never bundled.** There is no ` setupXCommand() ` returning an
449+ object of injected services for another command to spread, and no
450+ ` IXCommandServices ` type travelling between commands. A dependency is named
451+ where it is used, so reading a handler tells you exactly what it touches.
452+ Sharing is either of two things, and neither of them is a bag:
453+
454+ - ** Shared logic** — a plain function taking the typed ` ctx ` and plain values,
455+ resolving its own services through ` ctx.injector.get(...) ` :
456+
457+ ``` ts
458+ export async function canBuildFor(
459+ ctx : CommandContext <any >,
460+ platform : string ,
461+ ): Promise <boolean > {
462+ const validation = ctx .injector .get (PlatformValidationService );
463+ return validation .canBuild (platform );
464+ }
465+ ```
466+
467+ - ** A whole command's precondition** — ` canExecuteCommand(name, args) ` , which
468+ asks that command itself; see [ Asking another
469+ command] ( #asking-another-command ) .
470+
471+ ### ` setup ` , when a command has one
473472
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>> ` .
473+ ` setup(ctx) ` runs once per invocation, before ` canExecute ` , and its return
474+ value is handed to ` canExecute ` , ` run ` and ` postRun ` as their second argument.
475+ "Once per invocation" means once across the three together — whichever the CLI
476+ reaches first triggers it, and the rest reuse the value.
477+
478+ It is optional sugar for ** one** command's own handlers, for the case where
479+ ` canExecute ` and ` run ` would otherwise repeat the same per-invocation
480+ derivation. It is never a place to assemble services for anything but the
481+ command it belongs to, and a command with a single handler does not need it at
482+ all. When a command has enough structure to want one, the
483+ [ class form] ( #class-form ) usually says the same thing better: the instance * is*
484+ the setup, and each dependency is a field.
489485
490486` run ` 's return value, and ` postRun `
491487-----------------------------------
@@ -569,12 +565,19 @@ class does not declare is left out of the definition entirely, so a class
569565without ` postRun ` gets no ` postCommandAction ` , exactly as an object without one
570566does.
571567
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.
568+ ** Which form to use.** The class form is for a single named command with
569+ internal structure: state shared between ` canExecute ` and ` run ` , values derived
570+ once per invocation, several private steps, or enough collaborators that
571+ ` this.$service ` reads better than a local in every handler. Everything simpler
572+ — a handful of services and a short handler — is an object definition with its
573+ handlers written inline, where ` ctx ` is typed by inference and there is nothing
574+ to name.
575+
576+ When a function generates variants of one command — the ` run|ios ` /
577+ ` run|vision ` family, one definition per platform — the object form is what
578+ fits, because the thing being parameterized is a value and definitions are
579+ values. Registering the same class twice under two names is not the
580+ equivalent: the class is one definition.
578581
579582** The class is the setup.** One instance is constructed per invocation, as that
580583invocation's ` setup ` , before ` canExecute ` runs. So field initializers and the
@@ -597,26 +600,37 @@ the base class reads it. A provider registered for one command — through the
597600` providers ` argument of ` registerCommand ` or ` registerLazyCommand ` — can inject
598601it too, and resolves nothing outside a running invocation.
599602
600- ** Share through functions, not base classes .** Two commands that need the same
601- services share an ` inject() ` -based helper, not a common ancestor :
603+ ** One field per dependency .** Each service the class uses is its own field,
604+ read as ` this.$x ` :
602605
603606``` ts
604- export function injectPlatformCommandServices() {
605- const projectData = inject (ProjectData );
606- projectData .initializeProjectData ();
607- return { projectData , platformHelper: inject (PlatformCommandHelper ) };
608- }
609-
610607export class PlatformAddCommand extends Command ({ name: " platform|add" }) {
611- private services = injectPlatformCommandServices ();
608+ private $projectData = inject <IProjectData >(" projectData" );
609+ private $platformHelper = inject <IPlatformCommandHelper >(
610+ " platformCommandHelper" ,
611+ );
612+
613+ constructor () {
614+ super ();
615+ this .$projectData .initializeProjectData ();
616+ }
612617 // ...
613618}
614619```
615620
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.
621+ Never a ` private services = injectSomething() ` holding a bag — the fields are
622+ the point, and a bag puts the dependency list back behind one more hop. Two
623+ commands needing the same four services restate those four lines; that
624+ duplication is cheaper than a shared shape neither of them owns.
625+
626+ ** Share logic, not base classes and not services.** What two commands genuinely
627+ have in common is a check or a step, so share a function that takes
628+ ` this.context ` and plain values and resolves its own services — see [ Where a
629+ handler gets its services] ( #where-a-handler-gets-its-services ) . To reuse
630+ another command's precondition whole, ask that command: [ Asking another
631+ command] ( #asking-another-command ) . A base class between ` Command() ` and the
632+ command is the pattern the legacy ` ICommand ` hierarchy used, and untangling it
633+ is most of why this API exists.
620634
621635Registration takes the class itself; see
622636[ Registering a definition] ( #registering-a-definition ) :
@@ -848,6 +862,40 @@ the injector of the current injection context, and the CLI's own outside one.
848862` runCommand ` is a thin call onto ` CommandsService.executeCommandInProcess ` ,
849863where the pipeline itself lives.
850864
865+ ### Asking another command
866+
867+ ` canExecuteCommand(name, args) ` asks a registered command whether it * could*
868+ run, without running it:
869+
870+ ``` ts
871+ import { canExecuteCommand } from " ../common/services/command-definition-adapter" ;
872+
873+ async canExecute (): Promise < boolean > {
874+ if (!(await canExecuteCommand (" prepare" , [this .args [0 ]]))) {
875+ return false ;
876+ }
877+
878+ return !!this.hostProjectPath;
879+ }
880+ ```
881+
882+ This is how one command builds on another's precondition. ` embed ` prepares the
883+ project, so "could ` embed ` run" starts with "could ` prepare ` run" — and the way
884+ to ask that is to ask ` prepare ` , not to import its ` canExecute ` and hand it
885+ services. The named command is resolved and its options primed exactly as
886+ ` runCommand ` does, then its own ` canExecute ` returns the verdict. It builds its
887+ own setup from its own services; nothing crosses between the two commands but
888+ the name and the arguments.
889+
890+ Pass only the arguments the child's own ` arguments ` policy accepts. The child
891+ enforces that policy before its ` canExecute ` , so forwarding a caller's whole
892+ argument list to a child that declares fewer is a rejection, not a wider check.
893+
894+ ` canExecuteCommand ` is a thin call onto
895+ ` CommandsService.canExecuteCommandInProcess ` , and follows ` runCommand ` in
896+ everything else: the same injector rule, the same option priming and
897+ restoration.
898+
851899### Key shortcuts
852900
853901The interactive keys ` ns start ` and ` ns run ` offer are the CLI's own caller. A
0 commit comments