create-react-app with TypeScript のwebpack.config.jsを読む

webpackは以前からずっと使っていますが、業務で使用する機会が無く、個人プロジェクトで導入するのみという状況でした。
こうなるとwebpack.config.jsを触るのは自分だけなので、ネットから寄せ集めた設定を組み合わせて雰囲気で動かしていました。
このままでは他人から渡されたり自動生成されたwebpack.config.jsをメンテする際に苦労しそうなので、`create-react-app`のwebpack.config.jsを読んで、いろいろメモを書いていこうと思います。
トランスコンパイラには、最近勉強しているTypeScriptを使っています。

create-react-app: v1.5.2

# 準備

* アプリケーション生成

```
$ create-react-app --scripts-version=react-scripts-ts sample-app
```

* eject

```
$ cd sample-app
$ yarn eject
```

`config`配下に生成された以下のファイルが、webpack関連の設定ファイルです。

```
config
├── paths.js
├── webpack.config.dev.js
├── webpack.config.prod.js
└── webpackDevServer.config.js
```

# ファイル構成

* paths.js
    * エントリポイントやバンドルファイル出力先フォルダなどのパスを一括管理しています。

* webpack.config.dev.js
    * 開発時に使用する設定

* webpack.config.prod.js
    * リリース用ビルド時に使用する設定

* webpackDevServer.config.js
    * webpack-dev-serverの設定


# webpack.config.dev.js

今回は開発時の設定`webpack.config.dev.js`を読んでいきます。
上部の`require`部分は飛ばして、module.exportsの中身を見ていきます。


## devtool
```javascript:config/webpack.config.dev.js
  devtool: 'cheap-module-source-map',
```
設定値によって、ビルド/差分ビルドの速度と出力されるソースマップの質が変わります。
指定可能な設定は[公式ドキュメント](https://webpack.js.org/configuration/devtool/)にあります。

`cheap-module-source-map`の場合、ソースマップにはコンパイル前のオリジナルコードが表示されますが、ビルド速度は普通、差分ビルドは少し遅めのようです。
また、表のquality欄に(lines only)と記載されているものは、行単位のソースマップしか出力できず、周辺の変数の情報などを取得できません。
例として、`cheap-module-source-map`と`inline-source-map`を比べてみます。

ブレークポイントで実行を中断したとき、`inline-source-map`ではimportしている`logo`の中身が見えますが……
![image.png](https://qiita-image-store.s3.amazonaws.com/0/29000/2f862c77-bd3c-53ef-b7df-030c0bccabcf.png)

`cheap-module-source-map`では、not definedと表示されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/29000/14f8cd82-32d1-f210-fd6e-12438919138b.png)

`create-react-app`のデフォルト値は`cheap-module-source-map`ですが、デバッグするときは`inline-source-map`を使ったほうが捗りそうです。


## entry

```javascript:config/webpack.config.dev.js
  entry: [
    require.resolve('./polyfills'),
    // require.resolve('webpack-dev-server/client') + '?/',
    // require.resolve('webpack/hot/dev-server'),
    require.resolve('react-dev-utils/webpackHotDevClient'),
    paths.appIndexJs,
  ],
```

ビルドのエントリポイント。
中段でdevServer用のエントリポイントを指定していますが、デフォルトでは`webpack-dev-server`の代替として、[react-dev-utils](https://www.npmjs.com/package/react-dev-utils)のwebpackHotDevClientを使っています。
これを使うと、ビルドエラーの内容をブラウザ上に表示してくれます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/29000/8f02e017-75e7-a54e-f7ae-c6ce00c7440d.png)

この機能が不要な場合は、上2行のコメントを外せば通常の`webpack-dev-server`を使用できます。

```javascript:config/webpack.config.dev.js
  entry: [
    require.resolve('./polyfills'),
    require.resolve('webpack-dev-server/client') + '?/',
    require.resolve('webpack/hot/dev-server'),
    // require.resolve('react-dev-utils/webpackHotDevClient'),
    paths.appIndexJs,
  ],
```

新しくエントリポイントを追加するときは、ファイルの絶対パスか、**webpack.config.jsの場所から辿った**ファイルの場所を相対パスで指定します。

## output

バンドルしたファイルの出力ルール。

```javascript:config/webpack.config.dev.js
  output: {
    pathinfo: true,
    filename: 'static/js/bundle.js',
    chunkFilename: 'static/js/[name].chunk.js',
    publicPath: publicPath,
    devtoolModuleFilenameTemplate: info =>
      path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
  },
```

### output.pathinfo

require文に、実際のファイルの場所をコメント(/* ~~~ */)で付加します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/29000/af8c31a8-3328-9179-e818-dc792ddbfdcc.png)

