从 0 实现 Nest Entity Provider CLI

前言

Nest CLI 提供了一系列命令,可以帮助开发者快速初始化新的 Nest.js 项目,生成模块、控制器和服务等。

shell 复制代码
nest g res user

生成的模块如下所示:

tree user

shell 复制代码
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 中引入。如下所示:

ts 复制代码
// provider.factory.ts
type 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],
  }
}
ts 复制代码
// user.provider.ts
import { 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 中使用

diff 复制代码
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> 中的模块名。

ts 复制代码
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 模版。

shell 复制代码
pnpm i handlebars

定义模版文件:

js 复制代码
// provider.template.js
import { 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 并写入特定的路径中。

ts 复制代码
// plugins/generatorProviderTemplate/index.ts
let dryRun = false,
  isExistsEntity = false
// 从命令行中获取 provider 的名称和 生成路径的根目录
const { providerName: variable, pathRoot: originRoot } =
  (await getCommand()) || {}
// 对 provider名称的 kebab-case 转换成 camelCase
const 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 = variable
const providerName = `${variableName}_provider`.toUpperCase()
const providerNameUpper = providerName
// 导出的Provider名称
// 例如 pnpm provider user-role 生成的将是 userRoleProvider
const exportName = shortLine2VariableName([...variable.split('-'), 'provider'])
// 读取模版文件
const templateCode = await readFile(resolve(__dirname, './template.tmpl.js'), {
  encoding: 'utf-8',
})
// 对模版进行编译
const templateFn = compile(templateCode)
// 生成code
const 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 创建一个模版文件试试:

  1. 需要先使用 nest g res dep 生成对应的 entities
  2. 在运行 pnpm provider dep之前需要在package.json 中的 script 指定 provider 运行 provider.script.ts 文件。

运行完成后,对应的 dep 模块下会生成一个 provider 文件夹,里面会有一个dep.provider.ts 文件。

实现与 modules 联动

现在对应的模版文件虽然可以生成,但是没有和 dep.module.ts 文件产生关联。接下来,我们将使用 babelmodule 进行改造。

shell 复制代码
pnpm i @babel/traverse @babel/generator @babel/template @babel/types  @babel/parser --save-dev
ts 复制代码
// babel-parse.ts
export 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 进行改造:

diff 复制代码
-   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 模版的输出,并且使用 babelmodule 进行改造 ,实现生成provider 的同时自动在 module 中引入。

相关推荐
泰伦闲鱼1 天前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs
求知若饥3 天前
NestJS 项目实战-权限管理系统开发(六)
后端·node.js·nestjs
白筱汐4 天前
Nestjs 和 Prisma 实现 Restful Api:JWT 授权
javascript·后端·nestjs
寻找奶酪的mouse13 天前
告诉自己,请给予时间一点点耐心~
年终总结·nestjs
Zhangxinxin1 个月前
从0~1手写Nest.js - (5) 配置路由
nestjs
YanceyOfficial1 个月前
How to deploy Nest.js microservices using Kubernetes
微服务·kubernetes·nestjs
webxue1 个月前
NestJS配置环境变量、读取Yaml配置的保姆级教程
node.js·nestjs
超级无敌暴龙兽2 个月前
微服务架构的基础与实践:构建灵活的分布式系统
微服务·node.js·nestjs
寻找奶酪的mouse2 个月前
【NestJS全栈之旅】应用篇:通用爬虫服务三两事儿
前端·后端·nestjs
_jiang2 个月前
nestjs 入门实战最强篇
redis·typescript·nestjs