Next.js10用 Boilerplate(Typescript, Sass, Tailwind.css, ESLint/Prettier, Storybook, jest, Hygen ) 

久しぶりに Next.js で新規プロジェクトを作ろうと触ってみたところ、結構変わっていた。 
新しくBoilerplateを検討してみた際のメモです。


## この記事で扱う内容

- Next.jsのインストール
- Typescriptの導入
- sassの導入
- Tailwind.cssの導入
- ESLint/Prettierの導入
- Storybookの導入
- jestの導入
- コンポーネントの読み込み用エイリアスを設定
- Hygenテンプレート
 
## Next.jsのインストール

[こちら](https://nextjs.org/learn/basics/create-nextjs-app/setup)を参考に


```zsh
% npx create-next-app <アプリ名>
% cd <アプリ名> 
% npm run dev
```

- `http://localhost:3000/` にアクセスし起動することを確認。

## Typescriptの導入


必要パッケージを追加し、configを作成。

```zsh
% npm i -D typescript @types/react @types/node @types/react-dom
# tsconfig.jsonを生成
% tsc --init 
```

以下の手順で動作を確認。

- `tsconfig.json` 内、 `"strict": true,` にする。
- `/pages` 配下の `.js` ファイルを全て `.tsx` に変更する
- `http://localhost:3000/` にアクセスし起動することを確認。


## Sass の導入

react.js内では、cssをどの様に実装するかはいくつかの選択肢があるが、next.jsはデフォルトで [Build-in CSS](https://nextjs.org/docs/basic-features/built-in-css-support) がサポートされている。sassについてもパッケージの追加だけでOK。

公式が推奨しており、`CSS in JS` などよりはパフォーマンスの面でも有利らしいので、現状はこのまま採用するのが良さそう。


```zsh
% npm install sass
```

これだけで　`.modules.scss` ファイルを使用できるようになる。


## Tailwind.cssの導入

[こちら](https://tailwindcss.com/docs/guides/nextjs) に沿って導入。

```zsh
% npm install tailwindcss@latest postcss@latest autoprefixer@latest
% npx tailwindcss init -p
```

`tailwind.config.js` と `postcss.config.js` を生成。

`tailwind.config.js` の `purge` オプションに、`.tsx`を配置するdirを設定する。
コンポーネントは `./src/components/**` 配下に配置していく想定。

```tailwind.config.js
module.exports = {
  purge: ['./pages/**/*.{js,ts,tsx}', './src/components/**/*.{js,ts,tsx}'],
  ...
}
```

その他、必要なスタイル設定があれば追記していく。


`/pages/_app.tsx` の冒頭で import　すれば使えるようになる。

```/pages/_app.tsx
import 'tailwindcss/tailwind.css'
```


### その他 必要に応じて

- ベンダープレフィックスが必要 → `postcss.config.json` に `autoprefixer` を追加。[参考こちら](https://tailwindcss.com/docs/using-with-preprocessors)。
- 変数を使いたい → 基本的には `CSS Variables` で対応することを念頭に開発する。
- `scss`の変数をどうしても使いたい → `@use`,`@forward`を使う （`dart-sass` で `@import` は廃止予定）
- `scss variables` を `@use` で使うには `tsconfig.json` に path alias を設定しておくとよい

#### ※ scss variables 用の path alias

ざっくりこんな感じで`tsconfig.json` に path alias を設定しておくと便利。

```tsconfig.json
{
  "compilerOptions": {
     ...
    "paths": {
      "@variables": ["./src/styles/_variables.scss"],
    },
    ...
  },
  ...
}
```

呼び出す場合は

```example.modules.scss
@use "@variables" as v;

.hoge {
  color: v.$color-primary;
}
```

## ESLint, Prettierの導入

必要パッケージを追加し、configを作成。

```zsh
# ESLint関係のパッケージ
% npm i -D eslint @typescript-eslint/parser  @typescript-eslint/eslint-plugin  eslint-plugin-react  eslint-plugin-react-hooks  eslint-plugin-jsx-a11y  eslint-plugin-import  eslint-import-resolver-typescript

# prettier関連のパッケージ
% npm i -D prettier eslint-plugin-prettier eslint-config-prettier
```

`.eslintrc.js` を作成。ざっくり以下のように

```.eslintrc.js
module.exports = {
  root: true,
  env: {
    browser: true,
    es2020: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/eslint-recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:jsx-a11y/recommended',
    'plugin:import/errors',
    'plugin:import/warnings',
    'plugin:prettier/recommended',
    'prettier/@typescript-eslint',
    'prettier/react',
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 2020,
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint', 'react', 'import'],
  settings: {
    'import/resolver': {
      node: {
        extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
      },
      typescript: {
        config: 'tsconfig.json',
        alwaysTryTypes: true,
      },
    },
    react: {
      version: 'detect',
    },
  },
  rules: {
    'import/no-unresolved': 'off',
    '@typescript-eslint/ban-types': [
      'error',
      {
        types: {
          '{}': false,
        },
      },
    ],
    'react/prop-types': ['off'],
    'react/react-in-jsx-scope': 'off',
    'react/jsx-filename-extension': ['error', { extensions: ['.jsx', '.tsx'] }],
    'import/order': ['error'],
    'prettier/prettier': [
      'error',
      {
        trailingComma: 'all',
        endOfLine: 'lf',
        semi: false,
        singleQuote: true,
        printWidth: 80,
        tabWidth: 2,
      },
    ],
    'jsx-a11y/anchor-is-valid': [
      'error',
      {
        components: ['Link'],
        specialLink: ['hrefLeft', 'hrefRight'],
        aspects: ['invalidHref', 'preferButton'],
      },
    ],
  },
}

```
対象にしたくないファイルは `.eslintignore` を作り、これに追記する。


`package.json` に 起動するための script を追加

```package.json
{
  "scripts": {
    "lint": "eslint --ext .js,.jsx,.ts,.tsx .",
    "lintfix": "eslint --ext .js,.jsx,.ts,.tsx --fix .",
  },
}
```

## Storybookの導入

下記コマンドで、next.jsの構成を自動検知し、パッケージ追加と初期設定を行ってくれる。

```zsh
% npx sb init
```

`.storybook/main.js` を開き `*.stories.mdx` へのパスを調整、 使う addons を追加する。

またこのままでは scss variables 用の path alias がうまく読み込めない為 `webpack.config.js` を追加する。
`../src/styles/variables.scss` にある 変数を `@variables` というエイリアスで各コンポーネントから参照できるようにする。

```zsh
% npm i -D sass css-loader sass-loader style-loader babel-preset-react-app
```

```webpack.config.js
const path = require('path')
module.exports = ({ config }) => {
  config.resolve.alias = {
    ...config.resolve.alias,
    '@variables': path.resolve(__dirname, '../src/styles/variables.scss')
  };
  config.module.rules.push({
    test: /\.scss$/,
    loaders: ['style-loader', 'css-loader', 'sass-loader'],
    include: path.resolve(__dirname, '../')
  });
  config.module.rules.push({
    test: /\.css$/,
    loader: 'style-loader!css-loader',
    include: __dirname
  });
  config.module.rules.push({
    test: /\.(ts|tsx)$/,
    loader: require.resolve('babel-loader'),
    options: {
      presets: [require.resolve('babel-preset-react-app')],
    },
  });
  config.resolve.extensions.push('.ts', '.tsx');
  return config;
};
```

（※ エラーが出る場合はバージョンをチェック。ダウングレードしないとうまく動かないものがある。エラーを見て細かくバージョンをチェックして対応する。）

`package.json` に 起動するための script を追加

```package.json
{
  "scripts": {
    "storybook": "start-storybook -p 6006"
  },
}
```


## jest の導入

必要パッケージを追加。

```zsh
% npm i -D jest ts-jest @types/jest react-test-renderer enzyme enzyme-to-json @wojtekmaj/enzyme-adapter-react-17 @types/react-test-renderer @types/enzyme
% npx ts-jest config:init
```
（※ `@wojtekmaj/enzyme-adapter-react-17` は、Enzyme　が React 17　に対応したら不要になる様子。）


必要なコンフィグ類を作成。

`jest.config.js` に追記。test 時に CSS ファイルを読み込まないようにする設定も加える。

```jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: [
    '**/__tests__/**/*.+(ts|tsx|js)',
    '**/?(*.)+(spec|test).+(ts|tsx|js)',
  ],
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest',
  },
  moduleNameMapper: {
    '^src/(.*)$': '<rootDir>/src/$1',
    '\\.(css|scss)$': '<rootDir>/node_modules/jest-css-modules',
  },
  testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  snapshotSerializers: ['enzyme-to-json/serializer'],
  setupFilesAfterEnv: ['<rootDir>/test/setupTests.ts'],
  globals: {
    'ts-jest': {
      tsconfig: '<rootDir>/test/tsconfig.jest.json',
    },
  },
}
```

`test/tsconfig.jest.json` と `test/setupTests.ts` を作成。

```test/tsconfig.jest.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "jsx": "react"
  }
}
```

```test/setupTests.ts
import Enzyme from 'enzyme'
import Adapter from '@wojtekmaj/enzyme-adapter-react-17'

Enzyme.configure({ adapter: new Adapter() })

```

`package.json` に 起動するための script を追加。

```package.json
{
  "scripts": {
    "test": "jest"
  },
}
```

コンポーネントが .css を読み込んでいるとエラーが出る。`jest-css-modules`を追加。

```zsh
% npm i -D jest-css-modules
```

`jest.config.js` に以下を追加

```jest.config.js
module.exports = {.
  moduleNameMapper: {
    '\\.(css|scss)$': '<rootDir>/node_modules/jest-css-modules'
  },
}
```

## コンポーネントの読み込み用エイリアスを設定

Next.jsのデフォルトでは、コンポーネントのimportの際、 `../../` のような相対パスで書く必要がある。
通常は `next.config.js` に エイリアスを追加するが、今回はTypescriptを使っているので、 `tsconfig.json` に設定する。
`~` や `@` を以下の様に追加する。

```tsconfig.json
{
  "compilerOptions": {
    ...
    "paths": {
      "@/*": ["./*"],
      "~/*": ["./*"],
    },
    ...
  },
  ...
}
```

読み出し側では以下のように。

```pages/index.tsx
import ComponentName from '@/src/components/path/to/ComponentName'
import ComponentName from '~/src/components/path/to/ComponentName'
```

## hygenテンプレート

ここまでで設定してきたものをすべて手動で扱うのは一苦労なので、hygenで自動化する。

```zsh
% npm i -D hygen
```

ここで `% hygen init self` すると、サンプルファイルが `/_templates/generator/` に作られる。
これを削除し、同箇所に設定ファイルを作成するのだが、今回は `.hygen/` 以下に置きたいので `.hygen.js`を追加する。


```.hygen.js
module.exports = {
  templates: `${__dirname}/.hygen`,
}
```

これで、テンプレートファイルは `.hygen/` に配置できる。

一例として、コンポーネントを新規作成する際のテンプレートを作ってみる。
以下のような設定ファイルを作成する。

```
.hygen
└── new
    └── component
        ├── prompt.js
        ├── scss.ejs.t
        ├── story.ejs.t
        ├── test.ejs.t
        └── tsx.ejs.t
```

各ファイルは以下のような感じで。

```.hygen/new/component/prompt.js
module.exports = [
  {
    type: 'select',
    name: 'category',
    message: '作成するcomponentの粒度を選んでください',
    choices: ['atoms', 'molecules', 'organisms', 'templates'],
  },
  {
    type: 'input',
    name: 'componentname',
    message: 'component名をパスカルケースで入力してください',
    hint: '"MyButton"など',
  },
]
```
```.hygen/new/component/scss.ejs.t
---
to: src/components/<%= category %>/<%= componentname%>/<%= componentname%>.module.scss
---

@use "@variables" as v;

.root {}
```
```.hygen/new/component/story.ejs.t
---
to: src/components/<%= category %>/<%= componentname%>/<%= componentname%>.stories.tsx
---
import React from 'react'
import { Story, Meta } from '@storybook/react'
import { <%= componentname %>, Props } from './<%= componentname %>'

export default {
  component: <%= componentname %>,
  title: '<%= category %>/<%= componentname %>',
} as Meta

const Template: Story<Props> = (args) => <<%= componentname %> {...args} />

export const Default = Template.bind({})
Default.args = {}
```
```.hygen/new/component/test.ejs.t
---
to: src/components/<%= category %>/<%= componentname%>/<%= componentname%>.spec.tsx
---
import React from 'react'
import { shallow } from 'enzyme'
import { <%= componentname %> } from './<%= componentname %>'

describe('<%= componentname %> test', () => {
  const wrapper = shallow(<<%= componentname %>></<%= componentname %>>)

  it('renders correctly', () => {
    expect(wrapper).toMatchSnapshot()
  })
})
```
```.hygen/new/component/tsx.ejs.t
---
to: src/components/<%= category %>/<%= componentname%>/<%= componentname%>.tsx
---

import React from 'react'
import styles from './<%= componentname%>.module.scss'

export type Props = {}

export const <%= componentname%>: React.VFC<Props> = ({}) => <div className={styles.root}></div>

```

以下のコマンドで対話を起動。

```zsh
% hygen new component
```

対話に沿って、例えば `atoms/MyComponentName` を作成すると、 `src/components/atoms/` に以下のファイルが生成される。
この例は非常にシンプルだが、promptを工夫する事で、より細かい出し分けや、作り込みが可能になる。

## 参考

参考にさせていただきました。ありがとうございます。

- https://nextjs.org/learn/basics/create-nextjs-app/setup
- https://nextjs.org/docs/basic-features/built-in-css-support
- https://jestjs.io/ja/
- https://storybook.js.org/
- https://www.hygen.io/
- https://tailwindcss.com/docs/guides/nextjs
- https://qiita.com/tatane616/items/e3ee99a181662ad6824b
- https://qiita.com/282Haniwa/items/dcce1ba6bb6ae541893e
- https://qiita.com/syuji-higa/items/931e44046c17f53b432b
- https://qiita.com/github0013@github/items/303a32d3037d322e67c0
- https://qiita.com/tatane616/items/e3ee99a181662ad6824b
- https://qiita.com/taroodr/items/19c852aa5d43c1c587fb
- https://qiita.com/daku10/items/c00d087458f2f1de6545
- https://qiita.com/tashirimo_dev/items/fb29bd4d0318b0909d0b
- https://qiita.com/numa999/items/aef01affdb25bbaa3290
- https://zenn.dev/catnose99/scraps/5e3d51d75113d3
- https://zenn.dev/matamatanot/articles/e0e6371fe28b7479fb3f
- https://zenn.dev/takepepe/articles/hygen-template-generator
- https://zenn.dev/takepepe/scraps/6668e9fe402666
- https://panda-program.com/posts/nextjs-storybook-typescript-errors#nextjs%E3%81%ABstorybook%E3%82%92%E5%B0%8E%E5%85%A5%E3%81%99%E3%82%8B
- https://tech.playground.style/javascript/next-js-typescript/