脚手架和 Codemods
脚手架是指从静态模板(又名 stubs)生成源文件的过程,codemods 是指通过解析 AST 来更新 TypeScript 源代码。
AdonisJS 使用这两者来加速创建新文件和配置包的重复性任务。在本指南中,我们将介绍脚手架的构建块,并涵盖你可以在 Ace 命令中使用的 codemods API。
构建块
Stubs
Stubs 是指在给定操作时用于创建源文件的模板。例如,make:controller 命令使用控制器 stub 在宿主项目中创建控制器文件。
生成器
生成器强制执行命名约定,并根据预定义的约定生成文件、类或方法名称。
例如,控制器 stubs 使用 controllerName 和 controllerFileName 生成器来创建控制器。
由于生成器被定义为对象,你可以覆盖现有方法来调整约定。我们稍后将在本指南中详细介绍。
Codemods
codemods API 来自 @adonisjs/assembler 包,底层使用 ts-morph。
由于 @adonisjs/assembler 是开发依赖项,ts-morph 不会在生产环境中膨胀你的项目依赖项。此外,这意味着 codemods API 在生产环境中不可用。
AdonisJS 暴露的 codemods API 非常专门用于完成高级任务,如向 .adonisrc.ts 文件添加提供者,或在 start/kernel.ts 文件中注册中间件。此外,这些 API 依赖于默认的命名约定,因此如果你对项目进行了大幅更改,你将无法运行 codemods。
Configure 命令
configure 命令用于配置 AdonisJS 包。在底层,此命令导入主入口点文件并执行所提到包导出的 configure 方法。
包的 configure 方法接收 Configure 命令的实例,因此,它可以直接从命令实例访问 stubs 和 codemods API。
使用 Stubs
大多数情况下,你将在 Ace 命令中或你创建的包的 configure 方法中使用 stubs。在这两种情况下,你都可以通过 Ace 命令的 createCodemods 方法初始化 codemods 模块。
codemods.makeUsingStub 方法从 stub 模板创建源文件。它接受以下参数:
- 存储 stubs 的目录根目录的 URL。
- 从
STUBS_ROOT目录到 stub 文件的相对路径(包括扩展名)。 - 与 stub 共享的数据对象。
// title: 在命令中
import { BaseCommand } from '@adonisjs/core/ace'
const STUBS_ROOT = new URL('./stubs', import.meta.url)
export default class MakeApiResource extends BaseCommand {
async run() {
const codemods = await this.createCodemods()
await codemods.makeUsingStub(STUBS_ROOT, 'api_resource.stub', {})
}
}Stubs 模板
我们使用 Tempura 模板引擎用运行时数据处理 stubs。Tempura 是一个超轻量级的 JavaScript handlebars 风格模板引擎。
TIP
由于 Tempura 的语法与 handlebars 兼容,你可以将代码编辑器设置为对 .stub 文件使用 handlebar 语法高亮。
在下面的例子中,我们创建一个输出 JavaScript 类的 stub。它使用双花括号来计算运行时值。
使用生成器
如果你现在执行上面的 stub,它将失败,因为我们没有提供 modelName 和 modelReference 数据属性。
我们建议使用内联变量在 stub 中计算这些属性。这样,宿主应用程序可以弹出 stub 并修改变量。
{{#var entity = generators.createEntity('user')}}
{{#var modelName = generators.modelName(entity.name)}}
{{#var modelReference = string.camelCase(modelName)}}
export default class {{ modelName }}Resource {
serialize({{ modelReference }}: {{ modelName }}) {
return {{ modelReference }}.toJSON()
}
}输出目标
最后,我们必须指定将使用 stub 创建的文件的目标路径。我们再次在 stub 文件中指定目标路径,因为它允许宿主应用程序弹出 stub 并自定义其输出目标。
目标路径使用 exports 函数定义。该函数接受一个对象并将其导出为 stub 的输出状态。稍后,codemods API 使用此对象在指定位置创建文件。
{{#var entity = generators.createEntity('user')}}
{{#var modelName = generators.modelName(entity.name)}}
{{#var modelReference = string.camelCase(modelName)}}
{{#var resourceFileName = string(modelName).snakeCase().suffix('_resource').ext('.ts').toString()}}
{{{
exports({
to: app.makePath('app/api_resources', entity.path, resourceFileName)
})
}}}
export default class {{ modelName }}Resource {
serialize({{ modelReference }}: {{ modelName }}) {
return {{ modelReference }}.toJSON()
}
}通过命令接受实体名称
目前,我们在 stub 中将实体名称硬编码为 user。但是,你应该将其作为命令参数接受并作为模板状态与 stub 共享。
import { BaseCommand, args } from '@adonisjs/core/ace'
export default class MakeApiResource extends BaseCommand {
@args.string({
description: '资源的名称'
})
declare name: string
async run() {
const codemods = await this.createCodemods()
await codemods.makeUsingStub(STUBS_ROOT, 'api_resource.stub', {
name: this.name,
})
}
}{{#var entity = generators.createEntity(name)}}
{{#var modelName = generators.modelName(entity.name)}}
{{#var modelReference = string.camelCase(modelName)}}
{{#var resourceFileName = string(modelName).snakeCase().suffix('_resource').ext('.ts').toString()}}
{{{
exports({
to: app.makePath('app/api_resources', entity.path, resourceFileName)
})
}}}
export default class {{ modelName }}Resource {
serialize({{ modelReference }}: {{ modelName }}) {
return {{ modelReference }}.toJSON()
}
}全局变量
以下全局变量始终与 stub 共享。
| 变量 | 描述 |
|---|---|
app | 应用程序类实例的引用。 |
generators | 生成器模块的引用。 |
randomString | randomString 辅助函数的引用。 |
string | 创建字符串构建器实例的函数。你可以使用字符串构建器对字符串应用转换。 |
flags | 运行 ace 命令时定义的命令行标志。 |
弹出 Stubs
你可以使用 node ace eject 命令在 AdonisJS 应用程序中弹出/复制 stubs。eject 命令接受原始 stub 文件或其父目录的路径,并将模板复制到项目根目录的 stubs 目录中。
在下面的例子中,我们将从 @adonisjs/core 包复制 make/controller/main.stub 文件。
node ace eject make/controller/main.stub如果你打开 stub 文件,它将具有以下内容。
{{#var controllerName = generators.controllerName(entity.name)}}
{{#var controllerFileName = generators.controllerFileName(entity.name)}}
{{{
exports({
to: app.httpControllersPath(entity.path, controllerFileName)
})
}}}
// import type { HttpContext } from '@adonisjs/core/http'
export default class {{ controllerName }} {
}随意修改 stub。下次运行 make:controller 命令时,更改将被采用。
弹出目录
你可以使用 eject 命令弹出整个 stubs 目录。传递目录路径,命令将复制整个目录。
# 发布所有 make stubs
node ace eject make
# 发布所有 make:controller stubs
node ace eject make/controller使用 CLI 标志自定义 stub 输出目标
所有脚手架命令与 stub 模板共享 CLI 标志(包括不支持的标志)。因此,你可以使用它们创建自定义工作流程或更改输出目标。
在下面的例子中,我们使用 --feature 标志在提到的 features 目录中创建控制器。
node ace make:controller invoice --feature=billing// title: Controller stub
{{#var controllerName = generators.controllerName(entity.name)}}
{{#var featureDirectoryName = flags.feature}}
{{#var controllerFileName = generators.controllerFileName(entity.name)}}
{{{
exports({
to: app.makePath('features', featureDirectoryName, controllerFileName)
})
}}}
// import type { HttpContext } from '@adonisjs/core/http'
export default class {{ controllerName }} {
}从其他包弹出 stubs
默认情况下,eject 命令从 @adonisjs/core 包复制模板。但是,你可以使用 --pkg 标志从其他包复制 stubs。
node ace eject make/migration/main.stub --pkg=@adonisjs/lucid如何找到要复制的 stubs?
你可以通过访问其 GitHub 仓库找到包的 stubs。我们在包根级别的 stubs 目录中存储所有 stubs。
Stubs 执行流程
以下是我们如何通过 makeUsingStub 方法查找和执行 stubs 的可视化表示。

Codemods API
codemods API 由 ts-morph 提供支持,仅在开发期间可用。你可以使用 command.createCodemods 方法延迟实例化 codemods 模块。createCodemods 方法返回 Codemods 类的实例。
import type Configure from '@adonisjs/core/commands/configure'
export async function configure(command: ConfigureCommand) {
const codemods = await command.createCodemods()
}defineEnvValidations
为环境变量定义验证规则。该方法接受变量的键值对。key 是环境变量名称,value 是作为字符串的验证表达式。
TIP
此 codemod 期望 start/env.ts 文件存在,并且必须有 export default await Env.create 方法调用。
此外,codemod 不会覆盖给定环境变量的现有验证规则。这样做是为了尊重应用内的修改。
const codemods = await command.createCodemods()
try {
await codemods.defineEnvValidations({
leadingComment: '应用程序环境变量',
variables: {
PORT: 'Env.schema.number()',
HOST: 'Env.schema.string()',
}
})
} catch (error) {
console.error('无法定义环境验证')
console.error(error)
}defineEnvVariables
向 .env 和 .env.example 文件添加一个或多个新环境变量。该方法接受变量的键值对。
const codemods = await command.createCodemods()
try {
await codemods.defineEnvVariables({
MY_NEW_VARIABLE: 'some-value',
MY_OTHER_VARIABLE: 'other-value'
})
} catch (error) {
console.error('无法定义环境变量')
console.error(error)
}registerMiddleware
将 AdonisJS 中间件注册到已知的中间件栈之一。该方法接受中间件栈和要注册的中间件数组。
中间件栈可以是 server | router | named 之一。
TIP
此 codemod 期望 start/kernel.ts 文件存在,并且必须有你要注册中间件的中间件栈的函数调用。
const codemods = await command.createCodemods()
try {
await codemods.registerMiddleware('router', [
{
path: '@adonisjs/core/bodyparser_middleware'
}
])
} catch (error) {
console.error('无法注册中间件')
console.error(error)
}updateRcFile
在 adonisrc.ts 文件中注册 providers、commands,定义 metaFiles 和 commandAliases。
TIP
此 codemod 期望 adonisrc.ts 文件存在,并且必须有 export default defineConfig 函数调用。
const codemods = await command.createCodemods()
try {
await codemods.updateRcFile((rcFile) => {
rcFile
.addProvider('@adonisjs/lucid/db_provider')
.addCommand('@adonisjs/lucid/commands'),
.setCommandAlias('migrate', 'migration:run')
})
} catch (error) {
console.error('无法更新 adonisrc.ts 文件')
console.error(error)
}registerJapaPlugin
将 Japa 插件注册到 tests/bootstrap.ts 文件。
TIP
此 codemod 期望 tests/bootstrap.ts 文件存在,并且必须有 export const plugins: Config['plugins'] 导出。
const codemods = await command.createCodemods()
const imports = [
{
isNamed: false,
module: '@adonisjs/core/services/app',
identifier: 'app'
},
{
isNamed: true,
module: '@adonisjs/session/plugins/api_client',
identifier: 'sessionApiClient'
}
]
const pluginUsage = 'sessionApiClient(app)'
try {
await codemods.registerJapaPlugin(pluginUsage, imports)
} catch (error) {
console.error('无法注册 japa 插件')
console.error(error)
}registerPolicies
将 AdonisJS bouncer 策略注册到从 app/policies/main.ts 文件导出的 policies 对象列表中。
TIP
此 codemod 期望 app/policies/main.ts 文件存在,并且必须从中导出 policies 对象。
const codemods = await command.createCodemods()
try {
await codemods.registerPolicies([
{
name: 'PostPolicy',
path: '#policies/post_policy'
}
])
} catch (error) {
console.error('无法注册策略')
console.error(error)
}registerVitePlugin
将 Vite 插件注册到 vite.config.ts 文件。
TIP
此 codemod 期望 vite.config.ts 文件存在,并且必须有 export default defineConfig 函数调用。
const transformer = new CodeTransformer(appRoot)
const imports = [
{
isNamed: false,
module: '@vitejs/plugin-vue',
identifier: 'vue'
},
]
const pluginUsage = 'vue({ jsx: true })'
try {
await transformer.addVitePlugin(pluginUsage, imports)
} catch (error) {
console.error('无法注册 vite 插件')
console.error(error)
}installPackages
使用用户项目中检测到的包管理器安装一个或多个包。
const codemods = await command.createCodemods()
try {
await codemods.installPackages([
{ name: 'vinejs', isDevDependency: false },
{ name: 'edge', isDevDependency: false }
])
} catch (error) {
console.error('无法安装包')
console.error(error)
}getTsMorphProject
getTsMorphProject 方法返回 ts-morph 的实例。当你想执行 Codemods API 未涵盖的自定义文件转换时,这可能很有用。
const project = await codemods.getTsMorphProject()
project.getSourceFileOrThrow('start/routes.ts')请务必阅读 ts-morph 文档以了解更多可用的 API。