diff --git a/assets/create-template/templates/default/asyncapi.yaml b/assets/create-template/templates/default/asyncapi.yaml new file mode 100644 index 00000000000..e9dc3fa855e --- /dev/null +++ b/assets/create-template/templates/default/asyncapi.yaml @@ -0,0 +1,34 @@ +asyncapi: 3.0.0 +info: + title: Temperature Service + version: 1.0.0 + description: This service is in charge of processing all the events related to temperature. + +servers: + dev: + url: test.mosquitto.org + protocol: mqtt + +channels: + temperature/changed: + description: Updates the bedroom temperature in the database when the temperature drops or goes up. + publish: + operationId: temperatureChange + message: + description: Message that is being sent when the temperature in the bedroom changes. + contentType: application/json + payload: + type: object + additionalProperties: false + properties: + temperatureId: + type: string + +components: + schemas: + temperatureId: + type: object + additionalProperties: false + properties: + temperatureId: + type: string diff --git a/assets/create-template/templates/default/package.json b/assets/create-template/templates/default/package.json new file mode 100644 index 00000000000..33856c1861f --- /dev/null +++ b/assets/create-template/templates/default/package.json @@ -0,0 +1,10 @@ +{ + "name": "myTemplate", + "generator": { + "renderer": "react", + "supportedProtocols": [] + }, + "dependencies": { + "@asyncapi/generator-react-sdk": "^1.0.20" + } +} diff --git a/assets/create-template/templates/default/readme.md b/assets/create-template/templates/default/readme.md new file mode 100644 index 00000000000..5eb70fdea5e --- /dev/null +++ b/assets/create-template/templates/default/readme.md @@ -0,0 +1,4 @@ +### First install all the dependencies for template using below command: +npm install +### Run the template using for a specific asyncapi document + asyncapi generate fromTemplate ../asyncapi-template \ No newline at end of file diff --git a/assets/create-template/templates/default/template/index.js b/assets/create-template/templates/default/template/index.js new file mode 100644 index 00000000000..5a4ab9f2892 --- /dev/null +++ b/assets/create-template/templates/default/template/index.js @@ -0,0 +1,11 @@ +import { File, Text } from '@asyncapi/generator-react-sdk'; + +// Pass the others parameters to get the specificatin of the asyncapi document +export default function ({ asyncapi }) { + return ( + + My application's markdown file. + App name: **{asyncapi.info().title()}** + + ); +} diff --git a/docs/usage.md b/docs/usage.md index 3cd13ba8c04..55855a4f710 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -58,6 +58,7 @@ USAGE * [`asyncapi new`](#asyncapi-new) * [`asyncapi new file`](#asyncapi-new-file) * [`asyncapi new glee`](#asyncapi-new-glee) +* [`asyncapi new template`](#asyncapi-new-glee) * [`asyncapi optimize [SPEC-FILE]`](#asyncapi-optimize-spec-file) * [`asyncapi start`](#asyncapi-start) * [`asyncapi start studio`](#asyncapi-start-studio) @@ -634,6 +635,24 @@ DESCRIPTION _See code: [src/commands/new/glee.ts](https://github.com/asyncapi/cli/blob/v2.0.3/src/commands/new/glee.ts)_ +## `asyncapi new template` + +Creates a new template + +``` +USAGE + $ asyncapi new glee [-h] [-n ] [-t ] [-renderer ] + +FLAGS + -h, --help Show CLI help. + -n, --name= [default: project] Name of the Project + -t, --template= [default: default] Name of the Template + -r --renderer= [default: react] Name of the renderer engine + +DESCRIPTION + Creates a new template project +``` + ## `asyncapi optimize [SPEC-FILE]` optimize asyncapi specification file diff --git a/package-lock.json b/package-lock.json index 972a23ce39d..c17af122b66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5687,6 +5687,18 @@ "sisteransi": "^1.0.5" } }, + "node_modules/@clack/prompts/node_modules/is-unicode-supported": { + "version": "1.3.0", + "extraneous": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "license": "MIT", diff --git a/package.json b/package.json index b9fa8a9c619..dcaee91c6e9 100644 --- a/package.json +++ b/package.json @@ -175,4 +175,4 @@ "createhookinit": "oclif generate hook inithook --event=init" }, "types": "lib/index.d.ts" -} +} \ No newline at end of file diff --git a/src/commands/new/template.ts b/src/commands/new/template.ts new file mode 100644 index 00000000000..dcfb334b9f2 --- /dev/null +++ b/src/commands/new/template.ts @@ -0,0 +1,117 @@ +import { promises as fPromises } from 'fs'; +import Command from '../../core/base'; +import { resolve, join } from 'path'; +import { load } from '../../core/models/SpecificationFile'; +import fs from 'fs-extra'; +import { templateFlags } from '../../core/flags/new/template.flags'; +import { cyan, gray } from 'picocolors'; +import jsonfile from 'jsonfile'; +import path from 'path'; + +export const successMessage = (projectName: string) => + `🎉 Your template is succesfully 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')} + asyncapi generate fromTemplate ../${projectName} \t\t ${gray('# Execute the template from anasyncapi document')} + +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 template --name ') + gray(projectName) + gray('-1')}`, +}; + +export default class template extends Command { + static description = 'Creates a new template'; + protected commandName = 'template'; + static readonly successMessage = successMessage; + static readonly errorMessages = errorMessages; + static flags = templateFlags(); + + async run() { + const { flags } = await this.parse(template); // NOSONAR + + const { + name: projectName, + template: templateName, + renderer: rendererName + } = flags; + + const PROJECT_DIRECTORY = join(process.cwd(), projectName); + + if (rendererName!=='nunjucks' && rendererName!=='react') { + this.error('Invalid flag check the flag name of renderer'); + } + + const templateDirectory = resolve( + __dirname, + '../../../assets/create-template/templates/', + templateName + ); + + { + try { + await fPromises.mkdir(PROJECT_DIRECTORY); + } catch (err: any) { + switch (err.code) { + case 'EEXIST': + this.error(errorMessages.alreadyExists(projectName)); + break; + case 'EACCES': + this.error( + `Unable to create the project. We tried to access the "${PROJECT_DIRECTORY}" directory but it was not possible due to file access permissions. Please check the write permissions of your current working directory ("${process.cwd()}").` + ); + break; + case 'EPERM': + this.error( + `Unable to create the project. We tried to create the "${PROJECT_DIRECTORY}" directory but the operation requires elevated privileges. Please check the privileges for your current user.` + ); + break; + default: + this.error( + `Unable to create the project. Please check the following message for further info about the error:\n\n${err}` + ); + } + } + + try { + await copyAndModify(templateDirectory, PROJECT_DIRECTORY,rendererName, projectName); + 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}` + ); + } + this.specFile = await load(`${templateDirectory}/asyncapi.yaml`); + this.metricsMetadata.template = flags.template; + } + } +} + +async function copyAndModify(templateDirectory:string, PROJECT_DIRECTORY:string, rendererName:string, projectName:string) { + const packageJsonPath = path.join(templateDirectory, 'package.json'); + try { + await fs.copy(templateDirectory, PROJECT_DIRECTORY, { + filter: (src) => { + return !src.endsWith('package.json'); + } + }); + const packageData = await jsonfile.readFile(packageJsonPath); + if ((packageData.generator && 'renderer' in packageData.generator)) { + packageData.generator.renderer = rendererName; + } + if (packageData.name) { + packageData.name = projectName; + } + + await fs.writeJSON(`${PROJECT_DIRECTORY}/package.json`, packageData, { spaces: 2 }); + } catch (err) { + console.error('Error:', err); + } +} diff --git a/src/core/flags/new/template.flags.ts b/src/core/flags/new/template.flags.ts new file mode 100644 index 00000000000..eaf5d9bc958 --- /dev/null +++ b/src/core/flags/new/template.flags.ts @@ -0,0 +1,32 @@ +import { Flags } from '@oclif/core'; + +export const templateFlags = () => { + return { + help: Flags.help({ char: 'h' }), + name: Flags.string({ + char: 'n', + description: 'Name of the Project', + default: 'project', + }), + template: Flags.string({ + char: 't', + description: 'Name of the Template', + default: 'default', + }), + file: Flags.string({ + char: 'f', + description: + 'The path to the AsyncAPI file for generating a template.', + }), + 'force-write': Flags.boolean({ + default: false, + description: + 'Force writing of the generated files to given directory even if it is a git repo with unstaged files or not empty dir (defaults to false)', + }), + renderer: Flags.string({ + char: 'r', + default: 'react', + description: 'Creating a template for particular engine, Its value can be either react or nunjucks.' + }) + }; +}; diff --git a/test/integration/new/template.test.ts b/test/integration/new/template.test.ts new file mode 100644 index 00000000000..88e74380135 --- /dev/null +++ b/test/integration/new/template.test.ts @@ -0,0 +1,66 @@ +import { test } from '@oclif/test'; +import TestHelper from '../../helpers'; +import { expect } from '@oclif/test'; +import { cyan, gray } from 'picocolors'; +const testHelper = new TestHelper(); +const successMessage = (projectName: string) => + '🎉 Your template is succesfully created'; + +const errorMessages = { + alreadyExists: (projectName: string) => + 'Unable to create the project', +}; +describe('new template', () => { + before(() => { + try { + testHelper.deleteDummyProjectDirectory(); + } catch (e: any) { + if (e.code !== 'ENOENT') { + throw e; + } + } + }); + + describe('creation of new project is successful', () => { + afterEach(() => { + testHelper.deleteDummyProjectDirectory(); + }); + + test + .stderr() + .stdout() + .command(['new:template', '-n=test-project']) + .it('runs new glee command with name flag', async (ctx,done) => { + expect(ctx.stderr).to.equal(''); + expect(ctx.stdout).to.contains(successMessage('test-project')); + done(); + }); + }); + + describe('when new project name already exists', () => { + beforeEach(() => { + try { + testHelper.createDummyProjectDirectory(); + } catch (e: any) { + if (e.code !== 'EEXIST') { + throw e; + } + } + }); + + afterEach(() => { + testHelper.deleteDummyProjectDirectory(); + }); + + test + .stderr() + .stdout() + .command(['new:template', '-n=test-project']) + .it('should throw error if name of the new project already exists', async (ctx,done) => { + expect(ctx.stderr).to.contains(`Error: ${errorMessages.alreadyExists('test-project')}`); + expect(ctx.stdout).to.equal(''); + done(); + }); + }); +}); +