Import other modules into monorepo
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2021-06-01 20:59:40 -05:00
parent 26d54033af
commit 9be9c44a32
138 changed files with 11544 additions and 139 deletions

View File

@@ -0,0 +1,31 @@
import {Directive} from "../Directive"
import {CommandLineApplication} from "../service"
import {Injectable} from "../../di"
import {ErrorWithContext} from "../../util"
import {Unit} from "../../lifecycle/Unit";
/**
* 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 {
return 'run the application normally'
}
getKeywords(): string | string[] {
return ['run', 'up']
}
async handle(): Promise<void> {
if ( !CommandLineApplication.getReplacement() ) {
throw new ErrorWithContext(`Cannot run application: no run target specified.`)
}
const unit = <Unit> this.make(CommandLineApplication.getReplacement())
await this.app().startUnit(unit)
await this.app().stopUnit(unit)
}
}

View File

@@ -0,0 +1,47 @@
import {Directive} from "../Directive"
import * as colors from "colors/safe"
import * as repl from 'repl'
import {DependencyKey} from "../../di";
/**
* 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 {
return 'launch an interactive shell inside your application'
}
getKeywords(): string | string[] {
return ['shell']
}
getHelpText(): string {
return ''
}
async handle(): Promise<void> {
const state: any = {
app: this.app(),
make: (target: DependencyKey, ...parameters: any[]) => this.make(target, ...parameters),
}
await new Promise<void>(res => {
console.log(this.options.welcome)
this.repl = repl.start(this.options.prompt)
Object.assign(this.repl.context, state)
this.repl.on('exit', () => res())
})
}
}

View File

@@ -0,0 +1,90 @@
import {Directive, OptionDefinition} from "../Directive"
import {PositionalOption} from "./options/PositionalOption"
import {CommandLine} from "../service"
import {Inject, Injectable} from "../../di";
import {ErrorWithContext} from "../../util";
/**
* Create a new file based on a template registered with the CommandLine service.
*/
@Injectable()
export class TemplateDirective extends Directive {
@Inject()
protected readonly cli!: CommandLine
getKeywords(): string | string[] {
return ['new', 'make']
}
getDescription(): string {
return 'create a new file from a registered template'
}
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').all())
const destination = new PositionalOption('file_name', 'canonical name of the file to create (e.g. auth:Group, dash:Activity)')
return [template, destination]
}
getHelpText(): string {
const registeredTemplates = this.cli.getTemplates()
return [
'Modules in Extollo register templates that can be used to quickly create common file types.',
'',
'For example, you can create a new model from @extollo/orm using the "model" template:',
'',
'./ex new model auth:Group',
'',
'This would create a new Group model in the ./src/app/models/auth/Group.model.ts file.',
'',
'AVAILABLE TEMPLATES:',
'',
...(registeredTemplates.map(template => {
return ` - ${template.name}: ${template.description}`
}).all())
].join('\n')
}
async handle(argv: string[]) {
const templateName: string = this.option('template_name')
const destinationName: string = this.option('file_name')
if ( destinationName.includes('/') || destinationName.includes('\\') ) {
this.error(`The destination should be a canonical name, not a file path.`)
this.error(`Reference sub-directories using the : character instead.`)
this.error(`Did you mean ${destinationName.replace(/\/|\\/g, ':')}?`)
process.exitCode = 1
return
}
const template = this.cli.getTemplate(templateName)
if ( !template ) {
throw new ErrorWithContext(`Unable to find template supposedly registered with name: ${templateName}`, {
templateName,
destinationName,
})
}
const name = destinationName.split(':').reverse()[0]
const path = this.app().path('..', 'src', 'app', ...template.baseAppPath, ...(`${destinationName}${template.fileSuffix}`).split(':'))
if ( await path.exists() ) {
this.error(`File already exists: ${path}`)
process.exitCode = 1
return
}
// Make sure the parent direction exists
await path.concat('..').mkdir()
const contents = await template.render(name, destinationName, path.clone())
await path.write(contents)
this.success(`Created new ${template.name} in ${path}`)
}
}

