Typedoc all the things

This commit is contained in:
Garrett Mills 2021-03-23 10:10:23 -05:00
parent 3b7c9cc09f
commit d74566d1f7
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
16 changed files with 270 additions and 2 deletions

View File

@ -5,35 +5,93 @@ import {CLIOption} from "./directive/options/CLIOption"
import {PositionalOption} from "./directive/options/PositionalOption"; import {PositionalOption} from "./directive/options/PositionalOption";
import {FlagOption} from "./directive/options/FlagOption"; 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<any> | string export type OptionDefinition = CLIOption<any> | string
/**
* An error thrown when an invalid option was detected.
*/
export class OptionValidationError extends ErrorWithContext {} export class OptionValidationError extends ErrorWithContext {}
/**
* A base class representing a sub-command in the command-line utility.
*/
@Injectable() @Injectable()
export abstract class Directive extends AppClass { export abstract class Directive extends AppClass {
@Inject() @Inject()
protected readonly logging!: Logging protected readonly logging!: Logging
/** Parsed option values. */
private _optionValues: any 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[] public abstract getKeywords(): string | string[]
/**
* Get the usage description of this directive. Should be brief (1 sentence).
*/
public abstract getDescription(): string public abstract getDescription(): string
/**
* Optionally, specify a longer usage text that is shown on the directive's `--help` page.
*/
public getHelpText(): string { public getHelpText(): string {
return '' return ''
} }
/**
* Get an array of options defined for this command.
*/
public getOptions(): OptionDefinition[] { public getOptions(): OptionDefinition[] {
return [] 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<void> public abstract handle(argv: string[]): void | Promise<void>
/**
* Sets the parsed option values.
* @param optionValues
* @private
*/
private _setOptionValues(optionValues: any) { private _setOptionValues(optionValues: any) {
this._optionValues = optionValues; 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) { public option(name: string, defaultValue?: any) {
if ( name in this._optionValues ) { if ( name in this._optionValues ) {
return this._optionValues[name] return this._optionValues[name]
@ -42,6 +100,15 @@ export abstract class Directive extends AppClass {
return defaultValue 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[]) { async invoke(argv: string[]) {
const options = this.getResolvedOptions() 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<any>[] { public getResolvedOptions(): CLIOption<any>[] {
return this.getOptions().map(option => { return this.getOptions().map(option => {
if ( typeof option === 'string' ) { 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 { public getMainKeyword(): string {
const kws = this.getKeywords() const kws = this.getKeywords()
@ -135,32 +211,60 @@ export abstract class Directive extends AppClass {
return kws return kws
} }
/**
* Returns true if the given keyword should invoke this directive.
* @param name
*/
public matchesKeyword(name: string) { public matchesKeyword(name: string) {
let kws = this.getKeywords() let kws = this.getKeywords()
if ( !Array.isArray(kws) ) kws = [kws] if ( !Array.isArray(kws) ) kws = [kws]
return kws.includes(name) return kws.includes(name)
} }
/**
* Print the given output to the log as success text.
* @param output
*/
success(output: any) { success(output: any) {
this.logging.success(output, true) this.logging.success(output, true)
} }
/**
* Print the given output to the log as error text.
* @param output
*/
error(output: any) { error(output: any) {
this.logging.error(output, true) this.logging.error(output, true)
} }
/**
* Print the given output to the log as warning text.
* @param output
*/
warn(output: any) { warn(output: any) {
this.logging.warn(output, true) this.logging.warn(output, true)
} }
/**
* Print the given output to the log as info text.
* @param output
*/
info(output: any) { info(output: any) {
this.logging.info(output, true) this.logging.info(output, true)
} }
/**
* Print the given output to the log as debugging text.
* @param output
*/
debug(output: any) { debug(output: any) {
this.logging.debug(output, true) this.logging.debug(output, true)
} }
/**
* Print the given output to the log as verbose text.
* @param output
*/
verbose(output: any) { verbose(output: any) {
this.logging.verbose(output, true) this.logging.verbose(output, true)
} }

View File

@ -1,9 +1,64 @@
import {UniversalPath} from '@extollo/util' import {UniversalPath} from '@extollo/util'
/**
* Interface defining a template that can be generated using the TemplateDirective.
*/
export interface Template { 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, name: string,
/**
* The suffix of the file generated by this template.
* @example `.mytemplate.ts`
* @example `.controller.ts`
*/
fileSuffix: string, fileSuffix: string,
/**
* Brief description of the template displayed on the --help page for the TemplateDirective.
* Should be brief (1 sentence).
*/
description: string, description: string,
/**
* Array of path-strings that are resolved relative to the base `app` directory.
* @example `['http', 'controllers']`
* @example `['units']`
*/
baseAppPath: string[], 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<string> render: (name: string, fullCanonicalName: string, targetFilePath: UniversalPath) => string | Promise<string>
} }

View File

@ -4,6 +4,11 @@ import {Unit} from "@extollo/lib"
import {ErrorWithContext} from "@extollo/util" import {ErrorWithContext} from "@extollo/util"
import {CommandLineApplication} from "../service" 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() @Injectable()
export class RunDirective extends Directive { export class RunDirective extends Directive {
getDescription(): string { getDescription(): string {

View File

@ -3,12 +3,20 @@ import {Directive} from "../Directive"
import * as colors from "colors/safe" import * as colors from "colors/safe"
import * as repl from 'repl' 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 { export class ShellDirective extends Directive {
protected options: any = { protected options: any = {
welcome: `powered by Extollo, © ${(new Date).getFullYear()} Garrett Mills\nAccess your application using the "app" global.`, welcome: `powered by Extollo, © ${(new Date).getFullYear()} Garrett Mills\nAccess your application using the "app" global.`,
prompt: `${colors.blue('(')}extollo${colors.blue(') ➤ ')}`, prompt: `${colors.blue('(')}extollo${colors.blue(') ➤ ')}`,
} }
/**
* The created Node.js REPL server.
* @protected
*/
protected repl?: repl.REPLServer protected repl?: repl.REPLServer
getDescription(): string { getDescription(): string {

View File

@ -4,6 +4,9 @@ import {ErrorWithContext} from "@extollo/util"
import {PositionalOption} from "./options/PositionalOption" import {PositionalOption} from "./options/PositionalOption"
import {CommandLine} from "../service" import {CommandLine} from "../service"
/**
* Create a new file based on a template registered with the CommandLine service.
*/
@Injectable() @Injectable()
export class TemplateDirective extends Directive { export class TemplateDirective extends Directive {
@Inject() @Inject()
@ -20,7 +23,7 @@ export class TemplateDirective extends Directive {
getOptions(): OptionDefinition[] { getOptions(): OptionDefinition[] {
const registeredTemplates = this.cli.getTemplates() const registeredTemplates = this.cli.getTemplates()
const template = new PositionalOption('template_name', 'the template to base the new file on (e.g. model, controller)') 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)') 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 => { ...(registeredTemplates.map(template => {
return ` - ${template.name}: ${template.description}` return ` - ${template.name}: ${template.description}`
})) }).all())
].join('\n') ].join('\n')
} }

View File

@ -3,6 +3,10 @@ import {Injectable, Inject} from "@extollo/di"
import {padRight} from "@extollo/util" import {padRight} from "@extollo/util"
import {CommandLine} from "../service" import {CommandLine} from "../service"
/**
* Directive that prints the help message and usage information about
* directives registered with the command line utility.
*/
@Injectable() @Injectable()
export class UsageDirective extends Directive { export class UsageDirective extends Directive {
@Inject() @Inject()

View File

@ -6,9 +6,24 @@ import {CLIOption} from "./CLIOption"
export class FlagOption<T> extends CLIOption<T> { export class FlagOption<T> extends CLIOption<T> {
constructor( constructor(
/**
* The long-form flag for this option.
* @example --path, --create
*/
public readonly longFlag?: string, public readonly longFlag?: string,
/**
* The short-form flag for this option.
* @example -p, -c
*/
public readonly shortFlag?: string, public readonly shortFlag?: string,
/**
* Usage message describing this flag.
*/
public readonly message?: string, 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 public readonly argumentDescription?: string
) { super() } ) { super() }

View File

@ -11,7 +11,14 @@ export class PositionalOption<T> extends CLIOption<T> {
* @param {string} message - message describing the option * @param {string} message - message describing the option
*/ */
constructor( constructor(
/**
* The display name of this positional argument.
* @example path, filename
*/
public readonly name: string, public readonly name: string,
/**
* A usage message describing this parameter.
*/
public readonly message: string = '' public readonly message: string = ''
) { super() } ) { super() }

View File

@ -11,12 +11,19 @@ import {middleware_template} from "../templates/middleware";
import {routes_template} from "../templates/routes"; import {routes_template} from "../templates/routes";
import {config_template} from "../templates/config"; import {config_template} from "../templates/config";
/**
* Service for managing directives, templates, and other resources related
* to the command line utilities.
*/
@Singleton() @Singleton()
export class CommandLine extends Unit { export class CommandLine extends Unit {
@Inject() @Inject()
protected readonly logging!: Logging protected readonly logging!: Logging
/** Directive classes registered with the CLI command. */
protected directives: Collection<Instantiable<Directive>> = new Collection<Instantiable<Directive>>() protected directives: Collection<Instantiable<Directive>> = new Collection<Instantiable<Directive>>()
/** Templates registered with the CLI command. These can be created with the TemplateDirective. */
protected templates: Collection<Template> = new Collection<Template>() protected templates: Collection<Template> = new Collection<Template>()
constructor() { super() } constructor() { super() }
@ -30,10 +37,16 @@ export class CommandLine extends Unit {
this.registerTemplate(config_template) this.registerTemplate(config_template)
} }
/**
* Returns true if the application was started from the command line.
*/
public isCLI() { public isCLI() {
return this.app().hasUnit(CommandLineApplication) return this.app().hasUnit(CommandLineApplication)
} }
/**
* Returns a string containing the Extollo ASCII logo.
*/
public getASCIILogo() { public getASCIILogo() {
return ` _ return ` _
/ /\\ ______ _ _ _ / /\\ ______ _ _ _
@ -45,20 +58,37 @@ export class CommandLine extends Unit {
` `
} }
/**
* Register a Directive class with this service. This will make
* the directive available for use on the CLI.
* @param directiveClass
*/
public registerDirective(directiveClass: Instantiable<Directive>) { public registerDirective(directiveClass: Instantiable<Directive>) {
if ( !this.directives.includes(directiveClass) ) { if ( !this.directives.includes(directiveClass) ) {
this.directives.push(directiveClass) this.directives.push(directiveClass)
} }
} }
/**
* Returns true if the given directive is registered with this service.
* @param directiveClass
*/
public hasDirective(directiveClass: Instantiable<Directive>) { public hasDirective(directiveClass: Instantiable<Directive>) {
return this.directives.includes(directiveClass) return this.directives.includes(directiveClass)
} }
/**
* Get a collection of all registered directives.
*/
public getDirectives() { public getDirectives() {
return this.directives.clone() return this.directives.clone()
} }
/**
* Register the given template with this service. This makes the template
* available for use with the TemplateDirective service.
* @param template
*/
public registerTemplate(template: Template) { public registerTemplate(template: Template) {
if ( !this.templates.firstWhere('name', '=', template.name) ) { if ( !this.templates.firstWhere('name', '=', template.name) ) {
this.templates.push(template) this.templates.push(template)
@ -68,14 +98,25 @@ export class CommandLine extends Unit {
} }
} }
/**
* Returns true if a template with the given name exists.
* @param name
*/
public hasTemplate(name: string) { public hasTemplate(name: string) {
return !!this.templates.firstWhere('name', '=', name) return !!this.templates.firstWhere('name', '=', name)
} }
/**
* Returns the template with the given name, if one exists.
* @param name
*/
public getTemplate(name: string) { public getTemplate(name: string) {
return this.templates.firstWhere('name', '=', name) return this.templates.firstWhere('name', '=', name)
} }
/**
* Get a collection of all registered templates.
*/
public getTemplates() { public getTemplates() {
return this.templates.clone() return this.templates.clone()
} }

View File

@ -7,13 +7,21 @@ import {ShellDirective} from "../directive/ShellDirective";
import {TemplateDirective} from "../directive/TemplateDirective"; import {TemplateDirective} from "../directive/TemplateDirective";
import {RunDirective} from "../directive/RunDirective"; import {RunDirective} from "../directive/RunDirective";
/**
* Unit that takes the place of the final unit in the application that handles
* invocations from the command line.
*/
@Singleton() @Singleton()
export class CommandLineApplication extends Unit { export class CommandLineApplication extends Unit {
/** The unit that was replaced by the CLI app. */
private static replacement?: typeof Unit private static replacement?: typeof Unit
/** Set the replaced unit. */
public static setReplacement(unitClass?: typeof Unit) { public static setReplacement(unitClass?: typeof Unit) {
this.replacement = unitClass this.replacement = unitClass
} }
/** Get the replaced unit. */
public static getReplacement() { public static getReplacement() {
return this.replacement return this.replacement
} }

View File

@ -1,6 +1,9 @@
import {Template} from "../Template" import {Template} from "../Template"
import {UniversalPath} from "@extollo/util" import {UniversalPath} from "@extollo/util"
/**
* A template that generates a new configuration file in the app/configs directory.
*/
const config_template: Template = { const config_template: Template = {
name: 'config', name: 'config',
fileSuffix: '.config.ts', fileSuffix: '.config.ts',

View File

@ -1,6 +1,9 @@
import {Template} from "../Template" import {Template} from "../Template"
import {UniversalPath} from "@extollo/util" import {UniversalPath} from "@extollo/util"
/**
* Template that generates a new controller in the app/http/controllers directory.
*/
const controller_template: Template = { const controller_template: Template = {
name: 'controller', name: 'controller',
fileSuffix: '.controller.ts', fileSuffix: '.controller.ts',

View File

@ -1,6 +1,9 @@
import {Template} from "../Template" import {Template} from "../Template"
import {UniversalPath} from "@extollo/util" import {UniversalPath} from "@extollo/util"
/**
* Template that generates a new Directive class in the app/directives directory.
*/
const directive_template: Template = { const directive_template: Template = {
name: 'directive', name: 'directive',
fileSuffix: '.directive.ts', fileSuffix: '.directive.ts',

View File

@ -1,6 +1,9 @@
import {Template} from "../Template" import {Template} from "../Template"
import {UniversalPath} from "@extollo/util" import {UniversalPath} from "@extollo/util"
/**
* Template that generates a new middleware class in app/http/middlewares.
*/
const middleware_template: Template = { const middleware_template: Template = {
name: 'middleware', name: 'middleware',
fileSuffix: '.middleware.ts', fileSuffix: '.middleware.ts',

View File

@ -1,6 +1,9 @@
import {Template} from "../Template" import {Template} from "../Template"
import {UniversalPath} from "@extollo/util" import {UniversalPath} from "@extollo/util"
/**
* Template that generates a new route definition file in app/http/routes.
*/
const routes_template: Template = { const routes_template: Template = {
name: 'routes', name: 'routes',
fileSuffix: '.routes.ts', fileSuffix: '.routes.ts',

View File

@ -1,6 +1,9 @@
import {Template} from "../Template" import {Template} from "../Template"
import {UniversalPath} from "@extollo/util" import {UniversalPath} from "@extollo/util"
/**
* Template that generates a new application unit class in app/units.
*/
const unit_template: Template = { const unit_template: Template = {
name: 'unit', name: 'unit',
fileSuffix: '.ts', fileSuffix: '.ts',