@@ -117,8 +117,8 @@ export interface CommandContext<TSchema extends CommandOptionsSchema = {}> {
117117 /** Current value of every option declared in the schema, and nothing else. */
118118 options : CommandOptionValues < TSchema > ;
119119 /**
120- * The injector the command was registered against. `inject()` stops working
121- * after the first `await`; this is the supported late lookup.
120+ * This invocation's injector, the one `inject()` resolves against before the
121+ * first `await`; after it, `inject()` stops working and this is the lookup.
122122 */
123123 injector : Injector ;
124124 /** Fails the command with `message` and the usage help suggestion. */
@@ -539,6 +539,49 @@ const validateDefinition = (definition: any): void => {
539539 }
540540} ;
541541
542+ const HANDLER_FIELDS = [ "setup" , "canExecute" , "run" , "postRun" , "shortcuts" ] ;
543+
544+ const META_FIELDS = DEFINITION_FIELDS . filter (
545+ ( field ) => HANDLER_FIELDS . indexOf ( field ) === - 1 ,
546+ ) ;
547+
548+ /**
549+ * Everything defineCommand checks except the handlers, which the class form
550+ * only has once the subclass is declared, after Command() has returned.
551+ */
552+ const validateMeta = ( meta : any ) : void => {
553+ if ( ! isPlainObject ( meta ) ) {
554+ invalid ( meta , "Command() expects an object" ) ;
555+ }
556+
557+ const fields = Object . keys ( meta ) ;
558+ const handlers = fields . filter (
559+ ( field ) => HANDLER_FIELDS . indexOf ( field ) !== - 1 ,
560+ ) ;
561+ if ( handlers . length ) {
562+ invalid (
563+ meta ,
564+ `Command() was given the handler(s) ${ handlers
565+ . map ( ( field ) => `'${ field } '` )
566+ . join ( ", " ) } ; in the class form handlers are methods of the class`,
567+ ) ;
568+ }
569+
570+ const unknownFields = fields . filter (
571+ ( field ) => META_FIELDS . indexOf ( field ) === - 1 ,
572+ ) ;
573+ if ( unknownFields . length ) {
574+ invalid (
575+ meta ,
576+ `unknown field(s) ${ unknownFields
577+ . map ( ( field ) => `'${ field } '` )
578+ . join ( ", " ) } ; Command() accepts ${ META_FIELDS . join ( ", " ) } `,
579+ ) ;
580+ }
581+
582+ validateDefinition ( { ...meta , run : ( ) : void => undefined } ) ;
583+ } ;
584+
542585/**
543586 * A definition that carries the name it declares in its own type. `Omit` rather
544587 * than an intersection: intersecting the declared name with the wider `name` of
@@ -629,10 +672,7 @@ export type CommandMeta<
629672 * every `Command()` class — a subclass's declaration emit refers to it — not
630673 * because anything should extend it directly.
631674 */
632- export abstract class CommandBase <
633- TSchema extends CommandOptionsSchema = { } ,
634- TResult = void ,
635- > {
675+ export abstract class CommandBase < TSchema extends CommandOptionsSchema = { } > {
636676 /**
637677 * The instance is built once per invocation, as that invocation's `setup`,
638678 * so the context captured here is the one its own run was handed.
@@ -647,9 +687,10 @@ export abstract class CommandBase<
647687 return this . context . args ;
648688 }
649689
650- abstract run ( ) : Promise < TResult > | TResult ;
690+ abstract run ( ) : unknown ;
651691 canExecute ?( ) : Promise < boolean > | boolean ;
652- postRun ?( result : Awaited < TResult > ) : Promise < void > | void ;
692+ /** Typed off the subclass's own `run` through the polymorphic `this`. */
693+ postRun ?( result : Awaited < ReturnType < this[ "run" ] > > ) : Promise < void > | void ;
653694 shortcuts ?( ) : KeyShortcut [ ] ;
654695}
655696
@@ -661,24 +702,16 @@ export abstract class CommandBase<
661702export type CommandClass <
662703 TName extends CommandName = CommandName ,
663704 TSchema extends CommandOptionsSchema = { } ,
664- TResult = void ,
665- > = ( abstract new ( ) => CommandBase < TSchema , TResult > ) & {
666- readonly definition : NamedCommand <
667- TSchema ,
668- TResult ,
669- CommandBase < TSchema , TResult > ,
670- TName
671- > ;
705+ > = ( abstract new ( ) => CommandBase < TSchema > ) & {
706+ readonly definition : NamedCommand < TSchema , any , CommandBase < TSchema > , TName > ;
672707 readonly [ COMMAND_CLASS_MARKER ] : true ;
673708} ;
674709
675710/** Either accepted form of a command, as a registration site takes it. */
676711export type RegisterableCommand =
677- DefinedCommand < any , any , any > | CommandClass < any , any , any > ;
712+ DefinedCommand < any , any , any > | CommandClass < any , any > ;
678713
679- export function isCommandClass (
680- value : any ,
681- ) : value is CommandClass < any , any , any > {
714+ export function isCommandClass ( value : any ) : value is CommandClass < any , any > {
682715 return (
683716 typeof value === "function" && ( < any > value ) [ COMMAND_CLASS_MARKER ] === true
684717 ) ;
@@ -691,9 +724,19 @@ const buildClassDefinition = (ctor: any): DefinedCommand<any, any, any> => {
691724 typeof prototype [ method ] === "function" ;
692725
693726 if ( ! implementsMethod ( "run" ) ) {
727+ // The base Command() returns is never the author's class, and its
728+ // local name would only mislead.
729+ const isFactoryBase = Object . prototype . hasOwnProperty . call (
730+ ctor ,
731+ COMMAND_CLASS_META ,
732+ ) ;
694733 invalid (
695734 meta ,
696- `the class '${ ctor . name || "<anonymous>" } ' implements no 'run' method` ,
735+ isFactoryBase
736+ ? "the class returned by Command() implements no 'run' method; extend it with a class that does"
737+ : ctor . name
738+ ? `the class '${ ctor . name } ' implements no 'run' method`
739+ : "an anonymous class implements no 'run' method" ,
697740 ) ;
698741 }
699742
@@ -785,9 +828,10 @@ export type CommandReference = string | RegisterableCommand;
785828export function Command <
786829 const TName extends CommandName ,
787830 TSchema extends CommandOptionsSchema = { } ,
788- TResult = void ,
789- > ( meta : CommandMeta < TName , TSchema > ) : CommandClass < TName , TSchema , TResult > {
790- abstract class Base extends CommandBase < TSchema , TResult > {
831+ > ( meta : CommandMeta < TName , TSchema > ) : CommandClass < TName , TSchema > {
832+ validateMeta ( meta ) ;
833+
834+ abstract class Base extends CommandBase < TSchema > {
791835 // A getter, because `this` in a static accessor is the constructor the
792836 // property was read through: that is the only hook that resolves the
793837 // subclass without the subclass having to name itself.
0 commit comments