webpack 2 + babel 7 で動的なパスの require を使うとビルドに失敗する話

Airシリーズのフロントエンジニア日野澤 (@kt3k) です。

webpack でファイルを require する場合、通常は以下のように文字列リテラルでファイルパスを指定すると思います。

1
const foo = require('./path/to/foo')

しかし、実は webpack では以下のように require するパスに変数が入っていても読み込むことが出来ます。

1
2
const filename = 'foo'
const foo = require('./path/to/' + filename)

この場合、 ‘./path/to/’ ディレクトリ内の全てのファイルのマッピングが自動生成され、上記は以下のような呼び出しに変換されます

1
const foo = require(40)[filename]

ここで、 require(40) 部分は ./path/to/ 内の全てのファイルを値としてもつオブジェクトになっており、そこから [filename] と key で取り出すことで、動的パスの require を解決することが出来ます。

Babel を使うとどうなるか

require する動的ファイルパスをテンプレートリテラルで記述していた場合どうなるでしょうか?

1
const foo = require(`./path/to/${filename}`)

これは、 Babel (v6) で変換すると、以下のようになります。

1
const foo = require('./path/to/' + filename)

したがって、上と同じルールが適用されて、’./path/to/‘ 全体がバンドルに含まれて、動的パスの require が成功します。

ところで、Babel 7 ではテンプレートリテラルのトランスパイルの仕方が変更されています。Babel 7 では上の require は以下のようにトランスパイルされます。

1
const foo = require("./path/to/".concat(filename))

これは、filename が valueOf と toString を両方実装したオブジェクトだった場合に、 ${filename} では toString が使われるのに対して、 './path/to/' + filename では valueOf が使われてしまい、結果が正しくないからです。 "./path/to/".concat(filename) とトランスパイルすることで、正しく toString 側の実装を使うことが出来ます。

参考 issue: https://github.com/babel/babel/pull/5791

したがって、 Babel 7 ではテンプレートリテラルで動的パスを require すると require("./path/to/".concat(filename)) のような表現で webpack に渡ってきます。さてこの表現を webpack はきちんと解釈して、ディレクトリのマッピングを作ることが出来るでしょうか?

答えは webpack 2系の場合は No, webpack 3系であれば Yes になります。

もともと webpack はもともとは require('./path/to/' + filename) は動的パスの読み込みとして解釈出来ましたが、 require("./path/to/".concat(filename)) は表現が複雑なため、解釈することが出来ませんでした。

解決策

上の問題を解決したのが以下の PR です。

上の PR が ship されたのが webpack 3.7 のため、それ以上のバージョンに上げれば、Babel 7 でトランスパイルしたテンプレートリテラルの動的パスの require が成功することになります。逆にそれ以下のバージョンでは、動的なパスを正しく解釈できないため、バンドルに失敗します。

上は、webpack 2 と babel 7 でビルドして、動的なパスの require をした場合に出てくるエラーです。実際 webpack 3系にあげてこのエラーを解決することができましたが、エラーメッセージから何が起きているかを読み取ることは非常に困難です。

まとめ

今回は webpack 2 と Babel 7 を同時に使うと、webpack の特殊な機能が使えなくなるという例を紹介しました。

Babel 7 がリリースされた時にはすでに webpack の最新は 4 になっているため、きちんとガーデニングをしているプロジェクトではこの現象は起こらないと思います。今回上の現象が発生したプロジェクトでは、webpack を導入したエンジニアが離任していたこともあって、モジュールのガーデニングが行われていない状態になっていましたが、Babel 7 で使いたい機能が出てきたために、webpack を据え置きで Babel のみ最新にあげようとしたところ、上の現象にあってしまい分析に非常に時間がかかるということが起きました。

フロントエンドのビルド周りは非常に変化が激しいため、古いバージョンと新しいバージョンが混在するとこのような厄介な問題が起きることが多いと思います。ガーデニングはこまめにやりましょう!