From 2b75821a837b8d56c5413760ac05567de33705fb Mon Sep 17 00:00:00 2001 From: Ashish Padhy <100484401+Shurtu-gal@users.noreply.github.com> Date: Fri, 3 May 2024 13:18:06 +0530 Subject: [PATCH] chore: apply design system to `new` and `convert` command (#1398) Co-authored-by: asyncapi-bot %0ACo-authored-by: souvik --- src/commands/convert.ts | 10 ++++++--- src/commands/new/file.ts | 7 +++--- src/commands/new/glee.ts | 36 ++++++++++++++++++++++--------- src/models/Studio.ts | 4 +++- src/parser.ts | 32 ++++++++++++++++++++++++--- test/integration/convert.test.ts | 10 ++++----- test/integration/new/file.test.ts | 6 +++--- test/integration/new/glee.test.ts | 11 ++++++++-- test/integration/validate.test.ts | 10 ++++----- 9 files changed, 91 insertions(+), 35 deletions(-) diff --git a/src/commands/convert.ts b/src/commands/convert.ts index 3e6d4955a6c..1530c6ffa2d 100644 --- a/src/commands/convert.ts +++ b/src/commands/convert.ts @@ -7,6 +7,7 @@ import { load } from '../models/SpecificationFile'; import { SpecificationFileNotFound } from '../errors/specification-file'; import { convert } from '@asyncapi/converter'; import type { ConvertVersion } from '@asyncapi/converter'; +import { cyan, green } from 'picocolors'; // @ts-ignore import specs from '@asyncapi/specs'; @@ -35,15 +36,16 @@ export default class Convert extends Command { try { // LOAD FILE this.specFile = await load(filePath); + // eslint-disable-next-line sonarjs/no-duplicate-string this.metricsMetadata.to_version = flags['target-version']; // CONVERSION convertedFile = convert(this.specFile.text(), flags['target-version'] as ConvertVersion); if (convertedFile) { if (this.specFile.getFilePath()) { - this.log(`File ${this.specFile.getFilePath()} successfully converted!`); + this.log(`🎉 The ${cyan(this.specFile.getFilePath())} file has been successfully converted to version ${green(flags['target-version'])}!!`); } else if (this.specFile.getFileURL()) { - this.log(`URL ${this.specFile.getFileURL()} successfully converted!`); + this.log(`🎉 The URL ${cyan(this.specFile.getFileURL())} has been successfully converted to version ${green(flags['target-version'])}!!`); } } @@ -64,9 +66,11 @@ export default class Convert extends Command { type: 'invalid-file', filepath: filePath })); + } else if (this.specFile?.toJson().asyncapi > flags['target-version']) { + this.error(`The ${cyan(filePath)} file cannot be converted to an older version. Downgrading is not supported.`); } else { this.error(err as Error); } - } + } } } diff --git a/src/commands/new/file.ts b/src/commands/new/file.ts index fc5544269bc..52964b66763 100644 --- a/src/commands/new/file.ts +++ b/src/commands/new/file.ts @@ -5,6 +5,7 @@ import * as inquirer from 'inquirer'; import { start as startStudio, DEFAULT_PORT } from '../../models/Studio'; import { resolve } from 'path'; import { load } from '../../models/SpecificationFile'; +import { cyan } from 'picocolors'; const { writeFile, readFile } = fPromises; const DEFAULT_ASYNCAPI_FILE_NAME = 'asyncapi.yaml'; @@ -158,16 +159,16 @@ export default class NewFile extends Command { try { const content = await readFile(fileNameToWriteToDisk, { encoding: 'utf8' }); if (content !== undefined) { - console.log(`File ${fileNameToWriteToDisk} already exists. Ignoring...`); + console.log(`A file named ${fileNameToWriteToDisk} already exists. Please choose a different name.`); return; } } catch (e:any) { if (e.code === 'EACCES') { - this.error('Permission denied to read the file. You do not have the necessary permissions.'); + this.error('Permission has been denied to access the file.'); } } await writeFile(fileNameToWriteToDisk, asyncApiFile, { encoding: 'utf8' }); - console.log(`Created file ${fileNameToWriteToDisk}...`); + console.log(`The ${cyan(fileNameToWriteToDisk)} has been successfully created.`); this.specFile = await load(fileNameToWriteToDisk); this.metricsMetadata.selected_template = selectedTemplate; } diff --git a/src/commands/new/glee.ts b/src/commands/new/glee.ts index 9d91fa297ad..f717ac7ced6 100644 --- a/src/commands/new/glee.ts +++ b/src/commands/new/glee.ts @@ -9,10 +9,32 @@ import { prompt } from 'inquirer'; // eslint-disable-next-line // @ts-ignore import Generator from '@asyncapi/generator'; +import { cyan, gray } from 'picocolors'; + +export const successMessage = (projectName: string) => + `🎉 Your Glee project has been successfully created! +⏩ Next steps: follow the instructions ${cyan('below')} to manage your project: + + cd ${projectName}\t\t ${gray('# Navigate to the project directory')} + npm install\t\t ${gray('# Install the project dependencies')} + npm run dev\t\t ${gray('# Start the project in development mode')} + +You can also open the project in your favourite editor and start tweaking it. +`; + +const errorMessages = { + alreadyExists: (projectName: string) => + `Unable to create the project because the directory "${cyan(projectName)}" already exists at "${process.cwd()}/${projectName}". +To specify a different name for the new project, please run the command below with a unique project name: + + ${gray('asyncapi new glee --name ') + gray(projectName) + gray('-1')}`, +}; export default class NewGlee extends Command { static description = 'Creates a new Glee project'; protected commandName = 'glee'; + static readonly successMessage = successMessage; + static readonly errorMessages = errorMessages; static flags = { help: Flags.help({ char: 'h' }), @@ -93,12 +115,10 @@ export default class NewGlee extends Command { fs.existsSync(PROJECT_DIRECTORY) && fs.readdirSync(PROJECT_DIRECTORY).length > 0 ) { - throw new Error( - `Unable to create the project. We tried to use "${projectName}" as the directory of your new project but it already exists (${PROJECT_DIRECTORY}). Please specify a different name for the new project. For example, run the following command instead:\n\n asyncapi new ${this.commandName} -f ${file} --name ${projectName}-1\n` - ); + throw new Error(errorMessages.alreadyExists(projectName)); } } catch (error: any) { - this.error(error.message); + this.log(error.message); } } @@ -199,9 +219,7 @@ export default class NewGlee extends Command { } catch (err: any) { switch (err.code) { case 'EEXIST': - this.error( - `Unable to create the project. We tried to use "${projectName}" as the directory of your new project but it already exists (${PROJECT_DIRECTORY}). Please specify a different name for the new project. For example, run the following command instead:\n\n asyncapi new ${this.commandName} --name ${projectName}-1\n` - ); + this.error(errorMessages.alreadyExists(projectName)); break; case 'EACCES': this.error( @@ -234,9 +252,7 @@ export default class NewGlee extends Command { `${PROJECT_DIRECTORY}/README-template.md`, `${PROJECT_DIRECTORY}/README.md` ); - this.log( - `Your project "${projectName}" has been created successfully!\n\nNext steps:\n\n cd ${projectName}\n npm install\n npm run dev\n\nAlso, you can already open the project in your favorite editor and start tweaking it.` - ); + this.log(successMessage(projectName)); } catch (err) { this.error( `Unable to create the project. Please check the following message for further info about the error:\n\n${err}` diff --git a/src/models/Studio.ts b/src/models/Studio.ts index 37835075db5..d2ed51837dc 100644 --- a/src/models/Studio.ts +++ b/src/models/Studio.ts @@ -7,6 +7,7 @@ import chokidar from 'chokidar'; import open from 'open'; import path from 'path'; import { version as studioVersion } from '@asyncapi/studio/package.json'; +import { gray } from 'picocolors'; const { readFile, writeFile } = fPromises; @@ -99,7 +100,8 @@ export function start(filePath: string, port: number = DEFAULT_PORT): void { server.listen(port, () => { const url = `http://localhost:${port}?liveServer=${port}&studio-version=${studioVersion}`; - console.log(`Studio is running at ${url}`); + console.log(`Studio is now running at ${url}.`); + console.log(`You can open this URL in your web browser, and if needed, press ${gray('Ctrl + C')} to stop the process.`); console.log(`Watching changes on file ${filePath}`); open(url); }); diff --git a/src/parser.ts b/src/parser.ts index 0f5f3bece81..4c1371ac196 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,12 +1,13 @@ import { AvroSchemaParser } from '@asyncapi/avro-schema-parser'; import { OpenAPISchemaParser } from '@asyncapi/openapi-schema-parser'; -import { Parser, convertToOldAPI } from '@asyncapi/parser/cjs'; +import { DiagnosticSeverity, Parser, convertToOldAPI } from '@asyncapi/parser/cjs'; import { RamlDTSchemaParser } from '@asyncapi/raml-dt-schema-parser'; import { Flags } from '@oclif/core'; import { ProtoBuffSchemaParser } from '@asyncapi/protobuf-schema-parser'; import { getDiagnosticSeverity } from '@stoplight/spectral-core'; import { OutputFormat } from '@stoplight/spectral-cli/dist/services/config'; import { html, json, junit, pretty, stylish, teamcity, text } from '@stoplight/spectral-formatters'; +import { red, yellow, green, cyan } from 'chalk'; import type { Diagnostic } from '@asyncapi/parser/cjs'; import type Command from './base'; @@ -102,9 +103,10 @@ function logDiagnostics(diagnostics: Diagnostic[], command: Command, specFile: S } export function formatOutput(diagnostics: Diagnostic[], format: `${OutputFormat}`, failSeverity: SeverityKind) { - const options = { failSeverity: getDiagnosticSeverity(failSeverity) }; + const diagnosticSeverity = getDiagnosticSeverity(failSeverity); + const options = { failSeverity: diagnosticSeverity !== -1 ? diagnosticSeverity : DiagnosticSeverity.Error }; switch (format) { - case 'stylish': return stylish(diagnostics, options); + case 'stylish': return formatStylish(diagnostics, options); case 'json': return json(diagnostics, options); case 'junit': return junit(diagnostics, options); case 'html': return html(diagnostics, options); @@ -115,6 +117,30 @@ export function formatOutput(diagnostics: Diagnostic[], format: `${OutputFormat} } } +function formatStylish(diagnostics: Diagnostic[], options: { failSeverity: DiagnosticSeverity }) { + const groupedDiagnostics = diagnostics.reduce((acc, diagnostic) => { + const severity = diagnostic.severity; + if (!acc[severity as DiagnosticSeverity]) { + acc[severity as DiagnosticSeverity] = []; + } + acc[severity as DiagnosticSeverity].push(diagnostic); + return acc; + }, {} as Record); + + return Object.entries(groupedDiagnostics).map(([severity, diagnostics]) => { + return `${getSeverityTitle(Number(severity))} ${stylish(diagnostics, options)}`; + }).join('\n'); +} + +function getSeverityTitle(severity: DiagnosticSeverity) { + switch (severity) { + case DiagnosticSeverity.Error: return red('Errors'); + case DiagnosticSeverity.Warning: return yellow('Warnings'); + case DiagnosticSeverity.Information: return cyan('Information'); + case DiagnosticSeverity.Hint: return green('Hints'); + } +} + function hasFailSeverity(diagnostics: Diagnostic[], failSeverity: SeverityKind) { const diagnosticSeverity = getDiagnosticSeverity(failSeverity); return diagnostics.some(diagnostic => diagnostic.severity <= diagnosticSeverity); diff --git a/test/integration/convert.test.ts b/test/integration/convert.test.ts index 29c52d0a599..64c72ad1f9d 100644 --- a/test/integration/convert.test.ts +++ b/test/integration/convert.test.ts @@ -32,7 +32,7 @@ describe('convert', () => { .stdout() .command(['convert', filePath]) .it('works when file path is passed', (ctx, done) => { - expect(ctx.stdout).to.contain('File ./test/fixtures/specification.yml successfully converted!\n'); + expect(ctx.stdout).to.contain('The ./test/fixtures/specification.yml file has been successfully converted to version 3.0.0!!'); expect(ctx.stderr).to.equal(''); done(); }); @@ -52,7 +52,7 @@ describe('convert', () => { .stdout() .command(['convert', 'http://localhost:8080/dummySpec.yml']) .it('works when url is passed', (ctx, done) => { - expect(ctx.stdout).to.contain('URL http://localhost:8080/dummySpec.yml successfully converted!\n'); + expect(ctx.stdout).to.contain('The URL http://localhost:8080/dummySpec.yml has been successfully converted to version 3.0.0!!'); expect(ctx.stderr).to.equal(''); done(); }); @@ -73,7 +73,7 @@ describe('convert', () => { .stdout() .command(['convert']) .it('converts from current context', (ctx, done) => { - expect(ctx.stdout).to.contain(`File ${path.resolve(__dirname, '../fixtures/specification.yml')} successfully converted!\n`); + expect(ctx.stdout).to.contain(`The ${path.resolve(__dirname, '../fixtures/specification.yml')} file has been successfully converted to version 3.0.0!!\n`); expect(ctx.stderr).to.equal(''); done(); }); @@ -159,7 +159,7 @@ describe('convert', () => { .stdout() .command(['convert', filePath, '-o=./test/fixtures/specification_output.yml']) .it('works when .yml file is passed', (ctx, done) => { - expect(ctx.stdout).to.equal(`File ${filePath} successfully converted!\n`); + expect(ctx.stdout).to.contain(`The ${filePath} file has been successfully converted to version 3.0.0!!`); expect(fs.existsSync('./test/fixtures/specification_output.yml')).to.equal(true); expect(ctx.stderr).to.equal(''); fs.unlinkSync('./test/fixtures/specification_output.yml'); @@ -171,7 +171,7 @@ describe('convert', () => { .stdout() .command(['convert', JSONFilePath, '-o=./test/fixtures/specification_output.json']) .it('works when .json file is passed', (ctx, done) => { - expect(ctx.stdout).to.equal(`File ${JSONFilePath} successfully converted!\n`); + expect(ctx.stdout).to.contain(`The ${JSONFilePath} file has been successfully converted to version 3.0.0!!`); expect(fs.existsSync('./test/fixtures/specification_output.json')).to.equal(true); expect(ctx.stderr).to.equal(''); fs.unlinkSync('./test/fixtures/specification_output.json'); diff --git a/test/integration/new/file.test.ts b/test/integration/new/file.test.ts index 2fe078a860c..d957857d49f 100644 --- a/test/integration/new/file.test.ts +++ b/test/integration/new/file.test.ts @@ -26,7 +26,7 @@ describe('new', () => { .command(['new', '--no-tty', '-n=specification.yaml']) .it('runs new command', async (ctx,done) => { expect(ctx.stderr).to.equal(''); - expect(ctx.stdout).to.equal('Created file specification.yaml...\n'); + expect(ctx.stdout).to.equal('The specification.yaml has been successfully created.\n'); done(); }); @@ -36,7 +36,7 @@ describe('new', () => { .command(['new:file', '--no-tty', '-n=specification.yaml']) .it('runs new file command', async (ctx,done) => { expect(ctx.stderr).to.equal(''); - expect(ctx.stdout).to.equal('Created file specification.yaml...\n'); + expect(ctx.stdout).to.equal('The specification.yaml has been successfully created.\n'); done(); }); }); @@ -62,7 +62,7 @@ describe('new', () => { .command(['new:file', '--no-tty', '-n=specification.yaml']) .it('should inform about the existing file and finish the process', async (ctx,done) => { expect(ctx.stderr).to.equal(''); - expect(ctx.stdout).to.equal('File specification.yaml already exists. Ignoring...\n'); + expect(ctx.stdout).to.equal('A file named specification.yaml already exists. Please choose a different name.\n'); done(); }); }); diff --git a/test/integration/new/glee.test.ts b/test/integration/new/glee.test.ts index 82a7f6587fb..a7ded8abade 100644 --- a/test/integration/new/glee.test.ts +++ b/test/integration/new/glee.test.ts @@ -4,6 +4,13 @@ import { PROJECT_DIRECTORY_PATH } from '../../helpers'; import { expect } from '@oclif/test'; const testHelper = new TestHelper(); +const successMessage = (projectName: string) => + '🎉 Your Glee project has been successfully created!'; + +const errorMessages = { + alreadyExists: (projectName: string) => + `Unable to create the project because the directory "${projectName}" already exists at "${process.cwd()}/${projectName}". +To specify a different name for the new project, please run the command below with a unique project name:`}; describe('new glee', () => { before(() => { @@ -27,7 +34,7 @@ describe('new glee', () => { .command(['new:glee', '-n=test-project']) .it('runs new glee command with name flag', async (ctx,done) => { expect(ctx.stderr).to.equal(''); - expect(ctx.stdout).to.equal('Your project "test-project" has been created successfully!\n\nNext steps:\n\n cd test-project\n npm install\n npm run dev\n\nAlso, you can already open the project in your favorite editor and start tweaking it.\n'); + expect(ctx.stdout).to.contains(successMessage('test-project')); done(); }); }); @@ -52,7 +59,7 @@ describe('new glee', () => { .stdout() .command(['new:glee', '-n=test-project']) .it('should throw error if name of the new project already exists', async (ctx,done) => { - expect(ctx.stderr).to.equal(`Error: Unable to create the project. We tried to use "test-project" as the directory of your new project but it already exists (${PROJECT_DIRECTORY_PATH}). Please specify a different name for the new project. For example, run the following command instead:\n\n asyncapi new glee --name test-project-1\n\n`); + expect(ctx.stderr).to.contains(`Error: ${errorMessages.alreadyExists('test-project')}`); expect(ctx.stdout).to.equal(''); done(); }); diff --git a/test/integration/validate.test.ts b/test/integration/validate.test.ts index d63921f4b6f..72d5bbbdc51 100644 --- a/test/integration/validate.test.ts +++ b/test/integration/validate.test.ts @@ -31,7 +31,7 @@ describe('validate', () => { .stdout() .command(['validate', './test/fixtures/specification.yml']) .it('works when file path is passed', (ctx, done) => { - expect(ctx.stdout).to.match(/File .\/test\/fixtures\/specification.yml is valid but has \(itself and\/or referenced documents\) governance issues.\n\ntest\/fixtures\/specification.yml/); + expect(ctx.stdout).to.contain('File ./test/fixtures/specification.yml is valid but has (itself and/or referenced documents) governance issues.\n'); expect(ctx.stderr).to.equal(''); done(); }); @@ -41,7 +41,7 @@ describe('validate', () => { .stdout() .command(['validate', './test/fixtures/specification-avro.yml']) .it('works when file path is passed and schema is avro', (ctx, done) => { - expect(ctx.stdout).to.match(/File .\/test\/fixtures\/specification-avro.yml is valid but has \(itself and\/or referenced documents\) governance issues.\n/); + expect(ctx.stdout).to.contain('File ./test/fixtures/specification-avro.yml is valid but has (itself and/or referenced documents) governance issues.\n'); expect(ctx.stderr).to.equal(''); done(); }); @@ -61,7 +61,7 @@ describe('validate', () => { .stdout() .command(['validate', 'http://localhost:8080/dummySpec.yml']) .it('works when url is passed', (ctx, done) => { - expect(ctx.stdout).to.match(/URL http:\/\/localhost:8080\/dummySpec.yml is valid but has \(itself and\/or referenced documents\) governance issues.\n\nhttp:\/\/localhost:8080\/dummySpec.yml/); + expect(ctx.stdout).to.contain('URL http://localhost:8080/dummySpec.yml is valid but has (itself and/or referenced documents) governance issues.\n'); expect(ctx.stderr).to.equal(''); done(); }); @@ -180,7 +180,7 @@ describe('validate', () => { .stdout() .command(['validate', './test/fixtures/specification.yml', '--log-diagnostics']) .it('works with --log-diagnostics', (ctx, done) => { - expect(ctx.stdout).to.match(/File .\/test\/fixtures\/specification.yml is valid but has \(itself and\/or referenced documents\) governance issues.\n\ntest\/fixtures\/specification.yml/); + expect(ctx.stdout).to.contain('File ./test/fixtures/specification.yml is valid but has (itself and/or referenced documents) governance issues.\n'); expect(ctx.stderr).to.equal(''); done(); }); @@ -240,7 +240,7 @@ describe('validate', () => { .stdout() .command(['validate', './test/fixtures/specification.yml', '--fail-severity=warn']) .it('works with --fail-severity', (ctx, done) => { - expect(ctx.stderr).to.include('\nFile ./test/fixtures/specification.yml and/or referenced documents have governance issues.\n\ntest/fixtures/specification.yml'); + expect(ctx.stderr).to.contain('File ./test/fixtures/specification.yml and/or referenced documents have governance issues.'); expect(process.exitCode).to.equal(1); done(); });