乐趣区

关于nestjs:从-0-实现-Nest-Entity-Provider-CLI

前言

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.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],
  }
}
// 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 中应用

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.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 并写入特定的门路中。

// 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 进行革新。

pnpm i @babel/traverse @babel/generator @babel/template @babel/types  @babel/parser --save-dev
// 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 进行革新:

-   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 中引入。

退出移动版