アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

babel-preset-env を試す

npm として配布するものは純粋な Node 機能のみで構成したいため脱 Babelしたが、Web フロントエンドや Electron では最新の ECMAScript 機能を利用したい。

というわけで、これまでは Babel + babel-preset-latest で JavaScript を変換してきた。しかし latest だと Web ブラウザーや Electron が最新規格に対応しても個別に変換を無効化するのが難しい。例えば ES2015 Classes は大半の Web ブラウザーが対応済みにも関わらず

var Sample = function () {
  function Sample() {
    _classCallCheck(this, Sample);
  }
}

のように変換されてしまう。一方、機能単位で変換を無効にできるとしても Web ブラウザー毎の対応状況を調べるのは実に面倒。ECMAScript 6 compatibility table をマメにチェックしながら下限となる環境を決める必要がある。

こうした悩みを解決するツールとして babel-preset-env が提供されている。用途を latest と比較すると以下のようになる。

preset 用途
latest 最新 ECMAScript を常時 ES5 相当に変換。
env 最新 ECMAScript を指定された環境に基づき最小限 ES5 に変換。

対象環境を考えるのが面倒で変換コストや品質を気にしないなら latest、なるべく無駄な変換をなくしたいなら env を採用することになるだろう。

試してみる

babel-preset-env を試してみる。

Web フロントエンドや Electron だと Babel 単体よりも Browserify + babelifywebpack + babel-loader のように bundler と組み合わせて利用する機会が多い。しかし bundler 部分も含めると Babel が変換した結果に限定してチェックするのが難しいため実行には babel-cli を採用することにした。

変換対象として ES2015 と ES2016 の代表的な機能を使用した 2 種類の JavaScript を定義。まずは sample.js

// ES2015
export default class Sample {
  message (text) {
    console.log(text)
  }

  async asyncFunc () {
    const wait = (n) => {
      return new Promise((resolve) => setTimeout(() => resolve(n), n))
    }

    await wait(1000)
    console.log('finish!!')
  }

  static func (text = 'sample') {
    console.log(text)
  }
}

// ES2016: Exponentiation Operator
export function pow (a = 0, b = 0) {
  return a ** b
}

// ES2016: Array.prototype.includes
export function includes (arr = [], value) {
  return Array.isArray(arr) ? arr.includes(value) : false
}

sample.js を参照する index.js

import Sample, { pow, includes } from './sample.js'

{
  const sample = new Sample()
  sample.message('Message1')
}

Sample.func('Message2')

console.log(pow(2, 3))
console.log(includes([1, 2, 3], 2))

これらを babel-preset-env の README に掲載されている Examples と現時点で最新の Electron v1.6 向け設定で変換する npm-scripts を定義した。Babel の設定も含めて package.json で完結しているため長いけれど全掲載する。

