React + TypeScript 環境構築 + ビルド環境を Flavor で切り替える (not create-react-app)

# はじめに

この記事は `React` + `TypeScript` な環境を作成し、`Flavor` を設定してビルド環境( `Development`, `Staging`, `Production` )を切り替えられるような `Project` を作成することを目的としています。

# React + TypeScript 環境の構築

## 初期設定
サックっと初期設定してしまいましょう。

```
$ mkdir react-flavor-example
$ cd react-flavor-example
$ npm init
```
色々聞かれるので、適当に質問に答えましょう。

```
package name: (app) react-flavor-example
version: (1.0.0)
description:
entry point: (main.tsx) webpack.config.js
test command:
git repository:
keywords:
author: yukitaka13-1110
license: (ISC)
About to write to /app/package.json:
```

## Dependency の追加

### React 系
```
$ npm i -D react react-dom @types/react @types/react-dom
```

### Webpack 系
```
$ npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin
```

### TypeScript 系
```
$ npm i -D typescript ts-loader
```

## `tsconfig.json` の作成

```json:tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true,
    "module": "commonjs",
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "experimentalDecorators": true,
    "target": "es5",
    "jsx": "react",
    "lib": [
      "dom",
      "es6"
    ]
  },
  "include": [
    "src"
  ],
  "compileOnSave": true
}
```

## `webpack.config.js` の作成
```js:webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',

  entry: './src/main.tsx',
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
    }),
  ],
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
      },
    ],
  },
  resolve: {
    extensions: [
      '.ts', '.js', 'tsx',
    ],
  },
};

```

## `index.html` の作成
`public/` 以下に `index.html` を作成します。

```html:public/index.html
<html lang="ja">
  <body>
    <div id="root" />
  </body>
</html>
```

## `main.tsx` の作成
`src/` 以下に `main.tsx` を作成します。

```tsx:src/main.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";

const App: React.FC = () => {
  return <div>React Flavor Example</div>
};

ReactDOM.render(<App />, document.getElementById("root"));
```

この時点でディレクトリ構造が以下のようになっていればOKです。

```
$ tree ./ -L 2

./
├── node_modules
├── package-lock.json
├── package.json
├── public
│   └── index.html
├── src
│   └── main.tsx
├── tsconfig.json
└── webpack.config.js
```

`package.json` の `scripts` にコマンドを追加します。

```json:package.json
{
  ...
  "scripts": {
    "start": "webpack-dev-server",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  ...
}
```

`npm run start` で起動して `http://localhost:8080/` へアクセスして `React Flavor Example` という文字が表示されることを確認

<img width="700" alt="スクリーンショット 2020-07-14 01.19.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166345/2815e80b-2284-1b59-3047-cde5e295d2a9.png">


# Flavor の設定

ここからが本題です。
アプリで利用する環境変数を切り替えたいということは良くあることだと思います。
`node.js` では `process.env.NODE_ENV` という環境変数を利用することでアプリで利用する環境変数を切り替えることができるようです。
しかし、`process.env.NODE_ENV` が `staging` を受け取ることができません。型定義を見る感じは受け取れそうですが、どうやら`development` | `production` | `test` 以外は受け付けないようにないっているみたい...。（良く分かっていない）

```ts:@types/node
interface ProcessEnv {
    [key: string]: string | undefined;
}
```

そもそも型定義も貧弱なのでそこも含めて解決します。

## `process.env` の型を上書きする

`process.env` の型を上書きするには、プロジェクト内のどこかに `global.d.ts` ファイルを作成し、以下のように書くことで可能です。

```ts:src/types/global/global.d.ts
/// <reference types="node" />

declare namespace NodeJS {
  interface ProcessEnv {
    readonly NODE_ENV: 'development' | 'production' | 'test';
    readonly FLAVOR: 'development' | 'staging' | 'production';
  }
}
```

`NODE_ENV` は触っちゃいけない気がしたので以下のように `FLAVOR` を生やしてみました。

その後、`package.json` で `FLAVOR` を渡す `scripts` を追加します。

```diff:package.json
{
  "scripts": {
    "start": "webpack-dev-server",
+   "dev": "FLAVOR=\"development\" webpack-dev-server",
+   "stg": "FLAVOR=\"staging\" webpack-dev-server",
+   "prod": "FLAVOR=\"production\" webpack-dev-server",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
}
```

渡しただけではアプリ側では受け取れないので、`webpack.config.js` で工夫します。

```diff:webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const webpack = require('webpack');
+ const flavor = process.env.FLAVOR || 'development';

module.exports = {
  mode: 'development',

  entry: './src/main.tsx',
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
    }),
+   new webpack.DefinePlugin({
+     'process.env.FLAVOR': JSON.stringify(flavor),
+   })
  ],
}
```

`main.tsx` に `console.log()` を追加してみましょう。
先程、型定義を上書きしたので補完が効くようになっています。

<img width="779" alt="スクリーンショット 2020-07-14 01.56.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166345/7ed1f72b-d884-2984-dbbf-4ece5bef0f06.png">

試しに `npm run stg` を実行してみましょう。
ちゃんと値を受け取れています。

<img width="700" alt="スクリーンショット 2020-07-14 01.57.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166345/bae45f37-3281-9811-5dae-bc5c6b234bfa.png">

後は適当に環境毎に変数を定義して `process.env.FLAVOR` を使って分岐するだけです。
例えば以下のような `config` ディレクトリを用意しておけば良いでしょう。

```
./src/config
├── development
│   └── index.ts
├── index.ts
├── production
│   └── index.ts
└── staging
    └── index.ts
```

```ts:staging/index.ts
import { EnvVariables } from '../';

const stgVariables: EnvVariables = {
  HOGE: 'STAGING_HOGE',
  FUGA: 'STAGING_FUGA',
};

export default stgVariables;
```

```ts:index.ts
import devVariables from './development';
import stgVariables from './staging';
import prodVariables from './production';

export type EnvVariables = {
  HOGE: string;
  FUGA: string;
};

const envVariables = (): EnvVariables => {
  if (process.env.FLAVOR === 'production') {
    return prodVariables;
  }
  if (process.env.FLAVOR === 'staging') {
    return stgVariables;
  }
  return devVariables;
};

export default envVariables();
```

```tsx:src/main.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import envVariables from './config' // Add import;

const App: React.FC = () => {
  return (
    <div>
      <div>React Flavor Example</div>
      <div>{envVariables.HOGE}</div>
      <div>{envVariables.FUGA}</div>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
```

`npm run stg` を実行して `http://localhost:8080/` にアクセスします。

<img width="600" alt="スクリーンショット 2020-07-14 02.22.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166345/577e7775-95b4-02c2-67b7-6e6824632517.png">

ちゃんと値を表示することができました。


# 終わりに
今回使用したコードはこちらに載せています。
https://github.com/yukitaka13-1110/react-flavor-example