Next.jsのオレオレゴリゴリテンプレートを作りたい(作った)

## はじめに
初めましての方は初めまして。
土佐犬と申します。

今回はNext.jsのテンプレートを作ったのでその紹介をしていければと思います。

というのも私は割とハッカソンに出てるのですがその度に毎回やれstorybookだ、やれhygenだと環境構築してたら時間が足らなくなってしまうのでいっそのことテンプレートにしてしまおうということです。

## 使用方法

1.  clone

        git clone git@github.com:tosaken1116/nextjs-app-templates.git

    or

        git clone https://github.com/tosaken1116/nextjs-app-templates.git

2.  ディレクトリ移動

        cd nextjs-app-templates

3.  パッケージマネージャの選択

        bash setup.sh

    ```
    Choose a package manager (npm/yarn/pnpm/bun):
    Enter a new project name:
    ```
これを実行することでパッケージマネージャの選択、ライブラリインストール、`first commit`、envファイルの生成、`develop`ブランチの作成を行います


https://github.com/tosaken1116/nextjs-app-templates

## 使用したライブラリ
このテンプレートに使用しているライブラリは以下です

カタログ生成

- `storybook`

テスト
- `jest`

自動生成
- `hygen`

静的解析
- `markuplint`
- `eslint`
- `cspell`

フォーマッター
- `lint-staged`
- `prettier`

その他
- `husky`
- `commitlint`

## ライブラリの説明

### storybook
`storybook`とは UIコンポーネントを視覚的にドキュメント化・テストするためのツールです。
簡単に言ってしまえば作ったボタンや画面などを、カタログのように見やすくまとめてくれるツールです。

環境構築、使用方法においては公式ドキュメントがわかりやすかったです。

https://storybook.js.org/docs/react/get-started/install/

next13にも対応しておりドキュメントも整っています。

https://storybook.js.org/recipes/next

### jest
`jest`とは`javascript`のテストフレームワークです。
プログラムが正しく動いているかを確認することができます。

こちらはNext.jsのドキュメントがわかりやすかったのでそちらを参考に構築しました。

https://nextjs.org/docs/pages/building-your-application/optimizing/testing#jest-and-react-testing-library

### hygen
`hygen`とはコードのテンプレートを設定することで雛形を自動生成してくれるライブラリです。
これは個人的にstorybookとの相性がよく、`.stories`ファイルなどの自動生成にもよく使います。
導入した理由としてはどのファイルにおいても一貫した書き方をしたいというのと一貫したコードなら開発者が書かなくても雛形で生成したほうが楽だからです。

公式スタートガイド

https://www.hygen.io/docs/quick-start

個人的には以下の記事が見やすいと感じました。

https://kiyobl.com/hygen/

### markuplint
`markuplint`とはHTMLとXMLのマークアップを検証・修正するためのリンターです。
導入した理由としてはhtmlルールに反したコードを静的チェックで検出することで予期しないバグを生み出さないようにするためです。
公式ドキュメント

https://markuplint.dev/ja/

以下記事がわかりやすいです

https://zenn.dev/chot/articles/bcc178c481a9c2

### eslint
`eslint`とはJavaScriptコードの品質とコーディングスタイルの問題を検出するためのリンターツールです。
`eslint`に関しては`create-next-app`で標準で入るのでデフォルト設定で十分なら追加設定の必要はありません。


公式ドキュメント

https://eslint.org/

eslintはドキュメントの他にも記事が豊富なので設定には困りません

個人的には以下の記事が見やすかったです

https://zenn.dev/shimabukuromeg/articles/76b9e3c62b6984

### cspell
`cspell`とはコード内のスペルミスを検出するためのスペルチェッカーです。
よくvscode拡張として使われることが多いですがライブラリとしての使用もできます。
導入した理由としてはタイポしても静的チェックで検出できればレビューコストが下がると考えたためです。
公式ドキュメント

https://cspell.org/

この記事が読みやすかったです

https://qiita.com/mima_ita/items/579fcc3a65cee4ef9b01

### lint-staged
`lint-staged`はステージングエリアにあるファイルに特定の操作を行えます。
大体はlinterとformatterを通すことがほとんどです。
これは後に示す`husky`と併用することが前提となっています。
導入した理由としては`vscode`の`save on format`に依存しないコード整形ができるからです

