From dfe34e8fc84cffef4eeedfe32b6c81fa70a7c4c0 Mon Sep 17 00:00:00 2001 From: dwakshar Date: Wed, 22 Apr 2026 15:49:09 +0530 Subject: [PATCH 1/2] feat: add --boilerplate flag to create command --- src/commands/create.ts | 66 ++++++++++++++---- src/misc/BoilerplateCreator.ts | 121 +++++++++++++++++++++++++++++++++ src/misc/index.ts | 17 +++-- 3 files changed, 183 insertions(+), 21 deletions(-) create mode 100644 src/misc/BoilerplateCreator.ts diff --git a/src/commands/create.ts b/src/commands/create.ts index 79525ac..d045a2d 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -9,6 +9,7 @@ import * as uuid from 'uuid'; import { AppCreator, + BoilerplateCreator, FolderDetails, VariousUtils, } from '../misc'; @@ -18,17 +19,32 @@ export default class Create extends Command { public static flags = { help: flags.help({ char: 'h' }), - name: flags.string({char: 'n', description: 'Name of the app'}), - description: flags.string({char: 'd', description: 'Description of the app'}), - author: flags.string({char: 'a', description: 'Author\'s name'}), - homepage: flags.string({char: 'H', description: 'Author\'s or app\'s home page'}), - support: flags.string({char: 's', description: 'URL or email address to get support for the app'}), + name: flags.string({ char: 'n', description: 'Name of the app' }), + description: flags.string({ + char: 'd', + description: 'Description of the app', + }), + author: flags.string({ char: 'a', description: "Author's name" }), + homepage: flags.string({ + char: 'H', + description: "Author's or app's home page", + }), + boilerplate: flags.string({ + char: 'b', + description: + 'Boilerplate templates (slash-command, api-endpoint, settings)', + multiple: true, + options: ['slash-command', 'api-endpoint', 'settings'], + }), + support: flags.string({ + char: 's', + description: 'URL or email address to get support for the app', + }), }; public async run() { if (!semver.satisfies(process.version, '>=4.2.0')) { this.error('NodeJS version needs to be at least 4.2.0 or higher.'); - return; } const info: IAppInfo = { @@ -39,23 +55,35 @@ export default class Create extends Command { author: {}, } as IAppInfo; - this.log('Let\'s get started creating your app.'); + this.log("Let's get started creating your app."); this.log('We need some information first:'); this.log(''); const { flags } = this.parse(Create); - info.name = flags.name ? flags.name : await cli.prompt(chalk.bold(' App Name')); + info.name = flags.name + ? flags.name + : await cli.prompt(chalk.bold(' App Name')); info.nameSlug = VariousUtils.slugify(info.name); - info.classFile = `${ pascalCase(info.name) }App.ts`; + info.classFile = `${pascalCase(info.name)}App.ts`; - info.description = flags.description ? flags.description : await cli.prompt(chalk.bold(' App Description')); - info.author.name = flags.author ? flags.author : await cli.prompt(chalk.bold(' Author\'s Name')); - info.author.homepage = flags.homepage ? flags.homepage : await cli.prompt(chalk.bold(' Author\'s Home Page')); - info.author.support = flags.support ? flags.support : await cli.prompt(chalk.bold(' Author\'s Support Page')); + info.description = flags.description + ? flags.description + : await cli.prompt(chalk.bold(' App Description')); + info.author.name = flags.author + ? flags.author + : await cli.prompt(chalk.bold(" Author's Name")); + info.author.homepage = flags.homepage + ? flags.homepage + : await cli.prompt(chalk.bold(" Author's Home Page")); + info.author.support = flags.support + ? flags.support + : await cli.prompt(chalk.bold(" Author's Support Page")); const folder = path.join(process.cwd(), info.nameSlug); - cli.action.start(`Creating a Rocket.Chat App in ${ chalk.green(folder) }`); + cli.action.start( + `Creating a Rocket.Chat App in ${chalk.green(folder)}`, + ); const fd = new FolderDetails(this); fd.setAppInfo(info); @@ -68,9 +96,17 @@ export default class Create extends Command { await fd.readInfoFile(); } catch (e) { this.error(e && e.message ? e.message : e); - return; } cli.action.stop(chalk.cyan('done!')); + + const boilerplateFlags = flags.boilerplate || []; + if (boilerplateFlags.length > 0) { + this.log(''); + this.log('Generating boilerplate files:'); + const boilerplateCreator = new BoilerplateCreator(fd, this); + await boilerplateCreator.generate(boilerplateFlags); + this.log(chalk.cyan('Boilerplate done!')); + } } } diff --git a/src/misc/BoilerplateCreator.ts b/src/misc/BoilerplateCreator.ts new file mode 100644 index 0000000..a7b05c9 --- /dev/null +++ b/src/misc/BoilerplateCreator.ts @@ -0,0 +1,121 @@ +import { Command } from '@oclif/command'; +import * as fs from 'fs'; +import * as path from 'path'; +import { FolderDetails } from './folderDetails'; + +const TEMPLATES: Record< + string, + { file: string; content: (appName: string) => string } +> = { + 'slash-command': { + file: 'SlashCommand.ts', + content: ( + appName, + ) => `import { ISlashCommand, SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands'; +import { IModify, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { ${appName} } from '../${appName}'; + +export class MyCommand implements ISlashCommand { + public command = 'mycommand'; + public i18nParamsExample = 'MyCommand_Params'; + public i18nDescription = 'MyCommand_Description'; + public providesPreview = false; + + constructor(private readonly app: ${appName}) {} + + public async executor( + context: SlashCommandContext, + read: IRead, + modify: IModify, + ): Promise { + const sender = context.getSender(); + const room = context.getRoom(); + const msg = modify.getCreator().startMessage() + .setRoom(room) + .setText(\`Hello, \${sender.name}!\`); + await modify.getCreator().finish(msg); + } +} +`, + }, + 'api-endpoint': { + file: 'Endpoint.ts', + content: ( + _appName, + ) => `import { ApiEndpoint } from '@rocket.chat/apps-engine/definition/api'; +import { IApiEndpointInfo, IApiRequest, IApiResponse } from '@rocket.chat/apps-engine/definition/api'; + +export class MyEndpoint extends ApiEndpoint { + public path = 'my-endpoint'; + + public async get(request: IApiRequest, endpoint: IApiEndpointInfo): Promise { + return this.success({ message: 'Hello!' }); + } + + public async post(request: IApiRequest, endpoint: IApiEndpointInfo): Promise { + const { body } = request; + if (!body?.data) { + return this.notFound('Missing field: data'); + } + return this.success({ received: body.data }); + } +} +`, + }, + 'settings': { + file: 'Settings.ts', + content: ( + _appName, + ) => `import { ISetting, SettingType } from '@rocket.chat/apps-engine/definition/settings'; + +export enum AppSetting { + ApiToken = 'api_token', + WelcomeMessage = 'welcome_message', +} + +export const settings: Array = [ + { + id: AppSetting.ApiToken, + type: SettingType.STRING, + packageValue: '', + required: true, + public: false, + i18nLabel: 'ApiToken_Label', + i18nDescription: 'ApiToken_Description', + }, + { + id: AppSetting.WelcomeMessage, + type: SettingType.STRING, + packageValue: 'Welcome!', + required: false, + public: true, + i18nLabel: 'WelcomeMessage_Label', + i18nDescription: 'WelcomeMessage_Description', + }, +]; +`, + }, +}; + +export class BoilerplateCreator { + constructor( + private readonly fd: FolderDetails, + private readonly command: Command, + ) {} + + public async generate(templates: Array): Promise { + const appClassName = this.fd.info.classFile.replace('.ts', ''); + for (const template of templates) { + const tpl = TEMPLATES[template]; + if (!tpl) { + this.command.warn( + `Unknown boilerplate: "${template}", skipping.`, + ); + continue; + } + const destPath = path.join(this.fd.folder, tpl.file); + fs.writeFileSync(destPath, tpl.content(appClassName), 'utf8'); + this.command.log(` ✔ Generated ${tpl.file}`); + } + } +} diff --git a/src/misc/index.ts b/src/misc/index.ts index b76b78d..4a34bf2 100644 --- a/src/misc/index.ts +++ b/src/misc/index.ts @@ -5,21 +5,26 @@ import { AppPackager } from './appPackager'; import { compilerOptions } from './compilerOptions'; import { DiagnosticReport } from './diagnosticReport'; import { FolderDetails } from './folderDetails'; -import { IAppCategory, INormalLoginInfo, IPersonalAccessTokenLoginInfo} from './interfaces'; +import { + IAppCategory, + INormalLoginInfo, + IPersonalAccessTokenLoginInfo, +} from './interfaces'; import { unicodeSymbols } from './unicodeSymbols'; import { VariousUtils } from './variousUtils'; +export { BoilerplateCreator } from './BoilerplateCreator'; export { - IAppCategory, - INormalLoginInfo, - IPersonalAccessTokenLoginInfo, - appJsonSchema, + AppCompiler, AppCreator, + appJsonSchema, AppPackager, - AppCompiler, compilerOptions, DiagnosticReport, FolderDetails, + IAppCategory, + INormalLoginInfo, + IPersonalAccessTokenLoginInfo, unicodeSymbols, VariousUtils, }; From 3c647317285fffe89be75c913ae7f9d88ae13adb Mon Sep 17 00:00:00 2001 From: dwakshar Date: Wed, 22 Apr 2026 15:51:02 +0530 Subject: [PATCH 2/2] feat: add --boilerplate flag to create command --- src/misc/folderDetails.ts | 98 ++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 33 deletions(-) diff --git a/src/misc/folderDetails.ts b/src/misc/folderDetails.ts index aaa6f94..abe74e4 100644 --- a/src/misc/folderDetails.ts +++ b/src/misc/folderDetails.ts @@ -5,10 +5,7 @@ import * as figures from 'figures'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as process from 'process'; -import { - coerce as coerceVersion, - diff as diffVersion, -} from 'semver'; +import { coerce as coerceVersion, diff as diffVersion } from 'semver'; import * as tv4 from 'tv4'; import { appJsonSchema } from './appJsonSchema'; @@ -27,7 +24,7 @@ export class FolderDetails { } public async doesFileExist(file: string): Promise { - return await fs.pathExists(file) && fs.statSync(file).isFile(); + return (await fs.pathExists(file)) && fs.statSync(file).isFile(); } public mergeWithFolder(item: string): string { @@ -70,7 +67,11 @@ export class FolderDetails { } public writeToSettingsFile(toWrite: string): void { - fs.writeFileSync(path.join(this.folder, 'settings.ts'), toWrite, 'utf-8'); + fs.writeFileSync( + path.join(this.folder, 'settings.ts'), + toWrite, + 'utf-8', + ); } /** * Validates the "app.json" file, loads it, and then retrieves the classFile property from it. @@ -78,7 +79,9 @@ export class FolderDetails { */ public async readInfoFile(): Promise { if (!(await this.doesFileExist(this.infoFile))) { - throw new Error('No App found to package. Missing an "app.json" file.'); + throw new Error( + 'No App found to package. Missing an "app.json" file.', + ); } try { @@ -91,13 +94,17 @@ export class FolderDetails { this.validateAppDotJson(); if (!this.info.classFile) { - throw new Error('Invalid "app.json" file. The "classFile" is required.'); + throw new Error( + 'Invalid "app.json" file. The "classFile" is required.', + ); } this.mainFile = path.join(this.folder, this.info.classFile); if (!(await this.doesFileExist(this.mainFile))) { - throw new Error(`The specified classFile (${ this.mainFile }) does not exist.`); + throw new Error( + `The specified classFile (${this.mainFile}) does not exist.`, + ); } } @@ -106,29 +113,42 @@ export class FolderDetails { throw new Error('App Manifest not loaded. Exiting...'); } - if (!await this.doesFileExist('package.json')) { + if (!(await this.doesFileExist('package.json'))) { throw new Error('package.json not found. Exiting...'); } - const packageJson: Record = require(path.join(this.folder, 'package.json')); - const appsEngineEntry = packageJson.devDependencies['@rocket.chat/apps-engine'] as string; - const appsEngineVersion = appsEngineEntry.startsWith('file:') ? - require(path.join(appsEngineEntry.replace(/^file:/, ''), 'package.json')).version : - appsEngineEntry; - - if (diffVersion( - coerceVersion(appsEngineVersion), - coerceVersion(this.info.requiredApiVersion)) - ) { - + const packageJson: Record = require(path.join( + this.folder, + 'package.json', + )); + const appsEngineEntry = packageJson.devDependencies[ + '@rocket.chat/apps-engine' + ] as string; + const appsEngineVersion = appsEngineEntry.startsWith('file:') + ? require(path.join( + appsEngineEntry.replace(/^file:/, ''), + 'package.json', + )).version + : appsEngineEntry; + + if ( + diffVersion( + coerceVersion(appsEngineVersion), + coerceVersion(this.info.requiredApiVersion), + ) + ) { // tslint:disable-next-line:no-console - console.log(chalk.bgYellow('Warning:'), - chalk.yellow('Different versions of the Apps Engine were found between app.json (', - this.info.requiredApiVersion, - ') and package.json (', - appsEngineVersion, - ').', - '\nUpdating app.json to reflect the same version of Apps Engine from package.json')); + console.log( + chalk.bgYellow('Warning:'), + chalk.yellow( + 'Different versions of the Apps Engine were found between app.json (', + this.info.requiredApiVersion, + ') and package.json (', + appsEngineVersion, + ').', + '\nUpdating app.json to reflect the same version of Apps Engine from package.json', + ), + ); await this.updateInfoFileRequiredVersion(appsEngineVersion); } @@ -141,10 +161,14 @@ export class FolderDetails { if (!this.isValidResult(result)) { this.reportFailed(result.errors.length, result.missing.length); - result.errors.forEach((e: tv4.ValidationError) => this.reportError(e)); + result.errors.forEach((e: tv4.ValidationError) => + this.reportError(e), + ); result.missing.forEach((v: string) => this.reportMissing(v)); - throw new Error('Invalid "app.json" file, please ensure it matches the schema. (TODO: insert link here)'); + throw new Error( + 'Invalid "app.json" file, please ensure it matches the schema. (TODO: insert link here)', + ); } } @@ -185,7 +209,9 @@ export class FolderDetails { ); if (error.subErrors) { - error.subErrors.forEach((err) => this.reportError(err, `${indent} `)); + error.subErrors.forEach((err) => + this.reportError(err, `${indent} `), + ); } } @@ -197,13 +223,19 @@ export class FolderDetails { ); } - private async updateInfoFileRequiredVersion(requiredApiVersion: string): Promise { + private async updateInfoFileRequiredVersion( + requiredApiVersion: string, + ): Promise { const info = { ...this.info, requiredApiVersion, }; - await fs.writeFile(path.join(this.folder, 'app.json'), JSON.stringify(info), 'utf-8'); + await fs.writeFile( + path.join(this.folder, 'app.json'), + JSON.stringify(info), + 'utf-8', + ); await this.readInfoFile(); } }