66 * lib/common/services/command-definition-adapter.
77 */
88
9+ import { COMMAND_CONTEXT } from "./contracts/command-context" ;
910import type { KeyShortcut } from "./contracts/key-shortcuts" ;
11+ import { inject } from "./di/inject" ;
1012import type { Injector } from "./di/injector" ;
1113
1214/**
@@ -255,7 +257,10 @@ const OPTION_TYPES: CommandOptionType[] = [
255257const ACCEPTED_FORM =
256258 'defineCommand({ name: "widget|add", run(ctx) { ... } }) — with the ' +
257259 "optional fields description, options, arguments, allowUnknownOptions, " +
258- "setup, canExecute, shortcuts, postRun, disableAnalytics and enableHooks." ;
260+ "setup, canExecute, shortcuts, postRun, disableAnalytics and enableHooks. " +
261+ 'Or the class form, class WidgetAdd extends Command({ name: "widget|add" }) ' +
262+ "{ run() { ... } }, which declares the same fields except the handlers and " +
263+ "implements run, and optionally canExecute, postRun and shortcuts, as methods." ;
259264
260265const describeDefinition = ( definition : any ) : string => {
261266 const name = definition && definition . name ;
@@ -556,12 +561,18 @@ export type CommandName = string | readonly string[];
556561 * be checked against them.
557562 */
558563export type CommandNamesOf < TDefinition > = TDefinition extends {
559- name : infer TName ;
564+ definition : infer TClassDefinition ;
560565}
561- ? TName extends readonly ( infer TAlias ) [ ]
562- ? TAlias
563- : TName
564- : never ;
566+ ? // A constructor's own `name` is Function.name, so the class form has to be
567+ // read through its static definition before the `name` branch sees it.
568+ CommandNamesOf < TClassDefinition >
569+ : TDefinition extends {
570+ name : infer TName ;
571+ }
572+ ? TName extends readonly ( infer TAlias ) [ ]
573+ ? TAlias
574+ : TName
575+ : never ;
565576
566577export function defineCommand <
567578 TSchema extends CommandOptionsSchema = { } ,
@@ -583,3 +594,204 @@ export function isCommandDefinition(
583594) : value is DefinedCommand < any , any , any > {
584595 return ! ! value && ( < any > value ) [ COMMAND_DEFINITION_MARKER ] === true ;
585596}
597+
598+ /**
599+ * Marks a constructor produced by `Command()`. Same `Symbol.for` reasoning as
600+ * COMMAND_DEFINITION_MARKER, and the same reason it is read rather than
601+ * `instanceof`: an extension bundles its own copy of this module.
602+ */
603+ export const COMMAND_CLASS_MARKER : unique symbol = Symbol . for (
604+ "nativescript:cli:commandClass" ,
605+ ) ;
606+
607+ /** The meta `Command()` was called with, inherited by every subclass. */
608+ const COMMAND_CLASS_META = Symbol . for ( "nativescript:cli:commandClassMeta" ) ;
609+
610+ /** Per-constructor cache of the derived definition; own-property only. */
611+ const COMMAND_CLASS_DEFINITION = Symbol . for (
612+ "nativescript:command:classDefinition" ,
613+ ) ;
614+
615+ /**
616+ * What the class form declares up front: a definition without the handlers,
617+ * which the class supplies as methods instead.
618+ */
619+ export type CommandMeta <
620+ TName extends CommandName = CommandName ,
621+ TSchema extends CommandOptionsSchema = { } ,
622+ > = Omit <
623+ CommandDefinition < TSchema , any , any > ,
624+ "name" | "setup" | "canExecute" | "run" | "postRun" | "shortcuts"
625+ > & { name : TName } ;
626+
627+ /**
628+ * The instance side of the class form. Exported because it names the base of
629+ * every `Command()` class — a subclass's declaration emit refers to it — not
630+ * because anything should extend it directly.
631+ */
632+ export abstract class CommandBase <
633+ TSchema extends CommandOptionsSchema = { } ,
634+ TResult = void ,
635+ > {
636+ /**
637+ * The instance is built once per invocation, as that invocation's `setup`,
638+ * so the context captured here is the one its own run was handed.
639+ */
640+ protected readonly context : CommandContext < TSchema > = inject ( COMMAND_CONTEXT ) ;
641+
642+ protected get options ( ) : CommandOptionValues < TSchema > {
643+ return this . context . options ;
644+ }
645+
646+ protected get args ( ) : string [ ] {
647+ return this . context . args ;
648+ }
649+
650+ abstract run ( ) : Promise < TResult > | TResult ;
651+ canExecute ?( ) : Promise < boolean > | boolean ;
652+ postRun ?( result : Awaited < TResult > ) : Promise < void > | void ;
653+ shortcuts ?( ) : KeyShortcut [ ] ;
654+ }
655+
656+ /**
657+ * The static side. An abstract construct signature, so the compiler still
658+ * requires a subclass to implement `run`, and a named type, so declaration
659+ * emit for `class X extends Command({ ... })` has something to refer to.
660+ */
661+ export type CommandClass <
662+ TName extends CommandName = CommandName ,
663+ 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+ > ;
672+ readonly [ COMMAND_CLASS_MARKER ] : true ;
673+ } ;
674+
675+ /** Either accepted form of a command, as a registration site takes it. */
676+ export type RegisterableCommand =
677+ DefinedCommand < any , any , any > | CommandClass < any , any , any > ;
678+
679+ export function isCommandClass (
680+ value : any ,
681+ ) : value is CommandClass < any , any , any > {
682+ return (
683+ typeof value === "function" && ( < any > value ) [ COMMAND_CLASS_MARKER ] === true
684+ ) ;
685+ }
686+
687+ const buildClassDefinition = ( ctor : any ) : DefinedCommand < any , any , any > => {
688+ const meta = ctor [ COMMAND_CLASS_META ] ;
689+ const prototype = ctor . prototype ;
690+ const implementsMethod = ( method : string ) : boolean =>
691+ typeof prototype [ method ] === "function" ;
692+
693+ if ( ! implementsMethod ( "run" ) ) {
694+ invalid (
695+ meta ,
696+ `the class '${ ctor . name || "<anonymous>" } ' implements no 'run' method` ,
697+ ) ;
698+ }
699+
700+ // The instance IS the setup result, so every handler reaches it as the
701+ // second argument the adapter already threads through.
702+ const definition : any = {
703+ ...meta ,
704+ setup : ( ) => new ctor ( ) ,
705+ run : ( context : any , instance : any ) => instance . run ( ) ,
706+ } ;
707+
708+ if ( implementsMethod ( "canExecute" ) ) {
709+ definition . canExecute = ( context : any , instance : any ) =>
710+ instance . canExecute ( ) ;
711+ }
712+
713+ if ( implementsMethod ( "postRun" ) ) {
714+ definition . postRun = ( context : any , result : any , instance : any ) =>
715+ instance . postRun ( result ) ;
716+ }
717+
718+ if ( implementsMethod ( "shortcuts" ) ) {
719+ definition . shortcuts = ( context : any , instance : any ) =>
720+ instance . shortcuts ( ) ;
721+ }
722+
723+ return defineCommand ( definition ) ;
724+ } ;
725+
726+ /**
727+ * The definition a `Command()` class stands for, cached on the constructor it
728+ * was read from. The cache entry is an own property so a class extending
729+ * another command class never serves its parent's definition.
730+ */
731+ export function classCommandDefinition (
732+ ctor : any ,
733+ ) : DefinedCommand < any , any , any > {
734+ if ( ! isCommandClass ( ctor ) ) {
735+ throw new Error (
736+ `${ describeDefinition ( ctor ) } is not a command class: it did not come ` +
737+ `from Command(). Accepted form: ${ ACCEPTED_FORM } ` ,
738+ ) ;
739+ }
740+
741+ const target : any = ctor ;
742+ if ( Object . prototype . hasOwnProperty . call ( target , COMMAND_CLASS_DEFINITION ) ) {
743+ return target [ COMMAND_CLASS_DEFINITION ] ;
744+ }
745+
746+ const definition = buildClassDefinition ( target ) ;
747+ Object . defineProperty ( target , COMMAND_CLASS_DEFINITION , {
748+ value : definition ,
749+ } ) ;
750+
751+ return definition ;
752+ }
753+
754+ /** The definition behind either form, or null for anything else. */
755+ export function toCommandDefinition (
756+ value : any ,
757+ ) : DefinedCommand < any , any , any > | null {
758+ if ( isCommandClass ( value ) ) {
759+ return classCommandDefinition ( value ) ;
760+ }
761+
762+ return isCommandDefinition ( value ) ? value : null ;
763+ }
764+
765+ /**
766+ * The class authoring form: sugar over defineCommand, not a second execution
767+ * path. The returned base carries a `definition` that reads the class it is
768+ * accessed through, so the subclass — not this base — is what `setup`
769+ * instantiates, and registration keeps taking definitions only.
770+ *
771+ * export class PlatformClean extends Command({
772+ * name: "platform|clean",
773+ * options: { frameworkPath: stringOption() },
774+ * }) {
775+ * private $helper = inject<IPlatformCommandHelper>("platformCommandHelper");
776+ * run() { return this.$helper.clean(this.args, this.options.frameworkPath); }
777+ * }
778+ */
779+ export function Command <
780+ const TName extends CommandName ,
781+ TSchema extends CommandOptionsSchema = { } ,
782+ TResult = void ,
783+ > ( meta : CommandMeta < TName , TSchema > ) : CommandClass < TName , TSchema , TResult > {
784+ abstract class Base extends CommandBase < TSchema , TResult > {
785+ // A getter, because `this` in a static accessor is the constructor the
786+ // property was read through: that is the only hook that resolves the
787+ // subclass without the subclass having to name itself.
788+ static get definition ( ) : DefinedCommand < any , any , any > {
789+ return classCommandDefinition ( this ) ;
790+ }
791+ }
792+
793+ Object . defineProperty ( Base , COMMAND_CLASS_MARKER , { value : true } ) ;
794+ Object . defineProperty ( Base , COMMAND_CLASS_META , { value : meta } ) ;
795+
796+ return < any > Base ;
797+ }
0 commit comments