### output.pathinfo

require文に、実際のファイルの場所をコメントで付加します。

### output.filename, output.chunkFilename

出力ファイル名。
エントリポイントを複数指定した場合、`output.chunkFilename`の規則に従ってそれぞれのファイルが出力されます。

### output.publicPath

出力ファイルを`<script>`タグなどで読み込む際の、ベースURL。
webpack.config.dev.jsでは `/` が指定されていて、出力ファイル名(output.filename)が `static/js/bundle.js` となっているため、読み込むときはそれらを繋いだ

```html
<script type="text/javascript" src="/static/js/bundle.js"></script>
```

となります。

### output.devtoolModuleFilenameTemplate

ソースマップの出力先(ドメイン、パス等)。
webpackのデフォルトでは `webpack://` ドメイン配下に出力されますが、 `create-react-app` ではバンドルファイルの出力先と同じ場所に出力しています。
また、絶対パスで出力することで、開発しているプロジェクトフォルダと同一の構成になるため、わかりやすいです。

開発ディレクトリ構成

```
├── config
│   ├── env.js
│   ├── jest
│   ├── paths.js
│   ├── polyfills.js
│   ├── webpack.config.dev.js
│   ├── webpack.config.prod.js
│   └── webpackDevServer.config.js
├── node_modules
└── src
     ├── App.css
     ├── App.test.tsx
     ├── App.tsx
     ├── index.css
     ├── index.tsx
     ├── logo.svg
     └── registerServiceWorker.ts

```

