はじめての Next.js（環境構築）

# はじめに

Next.jsを使うことになったので、インストールして以下を設定します
今回は最低限な設定で構築したいと思います

- 静的サイトホスティング用の設定
- TypeScript
- ESLint + Prettier
- CSS
- Storybook
- 単体テスト

# Next.js をインストール

```sh
npx create-next-app@latest --typescript [プロジェクト名] --use-npm
```

デフォルトが Yarn みたいなので、use-npm オプションを付けている  
以下のコマンドで画面が見れたらオッケー

```sh
npm run dev
```

# コマンドを設定

デフォルトは以下

```json:package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}
```

今回作るアプリは S3 に静的サイトホスティングする
よって、以下に変更

```sh
npm i -D serve
```

```json:package.json
{
  "scripts": {
    "dev": "next dev",
    "export": "next build && next export",
    "serve": "next build && next export && serve ./out"
  }
}
```

dev は HMR をサポートしているので、普段の開発はこれでやる
export は CI で S3 にデプロイする時用
serve は静的ビルドの結果をローカルでサーバーを立てて確認する用

もし、アプリをサーバー上で動かす場合は`"start": "next build && next start"`を追加する

# ESLint + Prettier を設定

ESLint のルールは Airbnb+TypeScript にする
Lint 系は後でルール変えると修正が大変なので、最初からキッチリ設定しておく

```sh
npm i -D eslint eslint-loader eslint-config-airbnb eslint-plugin-react-hooks eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-import @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-config-prettier eslint-plugin-prettier
```

プロジェクトルートに`.eslintrc.js`を作成

```js:.eslintrc.js
module.exports = {
  env: { browser: true },
  extends: [
    'airbnb',
    'airbnb/hooks',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
    'prettier/@typescript-eslint',
    'prettier/react'
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: { ecmaFeatures: { jsx: true } },
  settings: { react: { version: 'detect' } },
  rules: {
    'prettier/prettier': [
      'error',
      {
        arrowParens: 'avoid',
        semi: false,
        singleQuote: true,
        trailingComma: 'none'
      }
    ]
  }
}
```

ここは好みだが、自分は常にリンターのエラーを修正しながら開発したいので、eslint-loader を設定する
プロジェクトルートに`next.config.js`を作成し、以下を設定

```js:next.config.js
module.exports = {
  webpack: config => {
    config.module.rules.push({
      test: /\.tsx?$/,
      loader: 'eslint-loader',
      exclude: /node_modules/
    })
    return config
  }
}
```

ちなみに`next.config.ts`はおそらくサポートしていないっぽい
https://github.com/vercel/next.js/issues/5318

# CSS 周り

Next には CSS in JS の styled-jsx がビルトインされているので、これを使うのであれば環境設定は不要

使い方は以下のように`<style jsx>`で囲む

```tsx:pages/index.tsx
import React from 'react'

const Home: React.FC = () => (
  <>
    <div>Hello Next.js!</div>
    <style jsx>{`
      div {
        color: blue;
      }
    `}</style>
  </>
)

export default Home
```

stylelint が使えたら良かったが、まだ検証段階だった
https://github.com/giuseppeg/styled-jsx-plugin-stylelint

# Storybook

```sh
npm i -D @storybook/react
```

プロジェクトルートに`.storybook/main.js`を作成し、以下を設定

```js:.storybook/main.js
const path = require('path')

module.exports = {
  stories: ['../stories/**/*.stories.tsx'],
  webpackFinal: async config => {
    config.resolve.alias['@'] = path.join(__dirname, '../')
    return config
  }
}
```

`stories/pages/index.stories.tsx`を作成

```tsx:stories/pages/index.stories.tsx
import App from '@/pages/index'

export default {
  title: 'pages'
}

export const Home = (): JSX.Element => <App />
```

以下のコマンドを追加

```json:package.json
{
  "scripts": {
    "sb": "start-storybook -p 9009"
  }
}
```

`npm run sb`で動作すれば成功

Storybook が TypeScript をサポートするようになったため、以前のように Storybook 用に webpack.config を用意する必要がなくなった
https://storybook.js.org/docs/react/configure/typescript

また、以下のコマンドで上記を書かなくても、全部セットアップしてくれる（不要なものも色々入るが）  
公式の最新の構成にできるので、いったんこちらを実行するのがいいかも

```sh
npx storybook init
```

# 単体テスト

テストフレームワークは軽く調べた感じ、以下で良さそう
1 年前調べた時と変わってなかった

- テストランナー
  - Jest
- テストフレームワーク
  - @testing-library/react
  - @testing-library/react-hooks

```sh
npm i -D jest @types/jest ts-jest @testing-library/react
```

以下のコマンドを追加

```json:package.json
{
  "scripts": {
    "test": "jest --watchAll"
  }
}
```

プロジェクトルートに`jest.config.js`を作成

```js:jest.config.js
module.exports = {
  testMatch: ['<rootDir>/test/unit/specs/**/*.spec.tsx'],
  preset: 'ts-jest',
  moduleNameMapper: { '^@/(.*)$': '<rootDir>/$1' },
  collectCoverage: true,
  collectCoverageFrom: ['<rootDir>/pages/**/*.tsx'],
  coverageDirectory: '<rootDir>/test/unit/coverage',
  coverageReporters: ['html', 'text-summary'],
  verbose: true,
  globals: { 'ts-jest': { tsconfig: { jsx: 'react' } } }
}
```

`tsconfig.json`の`jsx`オプションが`preserve`で ts-jest 使うとエラーになる
https://github.com/kulshekhar/ts-jest/issues/63
なので、jest の`globals`オプションで`jsx`を`react`に書き換えている

`test/unit/specs/pages/index.spec.tsx`を作成
（テスト内容は適当です）

```tsx:test/unit/specs/pages/index.spec.tsx
import { cleanup, render, RenderResult } from '@testing-library/react'
import React from 'react'
import Home from '@/pages/index'

let wrapper: RenderResult
beforeEach(() => (wrapper = render(<Home />)))
afterEach(cleanup)

test('Get by text', () => {
  expect(wrapper.getByText('Hello Next.js!'))
})
```

## ts-jest やめて、babel-jest にした

ts-jest を使っていると、テスト実行時に以下の警告が出る

```
Warning: Received `true` for a non-boolean attribute `jsx`.
```

https://github.com/vercel/styled-jsx#rendering-in-tests
この辺見てもよく分からなかったので、babel-jest を使うことにした

```sh
npm i -D babel-jest
npm remove ts-jest
```

プロジェクトルートに`.babelrc`を作成

```json:.babelrc
{
  "presets": ["next/babel"]
}
```

`jest.config.js`を以下に変更

```js:jest.config.js
module.exports = {
  testMatch: ['<rootDir>/test/unit/specs/**/*.spec.tsx'],
  transform: { '\\.tsx$': 'babel-jest' },
  moduleNameMapper: { '^@/(.*)$': '<rootDir>/$1' },
  collectCoverage: true,
  collectCoverageFrom: ['<rootDir>/pages/**/*.tsx'],
  coverageDirectory: '<rootDir>/test/unit/coverage',
  coverageReporters: ['html', 'text-summary'],
  verbose: true
}
```