@babel/preset-envは必要なプラグインをどう判定しているのか調査した

[Qiita株式会社のカレンダー](https://qiita.com/advent-calendar/2022/qiita-inc)の2日目は @kyntk が担当します。

---

今年はIEのサポート終了がありましたね。
今まではIEのサポートをするためにJavaScriptやCSSで使えない記法があったり、ポリフィルを入れていたりしていましたが、モダンブラウザだけを対応するようになるとこれらの設定が不要になりました。

ES2015以前の環境をサポートするために@babel/preset-envがよく使われます。
今回は、preset-envの設定を変更するにあたって、うっかりサポート対象環境で動かないバグを生んだり、逆にあまり無駄なコードを削れなかったりしないように、内部でどのような処理が行われているのか調べてみました。実際にbabelのソースコードを読みながらまとめています。

:::note
調査時点のbabelのバージョンは「7.20.6」です
:::

## 分かったこと

はじめに結論ですが、Babelを使ってJavaScriptがどのように変換されるかを知るためにこのあたりが重要になりそうです。

- まずは自分のアプリケーションがサポートするターゲット環境を決める or 把握する
    - browserslistを使っている場合、`npx browserslist`などで一覧を取得できます
    - ブラウザのstatsデータをアップデートするためにも[caniuse-lite](https://github.com/browserslist/caniuse-lite)のDBも更新する
- すべてのターゲットをサポートするようにpluginが追加されるので、どのプラグインが追加されるかを把握する
    - リストはこのあたりで管理している (これだけ見ても分からない...)
    - https://github.com/babel/babel/tree/v7.20.6/packages/babel-compat-data/data
- core-jsのポリフィルの読み込みもターゲットを参照していて、`useBuiltIns`で設定ができる

## 前提: Babelの役割

https://babeljs.io/docs/en/

ドキュメントには以下のようにあります。

- Transform syntax
- Polyfill features that are missing in your target environment (through a third-party polyfill such as core-js)
- Source code transformations (codemods)
- And more! (check out these [videos](https://babeljs.io/videos.html) for inspiration)

BabelはJavaScriptを別のJavaScriptに変換をしたり、ポリフィルを追加したりします。

JavaScriptを変換する理由としては、実行時には各ブラウザの環境で動作するコードにしたいものの、実装自体は別のモダンな書き方を使いたいといったものが挙げられます。
一方ポリフィルは、新しい記法を古い記法に変換するのではなく、新しい記法のまま使えるようにJavaScriptの関数の定義などをしているものです。

これを行うために様々なpluginが用意されており、必要なpluginを選択することでどのように変換するかを決めることができます。

### pluginについて

pluginのリストはこちらです。

https://babeljs.io/docs/en/plugins-list

すべてのpluginがコードの変換をするというわけではなく、JavaScriptのparseをするためのpluginなどもあるようです。

また個人的なイメージとして、今まではBabelは「ES2015に対応していない環境のためのツール」という印象を持っていました。しかし、TC39 Proposalsのpluginもあり、モダンブラウザにもまだ実装されていないものを先取りして使うこともできるようです。

### presetについて

presetはpluginのセットです。例えばpreset-envを使うと、pluginを1つ1つ選択しなくてもES2015未対応の環境でも動作するコードにすることができます。
また、core-jsのようなBabel以外のポリフィルを、必要なものだけを選択して追加できる設定もあります。

presetだけでなくpluginも同時に設定することができ、その場合はpluginのほうが先に実行されます。

## @babel/preset-env について

preset-envは、ターゲットブラウザの設定をすることで、その環境で動作するように必要なプラグインとポリフィルを選択してくれます。

例えばtargetsの中に「IE 11」が入っていると、IE 11で動作するJavaScriptを生成することができます。

https://babeljs.io/docs/en/babel-preset-env

### targetsの設定方法

https://babeljs.io/docs/en/babel-preset-env#targets

babelの設定としてtargetsがありますが、targetsを直接指定せずに、`.browserslistrc`を置いておくと、そちらを参照します。

#### babel-helper-compilation-targets

https://babeljs.io/docs/en/babel-helper-compilation-targets#gettargets

targetsには`> 0.25%`のような記述もすることができますが、具体的にどのブラウザのどのバージョンをサポートするかを`babel-helper-compilation-targets`で取得しています。

browserslistから対応しているバージョンを取得して、各ブラウザその一番低いバージョン1つをピックアップしたリストが作られます。

ブラウザごとのstatsデータなどはcaniuse-liteのdbを参照しているので、これをアップデートしておかないと最新ブラウザなどが反映されないので注意しましょう。

https://github.com/browserslist/browserslist#update-caniuse-lite

### pluginの選択

targetsのリストができたので、必要なpluginを選択します。

https://github.com/babel/babel/blob/v7.20.6/packages/babel-compat-data/data/plugins.json

このファイルには、各pluginごとにその機能が実装されているブラウザの最低バージョンが記載されています。
targetとなるブラウザのうち1つでもこのバージョンを下回っているときにはpluginとして選択されます。

この処理自体は`babel-helper-compilation-targets`で行っているようです。

#### bugfixesについて

たとえば、特定のブラウザの実装にバグがあったときでも、その記法が機能するようにコードの変換をするのですが、その際、元のコードよりもかなり多くのコードが生成されることがありました。
それも、バグがあるブラウザだけでなく、バグのないブラウザでも読み込まれるファイルサイズが大きくなることに繋がります。

これを防ぐために、`bugfixes: true`の場合はできる限りモダンで壊れないシンタックスに変換するようにします。

具体例はこちらにまとまっています。

https://babeljs.io/blog/2020/03/16/7.9.0#babelpreset-envs-bugfixes-option-11083httpsgithubcombabelbabelpull11083

### ポリフィルの選択

https://babeljs.io/docs/en/babel-preset-env#usebuiltins

pluginの他に、core-jsによるポリフィルの読み込み設定ができます。targetsに合わせて必要なポリフィルだけを読み込むコードに書き換えてくれます。

例えば、`useBuiltIns: 'entry'`にすると、

```js
import "core-js";
```

targetsに合わせて以下のように必要なものだけをimportするように変換してくれます。
これにより1つ1つ管理しなくても、不要なポリフィルを読み込まずに済み、バンドルサイズが大きくなるのを防ぎます。

```js
import "core-js/modules/es6.array.copy-within.js";
import "core-js/modules/es6.array.fill.js";
import "core-js/modules/es6.array.filter.js";
import "core-js/modules/es6.array.find.js";
import "core-js/modules/es6.array.find-index.js";
import "core-js/modules/es7.array.flat-map.js";
import "core-js/modules/es6.array.from.js";
import "core-js/modules/es7.array.includes.js";
import "core-js/modules/es6.array.iterator.js";
import "core-js/modules/es6.array.map.js";
import "core-js/modules/es6.array.of.js";
import "core-js/modules/es6.array.slice.js";
import "core-js/modules/es6.array.species.js";
import "core-js/modules/es6.date.to-primitive.js";
import "core-js/modules/es6.function.has-instance.js";
// ...
```

#### core-jsについて

ポリフィルがたくさん実装されているライブラリです。
ES2015へのポリフィルだけでなく、ES2023などProposalのものも含まれています。

https://github.com/zloirock/core-js

## まとめ

- まずは自分のアプリケーションがサポートするターゲット環境を決める or 把握する
    - browserslistを使っている場合、`npx browserslist`などで一覧を取得できます
    - ブラウザのstatsデータをアップデートするためにも[caniuse-lite](https://github.com/browserslist/caniuse-lite)のDBも更新する
- すべてのターゲットをサポートするようにpluginが追加されるので、どのプラグインが追加されるかを把握する
    - リストはこのあたりで管理している (これだけ見ても分からない...)
    - https://github.com/babel/babel/tree/v7.20.6/packages/babel-compat-data/data
- core-jsのポリフィルの読み込みもターゲットを参照していて、`useBuiltIns`で設定ができる

このあたりの設定を理解していないと、うっかりサポート対象環境で動かないバグにつながったり、逆に安全に倒そうと思うばかりにあまり無駄なコードを削れなかったりというのがあるので、知れてよかったです。

---

[Qiita株式会社のカレンダー](https://qiita.com/advent-calendar/2022/qiita-inc)3日目は @degudegu2510 と@masato930 が担当します！

ぜひ、[Qiita株式会社のカレンダー](https://qiita.com/advent-calendar/2022/qiita-inc)を購読設定して、明日の記事もご覧いただけると嬉しいです。


## 参考

- [What is Babel? · Babel](https://babeljs.io/docs/en/)
- [Serve modern code to modern browsers for faster page loads](https://web.dev/serve-modern-code-to-modern-browsers/)