Unverified Commit 1cd8c121 authored by ritwizsinha's avatar ritwizsinha Committed by GitHub

Adds template generation logic (#72)

* Adds template generation logic
Adds templates for
1. Api Endpoint Class
2. Slash Command Class
3. Settings

* Fixed settings boilerplate

* Removed hardcoded files from app create and added them to templates folder

* Separated boilerplate functions to different files
parent aef99a0f
import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import cli from 'cli-ux';
import * as fuzzy from 'fuzzy';
import * as inquirer from 'inquirer';
import { FolderDetails } from '../misc';
import { apiEndpointTemplate, appendNewSetting,
initialSettingTemplate, slashCommandTemplate } from '../templates/boilerplate';
export default class Generate extends Command {
public static description = 'Adds boilerplate code for various functions';
public static flags = {
help: flags.help({char: 'h'}),
options: flags.string({
char: 'o',
// tslint:disable-next-line:max-line-length
description: 'Choose the boilerplate needed a. Api Extension b. Slash Command Extension c. Settings Extension',
options: ['a', 'b', 'c'],
}),
};
public async run() {
const { flags } = this.parse(Generate);
const fd = new FolderDetails(this);
try {
await fd.readInfoFile();
} catch (e) {
this.error(chalk.bold.red(e && e.message ? e.message : e));
}
let option = flags.options;
const categories = [
'Api Extension',
'Slash Command Extension',
'Settings Extension',
];
if (!option) {
inquirer.registerPrompt('checkbox-plus', require('inquirer-checkbox-plus-prompt'));
const result = await inquirer.prompt([{
type: 'checkbox-plus',
name: 'categories',
message: 'Choose the boilerplate needed',
pageSize: 10,
highlight: true,
searchable: true,
validate: (answer: Array<string>) => {
if (answer.length === 0) {
return chalk.bold.redBright('You must choose at least one option.');
}
return true;
},
// tslint:disable:promise-function-async
source: (answersSoFar: object, input: string) => {
input = input || '';
return new Promise((resolve) => {
const fuzzyResult = fuzzy.filter(input, categories);
const data = fuzzyResult.map((element) => {
return element.original;
});
resolve(data);
});
},
}] as any);
option = (result as any).categories[0];
}
switch (option) {
case 'Api Extension':
this.ApiExtensionBoilerplate(fd);
break;
case 'Slash Command Extension':
this.SlashCommandExtension(fd);
break;
case 'Settings Extension':
this.SettingExtension(fd);
break;
default:
break;
}
}
private ApiExtensionBoilerplate = async (fd: FolderDetails): Promise<void> => {
const name = await cli.prompt(chalk.bold.greenBright('Name of endpoint class'));
const path = await cli.prompt(chalk.bold.greenBright('Path for endpoint'));
const toWrite = apiEndpointTemplate(name, path);
fd.generateEndpointClass(name, toWrite);
}
private SlashCommandExtension = async (fd: FolderDetails): Promise<void> => {
const name = await cli.prompt(chalk.bold.greenBright('Name of command class'));
const toWrite = slashCommandTemplate(name);
fd.generateCommandClass(name, toWrite);
}
private SettingExtension = async (fd: FolderDetails): Promise<void> => {
let data = '';
if (await fd.doesFileExist('settings.ts')) {
data = fd.readSettingsFile();
}
if (data === '') {
data = initialSettingTemplate();
}
data = appendNewSetting(data);
fd.writeToSettingsFile(data);
}
}
......@@ -3,6 +3,8 @@ import { exec } from 'child_process';
import * as fs from 'fs';
import pascalCase = require('pascal-case');
import { editorConfigTemplate, gitIgnoreTemplate, mainTemplate, packageJsonTemplate,
readmeTemplate, tsConfigTemplate, tsLintConfigTemplate, vsCodeExtsTemplate } from '../templates/app/index';
import { FolderDetails } from './folderDetails';
export class AppCreator {
......@@ -54,21 +56,7 @@ export class AppCreator {
}
private createMainTypeScriptFile(): void {
const toWrite =
`import {
IAppAccessors,
ILogger,
} from '@rocket.chat/apps-engine/definition/accessors';
import { App } from '@rocket.chat/apps-engine/definition/App';
import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
export class ${ pascalCase(this.fd.info.name) }App extends App {
constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) {
super(info, logger, accessors);
}
}
`;
const toWrite = mainTemplate(pascalCase(this.fd.info.name));
fs.writeFileSync(this.fd.mergeWithFolder(this.fd.info.classFile), toWrite, 'utf8');
}
......@@ -81,167 +69,37 @@ export class ${ pascalCase(this.fd.info.name) }App extends App {
// tslint:disable:max-line-length
private createdReadme(): void {
const toWrite =
`# ${ this.fd.info.name }
${ this.fd.info.description }
## Getting Started
Now that you have generated a blank default Rocket.Chat App, what are you supposed to do next?
Start developing! Open up your favorite editor, our recommended one is Visual Studio code,
and start working on your App. Once you have something ready to test, you can either
package it up and manually deploy it to your test instance or you can use the CLI to do so.
Here are some commands to get started:
- \`rc-apps package\`: this command will generate a packaged app file (zip) which can be installed **if** it compiles with TypeScript
- \`rc-apps deploy\`: this will do what \`package\` does but will then ask you for your server url, username, and password to deploy it for you
## Documentation
Here are some links to examples and documentation:
- [Rocket.Chat Apps TypeScript Definitions Documentation](https://rocketchat.github.io/Rocket.Chat.Apps-engine/)
- [Rocket.Chat Apps TypeScript Definitions Repository](https://github.com/RocketChat/Rocket.Chat.Apps-engine)
- [Example Rocket.Chat Apps](https://github.com/graywolf336/RocketChatApps)
- Community Forums
- [App Requests](https://forums.rocket.chat/c/rocket-chat-apps/requests)
- [App Guides](https://forums.rocket.chat/c/rocket-chat-apps/guides)
- [Top View of Both Categories](https://forums.rocket.chat/c/rocket-chat-apps)
- [#rocketchat-apps on Open.Rocket.Chat](https://open.rocket.chat/channel/rocketchat-apps)
`;
const toWrite = readmeTemplate(this.fd.info.name, this.fd.info.description);
fs.writeFileSync('README.md', toWrite, 'utf8');
}
private createTsConfig(): void {
const toWrite =
`{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"moduleResolution": "node",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"strictNullChecks": true,
"noImplicitReturns": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
},
"include": [
"**/*.ts"
]
}
`;
const toWrite = tsConfigTemplate();
fs.writeFileSync(this.fd.mergeWithFolder('tsconfig.json'), toWrite, 'utf8');
}
private createTsLintConfig(): void {
const toWrite =
`{
"extends": "tslint:recommended",
"rules": {
"array-type": [true, "generic"],
"member-access": true,
"no-console": [false],
"no-duplicate-variable": true,
"object-literal-sort-keys": false,
"quotemark": [true, "single"],
"max-line-length": [true, {
"limit": 160,
"ignore-pattern": "^import | *export .*? {"
}]
}
}
`;
const toWrite = tsLintConfigTemplate();
fs.writeFileSync(this.fd.mergeWithFolder('tslint.json'), toWrite, 'utf8');
}
private createPackageJson(): void {
const toWrite =
`{
"devDependencies": {
"@rocket.chat/apps-engine": "^1.12.0",
"@types/node": "^10.12.14",
"tslint": "^5.10.0",
"typescript": "^2.9.1"
}
}
`;
const toWrite = packageJsonTemplate();
fs.writeFileSync(this.fd.mergeWithFolder('package.json'), toWrite, 'utf8');
}
private createGitIgnore(): void {
const toWrite =
`# ignore modules pulled in from npm
node_modules/
# rc-apps package output
dist/
# JetBrains IDEs
out/
.idea/
.idea_modules/
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
`;
const toWrite = gitIgnoreTemplate();
fs.writeFileSync(this.fd.mergeWithFolder('.gitignore'), toWrite, 'utf8');
}
private createEditorConfig(): void {
const toWrite =
`# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
indent_style = space
indent_size = 4
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
`;
const toWrite = editorConfigTemplate();
fs.writeFileSync(this.fd.mergeWithFolder('.editorconfig'), toWrite, 'utf8');
}
private createVsCodeExts(): void {
const toWrite =
`{
"recommendations": [
"EditorConfig.editorconfig",
"eamodio.gitlens",
"eg2.vscode-npm-script",
"wayou.vscode-todo-highlight",
"minhthai.vscode-todo-parser",
"ms-vscode.vscode-typescript-tslint-plugin",
"rbbit.typescript-hero"
]
}`;
const toWrite = vsCodeExtsTemplate();
fs.mkdirSync(this.fd.mergeWithFolder('.vscode'));
fs.writeFileSync(this.fd.mergeWithFolder('.vscode/extensions.json'), toWrite, 'utf8');
}
......
......@@ -40,6 +40,34 @@ export class FolderDetails {
this.info = appInfo;
}
public generateDirectory(dirName: string): void {
const dirPath = path.join(this.folder, dirName);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
}
public generateEndpointClass(name: string, toWrite: string): void {
const dir = 'endpoints';
const dirPath = path.join(this.folder, dir);
this.generateDirectory(dir);
fs.writeFileSync(path.join(dirPath, `${name}.ts`), toWrite, 'utf8');
}
public generateCommandClass(name: string, toWrite: string): void {
const dir = 'slashCommands';
const dirPath = path.join(this.folder, dir);
this.generateDirectory(dir);
fs.writeFileSync(path.join(dirPath, `${name}.ts`), toWrite, 'utf8');
}
public readSettingsFile(): string {
return fs.readFileSync(path.join(this.folder, 'settings.ts'), 'utf-8');
}
public writeToSettingsFile(toWrite: string): void {
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.
* Throws an error when something isn't right.
......
export const editorConfigTemplate = (): string => {
return `# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
indent_style = space
indent_size = 4
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
`;
};
export const gitIgnoreTemplate = (): string => {
return `# ignore modules pulled in from npm
node_modules/
# rc-apps package output
dist/
# JetBrains IDEs
out/
.idea/
.idea_modules/
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
`;
};
import { editorConfigTemplate } from './editorConfigTemplate';
import { gitIgnoreTemplate } from './gitIgnoreTemplate';
import { mainTemplate } from './mainTemplate';
import { packageJsonTemplate } from './packageJsonTemplate';
import { readmeTemplate } from './readmeTemplate';
import { tsConfigTemplate } from './tsConfigTemplate';
import { tsLintConfigTemplate } from './tsLintConfigTemplate';
import { vsCodeExtsTemplate } from './vsCodeExtsTemplate';
export {
mainTemplate,
readmeTemplate,
tsConfigTemplate,
tsLintConfigTemplate,
packageJsonTemplate,
gitIgnoreTemplate,
editorConfigTemplate,
vsCodeExtsTemplate,
};
export const mainTemplate = (appName: string): string => {
return `import {
IAppAccessors,
ILogger,
} from '@rocket.chat/apps-engine/definition/accessors';
import { App } from '@rocket.chat/apps-engine/definition/App';
import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
export class ${ appName }App extends App {
constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) {
super(info, logger, accessors);
}
}
`;
};
export const packageJsonTemplate = (): string => {
return `{
"devDependencies": {
"@rocket.chat/apps-engine": "^1.12.0",
"@types/node": "^10.12.14",
"tslint": "^5.10.0",
"typescript": "^2.9.1"
}
}`;
};
export const readmeTemplate = (name: string, description: string): string => {
return `# ${ name }
${ description }
## Getting Started
Now that you have generated a blank default Rocket.Chat App, what are you supposed to do next?
Start developing! Open up your favorite editor, our recommended one is Visual Studio code,
and start working on your App. Once you have something ready to test, you can either
package it up and manually deploy it to your test instance or you can use the CLI to do so.
Here are some commands to get started:
- \`rc-apps package\`: this command will generate a packaged app file (zip) which can be installed **if** it compiles with TypeScript
- \`rc-apps deploy\`: this will do what \`package\` does but will then ask you for your server url, username, and password to deploy it for you
## Documentation
Here are some links to examples and documentation:
- [Rocket.Chat Apps TypeScript Definitions Documentation](https://rocketchat.github.io/Rocket.Chat.Apps-engine/)
- [Rocket.Chat Apps TypeScript Definitions Repository](https://github.com/RocketChat/Rocket.Chat.Apps-engine)
- [Example Rocket.Chat Apps](https://github.com/graywolf336/RocketChatApps)
- Community Forums
- [App Requests](https://forums.rocket.chat/c/rocket-chat-apps/requests)
- [App Guides](https://forums.rocket.chat/c/rocket-chat-apps/guides)
- [Top View of Both Categories](https://forums.rocket.chat/c/rocket-chat-apps)
- [#rocketchat-apps on Open.Rocket.Chat](https://open.rocket.chat/channel/rocketchat-apps)
`;
};
export const tsConfigTemplate = (): string => {
return `{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"moduleResolution": "node",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"strictNullChecks": true,
"noImplicitReturns": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
},
"include": [
"**/*.ts"
]
}
`;
};
export const tsLintConfigTemplate = (): string => {
return `{
"extends": "tslint:recommended",
"rules": {
"array-type": [true, "generic"],
"member-access": true,
"no-console": [false],
"no-duplicate-variable": true,
"object-literal-sort-keys": false,
"quotemark": [true, "single"],
"max-line-length": [true, {
"limit": 160,
"ignore-pattern": "^import | *export .*? {"
}]
}
}
`;
};
export const vsCodeExtsTemplate = (): string => {
return ` {
"recommendations": [
"EditorConfig.editorconfig",
"eamodio.gitlens",
"eg2.vscode-npm-script",
"wayou.vscode-todo-highlight",
"minhthai.vscode-todo-parser",
"ms-vscode.vscode-typescript-tslint-plugin",
"rbbit.typescript-hero"
]
}
`;
};
import pascalCase = require('pascal-case');
export const apiEndpointTemplate = (endpointClassName: string, path: string): string => {
return `
import { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors';
import { ApiEndpoint, IApiEndpointInfo, IApiRequest, IApiResponse } from '@rocket.chat/apps-engine/definition/api';
export class ${pascalCase(endpointClassName)} extends ApiEndpoint {
public path = '${path}';
public example = [];
public async get(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify,
http: IHttp, persis: IPersistence): Promise<IApiResponse> {
throw new Error('Method not implemented');
}
public async head(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify,
http: IHttp, persis: IPersistence): Promise<IApiResponse> {
throw new Error('Method not implemented');
}
public async options(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify,
http: IHttp, persis: IPersistence): Promise<IApiResponse> {
throw new Error('Method not implemented');
}
public async patch(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify,
http: IHttp, persis: IPersistence): Promise<IApiResponse> {
throw new Error('Method not implemented');
}
public post(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify,
http: IHttp, persis: IPersistence): Promise<IApiResponse> {
throw new Error('Method not implemented');
}
public put(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify,
http: IHttp, persis: IPersistence): Promise<IApiResponse> {
throw new Error('Method not implemented');
}
}
`;
};
import { apiEndpointTemplate } from './apiEndpoint';
import { appendNewSetting, initialSettingTemplate } from './setting';
import { slashCommandTemplate } from './slashCommand';
export {
apiEndpointTemplate,
appendNewSetting,
initialSettingTemplate,
slashCommandTemplate,
};
export const initialSettingTemplate = (): string => {
return `
import { ISetting, SettingType } from '@rocket.chat/apps-engine/definition/settings';
export const settings: Array<ISetting> = [
];
`;
};
export const appendNewSetting = (data: string): string => {
const toWrite = `
{
id: '',
type: SettingType.STRING,
packageValue: '',
required: false,
public: false,
i18nLabel: '',
i18nDescription: '',
},
`;
const index = data.lastIndexOf('];');
return data.slice(0, index) + toWrite + data.slice(index);
};
import pascalCase = require('pascal-case');
export const slashCommandTemplate = (commandName: string): string => {
return `
import { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors';
import { ISlashCommand, ISlashCommandPreview,
ISlashCommandPreviewItem, SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands';
export class ${pascalCase(commandName)} implements ISlashCommand {
public command = '';
public i18nDescription = '';
public i18nParamsExample = '';
public permission = '';
public providesPreview = false;
public async executePreviewItem(item: ISlashCommandPreviewItem, context: SlashCommandContext,
read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise<void> {
throw new Error('Method not implemented');
}
public async executor(context: SlashCommandContext,read: IRead, modify: IModify,
http: IHttp, persis: IPersistence): Promise<void> {