Skip to content

脚手架和 Codemods

脚手架是指从静态模板(又名 stubs)生成源文件的过程,codemods 是指通过解析 AST 来更新 TypeScript 源代码。

AdonisJS 使用这两者来加速创建新文件和配置包的重复性任务。在本指南中,我们将介绍脚手架的构建块,并涵盖你可以在 Ace 命令中使用的 codemods API。

构建块

Stubs

Stubs 是指在给定操作时用于创建源文件的模板。例如,make:controller 命令使用控制器 stub 在宿主项目中创建控制器文件。

生成器

生成器强制执行命名约定,并根据预定义的约定生成文件、类或方法名称。

例如,控制器 stubs 使用 controllerNamecontrollerFileName 生成器来创建控制器。

由于生成器被定义为对象,你可以覆盖现有方法来调整约定。我们稍后将在本指南中详细介绍。

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 共享的数据对象。
ts
// 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。它使用双花括号来计算运行时值。

handlebars
export default class {{ modelName }}Resource {
  serialize({{ modelReference }}: {{ modelName }}) {
    return {{ modelReference }}.toJSON()
  }
}

使用生成器

如果你现在执行上面的 stub,它将失败,因为我们没有提供 modelNamemodelReference 数据属性。

我们建议使用内联变量在 stub 中计算这些属性。这样,宿主应用程序可以弹出 stub 并修改变量。

js
{{#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 使用此对象在指定位置创建文件。

js
{{#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 共享。

ts
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,
    })
  }
}
js
{{#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生成器模块的引用。
randomStringrandomString 辅助函数的引用。
string创建字符串构建器实例的函数。你可以使用字符串构建器对字符串应用转换。
flags运行 ace 命令时定义的命令行标志。

弹出 Stubs

你可以使用 node ace eject 命令在 AdonisJS 应用程序中弹出/复制 stubs。eject 命令接受原始 stub 文件或其父目录的路径,并将模板复制到项目根目录的 stubs 目录中。

在下面的例子中,我们将从 @adonisjs/core 包复制 make/controller/main.stub 文件。

sh
node ace eject make/controller/main.stub

如果你打开 stub 文件,它将具有以下内容。

js
{{#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 }} {
}
  • 在前两行中,我们使用生成器模块生成控制器类名和控制器文件名。
  • 从第 3-7 行,我们使用 exports 函数定义目标路径
  • 最后,我们定义脚手架控制器的内容。

随意修改 stub。下次运行 make:controller 命令时,更改将被采用。

弹出目录

你可以使用 eject 命令弹出整个 stubs 目录。传递目录路径,命令将复制整个目录。

sh
# 发布所有 make stubs
node ace eject make

# 发布所有 make:controller stubs
node ace eject make/controller

使用 CLI 标志自定义 stub 输出目标

所有脚手架命令与 stub 模板共享 CLI 标志(包括不支持的标志)。因此,你可以使用它们创建自定义工作流程或更改输出目标。

在下面的例子中,我们使用 --feature 标志在提到的 features 目录中创建控制器。

sh
node ace make:controller invoice --feature=billing
js
// 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。

sh
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 类的实例。

ts
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 不会覆盖给定环境变量的现有验证规则。这样做是为了尊重应用内的修改。

ts
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 文件添加一个或多个新环境变量。该方法接受变量的键值对。

ts
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 文件存在,并且必须有你要注册中间件的中间件栈的函数调用。

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 文件中注册 providerscommands,定义 metaFilescommandAliases

TIP

此 codemod 期望 adonisrc.ts 文件存在,并且必须有 export default defineConfig 函数调用。

ts
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'] 导出。

ts
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 对象。

ts
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 函数调用。

ts
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

使用用户项目中检测到的包管理器安装一个或多个包。

ts
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 未涵盖的自定义文件转换时,这可能很有用。

ts
const project = await codemods.getTsMorphProject()

project.getSourceFileOrThrow('start/routes.ts')

请务必阅读 ts-morph 文档以了解更多可用的 API。