概要
この最近、ビルド環境ばっかりいじっていてビルド環境構築マニアというニッチなポジショニングを得たので共有のためここに記そうと思う。
ちなみに、僕の設定は基本的にはそれぞれのツール(babelやrollup)で基本構成を決めていて、これらの組み合わせによって用いる。自分が今すぐ作りたいっていう環境がある場合は、最初の方は読み飛ばしてもらって、必要になった際に、上の方に戻るといいだろう。
babel設定の基本構成
以降で語る各種ビルド環境に応じて、babelの設定には何種類かバリエーションがある。まずは僕がよく使うbabelのバリエーションについてここで記述しておこう。
パターン1 (ES6 + stage-2 -> ES5)
$ babel --presets es2015,stage-2 --plugins transform-runtime
もっともよく使う構成、ただ単にasync,awaitが必要なjavascriptを普通のES6に落とし込むのに使う。(async,awaitだけならstage-3でも可)
必要なnpmのパッケージは以下の通り。
- babel-plugin-transform-runtime
- babel-preset-es2015
- babel-preset-stage-2
パターン2 (ES6 + stage-2 -> ES5(Rollup併用))
これはRollupこの構成をRollupで用いる場合の設定だ。Rollupでは、transform-runtimeも使えないし、通常通りのes2015という設定ではいけない。そこで正しい設定は以下のようになる。(以下はコマンドではなく.babelrcとしての例(Rollup内で使わせたいため))
{
"presets":["es2015-rollup","stage-2"],
"plugins":["external-helpers"]
}
もっともよく使う構成、ただ単にasync,awaitが必要なjavascriptを普通のES6に落とし込むのに使う。(async,awaitだけならstage-3でも可)
必要なnpmのパッケージは以下の通り。
- babel-plugin-external-helpers
- babel-preset-es2015-rollup
- babel-preset-stage-2
パターン3(コンパイル済みTypescript -> ES5)
コンパイル済みのTypescriptには、__awaiter
などがすでに含まれているため、ES2015以外のプリセットや、transform-runtimeはいらない。
つまり、
$ babel --presets es2015
のみで良い。必要なパッケージは以下の通り。
- babel-preset-es2015
Rollupの基本構成
パターン1(通常のES6をバンドリングする場合)
このような場合、主に必要なのは以下の機能だけだろう。
* npmのパッケージのimportをバンドリングに含める
* commonjsパッケージを上手い感じにimportする
* sourcemap
なので、以下のような設定をすれば良い。
import npm from "rollup-plugin-node-resolve";
import sourcemaps from "rollup-plugin-sourcemaps";
import cjs from "rollup-plugin-commonjs";
export default {
entry:"入力となるjsファイル",
dest:"バンドリングされたファイルの出力先",
plugins:[
sourcemaps(),
npm({jsnext:true}),
cjs()
]
};
この際に必要なnpmのパッケージは以下の通り。
* rollup-plugin-node-resolve
* rollup-plugin-sourcemap
* rollup-plugin-commonjs
パターン2(ES6 + stage-2なjsファイルをバンドリングする場合)
以外にもこのケースは少し厄介だ。なぜなら、rollupは基本的にES6のスクリプトを吐くわけだが、asyncやawaitのようなものがあると例外を吐いて死んでしまう。
そのため、rollup内でbabelのプラグインを用いる必要があるが、この設定が少し技巧的で、前のbabelの設定のパターン2の.babelrcファイルがある状態かつ、babelのためのnpmのパッケージがインストールされている状態で以下を指定するといいだろう。
import npm from "rollup-plugin-node-resolve";
import sourcemaps from "rollup-plugin-sourcemaps";
import cjs from "rollup-plugin-commonjs";
import babel from "rollup-plugin-babel";
import inject from "rollup-plugin-inject";
export default {
entry:"入力となるjsファイル",
dest:"バンドリングされたファイルの出力先",
plugins:[
sourcemaps(),
inject({
modules:{
regeneratorRuntime:"regenerator-runtime"
}
),
npm({jsnext:true}),
cjs(),
babel({
runtimeHelpers:true
})
]
};
この際に必要なnpmのパッケージは以下の通り。
* rollup-plugin-node-resolve
* rollup-plugin-sourcemap
* rollup-plugin-commonjs
* rollup-plugin-babel
* rollup-plugin-inject
* regenerator-runtime
パターン3(コンパイル済みのTypescript(ES6)をバンドリングする場合)
このような場合、Typescirptファイルごとに本来、__awaiterなどのヘルパーが生成されるため無駄が多い。
そこであらかじめTypescriptのコンパイル時に--noEmitHelpers
を指定することにより、生成されるjavascriptファイルには__awaiterが無駄に生成されたりしない。
ただし、Rollupの処理で、代わりにtypescriptのawaiterを含めてあげる必要がある。したがって以下の通り設定すれば良い。
import npm from "rollup-plugin-node-resolve";
import sourcemaps from "rollup-plugin-sourcemaps";
import cjs from "rollup-plugin-commonjs";
import inject from "rollup-plugin-inject";
export default {
entry:"入力となるjsファイル",
dest:"バンドリングされたファイルの出力先",
plugins:[
sourcemaps(),
inject({
modules: {
__awaiter: 'typescript-awaiter'
}
}),
npm({jsnext:true}),
cjs()
]
};
したがって、必要なパッケージは以下の通り。
- rollup-plugin-node-resolve
- rollup-plugin-sourcemap
- rollup-plugin-commonjs
- rollup-plugin-inject
- typescript-awaiter
各種ケースごとのビルド環境
Node環境で実行するもの
Node環境で実行する場合、当然であるがrollupは必要ない。普通にES6からES5に落とせばいいはずだ。よってbabelの基本構成のパターン1を用いるだけで十分である。
以下のような形でpackage.jsonの中にscriptsを書けば必要十分であろう。
{
"scripts":{
"build":"babel ./src --out-dir ./lib --presets es2015,stage-2 --plugins transform-runtime",
"prepublish":"npm run build"
}
}
こうしておけば、srcフォルダ内のすべてのES6なファイルたちはES5に変換されてからlibフォルダの中に入るはずだ。
ブラウザ環境で実行するもの(ES6 + stage-2)
ブラウザ環境で実行する場合、バンドリングする必要がある。基本的には、babelの基本構成パターン2,rollupの基本構成パターン2を併用すれば良い。
すなわち、ほぼrollup内で完結するので以下のようにできる。
{
"scripts":{
"build":"rollup -c",
"prepublish":"npm run build"
}
}
ブラウザ環境で実行するもの(Typescript)
まずTypescriptをコンパイルしてからrollupでバンドリングする。rollupの基本構成パターン3を用いると良い。
ただし、これで出てくるjsはES6のままなので、最後にバンドリング後のスクリプトをbabelでES5に落とす。
{
"scripts":{
"compile":"tsc ./src/**/*.ts --declaration --outDir lib-es6 -m es6 -t es6 --moduleResolution node --sourcemap --noEmitHelpers",
"prebuild":"npm run compile",
"build":"rollup -c",
"postbuild":"npm run babel",
"babel":"babel rollupのdestのファイル名 --out-dir 出力先のディレクトリ --presets es2015",
"prepublish":"npm run build"
}
}
その他
glupもgruntももう嫌だけど、ロジック書かないとビルド環境が成立しない!もしくは他にも色々とビルドタスクが存在する場合。
基本的な上記のパターンだけなら、npm scriptsだけでいいと思うが、割と自分の中で最近気に入ってるのはビルドタスク記述用のjsをasync/await付きのES6で書いてしまうことだ。ややこしい非同期処理が大変だった時にはgulpやgruntで記述して楽にしていたが、もはや今はasync/awaitが簡易な設定で用いれる。
ファイルの読み出しや、外部コマンドの呼び出しなどをこれで書いてしまえば、上から下に単に実行されていくだけでわかりにくくなりにくい。非常にオススメだ。(コールバックを伴う関数をすべてPromiseでラップしてあげる必要はあるが)
このような場合、以下のような形でbabel-node
を用いると使いやすい。
{
"scripts":{
"build":"babel-node ./build.js --presets es2015,stage-2 --plugins transform-runtime",
"prepublish":"npm run build"
}