Add support for templates

This commit is contained in:
Garrett Mills 2021-03-21 19:46:32 -05:00
parent fb85e42dee
commit cae260b9ce
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
7 changed files with 186 additions and 7 deletions

View File

@ -24,6 +24,10 @@ export abstract class Directive extends AppClass {
return '' return ''
} }
public getOptions(): OptionDefinition[] {
return []
}
public abstract handle(argv: string[]): void | Promise<void> public abstract handle(argv: string[]): void | Promise<void>
private _setOptionValues(optionValues: any) { private _setOptionValues(optionValues: any) {
@ -111,10 +115,6 @@ export abstract class Directive extends AppClass {
} }
} }
public getOptions(): OptionDefinition[] {
return []
}
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' ) {

9
src/Template.ts Normal file
View File

@ -0,0 +1,9 @@
import {UniversalPath} from '@extollo/util'
export interface Template {
name: string,
fileSuffix: string,
description: string,
baseAppPath: string[],
render: (name: string, fullCanonicalName: string, targetFilePath: UniversalPath) => string | Promise<string>
}

View File

@ -0,0 +1,87 @@
import {Directive, OptionDefinition} from "../Directive"
import {Injectable, Inject} from "@extollo/di"
import {ErrorWithContext} from "@extollo/util"
import {PositionalOption} from "./options/PositionalOption"
import {CommandLine} from "../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'))
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}`
}))
].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

@ -1,15 +1,25 @@
import {Unit} from "@extollo/lib" import {Unit, Logging} from "@extollo/lib"
import {Singleton, Instantiable} from "@extollo/di" import {Singleton, Instantiable, Inject} from "@extollo/di"
import {Collection} from "@extollo/util" import {Collection} from "@extollo/util"
import {CommandLineApplication} from "./CommandLineApplication" import {CommandLineApplication} from "./CommandLineApplication"
import {Directive} from "../Directive"; import {Directive} from "../Directive"
import {Template} from "../Template"
import {directive_template} from "../templates/directive"
@Singleton() @Singleton()
export class CommandLine extends Unit { export class CommandLine extends Unit {
@Inject()
protected readonly logging!: Logging
protected directives: Collection<Instantiable<Directive>> = new Collection<Instantiable<Directive>>() protected directives: Collection<Instantiable<Directive>> = new Collection<Instantiable<Directive>>()
protected templates: Collection<Template> = new Collection<Template>()
constructor() { super() } constructor() { super() }
async up() {
this.registerTemplate(directive_template)
}
public isCLI() { public isCLI() {
return this.app().hasUnit(CommandLineApplication) return this.app().hasUnit(CommandLineApplication)
} }
@ -36,4 +46,25 @@ export class CommandLine extends Unit {
public getDirectives() { public getDirectives() {
return this.directives.clone() return this.directives.clone()
} }
public registerTemplate(template: Template) {
if ( !this.templates.firstWhere('name', '=', template.name) ) {
this.templates.push(template)
} else {
this.logging.warn(`Duplicate template will not be registered: ${template.name}`)
this.logging.debug(`Duplicate template registered at: ${(new Error()).stack}`)
}
}
public hasTemplate(name: string) {
return !!this.templates.firstWhere('name', '=', name)
}
public getTemplate(name: string) {
return this.templates.firstWhere('name', '=', name)
}
public getTemplates() {
return this.templates.clone()
}
} }

View File

@ -4,6 +4,7 @@ import {CommandLine} from "./CommandLine"
import {UsageDirective} from "../directive/UsageDirective"; import {UsageDirective} from "../directive/UsageDirective";
import {Directive} from "../Directive"; import {Directive} from "../Directive";
import {ShellDirective} from "../directive/ShellDirective"; import {ShellDirective} from "../directive/ShellDirective";
import {TemplateDirective} from "../directive/TemplateDirective";
@Singleton() @Singleton()
export class CommandLineApplication extends Unit { export class CommandLineApplication extends Unit {
@ -23,6 +24,7 @@ export class CommandLineApplication extends Unit {
public async up() { public async up() {
this.cli.registerDirective(UsageDirective) this.cli.registerDirective(UsageDirective)
this.cli.registerDirective(ShellDirective) this.cli.registerDirective(ShellDirective)
this.cli.registerDirective(TemplateDirective)
const argv = process.argv.slice(2) const argv = process.argv.slice(2)
const match = this.cli.getDirectives() const match = this.cli.getDirectives()

View File

@ -0,0 +1,40 @@
import {Template} from "../Template"
import {UniversalPath} from "@extollo/util"
const directive_template: Template = {
name: 'directive',
fileSuffix: '.directive.ts',
description: 'Create a new Directive class which adds functionality to the ./ex command.',
baseAppPath: ['directives'],
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
return `import {Directive, OptionDefinition} from "@extollo/cli"
import {Injectable} from "@extollo/di"
/**
* ${name} Directive
* ---------------------------------------------------
* Put some description here.
*/
@Injectable()
export class ${name}Directive extends Directive {
getKeywords(): string | string[] {
return ['${name.toLowerCase()}']
}
getDescription(): string {
return ''
}
getOptions(): OptionDefinition[] {
return []
}
async handle(argv: string[]) {
}
}
`
}
}
export { directive_template }

10
src/tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
`{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"sourceMap": true
},
"exclude": [
"node_modules"
]
}