webpack.config の黒魔術をとく

## はじめに

Node.js向けに書かれたJavaScriptのプログラムをブラウザ上で動作させるには `require()` によるモジュールの参照を解決した単一のJavaScriptファイルに変換する必要があります[^1]。
少し前まではこの作業には [Browserify](http://browserify.org/) が使われることが多かったようですが、現在は「画像もCSSも何でも1つにします」が売りの [webpack](https://webpack.js.org/) 一色になっている感じです。ですが、この webpack、とってもわかりにくいのです。設定ファイル `webpack.config.js` を見たときに私は TeX や sendmail の悪夢を思い出しました[^2]。この黒魔術[^3]を読み解いてみましょう。

## webpack の仕事

画像とかCSSとかのことはおいておくと、webpackの仕事は以下の3つになります。

 1. 複数の`.js`ファイルの依存関係を解決し、1つにバンドルする
    これが本来業務です。

 2. `.js`ファイルを読み込むときに変換する
    ES6で書かれたスクリプトを Babel でES5に変換したりする部分です。他のパッケージに丸投げですね。

 3. 圧縮したり、ソースマップを出力したりする
    追加の仕事ですが、これもサポートしないとと使ってもらえないですからね。

webpack への仕事の指示は2つの方法で与えることができます。

 1. コマンドラインオプション
    コマンドを起動するたびに変えられるので、やったりやらなかったりする仕事を指示するのに向いています。

 2. 設定ファイル webpack.config.js
    必ず行うルーチンワークを指示します。細かい指示が可能です。

webpack の分かりづらさは、設定ファイル `webpack.config.js` で使われる語彙の独特さに原因があると思います。それぞれの仕事の設定方法を見ていきましょう。

### バンドル処理

webpack は以下の3つの単位でファイルをまとめることができます。

 1. アプリケーション全体で1つのファイルにまとめる
 2. ページごとに1つのファイルにまとめる
 3. 互いに依存しない複数のファイルを1つにまとめる

`webpack.config.js` では `entry` と `output` で指示します。コマンドラインでも指定できるようですが、毎回同じ指定になるので`webpack.config.js`に書くべきでしょう。

#### 1. アプリケーション全体で1つのファイルにまとめる

webpack が元々想定していたと思われるまとめ方です。入力にはそのページの「メイン」となる`.js`ファイルを指定します。webpack はそこから `require` を芋づる式にたどってファイルをまとめます。`index.js`からたどって`bundle.js`にまとめる場合、以下の設定になります。

```webpack.config.js
  entry: "index.js",
  output: { path: __dirname, filename: "bundle.js" }
```

input/output ではなく、**entry**/output です。これはいいとしても、なぜ output だけ絶対パスなのか、なぜ output は path と filename に分かれているのか、謎が多いです。

#### 2. ページごとに1つのファイルにまとめる

しかし、普通のWebアプリには複数のページがあり、そのそれぞれに「メイン」があるのではないでしょうか。ページごとのメイン `page1.js`、`page2.js`、`page3.js` を `page1.bundle.js`、`page2.bundle.js`、`page3.bundle.js`にまとめる場合、以下の設定になります。

```webpack.config.js
  entry: {
    page1: "page1.js",
    page2: "page2.js",
    page3: "page3.js",
  },
  output: { path: __dirname, filename: "[name].bundle.js" }
```

#### 3. 互いに依存しない複数のファイルを1つにまとめる

まれに互いに依存しない複数のファイルをまとめることが必要になる場合があります[^4]。
`file1.js`、`file2.js`、`file3.js`を単一の`bundle.js`にまとめる場合、以下の設定になります。

```webpack.config.js
  entry: [ "file1.js", "file2.js", "file3.js" ],
  output: { path: __dirname, filename: "bundle.js" }
```

### 変換処理

各々の入力ファイルを読出しバンドル処理に渡す前に変換を行います。適切にJavaScriptに変換できるのであれば、CSSや画像ファイルもソースに指定できます。この処理を行うモジュールのことをwebpackではloaderと呼んでいます。名前がわかりにくいですね。
`.js`を Babel におまかせで変換する場合、Babelの提供するwebpack向けのインタフェースbabel-loaderを以下のように設定します。

```webpack.config.js
  module: {
    rules: [
      { test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader' }
    ]
  },
```
loaderなのになぜか`module`という項目に設定するところがさらにわかりにくいです。loaderはUnixのパイプのように複数つなげて適用することができるので、どのファイルにどのloader **群**を設定するのかという設定方法になります。これを `rules` に1つ1つ設定します。
ファイルの特定方法にはいろいろあってまた複雑なのですが、ここでは `test` と `exclude` を使っています。`test` はloaderを適用すべきファイルの正規表現ですが、名前から全く想像できません。ひどい命名センスですね。
`loader` は実際に前処理を行うJavaScriptのモジュール名です。おそらくは`require`でロードしますのでnpmのモジュール名と一致させればいいです。前述したようにloaderは複数適用できます。その場合は`loader`の箇所を`use`にして配列で指定するようです。しかし use って何だ？いい加減にしてくれ！[^5]

### 出力処理

おそらくはバンドル処理が終わって1つになったファイルに対して処理します。圧縮処理はpluginが担当し、ソースマップ出力はdevtoolが担当するようです。2つの概念の違いが判らないですが、判らなくてもたぶん大丈夫です。
出力処理はデバッグ時とリリース時で違うと思うので、コマンドラインから指定した方がいいです。`package.json` の `scripts` に以下のように設定します。

```package.json
  "scripts": {
    "build": "webpack --devtool inline-source-map",
    "release": "webpack -p"
  }
```
`npm run build` でデバッグ用のソースマップつきの出力が得られます。`npm run release` だとソースマップなしで圧縮します。コマンドラインからだと簡単ですね！
細かい話をすると、plugin とは特定の出力機能を担当する webpack のオブジェクトのようですが、`-p` で webpack おすすめの圧縮方法が使われるのでこちらの設定方法の方が簡単です[^6]。devtool は処理をスイッチさせるための単なる名称のようです。機能の指定方法が、モジュール名(loader)だったり、インスタンスの参照(plugin)だったり、スイッチの名称(devtool)だったり不統一にもほどがあります！

## おわりに

とりあえず最低限の仕事をするための`webpack.config.js`を解読しました。webpackのサイトにはちゃんと[マニュアル](https://webpack.js.org/configuration/)もあるので読んでみるとよいでしょう。ですが黒魔術なので分かりにくいですよ。Node界に登場したバッドノウハウですね！


[^1]: 大域変数を使えば1つにする必要はないのだが、Node.jsが流行る以前から`.js`ファイルを1つにまとめて圧縮することが流行っていたのでこの流れと思われる
[^2]: そもそもこのファイル、拡張子が`.js`であることから分かるように「プログラム」です。webpackがプログラムの一部としてロードして使用します
[^3]: Browserifyの方がサイトデザインは魔術っぽいのにね
[^4]: babel-polyfillを使う場合などがこれにあたります
[^5]: 私が設計するなら`input_filter: [ { match: /\.js$/, exclue: /node_modules/, modules: [ 'babel-loader' ] } ]`と設定するようにしますけどね。
[^6]: `webpack.conf.js`内でpuluginを指定するときはその場でnewを使ってインスタンスを生成していました。もう本当に黒魔術です。