diff --git a/src/Directive.ts b/src/Directive.ts index f85e0f0..bb8418b 100644 --- a/src/Directive.ts +++ b/src/Directive.ts @@ -5,35 +5,93 @@ import {CLIOption} from "./directive/options/CLIOption" import {PositionalOption} from "./directive/options/PositionalOption"; import {FlagOption} from "./directive/options/FlagOption"; +/** + * Type alias for a definition of a command-line option. + * + * This can be either an instance of CLIOption or a string describing an option. + * + * @example + * Some examples of positional/flag options defined by strings: + * `'{file name} | canonical name of the resource to create'` + * + * `'--push -p {value} | the value to be pushed'` + * + * `'--force -f | do a force push'` + */ export type OptionDefinition = CLIOption | string +/** + * An error thrown when an invalid option was detected. + */ export class OptionValidationError extends ErrorWithContext {} +/** + * A base class representing a sub-command in the command-line utility. + */ @Injectable() export abstract class Directive extends AppClass { @Inject() protected readonly logging!: Logging + /** Parsed option values. */ private _optionValues: any + /** + * Get the keyword or array of keywords that will specify this directive. + * + * @example + * If this returns `['up', 'start']`, the directive can be run by either of: + * + * ```shell + * ./ex up + * ./ex start + * ``` + */ public abstract getKeywords(): string | string[] + /** + * Get the usage description of this directive. Should be brief (1 sentence). + */ public abstract getDescription(): string + /** + * Optionally, specify a longer usage text that is shown on the directive's `--help` page. + */ public getHelpText(): string { return '' } + /** + * Get an array of options defined for this command. + */ public getOptions(): OptionDefinition[] { return [] } + /** + * Called when the directive is run from the command line. + * + * The raw arguments are provided as `argv`, but you are encouraged to use + * `getOptions()` and `option()` helpers to access the parsed options instead. + * + * @param argv + */ public abstract handle(argv: string[]): void | Promise + /** + * Sets the parsed option values. + * @param optionValues + * @private + */ private _setOptionValues(optionValues: any) { this._optionValues = optionValues; } + /** + * Get the value of a parsed option. If none exists, return `defaultValue`. + * @param name + * @param defaultValue + */ public option(name: string, defaultValue?: any) { if ( name in this._optionValues ) { return this._optionValues[name] @@ -42,6 +100,15 @@ export abstract class Directive extends AppClass { return defaultValue } + /** + * Invoke this directive with the specified arguments. + * + * If usage was requested (see `didRequestUsage()`), it prints the extended usage info. + * + * Otherwise, it parses the options from `argv` and calls `handle()`. + * + * @param argv + */ async invoke(argv: string[]) { const options = this.getResolvedOptions() @@ -115,6 +182,10 @@ export abstract class Directive extends AppClass { } } + /** + * Resolve the array of option definitions to CLIOption instances. + * Of note, this resolves the string-form definitions to actual CLIOption instances. + */ public getResolvedOptions(): CLIOption[] { return this.getOptions().map(option => { if ( typeof option === 'string' ) { @@ -125,6 +196,11 @@ export abstract class Directive extends AppClass { }) } + /** + * Get the main keyword displayed for this directive. + * @example + * If `getKeywords()` returns `['up', 'start']`, this will return `'up'`. + */ public getMainKeyword(): string { const kws = this.getKeywords() @@ -135,32 +211,60 @@ export abstract class Directive extends AppClass { return kws } + /** + * Returns true if the given keyword should invoke this directive. + * @param name + */ public matchesKeyword(name: string) { let kws = this.getKeywords() if ( !Array.isArray(kws) ) kws = [kws] return kws.includes(name) } + /** + * Print the given output to the log as success text. + * @param output + */ success(output: any) { this.logging.success(output, true) } + /** + * Print the given output to the log as error text. + * @param output + */ error(output: any) { this.logging.error(output, true) } + /** + * Print the given output to the log as warning text. + * @param output + */ warn(output: any) { this.logging.warn(output, true) } + /** + * Print the given output to the log as info text. + * @param output + */ info(output: any) { this.logging.info(output, true) } + /** + * Print the given output to the log as debugging text. + * @param output + */ debug(output: any) { this.logging.debug(output, true) } + /** + * Print the given output to the log as verbose text. + * @param output + */ verbose(output: any) { this.logging.verbose(output, true) } diff --git a/src/Template.ts b/src/Template.ts index df52e56..f7a2884 100644 --- a/src/Template.ts +++ b/src/Template.ts @@ -1,9 +1,64 @@ import {UniversalPath} from '@extollo/util' +/** + * Interface defining a template that can be generated using the TemplateDirective. + */ export interface Template { + /** + * The name of the template as it will be specified from the command line. + * + * @example + * If this is `'mytemplate'`, then the template will be created with: + * + * ```shell + * ./ex new mytemplate some:path + * ``` + */ name: string, + + /** + * The suffix of the file generated by this template. + * @example `.mytemplate.ts` + * @example `.controller.ts` + */ fileSuffix: string, + + /** + * Brief description of the template displayed on the --help page for the TemplateDirective. + * Should be brief (1 sentence). + */ description: string, + + /** + * Array of path-strings that are resolved relative to the base `app` directory. + * @example `['http', 'controllers']` + * @example `['units']` + */ baseAppPath: string[], + + /** + * Render the given template to a string which will be written to the file. + * Note: this method should NOT write the contents to `targetFilePath`. + * + * @example + * If the user enters: + * + * ```shell + * ./ex new mytemplate path:to:NewInstance + * ``` + * + * Then, the following params are: + * ```typescript + * { + * name: 'NewInstance', + * fullCanonicalPath: 'path:to:NewInstance', + * targetFilePath: UniversalPath { } + * } + * ``` + * + * @param name - the singular name of the resource + * @param fullCanonicalName - the full canonical name of the resource + * @param targetFilePath - the UniversalPath where the file will be written + */ render: (name: string, fullCanonicalName: string, targetFilePath: UniversalPath) => string | Promise } diff --git a/src/directive/RunDirective.ts b/src/directive/RunDirective.ts index 61b50c1..404df00 100644 --- a/src/directive/RunDirective.ts +++ b/src/directive/RunDirective.ts @@ -4,6 +4,11 @@ import {Unit} from "@extollo/lib" import {ErrorWithContext} from "@extollo/util" import {CommandLineApplication} from "../service" +/** + * A directive that starts the framework's final target normally. + * In most cases, this runs the HTTP server, which would have been replaced + * by the CommandLineApplication unit. + */ @Injectable() export class RunDirective extends Directive { getDescription(): string { diff --git a/src/directive/ShellDirective.ts b/src/directive/ShellDirective.ts index 141261d..cb0a581 100644 --- a/src/directive/ShellDirective.ts +++ b/src/directive/ShellDirective.ts @@ -3,12 +3,20 @@ import {Directive} from "../Directive" import * as colors from "colors/safe" import * as repl from 'repl' +/** + * Launch an interactive REPL shell from within the application. + * This is very useful for debugging and testing things during development. + */ export class ShellDirective extends Directive { protected options: any = { welcome: `powered by Extollo, © ${(new Date).getFullYear()} Garrett Mills\nAccess your application using the "app" global.`, prompt: `${colors.blue('(')}extollo${colors.blue(') ➤ ')}`, } + /** + * The created Node.js REPL server. + * @protected + */ protected repl?: repl.REPLServer getDescription(): string { diff --git a/src/directive/TemplateDirective.ts b/src/directive/TemplateDirective.ts index 1b82941..abc4a18 100644 --- a/src/directive/TemplateDirective.ts +++ b/src/directive/TemplateDirective.ts @@ -4,6 +4,9 @@ import {ErrorWithContext} from "@extollo/util" import {PositionalOption} from "./options/PositionalOption" import {CommandLine} from "../service" +/** + * Create a new file based on a template registered with the CommandLine service. + */ @Injectable() export class TemplateDirective extends Directive { @Inject() @@ -20,7 +23,7 @@ export class TemplateDirective extends Directive { getOptions(): OptionDefinition[] { const registeredTemplates = this.cli.getTemplates() const template = new PositionalOption('template_name', 'the template to base the new file on (e.g. model, controller)') - template.whitelist(...registeredTemplates.pluck('name')) + template.whitelist(...registeredTemplates.pluck('name').all()) const destination = new PositionalOption('file_name', 'canonical name of the file to create (e.g. auth:Group, dash:Activity)') @@ -43,7 +46,7 @@ export class TemplateDirective extends Directive { '', ...(registeredTemplates.map(template => { return ` - ${template.name}: ${template.description}` - })) + }).all()) ].join('\n') } diff --git a/src/directive/UsageDirective.ts b/src/directive/UsageDirective.ts index 66fddb3..47c2422 100644 --- a/src/directive/UsageDirective.ts +++ b/src/directive/UsageDirective.ts @@ -3,6 +3,10 @@ import {Injectable, Inject} from "@extollo/di" import {padRight} from "@extollo/util" import {CommandLine} from "../service" +/** + * Directive that prints the help message and usage information about + * directives registered with the command line utility. + */ @Injectable() export class UsageDirective extends Directive { @Inject() diff --git a/src/directive/options/FlagOption.ts b/src/directive/options/FlagOption.ts index 10ff10c..436b0d9 100644 --- a/src/directive/options/FlagOption.ts +++ b/src/directive/options/FlagOption.ts @@ -6,9 +6,24 @@ import {CLIOption} from "./CLIOption" export class FlagOption extends CLIOption { constructor( + /** + * The long-form flag for this option. + * @example --path, --create + */ public readonly longFlag?: string, + /** + * The short-form flag for this option. + * @example -p, -c + */ public readonly shortFlag?: string, + /** + * Usage message describing this flag. + */ public readonly message?: string, + /** + * Description of the argument required by this flag. + * If this is set, the flag will expect a positional argument to follow as a param. + */ public readonly argumentDescription?: string ) { super() } diff --git a/src/directive/options/PositionalOption.ts b/src/directive/options/PositionalOption.ts index a69dadd..7b0f173 100644 --- a/src/directive/options/PositionalOption.ts +++ b/src/directive/options/PositionalOption.ts @@ -11,7 +11,14 @@ export class PositionalOption extends CLIOption { * @param {string} message - message describing the option */ constructor( + /** + * The display name of this positional argument. + * @example path, filename + */ public readonly name: string, + /** + * A usage message describing this parameter. + */ public readonly message: string = '' ) { super() } diff --git a/src/service/CommandLine.ts b/src/service/CommandLine.ts index b43407a..ac62d13 100644 --- a/src/service/CommandLine.ts +++ b/src/service/CommandLine.ts @@ -11,12 +11,19 @@ import {middleware_template} from "../templates/middleware"; import {routes_template} from "../templates/routes"; import {config_template} from "../templates/config"; +/** + * Service for managing directives, templates, and other resources related + * to the command line utilities. + */ @Singleton() export class CommandLine extends Unit { @Inject() protected readonly logging!: Logging + /** Directive classes registered with the CLI command. */ protected directives: Collection> = new Collection>() + + /** Templates registered with the CLI command. These can be created with the TemplateDirective. */ protected templates: Collection