Links
https://github.com/EndyKaufman/kaufman-bot - source code of bot
https://telegram.me/KaufmanBot - current bot in telegram
https://github.com/kaufman-bot/schematics-example - project generated with @kaufman-bot/schematics
Description of work
In the current post, I'm copying the NestJS schemas for building apps and libraries, and after a lot of modifications, I'll make the schemas for building kaufman-bot apps and libraries.
Create new libraries
Create new library for schematics
npm run nx -- g lib schematics
endy@endy-virtual-machine:~/Projects/current/kaufman-bot$ npm run nx -- g lib schematics > kaufman-bot@2.2.2 nx > nx "g" "lib" "schematics" CREATE libs/schematics/README.md CREATE libs/schematics/.babelrc CREATE libs/schematics/src/index.ts CREATE libs/schematics/tsconfig.json CREATE libs/schematics/tsconfig.lib.json UPDATE tsconfig.base.json CREATE libs/schematics/project.json UPDATE workspace.json CREATE libs/schematics/.eslintrc.json CREATE libs/schematics/jest.config.js CREATE libs/schematics/tsconfig.spec.json CREATE libs/schematics/src/lib/schematics.module.ts
Append logic for schematics
Remove not need files
rm -rf libs/schematics/src/lib
Clone nrwl nx repository
git clone git@github.com:nrwl/nx.git
endy@endy-virtual-machine:~/Projects/current/kaufman-bot$ git clone git@github.com:nrwl/nx.git Cloning into 'nx'... remote: Enumerating objects: 94099, done. remote: Counting objects: 100% (10/10), done. remote: Compressing objects: 100% (10/10), done. remote: Total 94099 (delta 0), reused 2 (delta 0), pack-reused 94089 Receiving objects: 100% (94099/94099), 69.44 MiB | 4.43 MiB/s, done. Resolving deltas: 100% (64903/64903), done.
Copy-past logic
Copy nest schematics to kaufman-bot libs
cp -Rf ./nx/packages/nest/** ./libs/schematics
Remove not need files
rm -rf libs/schematics/src/generators/class libs/schematics/src/generators/controller libs/schematics/src/generators/convert-tslint-to-eslint libs/schematics/src/generators/decorator libs/schematics/src/generators/filter libs/schematics/src/generators/gateway libs/schematics/src/generators/guard libs/schematics/src/generators/interceptor libs/schematics/src/generators/interface libs/schematics/src/generators/middleware libs/schematics/src/generators/module libs/schematics/src/generators/pipe libs/schematics/src/generators/provider libs/schematics/src/generators/resolver libs/schematics/src/generators/resource libs/schematics/src/generators/service libs/schematics/src/lib libs/schematics/src/migrations libs/schematics/README.md libs/schematics/migrations.spec.ts libs/schematics/migrations.json libs/schematics/index.ts nx libs/schematics/index.ts libs/schematics/src/generators/library/library.spec.ts libs/schematics/src/generators/library/snapshots libs/schematics/src/index.ts libs/schematics/src/generators/init/init.spec.ts libs/schematics/src/generators/utils/run-nest-schematic.spec.ts libs/schematics/src/generators/application/application.spec.ts libs/schematics/src/generators/application/files/app/app.controller.spec.ts_tmpl_ libs/schematics/src/generators/application/files/app/app.controller.ts_tmpl_ libs/schematics/src/generators/application/files/app/app.service.spec.ts_tmpl_ libs/schematics/src/generators/application/files/app/app.service.ts_tmpl_ libs/schematics/src/generators/library/snapshots/library.spec.ts.snap libs/schematics/src/generators/library/library.spec.ts libs/schematics/src/generators/utils/run-nest-schematic.spec.ts libs/schematics/src/index.ts libs/schematics/src/generators/library/files/controller/src/lib/fileName.controller.spec.ts_tmpl_ libs/schematics/src/generators/library/files/controller/src/lib/fileName.controller.ts_tmpl_ libs/schematics/src/generators/library/files/service/src/lib/fileName.service.spec.ts_tmpl_
Update generators list
libs/schematics/generators.json
{ "name": "nx/nest", "version": "0.1", "extends": ["@nrwl/workspace"], "schematics": { "application": { "factory": "./src/generators/application/application#applicationSchematic", "schema": "./src/generators/application/schema.json", "aliases": ["app"], "x-type": "application", "description": "Create a KaufmanBot application." }, "init": { "factory": "./src/generators/init/init#initSchematic", "schema": "./src/generators/init/schema.json", "description": "Initialize the `@kaufman-bot/schematics` plugin.", "aliases": ["ng-add"], "hidden": true }, "library": { "factory": "./src/generators/library/library#librarySchematic", "schema": "./src/generators/library/schema.json", "aliases": ["lib"], "x-type": "library", "description": "Create a new KaufmanBot library." } }, "generators": { "application": { "factory": "./src/generators/application/application", "schema": "./src/generators/application/schema.json", "aliases": ["app"], "x-type": "application", "description": "Create a KaufmanBot application." }, "init": { "factory": "./src/generators/init/init", "schema": "./src/generators/init/schema.json", "description": "Initialize the `@kaufman-bot/schematics` plugin.", "aliases": ["ng-add"], "hidden": true }, "library": { "factory": "./src/generators/library/library", "schema": "./src/generators/library/schema.json", "aliases": ["lib"], "x-type": "library", "description": "Create a new KaufmanBot library." } } }
Replace all names
npx -y replace-in-files-cli --string="build/packages/nest" --replacement="dist/libs/schematics" './libs/schematics/**'
npx -y replace-in-files-cli --string="packages/nest" --replacement="libs/schematics" './libs/schematics/**'
Update template file of main
libs/schematics/src/generators/application/files/main.ts__tmpl__
// eslint-disable-next-line @typescript-eslint/no-var-requires require('source-map-support').install(); import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import env from 'env-var'; import { getBotToken } from 'nestjs-telegraf'; import { AppModule } from './app/app.module'; const logger = new Logger('Application'); //do something when app is closing process.on('exit', exitHandler.bind(null, { cleanup: true })); //catches ctrl+c event process.on('SIGINT', exitHandler.bind(null, { exit: true })); // catches "kill pid" (for example: nodemon restart) process.on('SIGUSR1', exitHandler.bind(null, { exit: true })); process.on('SIGUSR2', exitHandler.bind(null, { exit: true })); //catches uncaught exceptions process.on('uncaughtException', exitHandler.bind(null, { exit: true })); async function bootstrap() { const app = await NestFactory.create(AppModule); const TELEGRAM_BOT_WEB_HOOKS_PATH = env .get('TELEGRAM_BOT_WEB_HOOKS_PATH') .asString(); if (TELEGRAM_BOT_WEB_HOOKS_PATH) { const bot = app.get(getBotToken()); app.use(bot.webhookCallback(TELEGRAM_BOT_WEB_HOOKS_PATH)); } const port = env.get('PORT').default(3333).asPortNumber(); await app.listen(port); logger.log(`🚀 Application is running on: http://localhost:${port}`); } try { bootstrap().catch((err) => { logger.error(err, err.stack); }); } catch (err) { logger.error(err, err.stack); } function exitHandler(options, exitCode) { if (options.cleanup) { logger.log('exit: clean'); } if (exitCode || exitCode === 0) { if (exitCode !== 0) { logger.error(exitCode, exitCode.stack); logger.log(`exit: code - ${exitCode}`); } else { logger.log(`exit: code - ${exitCode}`); } } if (options.exit) { process.exit(); } }
Update template of service
libs/schematics/src/generators/application/files/app/app.service.ts__tmpl__
import { BotInGroupsProcessorService } from '@kaufman-bot/bot-in-groups-server'; import { BotCommandsService } from '@kaufman-bot/core-server'; import { Injectable, Logger } from '@nestjs/common'; import { On, Start, Update, Use } from 'nestjs-telegraf'; import { Context } from 'telegraf'; @Update() @Injectable() export class AppService { private readonly logger = new Logger(AppService.name); constructor( private readonly botCommandsService: BotCommandsService, private readonly botInGroupsProcessorService: BotInGroupsProcessorService ) {} @Start() async startCommand(ctx: Context) { await this.botCommandsService.start(ctx); } @Use() async use(ctx) { try { await this.botInGroupsProcessorService.process(ctx); } catch (err) { this.logger.error(err, err.stack); } } @On('sticker') async onSticker(ctx) { try { await this.botCommandsService.process(ctx); } catch (err) { this.logger.error(err, err.stack); } } @On('text') async onMessage(ctx) { try { await this.botCommandsService.process(ctx); } catch (err) { this.logger.error(err, err.stack); } } }
Update template of module
libs/schematics/src/generators/application/files/app/app.module.ts__tmpl__
import { BotInGroupsModule } from '@kaufman-bot/bot-in-groups-server'; import { BotCommandsModule } from '@kaufman-bot/core-server'; import { DebugMessagesModule } from '@kaufman-bot/debug-messages-server'; import { FactsGeneratorModule } from '@kaufman-bot/facts-generator-server'; import { LanguageSwitherModule } from '@kaufman-bot/language-swither-server'; import { ShortCommandsModule } from '@kaufman-bot/short-commands-server'; import { Module } from '@nestjs/common'; import env from 'env-var'; import { TelegrafModule } from 'nestjs-telegraf'; import { getDefaultTranslatesModuleOptions, TranslatesModule, } from 'nestjs-translates'; import { join } from 'path'; import { AppService } from './app.service'; const TELEGRAM_BOT_WEB_HOOKS_DOMAIN = env .get('TELEGRAM_BOT_WEB_HOOKS_DOMAIN') .asString(); const TELEGRAM_BOT_WEB_HOOKS_PATH = env .get('TELEGRAM_BOT_WEB_HOOKS_PATH') .asString(); const BOT_NAMES = env.get('BOT_NAMES').required().asArray(); @Module({ imports: [ TelegrafModule.forRoot({ token: env.get('TELEGRAM_BOT_TOKEN').required().asString(), launchOptions: { dropPendingUpdates: true, ...(TELEGRAM_BOT_WEB_HOOKS_DOMAIN && TELEGRAM_BOT_WEB_HOOKS_PATH ? { webhook: { domain: TELEGRAM_BOT_WEB_HOOKS_DOMAIN, hookPath: TELEGRAM_BOT_WEB_HOOKS_PATH, }, } : {}), }, }), TranslatesModule.forRoot( getDefaultTranslatesModuleOptions({ localePaths: [ join(__dirname, 'assets', 'i18n'), join(__dirname, 'assets', 'i18n', 'getText'), join(__dirname, 'assets', 'i18n', 'class-validator-messages'), ], vendorLocalePaths: [join(__dirname, 'assets', 'i18n')], locales: ['en'], }) ), DebugMessagesModule.forRoot(), BotCommandsModule.forRoot({ admins: env.get('TELEGRAM_BOT_ADMINS').default('').asArray(','), commit: env.get('DEPLOY_COMMIT').default('').asString(), date: env.get('DEPLOY_DATE').default('').asString(), version: env.get('DEPLOY_VERSION').default('').asString(), }), ShortCommandsModule.forRoot({ commands: { en: { '*fact*|history': 'get facts', '*what you can do*|faq': 'help', 'disable debug': 'debug off', 'enable debug': 'debug on', }, }, }), BotInGroupsModule.forRoot({ botNames: { en: BOT_NAMES, }, botMeetingInformation: { en: [`Hello! I'm ${BOT_NAMES[0]} 😉`, 'Hello!', 'Hello 🖖'], }, }), LanguageSwitherModule.forRoot(), FactsGeneratorModule.forRoot(), ], providers: [AppService], }) export class AppModule {}
Update library service template
libs/schematics/src/generators/library/files/service/src/lib/__fileName__.service.ts__tmpl__
import { BotCommandsEnum, BotCommandsProvider, BotCommandsProviderActionMsg, BotCommandsProviderActionResultType, BotCommandsToolsService, } from '@kaufman-bot/core-server'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { getText } from 'class-validator-multi-lang'; import { TranslatesService } from 'nestjs-translates'; export const <%= constantName %>_CONFIG = '<%= constantName %>_CONFIG'; export interface <%= className %>Config { title: string; name: string; descriptions: string; usage: string[]; spyWords: string[]; category: string; } @Injectable() export class <%= className %>Service implements BotCommandsProvider { private readonly logger = new Logger(<%= className %>Service.name); constructor( @Inject(<%= constantName %>_CONFIG) private readonly <%= propertyName %>Config: <%= className %>Config, private readonly translatesService: TranslatesService, private readonly commandToolsService: BotCommandsToolsService, private readonly botCommandsToolsService: BotCommandsToolsService ) {} async onHelp< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >(msg: TMsg): Promise<BotCommandsProviderActionResultType<TMsg>> { return await this.onMessage({ ...msg, text: `${this.<%= propertyName %>Config.name} ${BotCommandsEnum.help}`, }); } async onMessage< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >(msg: TMsg): Promise<BotCommandsProviderActionResultType<TMsg>> { const locale = this.botCommandsToolsService.getLocale(msg, 'en'); const spyWord = this.<%= propertyName %>Config.spyWords.find((spyWord) => this.commandToolsService.checkCommands(msg.text, [spyWord], locale) ); if (spyWord) { if ( this.commandToolsService.checkCommands( msg.text, [BotCommandsEnum.help], locale ) ) { return { type: 'markdown', message: msg, markdown: this.commandToolsService.generateHelpMessage(msg, { locale, name: this.<%= propertyName %>Config.title, descriptions: this.<%= propertyName %>Config.descriptions, usage: this.<%= propertyName %>Config.usage, category: this.<%= propertyName %>Config.category, }), }; } const processedMsg = await this.process(msg, locale); if (typeof processedMsg === 'string') { return { type: 'text', message: msg, text: processedMsg, }; } if (processedMsg) { return { type: 'message', message: processedMsg }; } this.logger.warn(`Unhandled commands for text: "${msg.text}"`); this.logger.debug(msg); } return null; } private async process< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >(msg: TMsg, locale: string) { if ( this.commandToolsService.checkCommands( msg.text, [getText('ping')], locale ) ) { return this.translatesService.translate( getText('pong'), locale ); } return null; } }
Update library module template
libs/schematics/src/generators/library/files/common/src/lib/__fileName__.module.ts__tmpl__
import { BotCommandsCategory, BotCommandsModule, BOT_COMMANDS_PROVIDER, } from '@kaufman-bot/core-server'; import { DynamicModule, Module } from '@nestjs/common'; import { getText } from 'class-validator-multi-lang'; import { TranslatesModule } from 'nestjs-translates'; import { <%= className %>Service, <%= className %>Config, <%= constantName %>_CONFIG, } from './<%= fileName %>.service'; @Module({ imports: [TranslatesModule, BotCommandsModule], exports: [TranslatesModule, BotCommandsModule], }) export class <%= className %>Module { static forRoot(): DynamicModule { return { module: <%= className %>Module, providers: [ { provide: <%= constantName %>_CONFIG, useValue: <<%= className %>Config>{ title: getText('<%= TitleName %> commands'), name: '<%= propertyName %>', usage: [ getText('<%= propertyName %> ping'), getText('<%= propertyName %> help'), ], descriptions: getText( 'Commands for <%= titleName %>' ), spyWords: [getText('<%= propertyName %>')], category: BotCommandsCategory.user, }, }, { provide: BOT_COMMANDS_PROVIDER, useClass: <%= className %>Service, }, ], exports: [<%= constantName %>_CONFIG], }; } }
Add custom schematics helpers
libs/schematics/src/generators/init/lib/add-custom.ts
import type { Tree } from '@nrwl/devkit'; import { updateJson } from '@nrwl/devkit'; export function updateTsConfig(tree: Tree) { if (tree.exists('tsconfig.base.json')) { updateJson(tree, 'tsconfig.base.json', (json) => { json['compilerOptions'] = { ...json['compilerOptions'], allowSyntheticDefaultImports: true, strictNullChecks: true, noImplicitOverride: true, strictPropertyInitialization: true, noImplicitReturns: true, noFallthroughCasesInSwitch: true, esModuleInterop: true, noImplicitAny: false, }; return json; }); } } export function addScript(tree: Tree, projectName: string) { updateJson(tree, 'package.json', (json) => { json['scripts'] = { ...json['scripts'], rucken: 'rucken', nx: 'nx', }; if (!json['scripts'][`serve:${projectName}-local`]) { json['scripts'][ `serve:${projectName}-local` ] = `export $(xargs < ./.env.local) > /dev/null 2>&1 && npm run nx -- serve ${projectName}`; } return json; }); } export function addGitIgnoreEntry(host: Tree) { if (host.exists('.gitignore')) { let content = host.read('.gitignore', 'utf-8'); if (!content?.includes('*.env.*')) { content = `${content}\n*.env.*\n`; } host.write('.gitignore', content); } else { host.write('.gitignore', `*.env.*\n`); } } export function addEnvFilesEntry(host: Tree, botName: string) { append('.env.local'); append('.env-example.local'); function append(filename: string) { let content = ''; if (host.exists(filename)) { content = host.read(filename, 'utf-8') || ''; } const contentRows = content.split('\n'); const newRows: string[] = []; const rows = [ `TELEGRAM_BOT_TOKEN=`, `TELEGRAM_BOT_WEB_HOOKS_DOMAIN=`, `TELEGRAM_BOT_WEB_HOOKS_PATH=`, `TELEGRAM_BOT_ADMINS=`, `BOT_NAMES=${botName}`, ]; for ( let contentRowindex = 0; contentRowindex < contentRows.length; contentRowindex++ ) { const contentRow = contentRows[contentRowindex]; for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { const row = rows[rowIndex]; if ((contentRow || '').split('=')[0] !== (row || '').split('=')[0]) { newRows.push(row); } } } host.write( filename, [ ...(contentRows.length === 1 && !contentRows[0] ? [] : contentRows), ...newRows, ].join('\n') ); } }
Update index file of init generator
libs/schematics/src/generators/init/lib/index.ts
export * from './add-custom'; export * from './add-dependencies'; export * from './normalize-options';
Update versions file
libs/schematics/src/utils/versions.ts
export const nxVersion = '*'; export const nestJsVersion7 = '^7.0.0'; export const nestJsVersion8 = '^8.0.0'; export const nestJsSchematicsVersion = '^8.0.0'; export const rxjsVersion6 = '~6.6.3'; export const rxjsVersion7 = '^7.0.0'; export const reflectMetadataVersion = '^0.1.13'; export const kaufmanBotVersion = '^2.2.2';
Update dependencies helper
libs/schematics/src/generators/init/lib/add-dependencies.ts
import type { GeneratorCallback, Tree } from '@nrwl/devkit'; import { addDependenciesToPackageJson, readJson } from '@nrwl/devkit'; import { satisfies } from 'semver'; import { kaufmanBotVersion, nestJsSchematicsVersion, nestJsVersion7, nestJsVersion8, nxVersion, reflectMetadataVersion, rxjsVersion6, rxjsVersion7, } from '../../../utils/versions'; export function addDependencies(tree: Tree): GeneratorCallback { // Old nest 7 and rxjs 6 by default let NEST_VERSION = nestJsVersion7; let RXJS = rxjsVersion6; const packageJson = readJson(tree, 'package.json'); if (packageJson.dependencies['@angular/core']) { let rxjs = packageJson.dependencies['rxjs']; if (rxjs.startsWith('~') || rxjs.startsWith('^')) { rxjs = rxjs.substring(1); } if (satisfies(rxjs, rxjsVersion7)) { NEST_VERSION = nestJsVersion8; RXJS = packageJson.dependencies['rxjs']; } } else { NEST_VERSION = nestJsVersion8; RXJS = rxjsVersion7; } return addDependenciesToPackageJson( tree, { '@nestjs/common': NEST_VERSION, '@nestjs/core': NEST_VERSION, '@nestjs/platform-express': NEST_VERSION, 'reflect-metadata': reflectMetadataVersion, '@kaufman-bot/bot-in-groups-server': kaufmanBotVersion, '@kaufman-bot/core-server': kaufmanBotVersion, '@kaufman-bot/debug-messages-server': kaufmanBotVersion, '@kaufman-bot/language-swither-server': kaufmanBotVersion, '@kaufman-bot/short-commands-server': kaufmanBotVersion, '@kaufman-bot/html-scraper-server': kaufmanBotVersion, '@kaufman-bot/facts-generator-server': kaufmanBotVersion, '@ngneat/transloco': '^4.0.0', '@ngneat/transloco-locale': '^4.0.0', 'class-validator-multi-lang': '^0.130.201', 'class-transformer': '^0.5.1', 'class-transformer-global-storage': '^0.4.1-1', 'env-var': '^7.1.1', 'nestjs-telegraf': '^2.4.0', 'nestjs-translates': '^1.0.3', rxjs: RXJS, tslib: '^2.0.0', }, { '@nestjs/schematics': nestJsSchematicsVersion, '@nestjs/testing': NEST_VERSION, '@nrwl/nest': nxVersion, '@ngneat/transloco-keys-manager': '^3.4.1', "source-map-support": "^0.5.21", rucken: '^3.5.3', } ); }
Update application generator helper
libs/schematics/src/generators/application/application.ts
import type { GeneratorCallback, Tree } from '@nrwl/devkit'; import { convertNxGenerator, formatFiles } from '@nrwl/devkit'; import { applicationGenerator as nodeApplicationGenerator } from '@nrwl/node'; import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; import { initGenerator } from '../init/init'; import { addScript } from '../init/lib'; import { createFiles, normalizeOptions, toNodeApplicationGeneratorOptions, updateTsConfig, } from './lib'; import type { ApplicationGeneratorOptions } from './schema'; export async function applicationGenerator( tree: Tree, rawOptions: ApplicationGeneratorOptions ): Promise<GeneratorCallback> { const options = normalizeOptions(tree, rawOptions); addScript(tree, rawOptions.name); const initTask = await initGenerator(tree, { botName: options.botName, unitTestRunner: options.unitTestRunner, skipFormat: true, }); const nodeApplicationTask = await nodeApplicationGenerator(tree, { ...toNodeApplicationGeneratorOptions(options), }); createFiles(tree, options); updateTsConfig(tree, options); if (!options.skipFormat) { await formatFiles(tree); } return runTasksInSerial(initTask, nodeApplicationTask); } export default applicationGenerator; export const applicationSchematic = convertNxGenerator(applicationGenerator);
Update init helper
libs/schematics/src/generators/init/init.ts
import type { GeneratorCallback, Tree } from '@nrwl/devkit'; import { convertNxGenerator, formatFiles } from '@nrwl/devkit'; import { initGenerator as nodeInitGenerator } from '@nrwl/node'; import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; import { setDefaultCollection } from '@nrwl/workspace/src/utilities/set-default-collection'; import { addDependencies, addEnvFilesEntry, addGitIgnoreEntry, normalizeOptions, updateTsConfig, } from './lib'; import type { InitGeneratorOptions } from './schema'; export async function initGenerator( tree: Tree, rawOptions: InitGeneratorOptions ): Promise<GeneratorCallback> { const options = normalizeOptions(rawOptions); setDefaultCollection(tree, '@kaufman-bot/schematics'); updateTsConfig(tree); addGitIgnoreEntry(tree); addEnvFilesEntry(tree, options.botName); const nodeInitTask = await nodeInitGenerator(tree, options); const installPackagesTask = addDependencies(tree); if (!options.skipFormat) { await formatFiles(tree); } return runTasksInSerial(nodeInitTask, installPackagesTask); } export default initGenerator; export const initSchematic = convertNxGenerator(initGenerator);
Create public.ts
libs/schematics/src/public.ts
export { applicationGenerator } from './generators/application/application'; export { initGenerator } from './generators/init/init'; export { libraryGenerator } from './generators/library/library';
Update package.json
libs/schematics/package.json
{ "name": "@kaufman-bot/schematics", "description": "The Nx Plugin for Nest that contains executors and generators for allowing your workspace to create KaufmanBot APIs.", "license": "MIT", "author": "EndyKaufman <admin@site15.ru>", "keywords": [ "Monorepo", "Node", "Nest", "CLI", "kaufman-bot", "nx", "schematics", "nests", "telegram" ], "bugs": { "url": "https://github.com/EndyKaufman/kaufman-bot/issues" }, "homepage": "https://github.com/EndyKaufman/kaufman-bot", "repository": { "type": "git", "url": "git+https://github.com/EndyKaufman/kaufman-bot.git" }, "maintainers": [ { "name": "EndyKaufman", "email": "admin@site15.ru" } ], "readme": "README.md", "main": "./index.js", "typings": "./index.d.ts", "schematics": "./generators.json", "dependencies": { "@nrwl/devkit": "13.8.1", "@nrwl/linter": "13.8.1", "@nrwl/node": "13.8.1", "@nrwl/js": "^13.10.2", "@nrwl/jest": "13.8.1", "@nestjs/schematics": "^8.0.0" }, "version": "2.2.2", "i18n": [ { "scope": "schematics", "path": "src/i18n", "strategy": "join" } ] }
Update project.json file
libs/schematics/project.json
{ "root": "libs/schematics", "sourceRoot": "libs/schematics", "projectType": "library", "targets": { "test": { "executor": "@nrwl/jest:jest", "options": { "jestConfig": "libs/schematics/jest.config.js", "passWithNoTests": true }, "outputs": ["coverage/libs/schematics"] }, "build": { "executor": "@nrwl/js:tsc", "options": { "outputPath": "dist/libs/schematics", "tsConfig": "libs/schematics/tsconfig.lib.json", "main": "libs/schematics/index.ts", "updateBuildableProjectDepsInPackageJson": false, "assets": [ { "input": "libs/schematics", "glob": "**/files/**", "output": "/" }, { "input": "libs/schematics", "glob": "**/files/**/.gitkeep", "output": "/" }, { "input": "libs/schematics", "glob": "**/*.json", "ignore": ["**/tsconfig*.json", "project.json"], "output": "/" }, { "input": "libs/schematics", "glob": "**/*.js", "ignore": ["**/jest.config.js"], "output": "/" }, { "input": "libs/schematics", "glob": "**/*.d.ts", "output": "/" }, { "input": "", "glob": "LICENSE", "output": "/" }, { "input": "libs/schematics", "glob": "**/*.md", "output": "/" } ] }, "outputs": ["{options.outputPath}"] }, "lint": { "executor": "@nrwl/linter:eslint", "options": { "lintFilePatterns": [ "libs/schematics/**/*.ts", "libs/schematics/**/*.spec.ts", "libs/schematics/**/*_spec.ts", "libs/schematics/**/*.spec.tsx", "libs/schematics/**/*.spec.js", "libs/schematics/**/*.spec.jsx", "libs/schematics/**/*.d.ts" ] }, "outputs": ["{options.outputFile}"] } } }
Install need dependencies in root project
npm i --save-dev @nrwl/js @nrwl/devkit
Add custom config for rucken tools for exclude folders from generated index file
rucken.json
{ "makeTsList": { "indexFileName": "index", "excludes": [ "*node_modules*", "*public_api.ts*", "*.spec*", "environment*", "*test*", "*e2e*", "*.stories.ts", "*.d.ts", "*files*", "*generators*", "*utils*" ] } }
Update options of init generator
libs/schematics/src/generators/init/lib/normalize-options.ts
import type { InitGeneratorOptions } from '../schema'; export function normalizeOptions( options: InitGeneratorOptions ): InitGeneratorOptions & Pick<Required<InitGeneratorOptions>, 'botName'> { return { ...options, unitTestRunner: options.unitTestRunner ?? 'jest', botName: options.botName ?? 'Bot', }; }
Update schema types of init generator
libs/schematics/src/generators/init/schema.d.ts
import { UnitTestRunner } from '../utils'; export interface InitGeneratorOptions { botName?: string; skipFormat?: boolean; unitTestRunner?: UnitTestRunner; }
Update schema json on init generator
libs/schematics/src/generators/init/schema.json
{ "$schema": "http://json-schema.org/schema", "$id": "NxNestInitGenerator", "title": "Init Nest Plugin", "description": "Init Nest Plugin.", "cli": "nx", "type": "object", "properties": { "botName": { "description": "Bot name.", "type": "string", "default": "Bot" }, "unitTestRunner": { "description": "Adds the specified unit test runner.", "type": "string", "enum": ["jest", "none"], "default": "jest" }, "skipFormat": { "description": "Skip formatting files.", "type": "boolean", "default": false } }, "additionalProperties": false, "required": [] }
Update application options
libs/schematics/src/generators/application/lib/normalize-options.ts
import type { Tree } from '@nrwl/devkit'; import { getWorkspaceLayout, joinPathFragments, names } from '@nrwl/devkit'; import { Linter } from '@nrwl/linter'; import type { Schema as NodeApplicationGeneratorOptions } from '@nrwl/node/src/generators/application/schema'; import type { ApplicationGeneratorOptions, NormalizedOptions } from '../schema'; export function normalizeOptions( tree: Tree, options: ApplicationGeneratorOptions ): NormalizedOptions { const appDirectory = options.directory ? `${names(options.directory).fileName}/${names(options.name).fileName}` : names(options.name).fileName; const appProjectRoot = joinPathFragments( getWorkspaceLayout(tree).appsDir, appDirectory ); return { ...options, appProjectRoot, linter: options.linter ?? Linter.EsLint, unitTestRunner: options.unitTestRunner ?? 'jest', botName: options.botName, }; } export function toNodeApplicationGeneratorOptions( options: NormalizedOptions ): NodeApplicationGeneratorOptions { return { name: options.name, directory: options.directory, frontendProject: options.frontendProject, linter: options.linter, skipFormat: true, skipPackageJson: options.skipPackageJson, standaloneConfig: options.standaloneConfig, tags: options.tags, unitTestRunner: options.unitTestRunner, setParserOptionsProject: options.setParserOptionsProject, }; }
Update application schema type
libs/schematics/src/generators/application/schema.d.ts
import { Linter } from '@nrwl/linter'; import { UnitTestRunner } from '../../utils/test-runners'; export interface ApplicationGeneratorOptions { name: string; directory?: string; frontendProject?: string; linter?: Exclude<Linter, Linter.TsLint>; skipFormat?: boolean; skipPackageJson?: boolean; standaloneConfig?: boolean; tags?: string; unitTestRunner?: UnitTestRunner; setParserOptionsProject?: boolean; botName?: string; } interface NormalizedOptions extends ApplicationGeneratorOptions { appProjectRoot: Path; }
Update application schema json
libs/schematics/src/generators/application/schema.json
{ "$schema": "http://json-schema.org/schema", "$id": "NxNestKaufmanBotApplicationGenerator", "title": "Nx Nest KaufmanBot Application Options Schema", "description": "Nx Nest KaufmanBot Application Options Schema.", "cli": "nx", "type": "object", "properties": { "name": { "description": "The name of the application.", "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What name would you like to use for the node application?" }, "botName": { "description": "Bot name.", "type": "string", "default": "Bot" }, "directory": { "description": "The directory of the new application.", "type": "string" }, "skipFormat": { "description": "Skip formatting files.", "type": "boolean", "default": false }, "skipPackageJson": { "description": "Do not add dependencies to `package.json`.", "type": "boolean", "default": false }, "linter": { "description": "The tool to use for running lint checks.", "type": "string", "enum": ["eslint", "none"], "default": "eslint" }, "unitTestRunner": { "description": "Test runner to use for unit tests.", "type": "string", "enum": ["jest", "none"], "default": "jest" }, "tags": { "description": "Add tags to the application (used for linting).", "type": "string" }, "frontendProject": { "description": "Frontend project that needs to access this application. This sets up proxy configuration.", "type": "string" }, "standaloneConfig": { "description": "Split the project configuration into `<projectRoot>/project.json` rather than including it inside `workspace.json`.", "type": "boolean" }, "setParserOptionsProject": { "type": "boolean", "description": "Whether or not to configure the ESLint `parserOptions.project` option. We do not do this by default for lint performance reasons.", "default": false } }, "additionalProperties": false, "required": ["name"] }
Update export to barrel
libs/schematics/src/generators/library/lib/add-exports-to-barrel.ts
import type { Tree } from '@nrwl/devkit'; import { addGlobal, removeChange, } from '@nrwl/workspace/src/utilities/ast-utils'; import * as ts from 'typescript'; import type { NormalizedOptions } from '../schema'; export function addExportsToBarrelFile( tree: Tree, options: NormalizedOptions ): void { const indexPath = `${options.projectRoot}/src/index.ts`; const indexContent = tree.read(indexPath, 'utf-8'); let sourceFile = ts.createSourceFile( indexPath, indexContent || '', ts.ScriptTarget.Latest, true ); sourceFile = removeChange( tree, sourceFile, indexPath, 0, `export * from './lib/${options.fileName}';` ); sourceFile = addGlobal( tree, // eslint-disable-next-line @typescript-eslint/no-unused-vars sourceFile, indexPath, `export * from './lib/${options.fileName}.module';` ); }
Update create files
libs/schematics/src/generators/library/lib/create-files.ts
import type { Tree } from '@nrwl/devkit'; import { generateFiles, joinPathFragments, names, offsetFromRoot, } from '@nrwl/devkit'; import type { NormalizedOptions } from '../schema'; function capitalizeFirstLetter(text: string | undefined, locale: string) { const [first, ...rest] = (text || '').trim(); return (first || '').toLocaleUpperCase(locale) + rest.join(''); } export function createFiles(tree: Tree, options: NormalizedOptions): void { const substitutions = { ...options, ...names(options.projectName), titleName: names(options.projectName).fileName.split('-').join(' '), TitleName: capitalizeFirstLetter( names(options.projectName).fileName.split('-').join(' '), 'en' ), tmpl: '', offsetFromRoot: offsetFromRoot(options.projectRoot), }; generateFiles( tree, joinPathFragments(__dirname, '..', 'files', 'common'), options.projectRoot, substitutions ); generateFiles( tree, joinPathFragments(__dirname, '..', 'files', 'service'), options.projectRoot, substitutions ); }
Update options
libs/schematics/src/generators/library/lib/normalize-options.ts
import type { Tree } from '@nrwl/devkit'; import { generateFiles, joinPathFragments, names, offsetFromRoot, } from '@nrwl/devkit'; import type { NormalizedOptions } from '../schema'; function capitalizeFirstLetter(text: string | undefined, locale: string) { const [first, ...rest] = (text || '').trim(); return (first || '').toLocaleUpperCase(locale) + rest.join(''); } export function createFiles(tree: Tree, options: NormalizedOptions): void { const substitutions = { ...options, ...names(options.projectName), titleName: names(options.projectName).fileName.split('-').join(' '), TitleName: capitalizeFirstLetter( names(options.projectName).fileName.split('-').join(' '), 'en' ), tmpl: '', offsetFromRoot: offsetFromRoot(options.projectRoot), }; generateFiles( tree, joinPathFragments(__dirname, '..', 'files', 'common'), options.projectRoot, substitutions ); generateFiles( tree, joinPathFragments(__dirname, '..', 'files', 'service'), options.projectRoot, substitutions ); }
Update library schema types
libs/schematics/src/generators/library/schema.d.ts
import { Linter } from '@nrwl/linter'; import { UnitTestRunner } from '../utils'; export interface LibraryGeneratorOptions { name: string; buildable?: boolean; directory?: string; importPath?: string; linter?: Exclude<Linter, Linter.TsLint>; publishable?: boolean; skipFormat?: boolean; skipTsConfig?: boolean; strict?: boolean; tags?: string; target?: | 'es5' | 'es6' | 'esnext' | 'es2015' | 'es2016' | 'es2017' | 'es2018' | 'es2019' | 'es2020'; testEnvironment?: 'jsdom' | 'node'; unitTestRunner?: UnitTestRunner; standaloneConfig?: boolean; setParserOptionsProject?: boolean; } export interface NormalizedOptions extends LibraryGeneratorOptions { fileName: string; parsedTags: string[]; prefix: string; projectDirectory: string; projectName: string; projectRoot: Path; }
Update all index files and translate
npm run generate
Check logic of work with @kaufman-bot/schematics
Create empty nx project
npx -y create-nx-workspace@13.8.1 --name=kaufman-bot-generated --preset=empty --interactive=false --nx-cloud=false
endy@endy-virtual-machine:~/Projects/current$ npx -y create-nx-workspace@13.8.1 --name=kaufman-bot-generated --preset=empty --interactive=false --nx-cloud=false > NX Nx is creating your v13.8.1 workspace. To make sure the command works reliably in all environments, and that the preset is applied correctly, Nx will run "npm install" several times. Please wait. ✔ Installing dependencies with npm ✔ Nx has successfully created the workspace. ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— > NX First time using Nx? Check out this interactive Nx tutorial. https://nx.dev/getting-started/nx-core
Go to created project
cd kaufman-bot-generated
Add all need schematics
npm install -D @nrwl/nest@13.8.1 @kaufman-bot/schematics@2.4.0
endy@endy-virtual-machine:~/Projects/current/kaufman-bot-generated$ npm install -D @nrwl/nest@13.8.1 @kaufman-bot/schematics@2.4.0 added 162 packages, and audited 567 packages in 12s 54 packages are looking for funding run `npm fund` for details found 0 vulnerabilities
Create kaufman-bot application
npx -y nx@13.8.1 g @kaufman-bot/schematics:app adam-bot --bot-name adam
endy@endy-virtual-machine:~/Projects/current/kaufman-bot-generated$ npx -y nx@13.8.1 g @kaufman-bot/schematics:app adam-bot --bot-name adam UPDATE package.json UPDATE nx.json UPDATE tsconfig.base.json UPDATE .gitignore CREATE .env.local CREATE .env-example.local CREATE jest.config.js CREATE jest.preset.js UPDATE .vscode/extensions.json CREATE apps/adam-bot/src/app/.gitkeep CREATE apps/adam-bot/src/assets/.gitkeep CREATE apps/adam-bot/src/environments/environment.prod.ts CREATE apps/adam-bot/src/environments/environment.ts CREATE apps/adam-bot/src/main.ts CREATE apps/adam-bot/tsconfig.app.json CREATE apps/adam-bot/tsconfig.json CREATE apps/adam-bot/project.json UPDATE workspace.json CREATE .eslintrc.json CREATE apps/adam-bot/.eslintrc.json CREATE apps/adam-bot/jest.config.js CREATE apps/adam-bot/tsconfig.spec.json CREATE apps/adam-bot/src/app/app.module.ts CREATE apps/adam-bot/src/app/app.service.ts added 343 packages, removed 1 package, changed 1 package, and audited 909 packages in 37s 102 packages are looking for funding run `npm fund` for details found 0 vulnerabilities up to date, audited 909 packages in 2s 102 packages are looking for funding run `npm fund` for details found 0 vulnerabilities
Create telegram bot in @botfather
Append token to env file
.env.local
TELEGRAM_BOT_TOKEN=5384981645:AAEKAfqNpZmoN1w5eQL2QxJtvY5h3O-71Zs TELEGRAM_BOT_WEB_HOOKS_DOMAIN= TELEGRAM_BOT_WEB_HOOKS_PATH= TELEGRAM_BOT_ADMINS= BOT_NAMES=adam
Check from telegram
npm run serve:adam-bot-local
endy@endy-virtual-machine:~/Projects/current/kaufman-bot-generated$ npm run serve:adam-bot-local > kaufman-bot-generated@0.0.0 serve:adam-bot-local > export $(xargs < ./.env.local) > /dev/null 2>&1 && npm run nx -- serve adam-bot > kaufman-bot-generated@0.0.0 nx > nx "serve" "adam-bot" > nx run adam-bot:serve chunk (runtime: main) main.js (main) 10.1 KiB [entry] [rendered] webpack compiled successfully (3e915c7195348378) Debugger listening on ws://localhost:9229/045c9820-61d9-42b1-a3b5-57dc00299eea For help, see: https://nodejs.org/en/docs/inspector Issues checking in progress... [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [NestFactory] Starting Nest application... [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] TelegrafModule dependencies initialized +49ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] DebugMessagesModule dependencies initialized +0ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] DebugMessagesModule dependencies initialized +0ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] TranslatesModule dependencies initialized +1ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] CustomInjectorModule dependencies initialized +0ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] LanguageSwitherModule dependencies initialized +0ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] LanguageSwitherModule dependencies initialized +0ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] FactsGeneratorModule dependencies initialized +0ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] DiscoveryModule dependencies initialized +1ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] CustomInjectorCoreModule dependencies initialized +1ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] TranslatesModuleCore dependencies initialized +0ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] TranslatesModule dependencies initialized +1ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] BotCommandsModule dependencies initialized +0ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] BotCommandsModule dependencies initialized +1ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] ShortCommandsModule dependencies initialized +0ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] ScraperModule dependencies initialized +0ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] ScraperModule dependencies initialized +0ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] CustomInjectorModule dependencies initialized +1ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] CustomInjectorModule dependencies initialized +0ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] ShortCommandsModule dependencies initialized +0ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] BotInGroupsModule dependencies initialized +1ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] CustomInjectorModule dependencies initialized +0ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] CustomInjectorModule dependencies initialized +1ms [Nest] 1363135 - 04/22/2022, 1:32:02 PM LOG [InstanceLoader] AppModule dependencies initialized +0ms No issues found. [Nest] 1363135 - 04/22/2022, 1:32:05 PM LOG [InstanceLoader] TelegrafCoreModule dependencies initialized +2985ms [Nest] 1363135 - 04/22/2022, 1:32:05 PM LOG [TranslatesBootstrapService] onModuleInit [Nest] 1363135 - 04/22/2022, 1:32:05 PM LOG [TranslatesStorage] Add 1 translates for locale: en [Nest] 1363135 - 04/22/2022, 1:32:05 PM LOG [NestApplication] Nest application successfully started +2ms [Nest] 1363135 - 04/22/2022, 1:32:05 PM LOG [Application] 🚀 Application is running on: http://localhost:3333
Create new command
npm run nx -- g @kaufman-bot/schematics:lib super
endy@endy-virtual-machine:~/Projects/current/kaufman-bot-generated$ npm run nx -- g @kaufman-bot/schematics:lib super > kaufman-bot-generated@0.0.0 nx > nx "g" "@kaufman-bot/schematics:lib" "super" CREATE libs/super/README.md CREATE libs/super/src/index.ts CREATE libs/super/tsconfig.json CREATE libs/super/tsconfig.lib.json CREATE libs/super/project.json UPDATE workspace.json UPDATE tsconfig.base.json CREATE libs/super/.eslintrc.json CREATE libs/super/jest.config.js CREATE libs/super/tsconfig.spec.json CREATE libs/super/src/lib/super.module.ts CREATE libs/super/src/lib/super.service.ts
Update app module
apps/adam-bot/src/app/app.module.ts
import { SuperModule } from '@kaufman-bot-generated/super'; ... @Module({ imports: [ ... SuperModule.forRoot(), ], providers: [AppService], }) export class AppModule {}
Restart application and check work in telegram
Generated commands service
libs/super/src/lib/super.service.ts
import { BotCommandsEnum, BotCommandsProvider, BotCommandsProviderActionMsg, BotCommandsProviderActionResultType, BotCommandsToolsService, } from '@kaufman-bot/core-server'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { getText } from 'class-validator-multi-lang'; import { TranslatesService } from 'nestjs-translates'; export const SUPER_CONFIG = 'SUPER_CONFIG'; export interface SuperConfig { title: string; name: string; descriptions: string; usage: string[]; spyWords: string[]; category: string; } @Injectable() export class SuperService implements BotCommandsProvider { private readonly logger = new Logger(SuperService.name); constructor( @Inject(SUPER_CONFIG) private readonly superConfig: SuperConfig, private readonly translatesService: TranslatesService, private readonly commandToolsService: BotCommandsToolsService, private readonly botCommandsToolsService: BotCommandsToolsService ) {} async onHelp< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >(msg: TMsg): Promise<BotCommandsProviderActionResultType<TMsg>> { return await this.onMessage({ ...msg, text: `${this.superConfig.name} ${BotCommandsEnum.help}`, }); } async onMessage< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >(msg: TMsg): Promise<BotCommandsProviderActionResultType<TMsg>> { const locale = this.botCommandsToolsService.getLocale(msg, 'en'); const spyWord = this.superConfig.spyWords.find((spyWord) => this.commandToolsService.checkCommands(msg.text, [spyWord], locale) ); if (spyWord) { if ( this.commandToolsService.checkCommands( msg.text, [BotCommandsEnum.help], locale ) ) { return { type: 'markdown', message: msg, markdown: this.commandToolsService.generateHelpMessage(msg, { locale, name: this.superConfig.title, descriptions: this.superConfig.descriptions, usage: this.superConfig.usage, category: this.superConfig.category, }), }; } const processedMsg = await this.process(msg, locale); if (typeof processedMsg === 'string') { return { type: 'text', message: msg, text: processedMsg, }; } if (processedMsg) { return { type: 'message', message: processedMsg }; } this.logger.warn(`Unhandled commands for text: "${msg.text}"`); this.logger.debug(msg); } return null; } private async process< TMsg extends BotCommandsProviderActionMsg = BotCommandsProviderActionMsg >(msg: TMsg, locale: string) { if ( this.commandToolsService.checkCommands( msg.text, [getText('ping')], locale ) ) { return this.translatesService.translate(getText('pong'), locale); } return null; } }
Generated commands module
libs/super/src/lib/super.module.ts
import { BotCommandsCategory, BotCommandsModule, BOT_COMMANDS_PROVIDER, } from '@kaufman-bot/core-server'; import { DynamicModule, Module } from '@nestjs/common'; import { getText } from 'class-validator-multi-lang'; import { TranslatesModule } from 'nestjs-translates'; import { SuperService, SuperConfig, SUPER_CONFIG } from './super.service'; @Module({ imports: [TranslatesModule, BotCommandsModule], exports: [TranslatesModule, BotCommandsModule], }) export class SuperModule { static forRoot(): DynamicModule { return { module: SuperModule, providers: [ { provide: SUPER_CONFIG, useValue: <SuperConfig>{ title: getText('Super commands'), name: 'super', usage: [getText('super ping'), getText('super help')], descriptions: getText('Commands for super'), spyWords: [getText('super')], category: BotCommandsCategory.user, }, }, { provide: BOT_COMMANDS_PROVIDER, useClass: SuperService, }, ], exports: [SUPER_CONFIG], }; } }
Generated files your may look in https://github.com/kaufman-bot/schematics-example
In next post I append menu for quick run commands for bot...
Top comments (0)