turbo/gen で対話式雛形コード生成
フロントエンドにおいてAtomic DesignにおけるComponent作成やfeatures作成を行う方は多いと思います。例えばComponent作成であればただ単一のjsx
やtsx
のみ存在することはあまりないと思います。storybookやtestやcssファイル、hooksのファイルを置く場合もあるでしょう。そうなった際に一つのComponentを作成する為に複数のファイルをその都度作るのは億劫になりますし、めんどくさいですよね。
例えば以下のようなComponentがあったとします。
└── organisms
└── HogeHeader
├── HogeHeader.stories.tsx
├── HogeHeader.test.tsx
├── PresentationHogeHeader.tsx
├── HogeHeader.tsx
└── index.tsx
この HogeComponent
だけが固有のものです。こいつを決めてしまい、ある程度書き始められる雛形が用意されていれば作業効率向上が見込めます。また、storybookやtestも出力してあげることでこれらのファイルを書く導線になります。これを実現できるcode generator toolsは実は複数あります、例えばNode.js製の template generator ツールであるhygen とかです。他にも調べたところ以下が該当しそうです。
・Plop: https://plopjs.com
・Yoeman: https://yeoman.io
・Hygen: https://www.hygen.io
hygen等もとても良きライブラリですが、今回は表題にある通り、turbo/gen
を紹介させていただければと思います。
turbo/gen
?
なぜプロダクトの都合になっていますが、現在私が関わっているいくつかのサービスではmonorepoを採用しており、かつturborepo
でそれを実現しているという背景があります。turborepoを採用した理由については今回は割愛させていただきますが、簡潔に述べると、段階的な導入が可能で、簡単にプロジェクトに統合できたり(npm/yarn/lerna の workspace 有効化するさえしていれば、他の要求が少ない点とか)また、パッケージのインストールには、引き続きpmpmやnpmを使用できたり、これらのパッケージマネージャーを補完し、効率的なタスク実行が可能である点を見て個人的には良いと感じ、採用しています。
turborepoを採用しているため、turbo/genが有力候補になった理由の一つです。他にも理由を挙げるとすれば、
- CLIから対話式でコード自動生成できるわかりやすさ
- templateからの生成も可能
- JS(TS)とHandlebarsで書ける
- 拡張性高い
- 書くのが容易である
- InquirerやPlopのAPIを使える
等々もありました。
どこでturbo/genを使っているか
以下の二例を挙げてみます。個人的な使用例なので参考になれば程度で書いています。
componentsの例
Atomic Design(Atomic Re-Design)をuiレイヤーにて採用しています。
packages
└── ui
├── .storybook
├── .turbo
└── src
└── atoms
├── molecules
└──organisms
ここでの対話例は以下の様にしています。
対話式
npm run gen
というスクリプトを登録し、turbo gen
を実行しています。
$ npm run gen
1.実行するジェネレーターを選択する(矢印キーを使用)
>>> Modify "frontend" using custom generators
? Select generator to run (Use arrow keys)
@hoge/features
❯ features: Adds a new features
@hoge/ui
component: Adds a new component
featuresはドメイン入りの機能部分です。bulletproof/react を参考にしているのでこちらを見ていただければわかりやすいと思います。複数のフォルダ、ファイルを含むのでコード生成できる様にしていますが今回紹介は割愛させていただきます。様々な生成を行うことができるよ!ということを知ってもらえれば幸いです。
2.そのコンポーネントはどのカテゴリーに属しますか?
Atomic Designのどのカテゴリーを生成するか選択します。ここで選択した際に指定のフォルダ配下にcomponentを生成します。
? Which category does the component belong to? (Use arrow keys)
❯ atoms
molecules
organisms
3. コンポーネントの名前は何ですか?
? What is the name of the component?
今回はHogeHeader
コンポーネントを生成してみましょう。
4.organismsのコンポーネントが属するアプリを選択します。
organismsは 「Product Context」 の依存を含めています。特定のユースケースが Product Context です。ドメイン特有の情報を保持しているものという位置付けにしています。そのためmonorepoなサービスに依存する為、ここではorganismsがどのサービス依存かを選択しています。
? Choose the app to which the organisms component belongs.: (Use arrow keys)
❯ web-app
doc-app
5. 出力
>>> Changes made:
• /src/organisms/hoge-app/HogeHeader/PresentationHeader.tsx (add)
• /src/organisms/hoge-app/HogeHeader/HogeHeader.tsx (add)
• /src/organisms/hoge-app/HogeHeader/PresentationHogeHeader.stories.tsx (add)
• /src/organisms/hoge-app/HogeHeader/index.ts (add)
• /src/organisms/hoge-app/index.ts (append)
>>> Success!
このように対話式で選択肢を選んでいく(コンポーネント名はタイピングしますが)だけで、簡単にComponentのファイルを作成できます。今回はindex.ts
にappendもしています。対話を経ると、対話内容に応じてファイルが自動生成されます。
設定周り
configを設定することによってどこのディレクトリ階層にどのtemplateを用いて自動生成するかをコントロールすることができます。今回は packages/ui/turbo/gen/config.ts
がこれに該当します。
templateはその名の通り生成するファイルのテンプレートです。packages/ui/turbo/gen/templates/*.hbs
がこれに該当します。また今回は.hbs
形式で記述しています。
config
configは以下のように書くことができます、plop.setGeneratorの第一引数にgeneratorの名称をセットでき、第二引数のオブジェクトにdescription
とprompts
、actions
を内包できます。
ここで見るべき箇所はprompts
とactions
です。
import type { PlopTypes } from '@turbo/gen';
export default function generator(plop: PlopTypes.NodePlopAPI): void {
plop.setGenerator('component', {
description: 'Adds a new component',
prompts: [{}],
actions: [{}]
});
}
prompts
promptsには、対話式で見た内容を記載できます、選択肢のメッセージや、変数の指定ができます。
上記のComponent作成の対話は以下の様になります。
prompts: [
{
type: 'list',
name: 'category',
message: 'Which category does the component belong to?',
choices: ['atoms', 'molecules', 'organisms']
},
{
type: 'input',
name: 'name',
message: 'What is the name of the component?'
},
{
type: 'list',
name: 'app',
message: 'Choose the app to which the organisms component belongs.:',
choices: ['web-app', 'doc-app'],
when: answers => answers['category'] === 'organisms'
}
],
type: 'list'
でchoicesの選択肢を配列で設定しています。type: 'input'
はユーザーからの入力を受け付けます。またname
はactionsで変数として使用されます。三要素目にあるwhen
はユーザーの選んだchoicesの結果によって条件分岐させています。これによってatoms、moleculesとorganismsでactionsを別のものを実行できる様にしました。
actions
actionsには、指定のpathにどのtemplateFileを用いて生成したものをどうするかを記述します。
{
type: 'add',
path: 'src/{{category}}/{{pascalCase name}}/{{pascalCase name}}.tsx',
templateFile: 'templates/component.hbs'
},
{
type: 'add',
path: 'src/{{category}}/{{pascalCase name}}/{{pascalCase name}}.stories.tsx',
templateFile: 'templates/stories.hbs'
},
{
type: 'add',
path: 'src/{{category}}/{{pascalCase name}}/index.ts',
template: "export * from './{{pascalCase name}}';"
},
{
type: 'append',
path: 'src/{{category}}/index.ts',
template: "export * from './{{pascalCase name}}';"
}
例えば一番上の要素を見るとtype: 'add'
なので指定したディレクトリ配下へのファイル追加です。この時追加するファイルは'templates/'配下にあるcomponent.hbs
です。また三要素目を見ると 'src/{{category}}/{{pascalCase name}}/index.ts'
のようになっていますが、{{}}
の様にすることでpromptsで定義した変数を代入できます。pascalCase
とすることでその変数をパスカルケースにして代入できます。またtemplate: "export * from './{{pascalCase name}}';"
の様に、指定のtemplatesを用いなくても内容を直接記述することもできます。今回はワンライナーなのでこの様に指定しています。
template
生成するファイルの雛形をhbsで書くことができます。ここは直感的な箇所なので特に説明は不要かと思います。
'use client';
import type { FC, ReactNode } from 'react';
type {{ pascalCase name }}Props = {
children: ReactNode;
};
export const {{ pascalCase name }}: FC<{{ pascalCase name }}Props> = ({}) => {
return <div></div>;
};
{{ pascalCase name }}.displayName = '{{ pascalCase name }}';
感想
導入してみて、多くのメリットがありました。スピード感を持って開発を進められるようになりましたし、雛形のコードを生成できるので、書き方強制もある程度できました。またconfigの書きやすさはとても良かったです。turborepoも採用していて、コード生成で煩わしさを解決したい場合はぜひ使ってみてください。今回はComponentのみの紹介でしたが例えばfeaturesの構成にしていた場合、featuresのフォルダセットを出力してみるのも良いかもしれません。また本記事では割愛していますが、RSCを含むコンポーネントとそうでないコンポーネントで生成物を分岐させたりしても良いかもしれません。いろいろな場面で活用できそうです。
Reference
Discussion