Next.jsでHygenを使う

## はじめに
Next.jsでHygenを使用して開発体験を向上させるための方法です。
今回はJestによるスナップショットテストとStorybookのストーリーの追加をHygenにさせてみます。

## 方法
### 環境
- Next.js 13
- Typescript
- Storybook
- Jest
なお、今回の記事では`src`へのエイリアスとして`@`を登録しています。

### 導入

リポジトリにHygenをインストールします。
```shell
> npm install -D hygen
```

`npx hygen init self`を使うと自動でHygenの設定をしてくれるのですが、今回は自分でしていきたいと思います。

### 設定

まず、プロジェクトルートに`.hygen.js`を作成します。
このファイルではhygenのテンプレートファイルを置くディレクトリを指定します。今回は`.hygen/`以下のディレクトリにテンプレートを配置します。
```.hygen.js
module.exports = {
  templates: `${__dirname}/.hygen`,
};
```

hygenに聞いてもらう対話リストを作成します。こちらはjsで記述することができます。
また、これ以降のファイルは`.hygen/component/new`以下に配置します。
```index.js
module.exports = {
  prompt: ({ inquirer }) => {
    const questions = [
      {
        type: "select",
        name: "category",
        message: "Which Atomic Design category?",
        choices: ["atoms", "molecules", "organisms"],
      },
      {
        type: "input",
        name: "name",
        message: "What is the component name?",
      },
      {
        type: "input",
        name: "dir",
        message: "Where is the directory? (No problem in blank)",
      },
      {
        type: "confirm",
        name: "have_props",
        message: "Is it have props?",
      },
    ];
    return inquirer.prompt(questions).then((answers) => {
      const { category, name, dir, have_props } = answers;
      const path = `${category}/${dir ? `${dir}/` : ``}${name}`;
      const abs_path = `src/components/${path}`;
      const alias_path = `@/components/${path}`
      const lower_name = name.toLowerCase();
      return { ...answers, path, abs_path, alias_path, lower_name };
    });
  },
};

```

questionsの中に対話する質問リストを記述します。
typeをselectにするとchoicesに記述した選択肢からの選択式になります。
typeをinputにすると任意の文字を入力することができます。
typeをconfirmにするとy/nの質問ができます。
テンプレートに渡す変数群は、テンプレートに渡す変数を返す関数を戻り値にすることで扱うことができます。

今回は、Atomic Designの分類、コンポーネント名、ディレクトリ名(省略可能)、プロパティの有無を対話で聞きます。

次にコンポーネント本体のtsxのテンプレートです。
```component.tsx.t
---
to: <%= abs_path %>/index.tsx
unless_exists: true
---
<% if (have_props) { %>
interface <%= name %>Props {
  str: string;
}
<%}%>
export const <%= name %> = (<% if (have_props) { %> props: <%= name %>Props <% } %>): JSX.Element => {
  return (
    <div><% if (have_props) { %> { props.str } <% } else { %>Hello, hygen!<% } %></div>
  );
};

```
テンプレート自体はejsのような記法で記述することができます。
二行目のtoはファイルの出力先で、unless_existsをtrueにすることで出力先にファイルがあった場合に上書きをしなくなります。

`<% if (have_props) { %>` `<$ } %>`でプロパティがありの時に出力する内容を指定しています。条件式がtureの時に{}で囲まれた中の部分が出力されます。

```component.test.tsx.t
---
to: <%= abs_path %>/<%=lower_name%>.test.tsx
---
import { render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

import { <%= name %> } from "<%= alias_path %>";

describe("<%= name %> rendering test", () => {
  it("<%= name %>", () => {
    const { asFragment } = render(<<%= name %> <% if(have_props) { %>str="Hello, jest"<% } %> />);
    expect(asFragment()).toMatchSnapshot();
  });
});

```
テストファイルのテンプレートです。
テスト名などは入力内容から自動でいれ、スナップショットのコードを生成します。

```component.stories.tsx.t
---
to: <%= abs_path %>/<%=lower_name%>.stories.tsx
---
import { ComponentStory, ComponentMeta } from "@storybook/react";
import React from "react";

import { <%= name %> } from "<%= alias_path %>";

export default {
  title: "<%= path %>",
  component: <%= name %>,
  argTypes: {
  },
} as ComponentMeta<typeof <%= name %>>;

const Template: ComponentStory<typeof <%= name %>> = (args) => <<%= name %> {...args} />;
export const Default = Template.bind({});
Default.args = {
<% if (have_props) { %>
  str: "Hello, storybook!",
<%}%>
};

```
ストーリーのテンプレートです。
プロパティがある場合は、プロパティを使用したストーリーを生成するようになっています。

### 実行
実行の前に、実行を簡単にするために`npm run`のスクリプトとして定義します。
```diff:package.json
{
  "name": "nextjs-template",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint && prettier --write src/",
    "test": "jest --verbose",
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook",
++  "new": "hygen component new"
  }
}

```

これにより。`npm run new`で実行できるようになります。