View File

@@ -0,0 +1,54 @@
import {Directive} from "../Directive"
import {Injectable, Inject} from "../../di"
import {padRight} from "../../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()
protected readonly cli!: CommandLine
public getKeywords(): string | string[] {
return 'help'
}
public getDescription(): string {
return 'print information about available commands'
}
public handle(argv: string[]): void | Promise<void> {
const directiveStrings = this.cli.getDirectives()
.map(cls => this.make<Directive>(cls))
.map<[string, string]>(dir => {
return [dir.getMainKeyword(), dir.getDescription()]
})
const maxLen = directiveStrings.max<number>(x => x[0].length)
const printStrings = directiveStrings.map(grp => {
return [padRight(grp[0], maxLen + 1), grp[1]]
})
.map(grp => {
return ` ${grp[0]}: ${grp[1]}`
})
.toArray()
console.log(this.cli.getASCIILogo())
console.log([
'',
'Welcome to Extollo! Specify a command to get started.',
'',
`USAGE: ex <directive> [..options]`,
'',
...printStrings,
'',
'For usage information about a particular command, pass the --help flag.',
'-------------------------------------------',
`powered by Extollo, © ${(new Date).getFullYear()} Garrett Mills`,
].join('\n'))
}
}

View File

@@ -0,0 +1,244 @@
/**
* A CLI option. Supports basic comparative, and set-based validation.
* @class
*/
export abstract class CLIOption<T> {
/**
* Do we use the whitelist?
* @type {boolean}
* @private
*/
protected _useWhitelist: boolean = false
/**
* Do we use the blacklist?
* @type {boolean}
* @private
*/
protected _useBlacklist: boolean = false
/**
* Do we use the less-than comparison?
* @type {boolean}
* @private
*/
protected _useLessThan: boolean = false
/**
* Do we use the greater-than comparison?
* @type {boolean}
* @private
*/
protected _useGreaterThan: boolean = false
/**
* Do we use the equality operator?
* @type {boolean}
* @private
*/
protected _useEquality: boolean = false
/**
* Is this option optional?
* @type {boolean}
* @private
*/
protected _optional: boolean = false
/**
* Whitelisted values.
* @type {Array<*>}
* @private
*/
protected _whitelist: T[] = []
/**
* Blacklisted values.
* @type {Array<*>}
* @private
*/
protected _blacklist: T[] = []
/**
* Value to be compared in less than.
* @type {*}
* @private
*/
protected _lessThanValue?: T
/**
* If true, the less than will be less than or equal to.
* @type {boolean}
* @private
*/
protected _lessThanBit: boolean = false
/**
* Value to be compared in greater than.
* @type {*}
* @private
*/
protected _greaterThanValue?: T
/**
* If true, the greater than will be greater than or equal to.
* @type {boolean}
* @private
*/
protected _greateerThanBit: boolean = false
/**
* The value to be used to check equality.
* @type {*}
* @private
*/
protected _equalityValue?: T
/**
* Whitelist the specified item or items and enable the whitelist.
* @param {...*} items - the items to whitelist
*/
whitelist(...items: T[]) {
this._useWhitelist = true
items.forEach(item => this._whitelist.push(item))
}
/**
* Blacklist the specified item or items and enable the blacklist.
* @param {...*} items - the items to blacklist
*/
blacklist(...items: T[]) {
this._useBlacklist = true
items.forEach(item => this._blacklist.push(item))
}
/**
* Specifies the value to be used in less-than comparison and enables less-than comparison.
* @param {*} value
*/
lessThan(value: T) {
this._useLessThan = true
this._lessThanValue = value
}
/**
* Specifies the value to be used in less-than or equal-to comparison and enables that comparison.
* @param {*} value
*/
lessThanOrEqualTo(value: T) {
this._lessThanBit = true
this.lessThan(value)
}
/**
* Specifies the value to be used in greater-than comparison and enables that comparison.
* @param {*} value
*/
greaterThan(value: T) {
this._useGreaterThan = true
this._greaterThanValue = value
}
/**
* Specifies the value to be used in greater-than or equal-to comparison and enables that comparison.
* @param {*} value
*/
greaterThanOrEqualTo(value: T) {
this._greateerThanBit = true
this.greaterThan(value)
}
/**
* Specifies the value to be used in equality comparison and enables that comparison.
* @param {*} value
*/
equals(value: T) {
this._useEquality = true
this._equalityValue = value
}
/**
* Checks if the specified value passes the configured comparisons.
* @param {*} value
* @returns {boolean}
*/
validate(value: any) {
let is_valid = true
if ( this._useEquality ) {
is_valid = is_valid && (this._equalityValue === value)
}
if ( this._useLessThan && typeof this._lessThanValue !== 'undefined' ) {
if ( this._lessThanBit ) {
is_valid = is_valid && (value <= this._lessThanValue)
} else {
is_valid = is_valid && (value < this._lessThanValue)
}
}
if ( this._useGreaterThan && typeof this._greaterThanValue !== 'undefined' ) {
if ( this._greateerThanBit ) {
is_valid = is_valid && (value >= this._greaterThanValue)
} else {
is_valid = is_valid && (value > this._greaterThanValue)
}
}
if ( this._useWhitelist ) {
is_valid = is_valid && this._whitelist.some(x => {
return x === value
})
}
if ( this._useBlacklist ) {
is_valid = is_valid && !(this._blacklist.some(x => x === value))
}
return is_valid
}
/**
* Sets the Option as optional.
*/
optional() {
this._optional = true
return this
}
/**
* Get the argument name. Should be overridden by child classes.
* @returns {string}
*/
abstract getArgumentName(): string
/**
* Get an array of strings denoting the human-readable requirements for this option to be valid.
* @returns {Array<string>}
*/
getRequirementDisplays() {
const clauses = []
if ( this._useBlacklist ) {
clauses.push(`must not be one of: ${this._blacklist.map(x => String(x)).join(', ')}`)
}
if ( this._useWhitelist ) {
clauses.push(`must be one of: ${this._whitelist.map(x => String(x)).join(', ')}`)
}
if ( this._useGreaterThan ) {
clauses.push(`must be greater than${this._greateerThanBit ? ' or equal to' : ''}: ${String(this._greaterThanValue)}`)
}
if ( this._useLessThan ) {
clauses.push(`must be less than${this._lessThanBit ? ' or equal to' : ''}: ${String(this._lessThanValue)}`)
}
if ( this._useEquality ) {
clauses.push(`must be equal to: ${String(this._equalityValue)}`)
}
return clauses
}
}

View File

@@ -0,0 +1,45 @@
import {CLIOption} from "./CLIOption"
/**
* Non-positional, flag-based CLI option.
*/
export class FlagOption<T> extends CLIOption<T> {
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() }
/**
* Get the referential name for this option.
* Defaults to the long flag (without the '--'). If this cannot
* be found, the short flag (without the '-') is used.
* @returns {string}
*/
getArgumentName() {
if ( this.longFlag ) {
return this.longFlag.replace('--', '')
} else if ( this.shortFlag ) {
return this.shortFlag.replace('-', '')
}
throw new Error('Missing either a long- or short-flag for FlagOption.')
}
}

View File

@@ -0,0 +1,32 @@
import {CLIOption} from "./CLIOption"
/**
* A positional CLI option. Defined without a flag.
*/
export class PositionalOption<T> extends CLIOption<T> {
/**
* Instantiate the option.
* @param {string} name - the name of the option
* @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() }
/**
* Gets the name of the option.
* @returns {string}
*/
getArgumentName () {
return this.name
}
}