🏭

turbo/gen で対話式雛形コード生成

2023/12/25に公開

フロントエンドにおいてAtomic DesignにおけるComponent作成やfeatures作成を行う方は多いと思います。例えばComponent作成であればただ単一のjsxtsxのみ存在することはあまりないと思います。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を紹介させていただければと思います。

https://turbo.build/repo/docs/reference/command-line-reference/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の名称をセットでき、第二引数のオブジェクトにdescriptionpromptsactionsを内包できます。
ここで見るべき箇所はpromptsactionsです。

config.ts
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作成の対話は以下の様になります。

config.ts
    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を用いて生成したものをどうするかを記述します。

config.ts
        {
          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で書くことができます。ここは直感的な箇所なので特に説明は不要かと思います。

component.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

https://turbo.build/repo/docs/reference/command-line-reference/gen
https://zenn.dev/takepepe/articles/hygen-template-generator
https://speakerdeck.com/shuta13/turborepo-code-generationniyoru-saibaezientogurupunohurontoendokai-fa-noxiao-lu-hua

Discussion