React + TypeScript の boilerplate を作って環境構築周りを学ぶ

最近、環境構築周りを見直す機会が多く、また、曖昧な知識だった箇所が多かったため、この機会に boilerplate を作成しながら設定周りを学習してみようと思いました。
作成した boilerplate は、React + TypeScript で SPA を実装する想定で、 webpack + babel + tsc の構成です。
また、テストは Jest + Enzyme、 storybook を用いた最低限必要そうな環境を用意しています。
サンプルコードの実装は、その後の実装にバイアスがかかってしまいそうなので行っていません。

構築中に学んだ内容の解説も出来たらなと思います。 

※そんなん知ってる、コードを見せてって方は、以下にリポジトリへのリンクがあるのでそちらから覗いてみて下さい。

作成した boilerplate は [こちら](https://github.com/hey3/starter-for-react) です。

# 使用しているツール

* React
* TypeScript
* webpack
* Babel
* Styled-Components
* ESLint
* stylelint
* Jest
* Enzyme
* Prettier
* StoryBook
* husky
* lint-staged
* fixpack
* yarn

以下、各種設定ファイルの必要なものに関して、学んだ事・今回使用した設定の解説を行っていきます。
`React` , `TypeScript` は既に追加している前提で記述していきます。

# Babel を学ぶ

本 boilerplate では、 Babel を用いて TypeScript から JavaScript へのトランスパイルを行うため、そのための設定を行います。

## 使うものをインストールする

本 boilerpalte では、以下の package をインストールしています。

```shell
$ yarn add -D @babel/core babel-loader @babel/preset-env @babel/preset-react @babel/preset-typescript
$ yarn add core-js // core-js@3
```

※ `core-js` は最近色々ありましたが、今回は触れないでおきます。。

Babel とは、 JavaScript Compiler です。
Babel 自体は、`@babel/core` を install することで使用出来ます。
(Babel をコマンドで実行したい場合は、別途 `@babel/cli` が必要になります。)
本 boilerplate では、 webpack を用いて Babel を実行したいため、  `babel-loader` を使用します。
`@babel/preset-env` は、各種設定を参照し、指定されたターゲットに向けた JavaScript を生成してくれる preset です。
`@babel/preset-react` は、 jsx をサポートするための preset です。
`@babel/preset-typescript` は、 Babel 単体で TypeScript を JavaScript にトランスパイルできるようにする preset です。

## Babel の設定ファイルを用意する

Babel の設定ファイルは、 `.babelrc` または、 `babel.config.js` というファイル名が使用されます。
公式が JavaScript として読み込むことを推奨していることと、 JavaScript であれば、コメントや lint の対象にできるなどの利点があるので、本 boilerplate では、 `babel.config.js` で記述しています。

設定は、以下のようにしています。

```javascript:babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage', // コードから必要な Polyfill のみを読み込んでくれる
        corejs: 3, // core-js のバージョンを指定する
      },
    ],
    '@babel/preset-react',
    '@babel/preset-typescript',
  ],
}
```

## browserslist を設定する

`@babel/preset-env` は、変換対象とするブラウザを指定するために、 `browserslist` を参照します。
Babel の設定ファイルに `browserslist` の指定がある場合や、 `ignoreBrowserslistConfig` のフラグが無い場合に、 `package.json` や `.browserslistrc` が参照されます。
本 boilerplate では、 Babel 公式が推奨している `.browserslistrc` ファイルでの指定をしています。
(browserslist 公式は `package.json` への記述を推奨)

```:.browserslistrc
> 0.25%
not dead
```

`> 0.25%` は、ブラウザの世界シェアが 0.25% 以上を対象とすることを表しています。
`not dead` は、公式のサポートが２年間無いバージョンを除外することを表しています。

デフォルト値は、 `> 0.5%, last 2 versions, Firefox ESR, not dead` です。

指定した設定に該当するブラウザは、 `$ npx browserslist` で確認できます。


# tsc の設定

tsc は TyapeScript 純正のコンパイラです。
`noEmit` オプションを用いる事で型チェックのみを行うようにも使えます。

本 boilerplate では Babel でコンパイルするため、 tsc は型チェックのために使用しています。
以下のように設定しています。

```json:tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@components/*": ["./src/components/*"],
      "@pages/*": ["./src/pages/*"],
      "@themes/*": ["./src/themes/*"],
      "@images/*": ["./static/images/*"]
    },
    "target": "esnext",
    "moduleResolution": "node",
    "jsx": "react",
    "types": ["react", "node", "jest"],
    "typeRoots": ["./node_modules/@types", "./types"],
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "noFallthroughCasesInSwitch": false,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "strict": true
  }
}
```

`baseUrl` : 相対パス指定時の基準となるパス
`paths` : TypeScript が path alias を解決してくれる（別途 webpack や eslint、Jest などでも解決してあげる必要がある）
`target` : どの動作環境向けにトランスパイルするかを指定するパラメータです。 target の値によってデフォルト値が変わるものがあります（module や lib）。特に lib は target に指定したバージョンのライブラリを暗黙的に指定してくれるので、 esnext としておけばとりあえず新しい es 構文を使えます。（ `lib` はコンパイルに含めるライブラリの指定ができるパラメータです。今回は tsc によるコンパイルは行いませんが、構文チェックなどにも影響するため、新しい ES構文を使う場合は相応の設定が必要になります）
`moduleResolution` : モジュールの解決方法が変わります
`jsx` : jsx のサポートのため（モードによって出力が変わったりしますが今回は割愛します）
`esModuleInterop` : CommonJS モジュールも default import を許可できるようにすることが出来るパラメータ(ヘルパー関数が挿入される) 。Babel, webpack では commonJS のdefault import を許可しているため有効にします
`experimentalDecorators` : デコレータサポートの有効
`forceConsistentCasingInFileNames` : ファイル名の大文字小文字を区別して参照を行うようにすることができます
`noImplicitReturns` : 関数内で全てのコードパスが値を返さない場合にエラーを吐いてくれるように設定できます
`noUnusedLocals` : 未使用のローカル変数があるとエラーを吐いてくれるように設定できます
`noUnusedParameters` : 未使用のパラメータがあるとエラーを吐いてくれるように設定できます
`strict` : `--noImplicitAny` , `--noImplicitThis` , `--alwaysStrict` , `--strictBindCallApply` , `--strictNullChecks` , `--strictFunctionTypes` , `--strictPropertyInitialization` を一括して有効に出来ます（個々の説明は割愛します）


# Lint の設定

Lint はソースコードの構文や品質を検査できるツールです。
本 boilerplate では、 TypeScript の検査に ESLint, style の検査に styleLint を用いています。

## ESLint
ESLint の設定は外だしして[別のリポジトリ](https://github.com/hey3/eslint-config)で管理しています。
本 boilerplate では、このリポジトリをインストールして、随時必要な設定を上書きして使用する想定で作成しました。
`@hey3/eslint-config` は、 TypeScript と React 用の ESLint の設定をプラスしています。
（現状上記 eslint-config を使うには `eslint prettier typescript react` のインストールが必要です。 ※ pretter が必要な理由は、 ESLint と pretter を共存させるための設定を含んでいるためです。いずれうまく直したい。。）

公式推奨(`eslint:recommended` など)の設定でも良さそう

### 使うものをインストールする

ESLint の設定を外だししているので、本 boilerplate では以下のものをインストールします。

```shell
$ yarn add -D eslint @hey3/eslint-config # prettier typescript react
```

### ESLint の設定ファイルを用意する

設定は `.eslintrc.js` に記述していきます。
外だしした設定を`extends` で指定する事で使用することができます。
また、本 boilerplate では、path alias を使用するため、その設定をプロジェクト側で指定しています。

```javascript:.eslintrc.js
const path = require('path')

module.exports = {
  extends: ['@hey3/eslint-config'],
  settings: {
    'import/resolver': {
      node: {
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      },
      webpack: { config: path.join(__dirname, './webpack.config.js') },
    },
  },
}
```

## StyleLint

### 使うものをインストールする

```shell
$ yarn add -D stylelint stylelint-processor-styled-components \
stylelint-config-standard stylelint-config-styled-components stylelint-config-prettier
```

本 boilerplate では、 `styled-components` を採用しているため、プロセッサに `stylelint-processor-styled-components` を追加します。
また、既存の構成を拡張するために `stylelint-config-standard` , `stylelint-config-styled-components` , `stylelint-config-prettier` を追加します。
`stylelint-config-prettier` は prettier との競合を防ぐために入れています。

### stylelint の設定ファイルを用意する

設定は `.stylelintrc` に記述していきます。

```:.stylelintrc
{
  "processors": ["stylelint-processor-styled-components"],
  "extends": [
    "stylelint-config-standard",
    "stylelint-config-styled-components",
    "stylelint-config-prettier"
  ],
  "rules": {
    "no-descending-specificity": null,
    "value-keyword-case": ["lower", {
      "ignoreKeywords": ["dummyValue"]
    }]
  },
  "syntax": "scss"
}
```

rules の `no-descending-specificity` は、セレクタの詳細度が高いものを後に書かせるためのルールですが、 scss 構文を用いる場合に引っかかる場合があるので切っています。
`value-keyword-case` に関しては、今回デフォルト CSS のリセットに `styled-reset` を用いており、その際にエラーになるので記述しています。（特異なので後でどうにかしたい。。）


# Webpack の設定

## 使うものをインストールする

本 boilerplate では、以下の package をインストールしています。

```shell
$ yarn add -D webpack webpack-cli webpack-dev-server html-webpack-plugin dotenv-webpack
```

設定ファイルは以下のように設定しています。

```javascript:webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
const webpack = require('webpack')
const Dotenv = require('dotenv-webpack')
const env = process.env.BUILD_MODE

const defineEnv = new webpack.DefinePlugin({
  'process.env': {
    REACT_APP_ENV: JSON.stringify(process.env.REACT_APP_ENV),
  },
})

module.exports = {
  mode: env || 'development',
  entry: './src/main.tsx',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'build.js',
  },
  module: {
    rules: [
      {
        test: /\.(js|ts|tsx)$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: 'file-loader',
      },
    ],
  },
  resolve: {
    alias: {
      '@components': path.resolve(__dirname, './src/components'),
      '@pages': path.resolve(__dirname, './src/pages'),
      '@themes': path.resolve(__dirname, './src/themes'),
      '@images': path.resolve(__dirname, './static/images'),
    },
    extensions: ['.js', '.ts', '.tsx'],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
    }),
    defineEnv,
    new Dotenv({
      path: `./src/envs/.env.${process.env.REACT_APP_ENV}`,
    }),
  ],
  devtool: env === 'production' ? false : 'inline-source-map',
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    historyApiFallback: true,
  },
}
```

今回 webpack は `BUILD_MODE` と `REACT_APP_ENV` の２つの環境変数を受け取って実行する想定で作成しています。
`BUILD_MODE` は、 production と development が入り、 主に mode の切り替えに用います。
`REACT_APP_ENV` は、 local, development, staging, production が入り、各実行環境毎の設定の切り替えの用途に用いる想定です。
webpack では、様々なことが行えるのですが、本 boilerplate では、ある程度の設定のみで最小の構成で作成しています。

# テストライブラリの設定

本 boilerplate では、 Jest + Enzyme でのテスト環境を用意しました。

## 使うものをインストールする

```shell
$ yarn add -D jest @types/jest ts-jest \
enzyme @types/enzyme enzyme-adapter-react-16 @types/enzyme-adapter-react-16 \
react-test-renderer @types/react-test-renderer
```

今回は Jest + Enzyme の構成を採用しているので、それぞれで必要なものをインストールしています。
また、 snapshot test を行うために `react-test-renderer` もインストールしています。
`enzyme-adapter-react-16` は、 React と Enzyme を連携するために必要です。
今回は React v16 環境なので `enzyme-adapter-react-16` を使います。

## 設定ファイルを用意する

まず、 Enzyme と React を連携させるための設定ファイルを用意します。
今回は `adapter` の設定のみ記述しています。

```ts:src/enzymeConfig.ts
import { configure } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'

configure({ adapter: new Adapter() })
```

そして、 Jest の設定ファイルを `jest.config.js` に記述します。

```js:jest.config.js
module.exports = {
  clearMocks: true,
  coverageDirectory: 'coverage',
  moduleNameMapper: {
    '^@(components|pages|themes)/(.+)': '<rootDir>/src/$1/$2',
    '^@images/(.+)': '<orotDr>/static/images/$1',
  },
  roots: ['<rootDir>/src'],
  setupFiles: ['<rootDir>/src/enzymeConfig.ts'],
  transform: {
    '^.+\\.jsx?$': 'babel-jest',
    '^.+\\.tsx?$': 'ts-jest',
  },
}
```

`clearMocks` : テスト間で自動的に mock をクリアするかどうかの設定です。毎回 `mockClear()` しなくて済むようになります。
`coverageDirectory` : カバレッジ情報を出力するディレクトリを指定します。本 boilerplate では `<root>/coverage` に保存します。
`moduleNameMapper` : Jest で alias を使ったモジュール参照をするための設定です。 `tsconfig` などと同じ設定にします。
`roots` : Jest がテストファイルを探索するディレクトリを指定できます。
`setupFiles` : 各テスト前にテスト環境の構成やセットアップを設定するファイルを指定できます。今回は `enzymeConfig.ts` を設定しています。
`transform` : 正規表現で transformer へのパスマップを指定できます。 transformer は各ソースファイルを同期処理で変換するモジュールです。今回は Babel を使っていますが、babel-jest のみだと[型チェックが出来ません](https://jestjs.io/docs/ja/getting-started#typescript-%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B)。 Jest でも型チェックをしたいため ts-jest も用いています。

### テストのサンプル

リポジトリにコードは無いですが、簡単な実行確認した例を載せておきます。

```tsx:src/components/Input/Input.test.tsx
describe('Input', () => {
  // react component test
  it('should be type passed from props', () => {
    const props: React.ComponentProps<typeof Input> = {
      type: 'text',
    }

    const wrapper = mount(
      <ThemeProvider>
        <Input {...props} />
      </ThemeProvider>
    )
    expect(wrapper.getDOMNode().getAttribute('type')).toBe(props.type)
  })

  // snapshot test
  it('renders correctly', () => {
    const props: React.ComponentProps<typeof Input> = {
      type: 'text',
    }
    const tree = renderer
      .create(
        <ThemeProvider>
          <Input {...props} />
        </ThemeProvider>
      )
      .toJSON()
    expect(tree).toMatchSnapshot()
  })
})
```

# storybook の設定

## 使うものをインストールする

```shell
$ yarn add -D @storybook/react @storybook/addons \
@storybook/addon-a11y @storybook/addon-actions @storybook/addon-knobs @storybook/addon-viewport \
@storybook/theming storybook-readme
```

`@storybook/react` をインストールして設定ファイルを少し書けば、とりあえず React 環境で `storybook` は使えます。
アドオンをいくつか追加することで開発に便利な機能が使えるので、本 boilerplate ではいくつかアドオンを入れています。
`a11y` : コンポーネントのWEBアクセシビリティのチェックを行ってくれます。
`actions` : storybook 上で UIアクションに対してログを吐いてくれます。 `onClick` などのイベントのシミュレートが出来るので便利です。
`knobs` : コンポーネントの props を動的に変更することが可能になります。 props により変化する UI の確認がしやすくなります。
`viewports` : storybook 上でデバイス毎の UI を確認できるようになります。レスポンシブデザインの確認などがやりやすくなります。
`theming` : storybook 自体のカスタマイズが可能になります。
`readme` : コンポーネントに README を追加出来るようになります。コンポーネントの仕様を書いたりするのに便利です。

## 設定ファイルを用意する

storybook はデフォルトの構成として、 Babel, webpack が使用されています。
Babel に関しては、プロジェクトの `.babelrc` を読み込んでくれます。(`babel.config.js` も急ぎで[対応](https://github.com/storybookjs/storybook/blob/v5.3.18/lib/core/src/server/utils/load-custom-babel-config.js#L88)されています)
webpack に関しても、デフォルト設定を使うか独自の設定を使うかを選べます。
今回、プロジェクトでも webpack を使用しているので、その設定を `.storybook/main.js` に記述して storybook でも利用できるようにします。

```js:.storybook/mainjs
const custom = require('../webpack.config.js')

module.exports = {
  webpackFinal: (config) => {
    return {
      ...config,
      module: { ...config.module, rules: custom.module.rules },
      resolve: custom.resolve,
    }
  },
}
```

`webpackFinal` を使うことでデフォルトの設定をカスタマイズすることができます。
今回は、プロジェクトの設定から `module.rules` と `resolve` をデフォルトにマージしています。
利用時の状況により変更が必要になるかと思います。

`.storybook/config.js` は以下のように設定しています。

```js:.storybook/config.js
import React from 'react'
import { configure, addDecorator, addParameters } from '@storybook/react'
import { create } from '@storybook/theming'
import { withA11y } from '@storybook/addon-a11y'
import { withKnobs } from '@storybook/addon-knobs'
import { addReadme } from 'storybook-readme'
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'

import ThemeProvider from '../src/themes/ThemeProvider'

const req = require.context('../src/components', true, /.stories.tsx$/)

function loadStories() {
  req.keys().forEach((filename) => req(filename))
}

addParameters({
  options: {
    theme: create({
      base: 'light',
      brandTitle: 'hey3 starter-for-react storybook',
      brandUrl: 'https://github.com/hey3/starter-for-react',
    }),
  },
})
addParameters({ viewport: { viewports: INITIAL_VIEWPORTS } })

addDecorator(withA11y)
addDecorator(withKnobs)
addDecorator(addReadme)
addDecorator((Story) => (
  <ThemeProvider>
    <Story />
  </ThemeProvider>
))

configure(loadStories, module)
```

`loadStories` で `*.stories.tsx` の読み込み、 `addParametes` , `addDecorater` で storybook のカスタマイズ（addon の適用やストーリーの wrap など）を行っています。

addon の読み込みは、 `.storybook/addons.js` に記述していきます。

```js:.storybook/addons.js
import '@storybook/addon-a11y/register'
import '@storybook/addon-actions/register'
import '@storybook/addon-knobs/register'
import '@storybook/addon-viewport/register'
import 'storybook-readme/register'
```

# その他の設定

## fixpack を使って package.json を整える

`fixpack` を使うことで、 package.json に規則性を持たせることが出来ます。
必須項目の設定や、項目がない場合に警告を表示することも出来ます。
設定ファイルは省略します。

## ThemeProvider の設定

本 boilerplate では、 Context にテーマを持たせています。
カラーテーマ、サイズテーマ、フレームテーマを持たせており、ダークモードの切り替えも用意しています。
コードは `src/theme` 以下に記述しています。
この辺コードが怪しいので、直していきたい気持ちです。。
詳細は省略します。

## husky の設定

本 boilerplate では、 `pre-commit` にフックして以下の処理を行います。
主に対応するファイル毎に linter をかけています。

```json
"*.{ts,tsx,js}": [
  "eslint --fix"
],
"*.{ts,tsx,js,json}": [
  "prettier --write"
],
"*package.json": [
  "fixpack"
]
```

## CircleCI の設定

本 boilerplate は、 CircleCI を用いて CI を回しています。
内容としては、基本的な内容の workflows を設定しました。

* node_modules の restore
* yarn install 
* node_modules の save

を各 job の前処理として実行し、

* test job の実行
* master branch では、 prod 環境の build(deploy)
* master branch 以外では、 dev 環境の　build(deploy)

を行うようにしています。
※deploy は job のみ用意し、 workflow には組み込んでいません

# 最後に

長々と記述してきましたが、つらつらと設定内容を記述しているだけになってしまったような。。
まだ学習中なのもあり、踏み込んだ内容は記述できませんでしたが、今回やった事で大分基礎の知識がついたかなと思いました。

ありがとうございます。