テンプレートファイルの基本の形をコマンドで自動生成する

# はじめに

こんにちは、Gakken LEAPのフロントエンドエンジニアの Okuma です。

今回はフロントエンド開発などでよく使用するコンポーネントファイルの雛形をコマンドで自動生成する**hygen**というツールについて紹介します。
今回はNext.jsのプロジェクトを想定して実装していますが、`package.json`を使うプロジェクトであれば実装可能ですので、ぜひ導入を考えてみてはいかがでしょうか？ 今回は対話型のコードジェネレータを使って任意のディレクトリ内にファイルを生成したいと思います！

# hygenとは

[hygen](https://github.com/jondot/hygen)とは、開発プロセスを加速させるシンプルかつ高速なコードジェネレーター。つまり、プロジェクト内のファイルなどを自動生成するためのツールです。
hygenを導入することで、カスタマイズ可能なテンプレートを使って自動生成タスクを実行することができます。
ReactのコンポーネントファイルやStorybookファイル、コンポーネントのテストファイルなどのある程度ベースとなるコードが決まっているファイルを自動生成するのにとても有効です！
hygenには一般的なコマンドライン引数を用いた方法と対話型の入力方法の2種類の生成方法が存在し、用途によって使い分けることができます。

# hygenの導入によるメリット

hygen導入によるメリットには以下のようなものがあると考えられます。

- CLIでファイルを生成できるため、コンポーネントファイル作成時にゼロからコードを記述する必要がない
- ベースのコードが記述された状態のファイルを生成することができる
- チーム開発において、個人によるコンポーネントの記述スタイルの違いを防ぐことができる
- 対話型のファイル生成により、配置するディレクトリを指定できる
- プロジェクトで設定されたディレクトリ構造に基づいてファイルを作成できる
- コンポーネントごとのStorybookファイルやテストファイルも一つのコマンドで一括作成できる
- ディレクトリ構造によってはファイル作成の効率が大幅に向上する

# hygenの導入方法

## インストール

```bash
npm install -D hygen
```

## 初期化

```bash
npm hygen init self
```

初期化コマンドを実行するとプロジェクトルートに`_templates`ディレクトリが生成されます。`_template`ディレクトリ内には`generator`と`init`というディレクトリがあり、初期状態ではサンプルコードが入っています。
自動生成するファイルはこの`_templates`内に作成していきます。

```txt
_template/
|-- generator/
|   |-- help/
|   |   |-- index.ejs.t
|   |-- new/
|   |   |-- hello.ejs.t
|   |-- with-prompt/
|   |   |-- hello.ejs.t
|   |   |-- prompt.ejs.t
|-- init/
|   |-- repo/
|   |   |-- new-repo.ejs.t
```

# テンプレートファイルの作成

今回は対話型のコードジェネレーターの機能を用いてコンポーネントファイル、Storybookファイル、コンポーネントテストファイルを作成したいため、`_template`ディレクトリの中は以下のように簡略化します。（この状態でも対話型のコードジェネレーターは問題なく動きます！）

```txt
_template/
|-- component/
|   |-- new/
|   |   |-- component.stories.tsx.ejs.t
|   |   |-- component.tsx.ejs.t
|   |   |-- index.js
```

今回はコンポーネント専用のディレクトリを作成し、その中にそれぞれコンポーネント名と同じStorybookファイルを配置する構造で作成してみたいと思います。
Storybookファイルやテストファイルが同じディレクトリに配置されていることで、コンポーネントを修正した際にすぐにStorybookやテストを修正できる利点があるため、今回はこの構造を採用してみました。

## コンポーネントのテンプレートファイル

```txt:component.tsx.ejs.t
---
to: <%= path %><%= h.changeCase.pascal(component_name) %>/<%= h.changeCase.pascal(component_name) %>.tsx
---

import type { FC } from 'react';

const <%= h.changeCase.pascal(component_name) %>: FC = () => {
  return (
    <div>
      <h1><%= h.changeCase.pascal(component_name) %></h1>
    </div>
  );
};

export default <%= h.changeCase.pascal(component_name) %>;
```

上記のようにコンポーネントのテンプレートファイルを作成しました。
ファイル上部の`to`にはファイルの出力先を設定します。

以降の行ではReactコンポーネントのベースの形を記述しています。各`component_name`の箇所にはプロンプトで入力したコンポーネント名がパスカルケースに変換されて代入されます。

## Storybookのテンプレートファイル

```txt:component.stories.tsx.ejs.t
---
to: <%= path %><%= h.changeCase.pascal(component_name) %>/<%= h.changeCase.pascal(component_name) %>.stories.tsx
---

import <%= h.changeCase.pascal(component_name) %> from './<%= h.changeCase.pascal(component_name) %>';

import type { Meta, StoryObj } from '@storybook/react';

const meta: Meta<typeof <%= h.changeCase.pascal(component_name) %>> = {
  component: <%= h.changeCase.pascal(component_name) %>,
  tags: ['autotags'],
};

export default meta;

type Story = StoryObj<typeof <%= h.changeCase.pascal(component_name) %>>;

export const Default: Story = {
  render: () => <<%= h.changeCase.pascal(component_name) %> />,
};
```

コンポーネントファイルと大きくは変わりませんが、`component_name`が入ってくるところ以外で共通する部分をこのテンプレートファイルに記載します。
特にTypeScriptでインポートした型（ここでは`Meta`や`StoryObj`など）を毎回使っている場合などは、ファイル作成のたびに記述したり、別ファイルをコピーしたりするのは手間がかかります。
テンプレートファイルとして雛形を用意し、変更がある部分（ここではPropsなど）だけ修正を加えることでチームとして同じ形でコードを管理することができる点も良いですね。

# プロンプトのカスタマイズ

```js:index.js
module.exports = {
  prompt: ({ inquirer }) => {
    const questions = [
      {
        message: 'What is directory name? (empty is root)',
        name: 'directory_name',
        type: 'input',
      },
      {
        message: 'What is component name?',
        name: 'component_name',
        type: 'input',
        validate: (answer) => answer !== '',
      },
    ];
    return inquirer.prompt(questions).then((answers) => {
      const { directory_name } = answers;
      const path = `src/components/${directory_name ? `${directory_name}/` : ``}`;
      return { ...answers, path };
    });
  },
};
```

今回はコンポーネントファイル類を生成する出力先（`directory_name`）とコンポーネント名（`component_name`）を入力させる簡単な内容にしています。

`questions`で定義しているプロパティは以下になります。

- `message` - コマンドライン上で聞かれる質問・メッセージ
- `type` - 利用者自身が文字列を入力する`input`や選択式の`select`などがある
- `name` - 入力や選択した値が入る変数名。これがテンプレートファイルに渡されて展開されます。

`type`で`select`を定義した場合は、`choices`という配列を用意してそこに実際の選択肢を定義することができます。コンポーネント設計にアトミックデザインを導入している場合などは以下のようにあらかじめ用意したディレクトリを選択させる方法が有効かもしれないですね。

```js
{
    message: 'In which directory do you want to create it?',
    name: 'dir',
    type: 'select',
    choices: ['atoms', 'molecules', 'organisms', 'templates', 'pages'], // アトミックデザインのどのディレクトリに配置するかを選択できる
}
```

最後に`packge.json`にhygenの実行コマンドを追加します。

```json:package.json
"scripts": {
    "hygen": "hygen component new"
}
```

以降は新規にコンポーネントが必要なタイミングになったら`npm run hygen`（npmの場合）を実行することで、対話型のコマンド入力を通じてコンポーネントファイル・Storybookファイルを同時に生成することができます。

# おわりに

今回は毎回同じ記述をしているコンポーネントファイルなどを、hygenのコードジェネレーターを使用することによって手間を省くことができました。
また、今回の例ではコンポーネントファイルとそのStorybookファイルを自動生成の対象にしていましたが、テストファイルを追加したり、CSS Modulesを導入している場合はCSSファイルを追加することでさらに開発効率を上げられそうです！！

# エンジニア募集中

Gakken LEAP では教育をアップデートしていきたいエンジニアを絶賛大募集しています！！
ぜひお気軽にカジュアル面談へお越しください！！

https://gakken-leap.co.jp/recruit/

# 参考

- [hygen](https://www.npmjs.com/package/hygen)
- [hygenを導入する](https://zenn.dev/takasmerio/articles/0af4fc8cdef0e0)
- [Hygen で加速する Next.js App Router 開発](https://tech.every.tv/entry/2023/12/20/2)