Tips & Tricks
Inheritance​
Because they're just plain old ES6 classes, commands can easily extend each other and inherit options:
tsabstract classBaseCommand extendsCommand {cwd =Option .String (`--cwd`, {hidden : true});Âabstractexecute ():Promise <number | void>;}ÂclassFooCommand extendsBaseCommand {foo =Option .String (`-f,--foo`);Âasyncexecute () {this.context .stdout .write (`Hello from ${this.cwd ??process .cwd ()}!\n`);this.context .stdout .write (`This is foo: ${this.foo }.\n`);}}
tsabstract classBaseCommand extendsCommand {cwd =Option .String (`--cwd`, {hidden : true});Âabstractexecute ():Promise <number | void>;}ÂclassFooCommand extendsBaseCommand {foo =Option .String (`-f,--foo`);Âasyncexecute () {this.context .stdout .write (`Hello from ${this.cwd ??process .cwd ()}!\n`);this.context .stdout .write (`This is foo: ${this.foo }.\n`);}}
Positionals can also be inherited. They will be consumed in order starting from the superclass:
tsabstract classBaseCommand extendsCommand {foo =Option .String ();Âabstractexecute ():Promise <number | void>;}ÂclassFooCommand extendsBaseCommand {bar =Option .String ();Âasyncexecute () {this.context .stdout .write (`This is foo: ${this.foo }.\n`);this.context .stdout .write (`This is bar: ${this.bar }.\n`);}}
tsabstract classBaseCommand extendsCommand {foo =Option .String ();Âabstractexecute ():Promise <number | void>;}ÂclassFooCommand extendsBaseCommand {bar =Option .String ();Âasyncexecute () {this.context .stdout .write (`This is foo: ${this.foo }.\n`);this.context .stdout .write (`This is bar: ${this.bar }.\n`);}}
hello world => Command {"foo": "hello", "bar": "world"}
hello world => Command {"foo": "hello", "bar": "world"}
Adding options to existing commands​
Adding options to existing commands can be achieved via inheritance and required options:
tsclassGreetCommand extendsCommand {staticpaths = [[`greet`]];Âname =Option .String ();Âgreeting =Option .String (`--greeting`, `Hello`);Âasyncexecute ():Promise <number | void> {this.context .stdout .write (`${this.greeting } ${this.name }!\n`);}}ÂclassGreetWithReverseCommand extendsGreetCommand {reverse =Option .Boolean (`--reverse`, {required : true});Âasyncexecute () {return await this.cli .run ([`greet`, this.reverse ? this.name .split (``).reverse ().join (``) : this.name , `--greeting`, this.greeting ]);}}
tsclassGreetCommand extendsCommand {staticpaths = [[`greet`]];Âname =Option .String ();Âgreeting =Option .String (`--greeting`, `Hello`);Âasyncexecute ():Promise <number | void> {this.context .stdout .write (`${this.greeting } ${this.name }!\n`);}}ÂclassGreetWithReverseCommand extendsGreetCommand {reverse =Option .Boolean (`--reverse`, {required : true});Âasyncexecute () {return await this.cli .run ([`greet`, this.reverse ? this.name .split (``).reverse ().join (``) : this.name , `--greeting`, this.greeting ]);}}
greet john => "Hello john!\n" greet john --greeting hey => "hey john!\n" greet john --reverse => "Hello nhoj!\n" greet john --greeting hey --reverse => "hey nhoj!\n"
greet john => "Hello john!\n" greet john --greeting hey => "hey john!\n" greet john --reverse => "Hello nhoj!\n" greet john --greeting hey --reverse => "hey nhoj!\n"
danger
To add an option to an existing command, you need to know its Command class. This means that if you want to add 2 options by using 2 different commands (e.g. if your application uses different plugins that can register their own commands), you need one of the Command classes to extend the other one and not the base.
Lazy evaluation​
Many commands have the following form:
tsimport {uniqBy } from 'lodash';ÂclassMyCommand extendsCommand {asyncexecute () {// ...}}
tsimport {uniqBy } from 'lodash';ÂclassMyCommand extendsCommand {asyncexecute () {// ...}}
While it works just fine, if you have a lot of commands that each have their own sets of dependencies (here lodash), the overall startup time may suffer. This is because the import statements will always be eagerly evaluated, even if the command doesn't end up being selected for execution.
To solve this problem you can move your imports inside the body of the execute function - thus making sure they'll only be evaluated if actually relevant:
tsclassMyCommand extendsCommand {asyncexecute () {const {uniqBy } = await import(`lodash`);// ...}}
tsclassMyCommand extendsCommand {asyncexecute () {const {uniqBy } = await import(`lodash`);// ...}}
This strategy is slightly harder to read, so it may not be necessary in every situation. If you like living on the edge, the babel-plugin-lazy-import plugin is meant to automatically apply this kind of transformation - although it requires you to run Babel on your sources.