Scaffolding

Scaffoldingとコードモッド

Scaffoldingは、静的なテンプレート(スタブとも呼ばれる)からソースファイルを生成するプロセスを指し、コードモッドはASTを解析してTypeScriptのソースコードを更新することを指します。

AdonisJSでは、新しいファイルを作成したりパッケージを設定したりする繰り返しのタスクを高速化するために、両方を使用しています。このガイドでは、scaffoldingの基本とAceコマンド内で使用できるコードモッドAPIについて説明します。

基本概念

スタブ

スタブは、特定のアクションでソースファイルを作成するために使用されるテンプレートです。たとえば、make:controllerコマンドは、controller stubを使用してホストプロジェクト内にコントローラファイルを作成します。

ジェネレータ

ジェネレータは、名前の規則を強制し、事前定義された規則に基づいてファイル、クラス、またはメソッドの名前を生成します。

たとえば、コントローラスタブでは、controllerNamecontrollerFileNameジェネレータを使用してコントローラを作成します。

ジェネレータはオブジェクトとして定義されているため、既存のメソッドを上書きして規則を調整することができます。このガイドの後半で詳しく説明します。

コードモッド

コードモッドAPIは、@adonisjs/assemblerパッケージから提供され、内部ではts-morphを使用しています。

@adonisjs/assemblerは開発時の依存関係であり、本番環境ではプロジェクトの依存関係を増やしません。また、コードモッドAPIは本番環境では使用できません。

AdonisJSが公開するコードモッドAPIは、.adonisrc.tsファイルにプロバイダを追加したり、start/kernel.tsファイル内でミドルウェアを登録したりするなど、高レベルのタスクを実行するために非常に特定のものです。また、これらのAPIはデフォルトの命名規則に依存しているため、プロジェクトに大幅な変更を加えるとコードモッドを実行できなくなる場合があります。

configureコマンド

configureコマンドは、AdonisJSパッケージを設定するために使用されます。内部では、このコマンドはメインエントリポイントファイルをインポートし、指定されたパッケージでエクスポートされたconfigureメソッドを実行します。

パッケージのconfigureメソッドは、Configureコマンドのインスタンスを受け取り、そのインスタンスからスタブとコードモッドAPIにアクセスできます。

スタブの使用方法

ほとんどの場合、スタブはAceコマンド内または作成したパッケージのconfigureメソッド内で使用します。両方の場合に、AceコマンドのcreateCodemodsメソッドを介してコードモッドモジュールを初期化できます。

codemods.makeUsingStubメソッドは、スタブテンプレートからソースファイルを作成します。次の引数を受け入れます。

  • スタブが保存されているディレクトリのルートへのURL
  • STUBS_ROOTディレクトリからスタブファイルまでの相対パス(拡張子を含む)
  • スタブと共有するデータオブジェクト
コマンド内部
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', {})
}
}

スタブのテンプレート

スタブの処理にはTempuraテンプレートエンジンを使用します。Tempuraは、JavaScript用の超軽量なハンドルバースタイルのテンプレートエンジンです。

Tempuraの構文はハンドルバーと互換性があるため、.stubファイルでハンドルバーの構文ハイライトを使用するようにコードエディタを設定できます。

次の例では、JavaScriptのクラスを出力するスタブを作成しています。ダブルカーリーブラケットを使用してランタイム値を評価します。

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

ジェネレータの使用

上記のスタブを実行すると失敗します。なぜなら、modelNamemodelReferenceのデータプロパティを提供していないからです。

これらのプロパティをスタブ内でインライン変数を使用して計算することをオススメします。これにより、ホストアプリケーションはスタブをejectして変数を変更できます。