リポジトリ

https://github.com/okonet/lint-staged

以下記事が読みやすかったです

https://qiita.com/akym03/items/7e4db720a9e1bfad7747


### prettier
`prettier`はコードを一貫したスタイルに自動整形するためのツールです
`eslint`と組み合わせることでプロジェクトでより統一されたコードを書くことができます。

公式ドキュメント

https://prettier.io/

参考になった記事

https://typescriptbook.jp/tutorials/prettier


### husky
`husky`はGitフックを簡単に設定し、コミットやプッシュ前のタスクを自動化するためのツールです
そもそもGitフックって何って話なんですけど前述の通りコミットやプッシュ前に特定の動作を行うことができます(他にもpullしたときなど)
導入した理由としてはコミット前静的チェックやプッシュ前ビルドなどをしたいからです。
公式ドキュメント

https://typicode.github.io/husky/

参考にした記事

https://qiita.com/akym03/items/7e4db720a9e1bfad7747

### commitlint
`commitlint`とはコミットメッセージが一貫した規約に従っているかを検証するためのツールです。
チーム開発をする際コミットメッセージの統一をしますが、コミットするときにメッセージがルールに従っていない場合コミットできないというものです。
個人的には`semantic release`と相性がいいと思っています
設定も簡単で使う安いので導入コストは低いと思います。
導入した理由としては友人に`today's commit`とかする人がいたりするからですね

公式ドキュメント

https://commitlint.js.org/#/

参考にした記事
https://zenn.dev/kalubi/articles/27fa889c338cdf

## 他に使用したツールなど

ライブラリ以外には以下のツールなどを使用しました。
- `github actions` 
    CI CDの実行
- `dependabot`
    パッケージ更新のプルリクエスト
- `volta`
    nodeのバージョン指定、パッケージマネージャのバージョン指定

## ディレクトリ構成
ディレクトリ構成は以下の通り
```tree
src
├── app
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── components
│   ├── functional
│   ├── model
│   ├── page
│   └── ui
└── libs
```
src配下に`app`と`components`と`libs`だけおいています。
`components`配下には`functional` `model` `page` `ui`ディレクトリが作成されておりコンポーネントをカテゴリごとに分類します。
これらは以下の記事を参考にしました。

https://zenn.dev/knowledgework/articles/99f8047555f700

## スクリプト
スクリプトは以下のように設定されています。
```json
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test:unit": "jest  --passWithNoTests",
"new:ui": "hygen generator ui",
"new:model": "hygen generator model",
"new:page": "hygen generator page",
"prepare": "husky install",
"commitmsg": "commitlint -e $GIT_PARAMS",
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx,json,css,scss}'",
"check:lint": "eslint 'src/**/*.{ts,tsx}' --report-unused-disable-directives --max-warnings 0",
"check:cspell": "cspell -c ./cspell.config.json \"./src/**/*\" --unique --no-progress --no-summary",
"check:markuplint": "markuplint -p \"./src/**/*.tsx\"",
"check": "run-p check:* test:*",
"ci:test-group": "run-p test:*",
"ci:check": "run-p check:*",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
```
開発時に使うのは`dev` `storybook` `new:*`くらいでしょうか
他は`husky`や`CI`で使います
`new:*`はhygenによる雛形の自動生成です。


## 自動生成テンプレート
hygenの自動生成により作成できるファイルは以下の通りです
```
_templates/generator
├── model
│   ├── component.ejs.t
│   ├── empty.ejs.t
│   ├── error.ejs.t
│   ├── hooks.ejs.t
│   ├── index.ejs.t
│   ├── loading.ejs.t
│   ├── prompt.js
│   ├── stories.ejs.t
│   └── test.ejs.t
├── page
│   ├── index.ejs.t
│   ├── page.ejs.t
│   ├── prompt.js
│   └── stories.ejs.t
└── ui
    ├── index.ejs.t
    ├── prompt.js
    └── stories.ejs.t
```
modelコンポーネントを作るときは`components`,`empty`,`error`,`loading`,`index`,`stories`,`test`の最大7ファイルが自動生成されます。
それぞれのファイルに雛形が書き込まれており
プロンプトは以下の通り
```console
$ npm run new:model

$ hygen generator model
✔ component name · Hoge
✔ have props (Y/n) · true
✔ have hooks (Y/n) · true
✔ gen_files · Empty, Error, Loading
```
コンポーネントの名前,上位コンポーネントからのpropsの有無,hooksの有無、empty,error,loadingの生成の可否が選択できます。(test,stories,component,indexは確実に生成されます。)
以下は自動生成されたファイルの一部です