出力
![image.png](https://qiita-image-store.s3.amazonaws.com/0/29000/bf3c1fa2-9487-d5c3-b411-667f9b13ee05.png)

実際のディレクトリから、バンドルしたファイルがそのままの構成で `localhost:3000` 配下に出力されています。

## resolve

バンドル時のモジュールの依存解決に関する設定。

### resolve.modules

モジュールの探索場所。

```javascript:config/webpack.config.dev.js
  resolve: {
    modules: ['node_modules', paths.appNodeModules].concat(
      process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
    ),
    ......
  }
```

`create-react-app`では、`NODE_PATH`にディレクトリを指定して起動すると、そのディレクトリを探索場所に追加するよう設定されています。
例えば以下のようなフォルダ構成で App.js -> Sample.js をimportするとき、普通は相対パスを使って記述します。

```
src
├── App.js
└── components
     └── Sample.js
```

```javascript:App.js
import Sample from './components/Sample'
```

しかし、`NODE_PATH`に基準となるディレクトリ(今回は`src`)を指定すると、こう書けるようになります。

```javascript:App.js
import Sample from 'components/Sample'
```

`Sample.js`の場所が変わっても`src`フォルダから見たパスを指定すればimportできるので、「2つ上のディレクトリだから `'../../'` で……」等と考えなくてもよくなります。

**注意**

TypeScriptでは上記の方法は使えません。代わりに`tsconfig.json`内で`baseUrl`プロパティを指定することで、同様のimportが可能になります。

```json:tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "src",
  }
}
```

### resolve.extensions

import文で拡張子を省略したときに、ここで指定した拡張子を補ってファイルを探索してくれます。

```javascript:config/webpack.config.dev.js
  resolve: {
    ......
    extensions: [
      '.mjs',
      '.web.ts',
      '.ts',
      '.web.tsx',
      '.tsx',
      '.web.js',
      '.js',
      '.json',
      '.web.jsx',
      '.jsx',
    ],
    ......
  }
```

### resolve.alias

import文で使えるエイリアスを設定します。

```javascript:config/webpack.config.dev.js
  resolve: {
    ......
    alias: {
      'react-native': 'react-native-web',
    },
    ......
  }
```

`create-react-app`では、[React Native for Web](https://github.com/necolas/react-native-web)サポートのために`'react-native'`を`'react-native-web'`に置き換えています。
これにより、React Native for Webのコンポーネントを`import ... from 'react-native'`で読み込めます。

### resolve.plugins

プラグイン設定。

```javascript:config/webpack.config.dev.js
  resolve: {
    ......
    plugins: [
      new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
      new TsconfigPathsPlugin({ configFile: paths.appTsConfig }),
    ],
  },
```

#### ModuleScopePlugin

importするモジュールを配置する場所を`src`フォルダ配下に限定しています。
試しに`src`と同階層に`sample`フォルダを作ってimportしてみると、ちゃんとエラーになりました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29000/4a21723d-70b6-6dd4-f4cf-99fdbd7bb9b0.png)

#### TsconfigPathsPlugin

`tsconfig.json`の場所を指定します。


## module

バンドルのルール設定。

```javascript:config/webpack.config.dev.js
  module: {
    strictExportPresence: true,
    rules: [
      ......
    ],
  },
```

### module.strictExportPresence

モジュールのexport忘れをエラー扱いにします。
`false`にするとexport忘れがエラーにならないためビルド自体は通りますが、そのモジュールを他モジュールから読み込んだ時点でエラーが発生します。
`true`にしておくと、exportしていないモジュールを発見した時点でビルドがコケるので、すぐに発見できます。

### module.rules

バンドル対象ファイルと、対象ファイルに対して使用するLoaderの設定。
処理対象ファイルのルール(正規表現)と、適用するLoaderを指定します。

Loaderは配列で複数指定できます。その場合、指定した**逆順**にLoaderが適用されていくので注意が必要です。
参考: [Loader Definitions](https://survivejs.com/webpack/loading/loader-definitions/)

簡単に書くと、

```
rules: [
  { test: /\.js$/, use: 'a-loader' },
  { test: /\.js$/, use: 'b-loader' },
  { test: /\.js$/, use: 'c-loader' },
]
```

この場合、拡張子`.js`のファイルが `c-loader` > `b-loader` > `a-loader` の順に処理されていきます。
同じ拡張子を対象にする場合はまとめて書くこともでき、上の例をまとめると

```
rules: [
  { test: /\.js$/, use: ['a-loader', 'b-loader', 'c-loader'] },
]
```

こうなります。

また、`oneOf`という特殊な記述方法もあります。
`oneOf`内に記述されたルールは、処理するファイルがどれか1つのルールにマッチし処理された時点で、他のルールとの照合を中止します。
1つのファイルに対して`oneOf`内のLoaderが複数適用されることはありません。
注意すべきなのは、`oneOf`配列のルール照合は**記述順**に行われるということです。

ここで`create-react-app`のwebpack.config.jsを見てみます。
`rules`部分を、重要なオプション以外を無視して擬似コードにするとこうなります。

```javascript:config/webpack.config.js
rules: [
  {
    enforce: 'pre',
    test: JavaScript,
    use: 'source-map-laoder'
  },
  oneOf: [
    {
      test: Image,
      use: 'url-loader',
      options: {
        limit: 10000,
        name: 'static/media/[name].[hash:8].[ext]',
      },
    },
    {
      test: JavaScript,
      use: 'babel-loader'
    },
    {
      test: TypeScript,
      use: 'ts-loader'
    },
    {
      test: Stylesheet,
      use: [
        'style-loader',
        'css-loader',
        { 'postcss-loader', options: autoprefixer }
      ]
    },
    {
      exclude: [JavaScript, HTML, JSON],
      use: 'file-loader',
      options: {
        name: 'static/media/[name].[hash:8].[ext]',
      },
    },
  ],
]
```

このルール群をあえて文章にすると、

* 全てのJavaScriptファイルに対して、処理前に`source-map-loader`でソースマップを生成する
* 以降はファイルタイプによって処理を分ける(1ファイルに付きどれか1つのみ適用)
    * 画像ファイルは、`url-loader`でdata URI schemaに変換する
        * 画像サイズが10000Byte以上なら、変換はせずに指定フォルダにコピーする
    * JavaScriptファイルは、`babel-loader`でトランスコンパイルする
    * TypeScriptファイルは、`ts-loader`でトランスコンパイルする
    * CSSファイルは`postcss-loader`でベンダープレフィックスを付けて変換したあと、`css-loader`でimportを解決し、`style-loader`で`<style>`タグに埋め込む
    * 上記で処理されなかったJavaScript, HTML, JSON以外のファイルは、そのまま指定フォルダにコピーする

こんな感じでしょうか。

## plugins

使用するプラグイン設定。


```javascript:config/webpack.config.js
  plugins: [
    new InterpolateHtmlPlugin(env.raw),
    new HtmlWebpackPlugin({
      inject: true,
      template: paths.appHtml,
    }),
    new webpack.NamedModulesPlugin(),
    new webpack.DefinePlugin(env.stringified),
    new webpack.HotModuleReplacementPlugin(),
    new CaseSensitivePathsPlugin(),
    new WatchMissingNodeModulesPlugin(paths.appNodeModules),
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
    new ForkTsCheckerWebpackPlugin({
      async: false,
      watch: paths.appSrc,
      tsconfig: paths.appTsConfig,
      tslint: paths.appTsLint,
    }),
  ],
```

### InterpolateHtmlPlugin

後述の`HtmlWebpackPlugin`と併用し、`index.html`に値を埋め込むために使用。

`public/index.html`を見てみると、ところどころに`%PUBLIC_URL%`という文字列があります。
`InterpolateHtmlPlugin`にKeyと対応するValueを渡すと、`HtmlWebpackPlugin`でファイルを配信する際にValueを埋め込みます。

### HtmlWebpackPlugin

テンプレートのHTMLに、webpackでバンドルした出力ファイルを読み込むための`<script>`コードを埋め込みます。
いろいろな[オプション](https://github.com/jantimon/html-webpack-plugin#options)が指定できますが、`create-react-app`では埋め込み先のテンプレート(`template: paths.appHtml`)、埋め込みを許可するかどうか(`inject: true`)を指定しています。
ちなみに`inject`オプションはデフォルトで`true`なので、こちらは削除しても正常に動きます。

### webpack.NamedModulesPlugin

[公式](https://webpack.js.org/plugins/named-modules-plugin/)に「HMR時にモジュールの相対パスを表示するためのプラグイン」とありますが、正直どこに表示されているのかよく分からなかったです。

### webpack.DefinePlugin

指定した環境変数を、フロントでも使用可能にする。
`src/registerServiceWorker.js`で`NODE_ENV`を使うために導入されているようです。

### webpack.HotModuleReplacementPlugin

MHRを有効化します。

現時点ではCSSの変更にのみ対応しているようで、コンポーネント全体のHMRを有効化するには以下のような対応が必要です。
[create-react-appでHMR](https://qiita.com/uehaj/items/88ca745b3f8cff8dddd2)

※ 追記
TypeScriptで利用する場合は[@types/webpack-env](https://www.npmjs.com/package/@types/webpack-env)パッケージが必要です。

```zsh
$ yarn add -D @types/webpack-env
```

### CaseSensitivePathsPlugin

Windows環境では、コード内でファイルパスを記述するときに大文字小文字を意識する必要がありません。
例えば以下のファイルをimportするとき、

```
src/Components/Sample.js
```

このようにimportしても正常に動作します。

```
import Sample from './components/sample.js'
```

しかしLinuxやmacOS環境では、上記のimport文はエラーとなります。

`CaseSensitivePathsPlugin`を有効化すると、Windows環境でもこれがエラー扱いとなり、正確なパス記述を矯正することができます。

### WatchMissingNodeModulesPlugin

足りてないパッケージをrequireなりimportなりしたときに、webpackは当然エラーを吐きます。
その後`npm install`しても、通常はdevServerを一旦killして再起動しなければパッケージを認識してくれません。
`WatchMissingNodeModulesPlugin`を導入すると、パッケージを新しくインストールしたとき、devServerの再起動をしなくても自動で認識してくれます。

### webpack.IgnorePlugin

[Moment.js](https://momentjs.com/)を使う場合に有用なプラグイン。
Moment.jsをそのままバンドルすると、膨大な量のロケールファイルが含まれ、ファイルサイズがとても大きくなってしまいます。
`IgnorePlugin`の設定で、使わないロケールファイルを無視して、必要なものだけバンドルすることができます。

参考: [webpack で moment.js の無駄なロケールファイルを除去する](https://qiita.com/jimbo/items/95da1c223ad25a33ed16)

### ForkTsCheckerWebpackPlugin

TypeScriptの型チェック用プラグイン。
型チェック自体は`ts-loader`のオプションでも指定できますが、トランスパイル時に型チェックも同時に行うと、ビルドに時間がかかってしまいます。
`ForkTsCheckerWebpackPlugin`を導入すると、`ts-loader`ではトランスパイルのみ行い、型チェックは別プロセスで実行することができ、ビルド速度が改善します。


## node

```javascript:config/webpack.config.dev.js
  node: {
    dgram: 'empty',
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
    child_process: 'empty',
  },
```

Node.js標準モジュールのpolyfillやmockを差し込む設定。
Node.js環境用に書かれたプログラムをブラウザなど別環境で動かしたいときに使えます。


## performance

```javascript:config/webpack.config.dev.js
  performance: {
    hints: false,
  },
```

実行時のパフォーマンスを上げるためのアドバイスを出してくれる機能ですが、`create-react-app`はそのあたりの調整に関心を持っていないので、機能をオフにしているようです。

`hints`プロパティを`'warning'`か`'error'`にすると機能が有効になりますが、バンドルファイルを250kB以下におさめる等、なかなか無茶なアドバイスが表示されてうっとおしいので、本当に必要な場合だけ有効化すればよいと思います。

```:表示例
Compiled with warnings.

asset size limit: The following asset(s) exceed the recommended size limit (250 kB).
This can impact web performance.
Assets:
  static/js/bundle.js (1.66 MB)

entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (250 kB). This can impact web performance.
Entrypoints:
  main (1.66 MB)
      static/js/bundle.js

webpack performance recommendations:
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit https://webpack.js.org/guides/code-splitting/

Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.
```

# まとめ

いい勉強になったとはいえ、まだ他にも様々な機能があるので、全てを覚えておくのはかなり難しそうです。
[公式](https://webpack.js.org/configuration/)をある程度眺めて、機能の概要だけでも頭に入れておくと良さそうです。

また、`create-react-app`のwebpackはv3.x.xですが、本家のwebpackでは最近[v4がリリース](https://github.com/webpack/webpack/releases/tag/v4.0.0)されました。
v4系に未対応のプラグインもあるようなので、アップデートは慎重に行ったほうがよいですね。