{
  "name": "using-babel-preset-env",
  "version": "1.0.0",
  "description": "",
  "author": "akabeko",
  "license": "MIT",
  "main": "index.js",
  "keywords": [
    "babel-preset-env"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/akabekobeko/examples-web-app.git"
  },
  "babel": {
    "env": {
      "default": {
        "presets": [["env"]]
      },
      "chrome52": {
        "presets": [
          ["env", {
            "targets": {
              "chrome": 52
            }
          }]
        ]
      },
      "chrome52webpack_loose": {
        "presets": [
          ["env", {
            "targets": {
              "chrome": 52
            },
            "modules": false,
            "loose": true
          }]
        ]
      },
      "browserslist": {
        "presets": [
          ["env", {
            "targets": {
              "chrome": 52,
              "browsers": ["last 2 versions", "safari 7"]
            }
          }]
        ]
      },
      "node_current": {
        "presets": [
          ["env", {
            "targets": {
              "node": "current"
            }
          }]
        ]
      },
      "debug_output": {
        "presets": [
          ["env", {
            "targets": {
              "safari": 10
            },
            "modules": false,
            "useBuiltIns": true,
            "debug": true
          }]
        ]
      },
      "include_exclude_plugins_buildin": {
        "presets": [
          ["env", {
            "targets": {
              "browsers": ["last 2 versions", "safari >= 7"]
            },
            "include": ["transform-es2015-arrow-functions", "es6.map"],
            "exclude": ["transform-regenerator", "es6.set"]
          }]
        ]
      },
      "electron": {
        "presets": [
          ["env", {
            "targets": {
              "electron": 1.6
            }
          }]
        ]
      }
    }
  },
  "scripts": {
    "start": "run-s build",
    "build:default": "cross-env NODE_ENV=default babel ./src --out-dir ./dist/default",
    "build:chrome52": "cross-env NODE_ENV=chrome52 babel ./src --out-dir ./dist/chrome52",
    "build:chrome52webpack_loose": "cross-env NODE_ENV=chrome52webpack_loose babel ./src --out-dir ./dist/chrome52webpack_loose",
    "build:browserslist": "cross-env NODE_ENV=browserslist babel ./src --out-dir ./dist/browserslist",
    "build:node_current": "cross-env NODE_ENV=node_current babel ./src --out-dir ./dist/node_current",
    "build:debug_output": "cross-env NODE_ENV=debug_output babel ./src --out-dir ./dist/debug_output",
    "build:include_exclude_plugins_buildin": "cross-env NODE_ENV=include_exclude_plugins_buildin babel ./src --out-dir ./dist/include_exclude_plugins_buildin",
    "build:electron": "cross-env NODE_ENV=electron babel ./src --out-dir ./dist/electron",
    "build": "run-s build:*"
  },
  "devDependencies": {
    "babel-cli": "^6.24.0",
    "babel-preset-env": "^1.2.2",
    "cross-env": "^3.2.4",
    "npm-run-all": "^4.0.2"
  }
}

Babel の設定は NODE_ENV 単位に定義できることを利用して cross-env により分岐。npm start を実行すると dist/ 配下に設定ごとの変換結果が出力される。普通に指定するなら presets 配下の内容を Babel 設定のルートに記述すればよい。例えば .babelrc に Electron 用の設定を定義するなら以下のようにする。

{
  "presets": [
    ["env", {
      "targets": {
        "electron": 1.6
      }
    }]
  ]
}

サンプルの出力結果を比較すると、例えば default なら全変換され chrome52webpack_loose だと async/await 以外はそのままであることが分かる。また chrome52 では async/await 変換があり electron だと v1.6.0 から Chromium 56.0.2924.87 を採用しているため async/await はそのままに Modules は共通して変換されていた。

babel-preset-env の README によれば、変換の基準は前述の ECMAScript 6 compatibility table を基準として判断しているのだという。つまり機能と対象環境の組み合わせ管理を babel-preset-env に丸投げできるわけだ。本当に ECMAScript 6 compatibility table や Electron のバージョンをチェックしているのか?と targets にデタラメな値を設定してみたら実行時にエラーとなった。Electron に関しては 1.0 以降が対象で最新の 1.6 系まで指定可能。

ただし targets のバージョンは JSON の number になるため小数点第一位までしか対応しておらず 1.6.2 のように semver 形式は受け付けていない。また "node": "current" は可能だが "electron": "current" を指定したらエラーになった。Electron も browsers のように相対値で指定したいものだ。

とはいえ babel-preset-latest を使用しつつ「将来 ES2015 変換をやめたくなったらどうしよう?」などと考えていたので、そのような処理を動作環境の指定へ抽象化して変換してくれる babel-preset-env は実にありがたい。

babel-preset-env 設定により想定している動作環境が明示される点もメリット。これはアプリの仕様を外部へ説明するのに役立つ。また設定を Git リポジトリなどで管理しているなら動作環境の変遷も記録される。例えば IE サポートを打ち切るとして、その履歴をきちんと残せるのだ。

最後に動作確認で使用したサンプル プロジェクトを公開しておく。

Copyright © 2009 - 2023 akabeko.me All Rights Reserved.