ES2015+で実装するためにBabelのpolyfillを利用する場面は多いと思いますが、Babel6.xまでと7.xではその導入方法が変わっているので注意が必要です。今回はBabel7.xでの用途別polyfillの設定方法と、キモとなるuseBuiltInsオプションの挙動についてまとめてみます(執筆時点でのBabelのバージョンは7.1.0です)。

なお、6.xまでの設定方法は「Babelの設定を見直すための逆引きガイド」にまとめてあります。polyfillのことだけでなく、Babelとは何か、どのように利用するのか、といったことも併せてまとめてありますので良ければご参考にどうぞ。

2019/06/21 追記

Babel7.4.0から @babel/polyfill が非推奨となっています。変更点や新しい設定方法は「Babel7.4で非推奨になったbabel/polyfillの代替手段と設定方法」の記事をご確認ください。

用途別polyfillの入れ方

polyfillの入れ方には大きく3種類あります。

用途 方法
必要なpolyfillだけ入れる useBuiltInsオプション usage を使う
全てのpolyfillを入れる @babel/polyfillをimport/requireする
グローバル汚染せずにpolyfillを適用する @babel/runtime
@babel/plugin-transform-runtimeを使う

大幅に変更されたのがuseBuiltInsオプションの挙動です。6.xではまずbabel-polyfillを入れた上で「それをどのようにcore-jsに置き換えるか」をuseBuiltInsオプションで指定する形でした。 対して7.xではuseBuiltInsオプションに応じて@babel/polyfillを入れたり入れなかったりします。

ではそれぞれの方法の詳細を見ていきます。

1. 必要なpolyfillだけを入れる方法

@babel/preset-envuseBuiltInsオプションをusageとすると、@babel/polyfillをソース内でimportせずとも必要なpolyfillだけを自動で選別して入れてくれます。ただし、@babel/polyfillをnpm installしておく必要はあります。

@babel/polyfill, @babel/preset-envをインストールする

$ npm install -D @babel/preset-env
$ npm install -S @babel/polyfill

.babelrcを記述する

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage"
      }
    ]
  ]
}

この方法はメリットが大きいですが、公式ドキュメントにexperimentalと記されている点を忘れてはいけません。
試しにpolyfillが必要な実装例をいくつか試してみたところ、polyfillが入らないケースがありました。分かりやすかった例をピックアップしてみます。

obj = {};
let letObj = {};
const constObj = {};

obj['includes'];      // array.includes と string.includes のpolyfillが入る
letObj['includes'];   // polyfillが入らない
constObj['includes']; // polyfillが入らない

// --------

const getName = () => 'includes';
'str'[getName()];     // string.includes のpolyfillが入らない

静的解析してimportするpolyfillを特定している感じでしょうか。
スコープが不明確だと中身を特定しきれないので、使われる可能性のあるpolyfillを入れていますね。また、関数は実行しないと結果を判断できないようです。この感じだと、個人的にはproduction利用はまだちょっと怖い…と思ってしまいます。

ちなみに「importされているか怪しいpolyfillを個別に手動でimportする」案も考えたのですが、手動で入れたものとの重複判定はしてくれないようでしたので、この案もダメそうですね😳

2. 全てのpolyfillを入れる方法

Babel6.xまでと同じように@babel/polyfillをimport/requireする方法です。合わせてuseBuiltInsオプションにentryfalse(default)を指定することができます。

@babel/polyfill, @babel/preset-envをインストールする

$ npm install -D @babel/preset-env
$ npm install -S @babel/polyfill

エントリーポイントでpolyfillを読み込む

// index.js

import '@babel/polyfill';

Promise.resolve();  // polyfillが必要な実装

.babelrcを記述する

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry"  // 任意
      }
    ]
  ]
}

useBuiltInsentryを指定すると、@babel/polyfillのimportを生のcore-jsのimportに置換してくれます。また、今まで通り同じpolyfillを複数回importするのはNGなのでその点もお忘れなく。

3. グローバル汚染せずにpolyfillを適用する方法

Babel6.xまでではbabel-plugin-transform-runtimeを使っていましたが、7.xからは2つのmoduleを使います。

@babel/runtime, @babel/plugin-transform-runtimeをインストールする

$ npm install -S @babel/runtime
$ npm install -D @babel/plugin-transform-runtime

.babelrcを記述する

{
  "plugins": ["@babel/plugin-transform-runtime"]
}

利用の際は、Babel6.xまでと同様にインスタンスメソッドは使えないということに注意する必要があります。

useBuiltInsのコードリーディング

ここからは余談です。
useBuiltInsオプションがどのように実装されているか気になったので、軽くコードリーディングしてみました。そのときのメモをまとめておきます。

babel-preset-env/src/index.js#L285

useBuiltInsの値に応じて利用するプラグインを分岐しています。

これらのプラグインは、どちらも以下のようなPluginオブジェクトを返す関数を実装しています。

export default function({ types: t }: { types: Object }): Plugin;

type Plugin = {
  visitor: Object,  // polyfill の import を解決するための処理群
  pre: Function,   // メイン処理前に実行すること
  name: string     // pluginの識別子のようなもの
}

Plugin.visitorが大きく差が出る部分ですね。

babel-preset-env/src/use-built-ins-entry-plugin.js#L31

Plugin.visitorに渡されるisPolyfillImportオブジェクトを見ると、ImportDeclaration関数に「@babel/polyfillがimportされてるならフラグをたてて、それを必要なmoduleのimportにreplaceしていく」といった実装があります。

babel-preset-env/src/use-built-ins-plugin.js#L68

Plugin.visitorに渡されるaddAndRemovePolyfillImportsオブジェクトを見ると、「@babel/polyfillがimportされていないことを確認するImportDeclaration関数」と、「個々のpolyfillのimportするための関数」が存在します。後者については、ソースを解析して実装方法に応じて呼び出す関数を使い分けている感じですかね。

個々のpolyfillをimportするための関数は、最終的にutilsのcreateImport関数を呼び出し、その中で更に@babel/helper-module-importsに処理を委譲しています。この@babel/helper-module-importsがpolyfillを個別にimportする本体ですね。

@babel/helper-module-imports

src以下には3つのファイルがあります。

index.js
外部から呼び出されるpublicな関数。処理の実態は以下の2つに委譲。

import-injector.js
polyfillの注入を担うclass。1つのpolyfillにつき1インスタンスを生成し、polyfillの性質(クラスメソッドなのか、インスタンスメソッドなのか、等)に応じた方法で注入する処理を呼び出す。

import-bulder.js
上述の「注入する処理」の本体。慎ましいBuilderパターンで実装されている。

以上、ざっくりとしたコードリーディングでした。@babel/coreのほうまではちゃんと読んでないので誤りがあるかもしれませんが、なんとなくuseBuiltInsによる挙動の違いを想定できたので良しとします。

あとがき

今回の記事はBabel7.1.0のドキュメントとソースを参考にしています。今後また仕様が変わることもあると思うので、記事内に古い情報や誤りを見つけた際は@aloerina_までご連絡いただければと思います。