```tsx:components/model/Hoge/index.stories.tsx
import { HogePresentation } from './presentations';
import { HogeEmptyPresentation } from './presentations/empty';
import { HogeErrorPresentation } from './presentations/error';
import { HogeLoadingPresentation } from './presentations/loading';

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

const meta: Meta<typeof HogePresentation> = {
  component: HogePresentation,
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
};

export default meta;

type Story = StoryObj<typeof HogePresentation>;

export const Default: Story = {
  args: {},
};

export const Empty: Story = {
  render: () => <HogeEmptyPresentation />,
};

export const Error: Story = {
  render: () => <HogeErrorPresentation />,
};

export const Loading: Story = {
  render: () => <HogeLoadingPresentation />,
};
```
これをコマンド一発で作成することができます。

同様にuiコンポーネントはindex,storiesの2ファイルが生成されます
プロンプトは以下の通り

```console
$ npm run new:ui

$ hygen generator ui
✔ component name · Hoge

Loaded templates: _templates
       added: src/components/ui/Hoge/index.tsx
       added: src/components/ui/Hoge/index.stories.tsx
```
コンポーネントの名前だけ入力できます

最後にpageコンポーネントですがindex,stories,page.tsxが生成されます
プロンプトは以下

```console
$ npm run new:page
$ hygen generator page
✔ component name · Profile
✔ use model · Hoge
✔ page_url localhost:3000/[] example:user/my · profile
```
コンポーネントの名前と使用するmodelコンポーネント、pageのパスを設定することができます。
modelコンポーネントがないときは`use model`のプロンプトはスキップされます。


## husky
`husky`はそれぞれ以下のようにコマンドを実行

`commit-msg`:コミットメッセージに`commitlint`を実行
```
npm run commitmsg
```
`post-merge`:マージした時にパッケージインストールを実行
```
npm install
```
`pre-commit`:コミット前に静的チェックと`lint-staged`を実行
 ```
npm run check
npm run lint-staged --config .lint-staged.config.mjs
```

`pre-push`:プッシュ前にビルドとstorybookのビルドを実行
```
npm run build
npm run build-storybook
```

## eslint
eslintの設定は以下の通り
あまり設定したことなかったので新鮮でした
長いので畳みます。
<details><summary>eslint.config.json</summary><div>