{{#var entity = generators.createEntity('user')}}
{{#var modelName = generators.modelName(entity.name)}}
{{#var modelReference = string.toCamelCase(modelName)}}
export default class {{ modelName }}Resource {
serialize({{ modelReference }}: {{ modelName }}) {
return {{ modelReference }}.toJSON()
}
}

出力先の指定

最後に、スタブを使用して作成されるファイルの出力先パスを指定する必要があります。再度、スタブファイル内で出力先パスを指定します。これにより、ホストアプリケーションはスタブをejectして出力先をカスタマイズできます。

出力先パスはexports関数を使用して定義されます。この関数はオブジェクトを受け入れ、それをスタブの出力状態としてエクスポートします。後で、コードモッドAPIはこのオブジェクトを使用して指定された場所にファイルを作成します。

{{#var entity = generators.createEntity('user')}}
{{#var modelName = generators.modelName(entity.name)}}
{{#var modelReference = string.toCamelCase(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()
}
}

コマンドを介したエンティティ名の受け入れ

現時点では、スタブ内でエンティティ名をuserとしてハードコーディングしています。ただし、コマンド引数として受け入れ、テンプレートの状態としてスタブと共有する必要があります。

import { BaseCommand, args } from '@adonisjs/core/ace'
export default class MakeApiResource extends BaseCommand {
@args.string({
description: 'The name of the resource'
})
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('user')}}
{{#var entity = generators.createEntity(name)}}
{{#var modelName = generators.modelName(entity.name)}}
{{#var modelReference = string.toCamelCase(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()
}
}

グローバル変数

次のグローバル変数は常にスタブと共有されます。

変数名説明
appapplicationクラスのインスタンスへの参照。
generatorsgeneratorsモジュールへの参照。
randomStringrandomStringヘルパー関数への参照。
stringstring builderインスタンスを作成するための関数。文字列に変換を適用するために文字列ビルダーを使用できます。
flagsAceコマンドを実行する際に定義されるコマンドラインフラグ。

スタブのeject

node ace ejectコマンドを使用して、AdonisJSアプリケーション内にスタブをeject(コピー)できます。ejectコマンドは、元のスタブファイルまたはその親ディレクトリへのパスを受け入れ、テンプレートをプロジェクトのルートのstubsディレクトリにコピーします。

次の例では、@adonisjs/coreパッケージからmake/controller/main.stubファイルをコピーします。

node ace eject make/controller/main.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 }} {
}
  • 最初の2行では、generatorsモジュールを使用してコントローラクラス名とコントローラファイル名を生成しています。
  • 3行から7行では、exports関数を使用して出力先パスを定義しています。
  • 最後に、scaffoldingされたコントローラの内容を定義しています。

スタブを変更しても問題ありません。次回、make:controllerコマンドを実行すると変更が反映されます。

ディレクトリのeject

ejectコマンドを使用して、スタブのディレクトリ全体をeject(コピー)できます。ディレクトリへのパスを渡すと、コマンドはディレクトリ全体をコピーします。

# makeスタブをすべて公開する
node ace eject make
# make:controllerスタブをすべて公開する
node ace eject make/controller

CLIフラグを使用してスタブの出力先をカスタマイズする

すべてのscaffoldingコマンドは、スタブテンプレートと共にCLIフラグ(サポートされていないフラグも含む)を共有します。したがって、カスタムワークフローや出力先の変更に使用できます。

次の例では、--featureフラグを使用して、指定したfeaturesディレクトリ内にコントローラを作成します。

node ace make:controller invoice --feature=billing
コントローラスタブ
{{#var controllerName = generators.controllerName(entity.name)}}
{{#var featureDirectoryName = generators.makePath('features', flags.feature)}}
{{#var controllerFileName = generators.controllerFileName(entity.name)}}
{{{
exports({
to: app.httpControllersPath(entity.path, controllerFileName)
to: app.makePath(featureDirectoryName, entity.path, controllerFileName)
})
}}}
// import type { HttpContext } from '@adonisjs/core/http'
export default class {{ controllerName }} {
}

他のパッケージからのスタブのeject

デフォルトでは、ejectコマンドは@adonisjs/coreパッケージからテンプレートをコピーします。ただし、--pkgフラグを使用して他のパッケージからスタブをコピーすることもできます。

node ace eject make/migration/main.stub --pkg=@adonisjs/lucid

どのスタブをコピーするかを見つける方法

パッケージのスタブは、そのGitHubリポジトリを訪れることで見つけることができます。すべてのスタブは、パッケージのルートレベルにstubsディレクトリ内に保存されています。

スタブの実行フロー

makeUsingStubメソッドを介してスタブを見つけて実行するフローを以下に示します。

コードモッドAPI

コードモッドAPIは、ts-morphによって提供され、開発中にのみ利用できます。command.createCodemodsメソッドを使用して、コードモッドモジュールを遅延初期化できます。createCodemodsメソッドは、Codemodsクラスのインスタンスを返します。

import type Configure from '@adonisjs/core/commands/configure'
export async function configure(command: ConfigureCommand) {
const codemods = await command.createCodemods()
}

defineEnvValidations

環境変数のバリデーションルールを定義します。このメソッドは、変数のキーと値のペアを受け入れます。keyは環境変数の名前であり、valueはバリデーション式の文字列です。

このコードモッドは、start/env.tsファイルが存在し、export default await Env.createメソッド呼び出しがあることを前提としています。

また、このコードモッドは、既存の環境変数のバリデーションルールを上書きしません。これは、アプリ内の変更を尊重するためです。

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)
}
出力
import { Env } from '@adonisjs/core/env'
export default await Env.create(new URL('../', import.meta.url), {
/**
* アプリの環境変数
*/
PORT: Env.schema.number(),
HOST: Env.schema.string(),
})

defineEnvVariables

.envファイルと.env.exampleファイルに1つまたは複数の新しい環境変数を追加します。このメソッドは、変数のキーと値のペアを受け入れます。

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)
}

場合によっては、.env.exampleファイルに変数の値を挿入したくない場合があります。omitFromExampleオプションを使用することで、そのような場合に対応できます。

const codemods = await command.createCodemods()
await codemods.defineEnvVariables({
MY_NEW_VARIABLE: 'SOME_VALUE',
}, {
omitFromExample: ['MY_NEW_VARIABLE']
})

上記のコードは、.envファイルにMY_NEW_VARIABLE=SOME_VALUEを挿入し、.env.exampleファイルにMY_NEW_VARIABLE=を挿入します。

registerMiddleware

AdonisJSのミドルウェアを既知のミドルウェアスタックの1つに登録します。このメソッドは、ミドルウェアスタックと登録するミドルウェアの配列を受け入れます。

ミドルウェアスタックは、server | router | namedのいずれかです。

このコードモッドは、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)
}
出力
import router from '@adonisjs/core/services/router'
router.use([
() => import('@adonisjs/core/bodyparser_middleware')
])

名前付きミドルウェアを次のように定義することもできます。

const codemods = await command.createCodemods()
try {
await codemods.registerMiddleware('named', [
{
name: 'auth',
path: '@adonisjs/auth/auth_middleware'
}
])
} catch (error) {
console.error('ミドルウェアを登録できませんでした')
console.error(error)
}

updateRcFile

adonisrc.tsファイルにproviderscommandsmetaFilescommandAliasesを登録します。

このコードモッドは、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)
}
出力
import { defineConfig } from '@adonisjs/core/app'
export default defineConfig({
commands: [
() => import('@adonisjs/lucid/commands')
],
providers: [
() => import('@adonisjs/lucid/db_provider')
],
commandAliases: {
migrate: 'migration:run'
}
})

registerJapaPlugin

Japaプラグインをtests/bootstrap.tsファイルに登録します。

このコードモッドは、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)
}
出力
import app from '@adonisjs/core/services/app'
import { sessionApiClient } from '@adonisjs/session/plugins/api_client'
export const plugins: Config['plugins'] = [
sessionApiClient(app)
]

registerPolicies

AdonisJSのバウンサーポリシーをapp/policies/main.tsファイルからpoliciesオブジェクトのリストに登録します。

このコードモッドは、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)
}
出力
export const policies = {
PostPolicy: () => import('#policies/post_policy')
}

registerVitePlugin

Viteプラグインをvite.config.tsファイルに登録します。

このコードモッドは、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)
}
出力
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({ jsx: true })
]
})

installPackages

ユーザーのプロジェクトで検出されたパッケージマネージャーを使用して、1つまたは複数のパッケージをインストールします。

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')

利用可能なAPIについては、ts-morphのドキュメントを参照してください。