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 + babelify や webpack + 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 サポートを打ち切るとして、その履歴をきちんと残せるのだ。
最後に動作確認で使用したサンプル プロジェクトを公開しておく。