```json
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking",
    "plugin:jsx-a11y/recommended",
    "next/core-web-vitals",
    "prettier",
    "plugin:storybook/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json"
  },
  "plugins": [
    "react",
    "import",
    "unused-imports",
    "jsx-a11y",
    "@typescript-eslint"
  ],
  "rules": {
    // 関数の戻り値の型を明示的に指定する
    "@typescript-eslint/explicit-function-return-type": "error",

    // 必要な場合にのみアロー関数の本体を使用する
    "arrow-body-style": ["error", "as-needed"],

    // テンプレートリテラルの使用を推奨
    "prefer-template": "warn",

    // アロー関数のコールバックの使用を推奨
    "prefer-arrow-callback": "warn",

    // 未使用のインポートを禁止
    "unused-imports/no-unused-imports": "error",

    // JSXのブール属性の値を明示的に指定する
    "react/jsx-boolean-value": "error",

    // JSXの中括弧のスペースを禁止
    "react/jsx-curly-brace-presence": "error",

    // 自己終了タグを使用する
    "react/self-closing-comp": [
      "error",
      {
        "component": true,
        "html": true
      }
    ],

    // 型の名前をパスカルケースにする
    "@typescript-eslint/naming-convention": [
      "error",
      {
        "selector": "typeLike",
        "format": ["PascalCase"]
      }
    ],

    // 特定のディレクトリからのimportを禁止する
    "import/no-restricted-paths": [
      "error",
      {
        "zones": [
          // uiコンポーネントにおいて、modelとpageのimportを禁止する
          {
            "target": "./src/components/ui",
            "from": ["./src/components/model", "./src/components/page"]
          },
          // modelコンポーネントにおいて、pageのimportを禁止する
          {
            "target": "./src/components/model",
            "from": ["./src/components/page"]
          },
          // pageコンポーネントにおいて、他のpageコンポーネントからのimportを禁止する
          {
            "target": "./src/components/page/**/index.tsx",
            "from": ["./src/components/page/**/index.tsx"]
          },
          // appディレクトリ配下においてmodel,uiのimportを禁止する
          {
            "target": "./src/app/**/page.tsx",
            "from": ["./src/components/model", "./src/components/ui"]
          }
        ]
      }
    ],

    // ステートメント間の空行
    "padding-line-between-statements": [
      "error",
      {
        // 特定の宣言の間に空行を設ける
        "blankLine": "always",
        "prev": [
          "import",
          "multiline-const",
          "multiline-expression",
          "multiline-let",
          "export",
          "cjs-export",
          "directive",
          "function"
        ],
        "next": [
          "import",
          "multiline-const",
          "multiline-expression",
          "multiline-let",
          "export",
          "cjs-export",
          "directive",
          "function"
        ]
      },
      // import宣言の間に必要があれば空行を設ける
      { "blankLine": "any", "prev": "import", "next": "import" }
    ],

    // JSXを含むファイルの拡張子を指定します。
    "react/jsx-filename-extension": [
      "error",
      {
        "extensions": ["tsx"]
      }
    ],

    // コンポーネントをarrow関数で定義する
    "react/function-component-definition": [
      2,
      {
        "namedComponents": "arrow-function",
        "unnamedComponents": "arrow-function"
      }
    ],

    // importの順序を制御
    "import/order": [
      "error",
      {
        "groups": [
          "builtin",
          "external",
          "parent",
          "sibling",
          "index",
          "object",
          "type"
        ],
        "pathGroups": [
          {
            "pattern": "{react,react-dom/**,react-router-dom}",
            "group": "builtin",
            "position": "before"
          },
          {
            "pattern": "@src/**",
            "group": "parent",
            "position": "before"
          }
        ],
        "pathGroupsExcludedImportTypes": ["builtin"],
        "alphabetize": {
          "order": "asc"
        },
        "newlines-between": "always"
      }
    ],

    // 型のimportにおいてtypeをつけることを推奨
    "@typescript-eslint/consistent-type-imports": [
      "error",
      {
        "prefer": "type-imports"
      }
    ]
  }
}

```
</div></details>


## CI
`CI`はデフォルトでは`static-check`と`test`を実行
使用しているパッケージマネージャに基づいてinstallを行います

```yaml
name: ci
on: push
jobs:
    lint:
        runs-on: ubuntu-latest

        steps:
            - uses: actions/checkout@v3
            - name: Setup
              uses: ./.github/actions/npm

            - name: static check
              run: npm run ci:check
    test:
        runs-on: ubuntu-latest

        steps:
            - uses: actions/checkout@v3
            - name: Setup
              uses: ./.github/actions/npm

            - name: test
              run: npm run ci:test-group
```

## dependabot
`dependabot`は依存パッケージのバージョンが変わった時に自動でパッケージ更新のPRを送ってくれます。
これはgithubの機能で`.github`ディレクトリ配下に`dependabot.yml`ファイルを置くことで使うことができます

```yaml:dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "daily"
      time: "12:00"
      timezone: "Asia/Tokyo"
    target-branch: "develop"
```

この設定だと毎日12:00にdevelopブランチにPRが出されます。
(正直毎日は多いので週１とかでもいいかもしれません)

公式ドキュメント

https://docs.github.com/ja/code-security/dependabot/working-with-dependabot


## 終わりに
今回はオレオレNext.jsテンプレートを作ったという話でした。
正直lintなどに関してはやり過ぎな部分もあるんじゃないかとは思ってます。
今後の展望としてはcssライブラリ,uiライブラリもセットアップ時に動的に設定できればいいなあと思いつつ、なかなか厳しそうだなあと思ってるところです。