From a28601e9576cfc64a4d0a5c3161070f770bf23ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lenon?= Date: Thu, 21 Apr 2022 17:15:05 -0300 Subject: [PATCH 1/4] feat: add route and list command alias --- package-lock.json | 99 +++++++++++++++++++---- package.json | 6 +- src/Artisan.ts | 60 +++++++++++++- src/Commands/Command.ts | 28 +++++++ src/Commands/List/Eslint.ts | 52 ++++++++++++ src/Commands/List/Make.ts | 52 ++++++++++++ src/Commands/List/Route.ts | 52 ++++++++++++ src/Commands/Route/List.ts | 106 +++++++++++++++++++++++++ src/Contracts/TableOptions.ts | 34 ++++++++ src/Utils/ListHelper.ts | 14 ++++ tests/Stubs/Kernel.ts | 4 + tests/Unit/Commands/List/EslintTest.ts | 38 +++++++++ tests/Unit/Commands/List/MakeTest.ts | 38 +++++++++ tests/Unit/Commands/List/RouteTest.ts | 38 +++++++++ 14 files changed, 601 insertions(+), 20 deletions(-) create mode 100644 src/Commands/List/Eslint.ts create mode 100644 src/Commands/List/Make.ts create mode 100644 src/Commands/List/Route.ts create mode 100644 src/Commands/Route/List.ts create mode 100644 src/Contracts/TableOptions.ts create mode 100644 src/Utils/ListHelper.ts create mode 100644 tests/Unit/Commands/List/EslintTest.ts create mode 100644 tests/Unit/Commands/List/MakeTest.ts create mode 100644 tests/Unit/Commands/List/RouteTest.ts diff --git a/package-lock.json b/package-lock.json index 9478136..a0d6b65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/artisan", - "version": "1.0.0", + "version": "1.0.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@athenna/artisan", - "version": "1.0.0", + "version": "1.0.7", "license": "MIT", "dependencies": { "@athenna/config": "1.0.8", @@ -14,17 +14,19 @@ "@athenna/logger": "1.1.8", "@secjs/utils": "1.8.3", "chalk-rainbow": "1.0.0", + "cli-table": "^0.3.11", + "columnify": "^1.6.0", "commander": "9.2.0", "ejs": "3.1.6", "figlet": "1.5.2", - "lodash": "^4.17.21", "ora": "5.4.1", "reflect-metadata": "0.1.13", "tscpaths": "0.0.9" }, "devDependencies": { + "@types/cli-table": "0.3.0", + "@types/columnify": "1.5.1", "@types/jest": "27.0.1", - "@types/lodash": "^4.14.182", "@types/node": "14.17.0", "@typescript-eslint/eslint-plugin": "4.31.0", "@typescript-eslint/parser": "4.31.0", @@ -2028,6 +2030,18 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/cli-table": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@types/cli-table/-/cli-table-0.3.0.tgz", + "integrity": "sha512-QnZUISJJXyhyD6L1e5QwXDV/A5i2W1/gl6D6YMc8u0ncPepbv/B4w3S+izVvtAg60m6h+JP09+Y/0zF2mojlFQ==", + "dev": true + }, + "node_modules/@types/columnify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/columnify/-/columnify-1.5.1.tgz", + "integrity": "sha512-pXWQTEvwcO7GAwFvSvaMOIktLcJ2ajIAUyMR93KDZZVWN2G7uieEDBPJskEmXb0Qv8JMPHhaRA52azCPfiFmQA==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", @@ -2121,12 +2135,6 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, - "node_modules/@types/lodash": { - "version": "4.14.182", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", - "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", - "dev": true - }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -3633,6 +3641,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "dependencies": { + "colors": "1.0.3" + }, + "engines": { + "node": ">= 0.2.0" + } + }, "node_modules/cli-truncate": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", @@ -3846,6 +3865,26 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==" }, + "node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/columnify": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz", + "integrity": "sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==", + "dependencies": { + "strip-ansi": "^6.0.1", + "wcwidth": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -15387,6 +15426,18 @@ "@babel/types": "^7.3.0" } }, + "@types/cli-table": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@types/cli-table/-/cli-table-0.3.0.tgz", + "integrity": "sha512-QnZUISJJXyhyD6L1e5QwXDV/A5i2W1/gl6D6YMc8u0ncPepbv/B4w3S+izVvtAg60m6h+JP09+Y/0zF2mojlFQ==", + "dev": true + }, + "@types/columnify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/columnify/-/columnify-1.5.1.tgz", + "integrity": "sha512-pXWQTEvwcO7GAwFvSvaMOIktLcJ2ajIAUyMR93KDZZVWN2G7uieEDBPJskEmXb0Qv8JMPHhaRA52azCPfiFmQA==", + "dev": true + }, "@types/eslint": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", @@ -15480,12 +15531,6 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, - "@types/lodash": { - "version": "4.14.182", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", - "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", - "dev": true - }, "@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -16643,6 +16688,14 @@ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==" }, + "cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "requires": { + "colors": "1.0.3" + } + }, "cli-truncate": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", @@ -16811,6 +16864,20 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==" }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + }, + "columnify": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz", + "integrity": "sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==", + "requires": { + "strip-ansi": "^6.0.1", + "wcwidth": "^1.0.0" + } + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", diff --git a/package.json b/package.json index 8fe0c90..93ae3bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/artisan", - "version": "1.0.6", + "version": "1.0.7", "description": "", "license": "MIT", "author": "João Lenon ", @@ -19,6 +19,8 @@ "typescript" ], "devDependencies": { + "@types/cli-table": "0.3.0", + "@types/columnify": "1.5.1", "@types/jest": "27.0.1", "@types/node": "14.17.0", "@typescript-eslint/eslint-plugin": "4.31.0", @@ -160,6 +162,8 @@ "@athenna/logger": "1.1.8", "@secjs/utils": "1.8.3", "chalk-rainbow": "1.0.0", + "cli-table": "^0.3.11", + "columnify": "^1.6.0", "commander": "9.2.0", "ejs": "3.1.6", "figlet": "1.5.2", diff --git a/src/Artisan.ts b/src/Artisan.ts index 78f6cf3..d15f127 100644 --- a/src/Artisan.ts +++ b/src/Artisan.ts @@ -8,6 +8,7 @@ */ import figlet from 'figlet' +import columnify from 'columnify' import chalkRainbow from 'chalk-rainbow' import { Path } from '@secjs/utils' @@ -46,6 +47,48 @@ export class Artisan { ]) } + /** + * List all commands with description. + * + * @param asColumn + * @param alias + */ + listCommands(asColumn: boolean, alias?: string): string + listCommands( + asColumn = false, + alias?: string, + ): Record | string { + const commands = {} + + this.commander.commands.forEach((command: any) => { + if (alias && !command._name.startsWith(`${alias}:`)) { + return + } + + let name = command._name + + if (command.options.length) name = `${name} [options]` + + command._args.forEach(arg => { + if (arg.required) { + name = name.concat(` <${arg._name}>`) + + return + } + + name = name.concat(` [${arg._name}]`) + }) + + commands[name] = command._description + }) + + if (asColumn) { + return columnify(commands).replace('KEY', '').replace('VALUE', '') + } + + return commands + } + /** * Set the version of the CLI. * @@ -117,9 +160,20 @@ export class Artisan { * * @return Promise */ - async main(): Promise { - if (process.argv.length === 2) { - const appNameFiglet = figlet.textSync(Config.get('app.name')) + async main(appName?: string): Promise { + /** + * If argv is less or equal two, means that + * the command that are being run is just + * the CLI entrypoint. Example: + * + * - artisan + * - node artisan + * + * In CLI entrypoint we are going to log the + * chalkRainbow with his application name. + */ + if (process.argv.length <= 2) { + const appNameFiglet = figlet.textSync(appName || Config.get('app.name')) const appNameFigletColorized = chalkRainbow(appNameFiglet) process.stdout.write(appNameFigletColorized + '\n' + '\n') diff --git a/src/Commands/Command.ts b/src/Commands/Command.ts index 402bd02..f47a21f 100644 --- a/src/Commands/Command.ts +++ b/src/Commands/Command.ts @@ -9,11 +9,14 @@ import ora from 'ora' import chalk from 'chalk' +import Table from 'cli-table' +import columnify from 'columnify' import { Log } from '@athenna/logger' import { Exec } from 'src/Utils/Exec' import { Config } from '@athenna/config' import { Command as Commander } from 'commander' +import { TableOptions } from 'src/Contracts/TableOptions' import { NodeExecException } from 'src/Exceptions/NodeExecException' export abstract class Command { @@ -115,6 +118,31 @@ export abstract class Command { return ora(options) } + /** + * Log a table using cli-table API. + * + * @return {ora.Ora} + */ + public logTable(tableOptions: Partial, ...rows: any[]): void { + const table = new Table(tableOptions) + + if (rows && rows.length) table.push(...rows) + + process.stdout.write(table.toString() + '\n') + } + + /** + * Log a table using cli-table API. + * + * @return {ora.Ora} + */ + public columnify( + data: Record | any[], + options: columnify.GlobalOptions, + ): string { + return columnify(data, options) + } + /** * Execute a command using the exec of child process. * diff --git a/src/Commands/List/Eslint.ts b/src/Commands/List/Eslint.ts new file mode 100644 index 0000000..4aa3e7c --- /dev/null +++ b/src/Commands/List/Eslint.ts @@ -0,0 +1,52 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Artisan } from 'src/Facades/Artisan' +import { Command } from 'src/Commands/Command' +import { Command as Commander } from 'commander' + +export class Eslint extends Command { + /** + * The name and signature of the console command. + */ + protected signature = 'list:eslint' + + /** + * The console command description. + */ + protected description = 'List all eslint commands.' + + /** + * Set additional flags in the commander instance. + * This method is executed when registering your command. + * + * @return {void} + */ + public addFlags(commander: Commander): Commander { + return commander + } + + /** + * Execute the console command. + * + * @return {Promise} + */ + async handle(): Promise { + this.simpleLog('[ LISTING ESLINT ]', 'bold', 'green') + + const commands = + 'Commands:' + + Artisan.listCommands(true, 'eslint') + .split('\n') + .map(line => ` ${line}`) + .join('\n') + + this.simpleLog(commands) + } +} diff --git a/src/Commands/List/Make.ts b/src/Commands/List/Make.ts new file mode 100644 index 0000000..35a7d2f --- /dev/null +++ b/src/Commands/List/Make.ts @@ -0,0 +1,52 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Artisan } from 'src/Facades/Artisan' +import { Command } from 'src/Commands/Command' +import { Command as Commander } from 'commander' + +export class Make extends Command { + /** + * The name and signature of the console command. + */ + protected signature = 'list:make' + + /** + * The console command description. + */ + protected description = 'List all make commands.' + + /** + * Set additional flags in the commander instance. + * This method is executed when registering your command. + * + * @return {void} + */ + public addFlags(commander: Commander): Commander { + return commander + } + + /** + * Execute the console command. + * + * @return {Promise} + */ + async handle(): Promise { + this.simpleLog('[ LISTING MAKE ]', 'bold', 'green') + + const commands = + 'Commands:' + + Artisan.listCommands(true, 'make') + .split('\n') + .map(line => ` ${line}`) + .join('\n') + + this.simpleLog(commands) + } +} diff --git a/src/Commands/List/Route.ts b/src/Commands/List/Route.ts new file mode 100644 index 0000000..ec7b7d7 --- /dev/null +++ b/src/Commands/List/Route.ts @@ -0,0 +1,52 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Artisan } from 'src/Facades/Artisan' +import { Command } from 'src/Commands/Command' +import { Command as Commander } from 'commander' + +export class Route extends Command { + /** + * The name and signature of the console command. + */ + protected signature = 'list:route' + + /** + * The console command description. + */ + protected description = 'List all route commands.' + + /** + * Set additional flags in the commander instance. + * This method is executed when registering your command. + * + * @return {void} + */ + public addFlags(commander: Commander): Commander { + return commander + } + + /** + * Execute the console command. + * + * @return {Promise} + */ + async handle(): Promise { + this.simpleLog('[ LISTING ROUTE ]', 'bold', 'green') + + const commands = + 'Commands:' + + Artisan.listCommands(true, 'route') + .split('\n') + .map(line => ` ${line}`) + .join('\n') + + this.simpleLog(commands) + } +} diff --git a/src/Commands/Route/List.ts b/src/Commands/Route/List.ts new file mode 100644 index 0000000..c5611ce --- /dev/null +++ b/src/Commands/Route/List.ts @@ -0,0 +1,106 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Path, String } from '@secjs/utils' +import { Command } from 'src/Commands/Command' +import { Command as Commander } from 'commander' +import { existsSync } from 'fs' + +export class List extends Command { + /** + * The name and signature of the console command. + */ + protected signature = 'route:list' + + /** + * The console command description. + */ + protected description = 'List all the routes of your application.' + + /** + * Set additional flags in the commander instance. + * This method is executed when registering your command. + * + * @return {void} + */ + public addFlags(commander: Commander): Commander { + return commander.option( + '-m, --middleware', + 'List the middlewares of each route.', + false, + ) + } + + /** + * Execute the console command. + * + * @return {Promise} + */ + async handle(options: any): Promise { + this.simpleLog('[ ROUTE LISTING ]', 'bold', 'green') + + const Route = ioc.use('Athenna/Core/HttpRoute') + + if (!Route) { + this.error('The facade ({yellow} "Route") has not been found.') + + return + } + + let routePath = Path.pwd('routes/http.ts') + + if (!existsSync(routePath)) { + if (!existsSync(routePath.replace('ts', 'js'))) { + this.error( + `The file ({yellow} "routes/http") has not been found in any extension (js, ts).`, + ) + + return + } else { + routePath = routePath.replace('ts', 'js') + } + } + + await import(routePath) + const routes = Route.listRoutes() + + const header = ['Method', 'Route', 'Handler'] + const rows: any[] = [] + + if (options.middleware) header.push('Middlewares') + + routes.forEach(route => { + const row = [route.methods.join('|'), route.url, 'Closure'] + + if (options.middleware) { + let middlewares = '' + + Object.keys(route.middlewares).forEach(key => { + if (route.middlewares[key].length) { + const number = route.middlewares[key].length + + if (middlewares) { + middlewares = middlewares + '\n' + } + + middlewares = middlewares + `${String.toPascalCase(key)}: ${number}` + } + }) + + if (!middlewares) middlewares = 'Not found' + + row.push(middlewares) + } + + rows.push(row) + }) + + this.logTable({ head: header }, ...rows) + } +} diff --git a/src/Contracts/TableOptions.ts b/src/Contracts/TableOptions.ts new file mode 100644 index 0000000..0b3ea45 --- /dev/null +++ b/src/Contracts/TableOptions.ts @@ -0,0 +1,34 @@ +export interface TableOptions { + chars: Partial< + Record< + | 'top' + | 'top-mid' + | 'top-left' + | 'top-right' + | 'bottom' + | 'bottom-mid' + | 'bottom-left' + | 'bottom-right' + | 'left' + | 'left-mid' + | 'mid' + | 'mid-mid' + | 'right' + | 'right-mid' + | 'middle', + string + > + > + truncate: string + colors: boolean + colWidths: number[] + colAligns: Array<'left' | 'middle' | 'right'> + style: Partial<{ + 'padding-left': number + 'padding-right': number + head: string[] + border: string[] + compact: boolean + }> + head: string[] +} diff --git a/src/Utils/ListHelper.ts b/src/Utils/ListHelper.ts new file mode 100644 index 0000000..d1c7402 --- /dev/null +++ b/src/Utils/ListHelper.ts @@ -0,0 +1,14 @@ +export class ListHelper { + /** + * Get all commands signature and description from command. + * + * @param command + */ + static async getAllCommandsFrom(command: string) { + // const commands: any = {} + + // await Artisan.call(`${command} --help`) + + return command + } +} diff --git a/tests/Stubs/Kernel.ts b/tests/Stubs/Kernel.ts index eac9989..97cce20 100644 --- a/tests/Stubs/Kernel.ts +++ b/tests/Stubs/Kernel.ts @@ -16,7 +16,11 @@ export class Kernel extends ConsoleKernel { * @return void */ protected commands = [ + import('src/Commands/Route/List'), import('src/Commands/Eslint/Fix'), + import('src/Commands/List/Make'), + import('src/Commands/List/Route'), + import('src/Commands/List/Eslint'), import('src/Commands/Make/Facade'), import('src/Commands/Make/Command'), import('src/Commands/Make/Service'), diff --git a/tests/Unit/Commands/List/EslintTest.ts b/tests/Unit/Commands/List/EslintTest.ts new file mode 100644 index 0000000..4f0d3ff --- /dev/null +++ b/tests/Unit/Commands/List/EslintTest.ts @@ -0,0 +1,38 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import '@athenna/ioc' +import { Config } from '@athenna/config' +import { Kernel } from 'tests/Stubs/Kernel' +import { Folder, Path } from '@secjs/utils' +import { Artisan } from 'src/Facades/Artisan' +import { ArtisanProvider } from 'src/Providers/ArtisanProvider' +import { LoggerProvider } from '@athenna/logger/src/Providers/LoggerProvider' + +describe('\n ListEslintTest', () => { + beforeEach(async () => { + new Folder(Path.tests('Stubs/providers')).loadSync().copySync(Path.providers()) + new Folder(Path.tests('Stubs/config')).loadSync().copySync(Path.config()) + + await Config.load() + new LoggerProvider().register() + new ArtisanProvider().register() + await new Kernel().registerCommands() + }) + + it('should be able to list all commands from eslint', async () => { + await Artisan.call('list:eslint') + }, 60000) + + afterEach(async () => { + await Folder.safeRemove(Path.providers()) + await Folder.safeRemove(Path.config()) + await Folder.safeRemove(Path.app()) + }) +}) diff --git a/tests/Unit/Commands/List/MakeTest.ts b/tests/Unit/Commands/List/MakeTest.ts new file mode 100644 index 0000000..c0c06aa --- /dev/null +++ b/tests/Unit/Commands/List/MakeTest.ts @@ -0,0 +1,38 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import '@athenna/ioc' +import { Config } from '@athenna/config' +import { Kernel } from 'tests/Stubs/Kernel' +import { Artisan } from 'src/Facades/Artisan' +import { Folder, Path } from '@secjs/utils' +import { ArtisanProvider } from 'src/Providers/ArtisanProvider' +import { LoggerProvider } from '@athenna/logger/src/Providers/LoggerProvider' + +describe('\n ListMakeTest', () => { + beforeEach(async () => { + new Folder(Path.tests('Stubs/providers')).loadSync().copySync(Path.providers()) + new Folder(Path.tests('Stubs/config')).loadSync().copySync(Path.config()) + + await Config.load() + new LoggerProvider().register() + new ArtisanProvider().register() + await new Kernel().registerCommands() + }) + + it('should be able to list all commands from make', async () => { + await Artisan.call('list:make') + }, 60000) + + afterEach(async () => { + await Folder.safeRemove(Path.providers()) + await Folder.safeRemove(Path.config()) + await Folder.safeRemove(Path.app()) + }) +}) diff --git a/tests/Unit/Commands/List/RouteTest.ts b/tests/Unit/Commands/List/RouteTest.ts new file mode 100644 index 0000000..9481785 --- /dev/null +++ b/tests/Unit/Commands/List/RouteTest.ts @@ -0,0 +1,38 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import '@athenna/ioc' +import { Config } from '@athenna/config' +import { Kernel } from 'tests/Stubs/Kernel' +import { Artisan } from 'src/Facades/Artisan' +import { Folder, Path } from '@secjs/utils' +import { ArtisanProvider } from 'src/Providers/ArtisanProvider' +import { LoggerProvider } from '@athenna/logger/src/Providers/LoggerProvider' + +describe('\n ListRouteTest', () => { + beforeEach(async () => { + new Folder(Path.tests('Stubs/providers')).loadSync().copySync(Path.providers()) + new Folder(Path.tests('Stubs/config')).loadSync().copySync(Path.config()) + + await Config.load() + new LoggerProvider().register() + new ArtisanProvider().register() + await new Kernel().registerCommands() + }) + + it('should be able to list all commands from route', async () => { + await Artisan.call('list:route') + }, 60000) + + afterEach(async () => { + await Folder.safeRemove(Path.providers()) + await Folder.safeRemove(Path.config()) + await Folder.safeRemove(Path.app()) + }) +}) From 0f051497b7d49ad24adfc62e2989b0ed5056c407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lenon?= Date: Thu, 21 Apr 2022 20:27:57 -0300 Subject: [PATCH 2/4] feat: add start, build and start:dev commands --- package-lock.json | 4 +-- package.json | 4 +-- src/Commands/Build.ts | 61 +++++++++++++++++++++++++++++++++ src/Commands/Command.ts | 14 +++++++- src/Commands/Eslint/Fix.ts | 13 +++++-- src/Commands/List/Eslint.ts | 2 +- src/Commands/List/Make.ts | 2 +- src/Commands/List/Route.ts | 2 +- src/Commands/Make/Command.ts | 2 +- src/Commands/Make/Controller.ts | 2 +- src/Commands/Make/Facade.ts | 2 +- src/Commands/Make/Middleware.ts | 2 +- src/Commands/Make/Provider.ts | 2 +- src/Commands/Make/Service.ts | 2 +- src/Commands/Route/List.ts | 2 +- src/Commands/Start.ts | 57 ++++++++++++++++++++++++++++++ src/Commands/StartDev.ts | 60 ++++++++++++++++++++++++++++++++ 17 files changed, 215 insertions(+), 18 deletions(-) create mode 100644 src/Commands/Build.ts create mode 100644 src/Commands/Start.ts create mode 100644 src/Commands/StartDev.ts diff --git a/package-lock.json b/package-lock.json index a0d6b65..9598b8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,8 @@ "@athenna/logger": "1.1.8", "@secjs/utils": "1.8.3", "chalk-rainbow": "1.0.0", - "cli-table": "^0.3.11", - "columnify": "^1.6.0", + "cli-table": "0.3.11", + "columnify": "1.6.0", "commander": "9.2.0", "ejs": "3.1.6", "figlet": "1.5.2", diff --git a/package.json b/package.json index 93ae3bd..081f1b0 100644 --- a/package.json +++ b/package.json @@ -162,8 +162,8 @@ "@athenna/logger": "1.1.8", "@secjs/utils": "1.8.3", "chalk-rainbow": "1.0.0", - "cli-table": "^0.3.11", - "columnify": "^1.6.0", + "cli-table": "0.3.11", + "columnify": "1.6.0", "commander": "9.2.0", "ejs": "3.1.6", "figlet": "1.5.2", diff --git a/src/Commands/Build.ts b/src/Commands/Build.ts new file mode 100644 index 0000000..ad5c891 --- /dev/null +++ b/src/Commands/Build.ts @@ -0,0 +1,61 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Path } from '@secjs/utils' +import { Command } from 'src/Commands/Command' +import { Command as Commander } from 'commander' + +export class Build extends Command { + /** + * The name and signature of the console command. + */ + protected signature = 'build' + + /** + * The console command description. + */ + protected description = 'Compile project from Typescript to Javascript.' + + /** + * Set additional flags in the commander instance. + * This method is executed when registering your command. + * + * @return {void} + */ + public addFlags(commander: Commander): Commander { + return commander + } + + /** + * Execute the console command. + * + * @return {Promise} + */ + async handle(_: any, commander: any): Promise { + this.simpleLog('[ BUILDING PROJECT ]', 'rmNewLineStart', 'bold', 'green') + + try { + const rimrafPath = Path.noBuild().pwd('node_modules/.bin/rimraf') + let tscPath = Path.noBuild().pwd('node_modules/.bin/tsc') + + const tscArgs = commander.args.join(' ') + + if (tscArgs) { + tscPath = tscPath.concat(` ${tscArgs}`) + } + + await this.execCommand(`${rimrafPath} dist`) + await this.execCommand(tscPath) + + this.success('Built successfully.') + } catch (error) { + this.error('Failed to build project.') + } + } +} diff --git a/src/Commands/Command.ts b/src/Commands/Command.ts index f47a21f..68f97b7 100644 --- a/src/Commands/Command.ts +++ b/src/Commands/Command.ts @@ -55,13 +55,25 @@ export abstract class Command { public simpleLog(message: string, ...chalkArgs: string[]): void { let colors = chalk + const rmNewLineStart = chalkArgs[0] === 'rmNewLineStart' + + if (rmNewLineStart) chalkArgs.shift() + chalkArgs.forEach(arg => (colors = colors[arg])) if (Config.get('logging.channels.console.driver') === 'null') { return } - process.stdout.write('\n' + colors(message) + '\n') + const log = colors(message).concat('\n') + + if (rmNewLineStart) { + process.stdout.write(log) + + return + } + + process.stdout.write('\n'.concat(log)) } /** diff --git a/src/Commands/Eslint/Fix.ts b/src/Commands/Eslint/Fix.ts index 355374f..c360b37 100644 --- a/src/Commands/Eslint/Fix.ts +++ b/src/Commands/Eslint/Fix.ts @@ -10,6 +10,8 @@ import { parse } from 'path' import { Command } from 'src/Commands/Command' import { Command as Commander } from 'commander' +import { Path } from '@secjs/utils' +import { existsSync } from 'fs' export class Fix extends Command { /** @@ -46,6 +48,7 @@ export class Fix extends Command { if (!options.quiet) { this.simpleLog( `[ LINTING ${options.resource.toUpperCase()} ]`, + 'rmNewLineStart', 'bold', 'green', ) @@ -54,9 +57,13 @@ export class Fix extends Command { const { name } = parse(filePath) try { - await this.execCommand( - `./node_modules/.bin/eslint ${filePath} --fix --quiet`, - ) + let eslintPath = Path.noBuild().pwd('node_modules/.bin/eslint') + + if (!existsSync(eslintPath)) { + eslintPath = './node_modules/.bin/eslint' + } + + await this.execCommand(`${eslintPath} ${filePath} --fix --quiet`) if (!options.quiet) { this.success( diff --git a/src/Commands/List/Eslint.ts b/src/Commands/List/Eslint.ts index 4aa3e7c..33061e2 100644 --- a/src/Commands/List/Eslint.ts +++ b/src/Commands/List/Eslint.ts @@ -38,7 +38,7 @@ export class Eslint extends Command { * @return {Promise} */ async handle(): Promise { - this.simpleLog('[ LISTING ESLINT ]', 'bold', 'green') + this.simpleLog('[ LISTING ESLINT ]', 'rmNewLineStart', 'bold', 'green') const commands = 'Commands:' + diff --git a/src/Commands/List/Make.ts b/src/Commands/List/Make.ts index 35a7d2f..6e931f4 100644 --- a/src/Commands/List/Make.ts +++ b/src/Commands/List/Make.ts @@ -38,7 +38,7 @@ export class Make extends Command { * @return {Promise} */ async handle(): Promise { - this.simpleLog('[ LISTING MAKE ]', 'bold', 'green') + this.simpleLog('[ LISTING MAKE ]', 'rmNewLineStart', 'bold', 'green') const commands = 'Commands:' + diff --git a/src/Commands/List/Route.ts b/src/Commands/List/Route.ts index ec7b7d7..facfe30 100644 --- a/src/Commands/List/Route.ts +++ b/src/Commands/List/Route.ts @@ -38,7 +38,7 @@ export class Route extends Command { * @return {Promise} */ async handle(): Promise { - this.simpleLog('[ LISTING ROUTE ]', 'bold', 'green') + this.simpleLog('[ LISTING ROUTE ]', 'rmNewLineStart', 'bold', 'green') const commands = 'Commands:' + diff --git a/src/Commands/Make/Command.ts b/src/Commands/Make/Command.ts index cdef5b3..14903f6 100644 --- a/src/Commands/Make/Command.ts +++ b/src/Commands/Make/Command.ts @@ -53,7 +53,7 @@ export class Command extends AbstractCommand { * @return {Promise} */ async handle(name: string, options: any): Promise { - this.simpleLog('[ MAKING COMMAND ]', 'bold', 'green') + this.simpleLog('[ MAKING COMMAND ]', 'rmNewLineStart', 'bold', 'green') name = TemplateHelper.normalizeName(name, 'Command') const template = TemplateHelper.getTemplate('__name__Command', options) diff --git a/src/Commands/Make/Controller.ts b/src/Commands/Make/Controller.ts index 05a41e4..a393571 100644 --- a/src/Commands/Make/Controller.ts +++ b/src/Commands/Make/Controller.ts @@ -48,7 +48,7 @@ export class Controller extends Command { * @return {Promise} */ async handle(name: string, options: any): Promise { - this.simpleLog('[ MAKING CONTROLLER ]', 'bold', 'green') + this.simpleLog('[ MAKING CONTROLLER ]', 'rmNewLineStart', 'bold', 'green') name = TemplateHelper.normalizeName(name, 'Controller') const template = TemplateHelper.getTemplate('__name__Controller', options) diff --git a/src/Commands/Make/Facade.ts b/src/Commands/Make/Facade.ts index d6aab13..19ee170 100644 --- a/src/Commands/Make/Facade.ts +++ b/src/Commands/Make/Facade.ts @@ -48,7 +48,7 @@ export class Facade extends Command { * @return {Promise} */ async handle(name: string, options: any): Promise { - this.simpleLog('[ MAKING FACADE ]', 'bold', 'green') + this.simpleLog('[ MAKING FACADE ]', 'rmNewLineStart', 'bold', 'green') const template = TemplateHelper.getTemplate('__name__Facade', options) diff --git a/src/Commands/Make/Middleware.ts b/src/Commands/Make/Middleware.ts index 6674233..17ba2a8 100644 --- a/src/Commands/Make/Middleware.ts +++ b/src/Commands/Make/Middleware.ts @@ -53,7 +53,7 @@ export class Middleware extends Command { * @return {Promise} */ async handle(name: string, options: any): Promise { - this.simpleLog('[ MAKING MIDDLEWARE ]', 'bold', 'green') + this.simpleLog('[ MAKING MIDDLEWARE ]', 'rmNewLineStart', 'bold', 'green') name = TemplateHelper.normalizeName(name, 'Middleware') const template = TemplateHelper.getTemplate('__name__Middleware', options) diff --git a/src/Commands/Make/Provider.ts b/src/Commands/Make/Provider.ts index fa6d1d6..9aef928 100644 --- a/src/Commands/Make/Provider.ts +++ b/src/Commands/Make/Provider.ts @@ -53,7 +53,7 @@ export class Provider extends Command { * @return {Promise} */ async handle(name: string, options: any): Promise { - this.simpleLog('[ MAKING PROVIDER ]', 'bold', 'green') + this.simpleLog('[ MAKING PROVIDER ]', 'rmNewLineStart', 'bold', 'green') name = TemplateHelper.normalizeName(name, 'Provider') const template = TemplateHelper.getTemplate('__name__Provider', options) diff --git a/src/Commands/Make/Service.ts b/src/Commands/Make/Service.ts index 65041d8..d1ae30c 100644 --- a/src/Commands/Make/Service.ts +++ b/src/Commands/Make/Service.ts @@ -53,7 +53,7 @@ export class Service extends Command { * @return {Promise} */ async handle(name: string, options: any): Promise { - this.simpleLog('[ MAKING SERVICE ]', 'bold', 'green') + this.simpleLog('[ MAKING SERVICE ]', 'rmNewLineStart', 'bold', 'green') name = TemplateHelper.normalizeName(name, 'Service') const template = TemplateHelper.getTemplate('__name__Service', options) diff --git a/src/Commands/Route/List.ts b/src/Commands/Route/List.ts index c5611ce..a9246cf 100644 --- a/src/Commands/Route/List.ts +++ b/src/Commands/Route/List.ts @@ -43,7 +43,7 @@ export class List extends Command { * @return {Promise} */ async handle(options: any): Promise { - this.simpleLog('[ ROUTE LISTING ]', 'bold', 'green') + this.simpleLog('[ ROUTE LISTING ]', 'rmNewLineStart', 'bold', 'green') const Route = ioc.use('Athenna/Core/HttpRoute') diff --git a/src/Commands/Start.ts b/src/Commands/Start.ts new file mode 100644 index 0000000..d102acc --- /dev/null +++ b/src/Commands/Start.ts @@ -0,0 +1,57 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Path } from '@secjs/utils' +import { Artisan } from 'src/Facades/Artisan' +import { Command } from 'src/Commands/Command' +import { Command as Commander } from 'commander' + +export class Start extends Command { + /** + * The name and signature of the console command. + */ + protected signature = 'start' + + /** + * The console command description. + */ + protected description = 'Starts your project using node.' + + /** + * Set additional flags in the commander instance. + * This method is executed when registering your command. + * + * @return {void} + */ + public addFlags(commander: Commander): Commander { + return commander + } + + /** + * Execute the console command. + * + * @return {Promise} + */ + async handle(): Promise { + await Artisan.call('build') + + try { + const mainPath = Path.noBuild().pwd('dist/bootstrap/main.js') + const tsconfigPathsPath = Path.noBuild().pwd( + 'node_modules/tsconfig-paths', + ) + + await this.execCommand( + `TS_NODE_BASEURL=./dist node -r ${tsconfigPathsPath}/register ${mainPath}`, + ) + } catch (error) { + this.error('Failed to start project.') + } + } +} diff --git a/src/Commands/StartDev.ts b/src/Commands/StartDev.ts new file mode 100644 index 0000000..fc8fa7d --- /dev/null +++ b/src/Commands/StartDev.ts @@ -0,0 +1,60 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Path } from '@secjs/utils' +import { Command } from 'src/Commands/Command' +import { Command as Commander } from 'commander' + +export class StartDev extends Command { + /** + * The name and signature of the console command. + */ + protected signature = 'start:dev' + + /** + * The console command description. + */ + protected description = 'Starts your project using ts-node and nodemon.' + + /** + * Set additional flags in the commander instance. + * This method is executed when registering your command. + * + * @return {void} + */ + public addFlags(commander: Commander): Commander { + return commander + } + + /** + * Execute the console command. + * + * @return {Promise} + */ + async handle(options: any): Promise { + try { + const nodemonPath = Path.noBuild().pwd('node_modules/.bin/nodemon') + const tsNodePath = Path.noBuild().pwd('node_modules/.bin/ts-node') + const mainPath = Path.noBuild().pwd('bootstrap/main.ts') + const tsconfigPathsPath = Path.noBuild().pwd( + 'node_modules/tsconfig-paths', + ) + + let command = `${tsNodePath} -r tsconfig-paths/register ${mainPath}` + + if (options.nodemon) { + command = `${nodemonPath} --quiet --ignore tests storage node_modules --watch '.' --exec '${tsNodePath} -r ${tsconfigPathsPath}/register ${mainPath}' -e ts` + } + + await this.execCommand(command) + } catch (error) { + this.error('Failed to start project.') + } + } +} From 9081461d76bdc0e564894ba9129452e2f99f6a9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lenon?= Date: Thu, 21 Apr 2022 20:34:06 -0300 Subject: [PATCH 3/4] feat: add test command --- src/Commands/StartDev.ts | 2 +- src/Commands/Test.ts | 56 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 src/Commands/Test.ts diff --git a/src/Commands/StartDev.ts b/src/Commands/StartDev.ts index fc8fa7d..8f7b908 100644 --- a/src/Commands/StartDev.ts +++ b/src/Commands/StartDev.ts @@ -54,7 +54,7 @@ export class StartDev extends Command { await this.execCommand(command) } catch (error) { - this.error('Failed to start project.') + this.error('Failed to start development project.') } } } diff --git a/src/Commands/Test.ts b/src/Commands/Test.ts new file mode 100644 index 0000000..c02bf65 --- /dev/null +++ b/src/Commands/Test.ts @@ -0,0 +1,56 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Path } from '@secjs/utils' +import { Command } from 'src/Commands/Command' +import { Command as Commander } from 'commander' + +export class Test extends Command { + /** + * The name and signature of the console command. + */ + protected signature = 'test' + + /** + * The console command description. + */ + protected description = 'Run your project tests using jest.' + + /** + * Set additional flags in the commander instance. + * This method is executed when registering your command. + * + * @return {void} + */ + public addFlags(commander: Commander): Commander { + return commander + } + + /** + * Execute the console command. + * + * @return {Promise} + */ + async handle(_: any, commander: any): Promise { + try { + const jestPath = Path.noBuild().pwd('node_modules/.bin/jest') + const jestArgs = commander.args.join(' ') + + let command = `${jestPath}` + + if (jestArgs) { + command = command.concat(` ${jestArgs}`) + } + + await this.execCommand(command) + } catch (error) { + this.error('Failed to test project.') + } + } +} From 0e840c446e8092982b8447eaec76a52d0bdfc198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lenon?= Date: Thu, 21 Apr 2022 20:40:01 -0300 Subject: [PATCH 4/4] fix: set test match on jest config --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 081f1b0..a4b9a02 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,9 @@ "ts" ], "rootDir": ".", - "testRegex": "Test.ts$", + "testMatch": [ + "**/tests/**/*Test.ts" + ], "transform": { "^.+\\.(t|j)s$": "ts-jest" },