前言
Nest CLI 提供了一系列命令,能够帮忙开发者疾速初始化新的 Nest.js 我的项目,生成模块、控制器和服务等。
nest g res user
生成的模块如下所示:
tree user
user├── dto│ ├── create-user.dto.ts│ └── update-user.dto.ts├── entities│ └── user.entity.ts├── user.controller.spec.ts├── user.controller.ts├── user.module.ts├── user.service.spec.ts└── user.service.ts
尽管生成了 user.entity.ts
,但此时与 user.module.ts
模块却没有任何关联。个别还须要开发者手动去定义 provider
而后在 user.module.ts
中引入。如下所示:
// provider.factory.tstype ctor = { new (...args: any): object }export const ProviderFactory = (provide: string | ctor, repository: ctor) => { return { provide, useFactory: (dataSource: DataSource) => dataSource.getRepository(repository), // DATA_SOURCE 就是 databaseProvider的Key // 通过在 inject 指明 Provider 的 token,能够在 useFactory 中注入值 inject: [DATA_SOURCE], }}
// user.provider.tsimport { USER_PROVIDER } from '../constants/user.constants'import { User } from '../entities/user.entity'import { ProviderFactory } from '../../utils/provider.factory'export const UserProvider = ProviderFactory(USER_PROVIDER, User)
在 user.module.ts
中应用
import { Module } from '@nestjs/common';import { UserService } from './user.service';import { UserController } from './user.controller';+ import { UserProvider } from './provider/user.provider';@Module({ controllers: [UserController],- providers: [UserService],- exports: [UserService],+ providers: [UserService, UserProvider],+ exports: [UserService, UserProvider],})export class UserModule {}
对于每一个新的模块,根本都要这样去批改文件,非常繁琐。上面,咱们将逐渐实现一个 Entity Provider CLI
。
command 的实现
根据 nest res user
的命令,会在 src
下生成 user
目录并且生成 user.module.ts
等文件。 当初,咱们实现一个 pnpm provider user
命令,使其也能在 src
目录下生成 user/provider
目录,并且生成 user.provider.ts
。
首先,须要获取 pnpm provider <module_name>
中的模块名。
export async function getCommand() { const argv = process.argv.slice(2) let [providerName] = argv if (!providerName) { throw new Error('generator provider template is invalid name') } // 获取须要生成的模块名 providerName = providerName.trim() try { let nestCliJson: Record<string, unknown> | string = await readFile( resolve(process.cwd(), 'nest-cli.json'), { encoding: 'utf-8' } ) nestCliJson = JSON.parse(nestCliJson) if (isObject(nestCliJson)) { const sourceRoot = Reflect.get(nestCliJson, 'sourceRoot') as string const pathRoot = resolve(process.cwd(), sourceRoot) // 获取 CLI生成的门路的根目录 是 nest-cli.json 中的 sourceRoot 字段 return { pathRoot, providerName } } return null } catch (e) { console.log(e) }}
定义模版文件
这里应用 handlebars
来实现 provider
模版。
pnpm i handlebars
定义模版文件:
// provider.template.jsimport { ProviderFactory } from '../../utils/provider.factory';{{#if isExistsEntity}}import { {{ providerEntityImportName }} } from '../entities/{{ providerEntityFileName }}.entity';{{ else }}class {{ providerEntityImportName }} {}{{/if}}export const {{ providerName }} = '{{ providerNameUpper }}';export const {{ exportName }} = ProviderFactory({{ providerName }}, {{ providerEntityImportName }});
至此,筹备工作曾经实现,接下来将模版编译成 provider
并写入特定的门路中。
// plugins/generatorProviderTemplate/index.tslet dryRun = false, isExistsEntity = false// 从命令行中获取 provider 的名称和 生成门路的根目录const { providerName: variable, pathRoot: originRoot } = (await getCommand()) || {}// 对 provider名称的 kebab-case 转换成 camelCaseconst variableName = variable .split('-') .map((val) => capitalize(val)) .join('_')const pathRoot = resolve(originRoot, variable)// 导入的名称 对应生成的 entity 中的 class 名称const providerEntityImportName = shortLine2VariableName(variable.split('-'))if ( existsSync(resolve(process.cwd(), pathRoot, `entities/${variable}.entity.ts`))) { isExistsEntity = true}const providerEntityFileName = variableconst providerName = `${variableName}_provider`.toUpperCase()const providerNameUpper = providerName// 导出的Provider名称// 例如 pnpm provider user-role 生成的将是 userRoleProviderconst exportName = shortLine2VariableName([...variable.split('-'), 'provider'])// 读取模版文件const templateCode = await readFile(resolve(__dirname, './template.tmpl.js'), { encoding: 'utf-8',})// 对模版进行编译const templateFn = compile(templateCode)// 生成codeconst code = templateFn({ providerEntityImportName, providerEntityFileName, providerName, providerNameUpper, exportName, isExistsEntity,})const fileDirPath = resolve(process.cwd(), pathRoot, 'providers')const filePath = resolve(fileDirPath, `${variable}.provider.ts`)if (existsSync(filePath)) { dryRun = true}if (dryRun) { // dryRun 为 true 时 不写入磁盘 console.log(`dry run generator file path: ${filePath} success`)} else { // 生成的code 写入文件 await writeProviderFile(fileDirPath, filePath, code) console.log(`generator file path: ${filePath} success`)}
通过 CLI
生成一个 Provider 当初已初步实现。
应用 pnpm provider dep
创立一个模版文件试试:
- 须要先应用
nest g res dep
生成对应的entities
。- 在运行
pnpm provider dep
之前须要在package.json
中的script
指定provider
运行provider.script.ts
文件。
运行实现后,对应的 dep
模块下会生成一个 provider
文件夹,外面会有一个dep.provider.ts
文件。
实现与 modules 联动
当初对应的模版文件尽管能够生成,然而没有和 dep.module.ts
文件产生关联。接下来,咱们将应用 babel
对 module
进行革新。
pnpm i @babel/traverse @babel/generator @babel/template @babel/types @babel/parser --save-dev
// babel-parse.tsexport const generatorModulesProvider = ( sourceCode: string, importPath: string, providerName: string) => { const ast = parse(sourceCode, { sourceType: 'module', presets: ['@babel/preset-typescript'], plugins: ['decorators'], } as ParserOptions) traverse(ast, { Program(path) { let importDeclarationIndex = 0 if (Array.isArray(path.node.body)) { for (let i = 0; i < path.node.body.length; i++) { if (path.node.body[i].type !== 'ImportDeclaration') { importDeclarationIndex = i break } } } const importAst = template.ast(importPath) if (!isExistsImportProviderName(path.node.body, providerName)) path.node.body.splice(importDeclarationIndex, 0, importAst) }, ClassDeclaration(path) { for (let i = 0; i < path.node.decorators.length; i++) { if (path.node.decorators[i].expression.callee.name === 'Module') { const target = path.node.decorators[i].expression.arguments[0].properties if (Array.isArray(target)) { for (let j = 0; j < target.length; j++) { if (target[j].key.name === 'providers') { if ( !isExistsModuleProvider( target[j].value.elements, providerName ) ) { const ast = identifier(providerName) target[j].value.elements.push(ast) } } } } } } }, }) const { code } = generate(ast) return code}// 判断是否曾经导入过该文件function isExistsModuleProvider(elements, providerName: string) { if (Array.isArray(elements)) { for (let i = 0; i < elements.length; i++) { if (elements[i].name === providerName) { return true } } } return false}// 判断是否曾经有 Provider 重名的模块function isExistsImportProviderName(elements, providerName) { if (Array.isArray(elements)) { for (let i = 0; i < elements.length; i++) { const element = elements[i] if ( element.type === 'ImportDeclaration' && element.specifiers && Array.isArray(element.specifiers) ) { for (let j = 0; j < element.specifiers.length; j++) { if (element.specifiers[j].local.name === providerName) { return true } } } } } return false}
对 plugins/generatorProviderTemplate/index.ts
进行革新:
- await writeProviderFile(fileDirPath, filePath, code)+ let importRelativePath = relative(+ resolve(process.cwd(), pathRoot),+ filePath,+ );+ importRelativePath = importRelativePath.substring(+ 0,+ importRelativePath.lastIndexOf('.'),+ );+ await Promise.all([+ writeProviderFile(fileDirPath, filePath, code),+ writeModuleProviderFile(+ fileDirPath,+ variable,+ importRelativePath,+ exportName,+ ),+ ]);console.log(`generator file path: ${filePath} success`)
删除 provider
文件夹 再次应用 pnpm provider dep
生成,即可看到会 module
会导入此生成的 provider
。
结语
本文实现了一个繁难的 Nest Entity Provider CLI。通过 应用handlebars
模版引擎实现 provider
模版的输入,并且应用 babel
对 module
进行革新 ,实现生成provider
的同时主动在 module
中引入。