詳解 snowpack v2.7.6
はじめに
アプリケーションのソースコードを esbuild で JS に変換し、これを解析して使われている各パッケージを node_modules から読み取り rollup で ESM に変換して、最終的にブラウザのネイティブな ESM を活用した開発環境を提供する。 また「バンドラーの設定は必要になってからでよい」といったコンセプトも掲げており、一応 snowpack build でバンドルはしないが esbuild を用いた JS の最適化だけは効くようになっている。
ここではこれらの挙動について理解し実用段階にあるのか判断するため、snowpack build コマンドや @snowpack/plugin-webpack, @snowpack/plugin-babel の snowpack@2.7.6 時点での実装をざっくりと見ていく。 対象読者は snowpack のドキュメントをとりあえず一通り読んだ人を想定しているが、まあ読めなくもないと思う。
ちなみに個人的な結論としては、snowpack build が CSS の最適化やバンドルまでは行わないため別途 webpack などによる最適化の設定が必要で、しかしこれを snowpack build と統合するための @snowpack/plugin-webpack がまだ未熟で rollup に至ってはプラグインが存在しないという状況にあるため、snowpack dev は強力だが本番用のビルドに関しては普通に webpack の設定を書いて NODE_ENV=production webpack -p したほうがよさそうだと思った。
とはいえ webpack の最適化も肝はコードの分割によるロードやキャッシュの制御で、その意味では snowpack も webpack ほど細かく出力の粒度を制御することはできないが、webpack よりずっと簡単な設定である程度の効果が得られるはずではある。
このあたりの最適化と設定の複雑さのトレードオフは snowpack でも初期の頃から認識されており、設計上の決定でもある。
rollup のプラグインと snowpack のプラグイン
snowpack は rollup の上に構築されており、esbuild なども自前の rollup のプラグインとして呼ばれている。
また @snowpack/plugin-babel などの snowpack のプラグインも rollup のプラグインと類似の仕組みで定義されるため、実装を見る前に軽くこれらを確認しておく。
まず rollup のプラグインは、ビルドの様々な段階で呼び出される「フック」という関数の集まりとして定義される。
基本的なフックは load と ransform で、snowpack では run と optimize が追加されている。
load はファイルをロードするフックで、ロードしたファイルを *.js や *.css など一般的な形式に変換する。
例えば @snowpack/plugin-babel では、load で babel を使い *.ts, *.jsx, *.tsx を *.js に変換している。
ちなみに @snowpack/plugin-babel は webpack の babel-loader をかなり簡略化した実装になっている。 また @snowpack/plugin-build-script は load でコマンドを実行する。
transform は load の後に呼ばれるフックで、*.js や *.css のコードに手を入れる。
run は最初に呼ばれるフックで、CLI コマンドを実行しその出力を snowpack に接続するのに使われる。
例えば tsc や eslint によるチェックは、run でコマンドを実行する @snowpack/plugin-run-script で設定する。
optimize は snowpack build の最後の snowpack による最適化の前に呼ばれるフックで、これを無効にしたり webpack や rollup でユーザー定義の最適化を挟んだりするのに使われる。
@snowpack/plugin-webpack は optimize で snowpack の esbuild による最適化を無効にし webpack で最適化している。
snowpack build の処理の流れ
これは esbuild を用いて load でアプリケーションのソースコードを JS に変換するのに使われる rollup のプラグインである。
src/index.ts に戻ると、build や dev の他に add, rm, install といったサブコマンドも存在することが分かる。
install は各パッケージを ESM に変換するコマンドであり、中枢である run() は build や dev でも使われている。 最後にいよいよ snowpack build の実装である src/commands/build.ts の実装を見ていく。
基本的には各プラグインのフックを順番通り run, load, transform と呼んでいき、パッケージを処理したら optimize を呼んで最後に esbuild による minify が走るという流れになっている。
まず run を呼んでいる部分は L101-L116 で、ここからアプリケーションのソースコードのビルドが始まる。 ここでは for...of で config.plugins を回しているため、plugins のプラグインは先頭から順に実行されることになる。 これは load, transform, optimize でも同様である。
その後 install の run() を使ったパッケージを ESM に変換する installOptimizedDependencies() が L211-L215 で呼ばれ、一通りビルドが終わった後に L281-L286 で optimize が呼び出される。 最後に config.buildOptions.minify が true であれば L288-L305 で esbuild による *.js の最適化が行われる。 @snowpack/plugin-webpack の現状
上で見たように snowpack build の最適化は JS に対してのみでバンドルも行われない。
そのため現実的には optimize で最適化する必要があり、@snowpack/plugin-webpack ではこれに webpack を使う。
ちなみに test: /\.js$/ となっているが、ここまでの変換で *.[jt]sx? は全て *.js になっているので問題ない。
ところでこのプラグインは「snowpack による出力を webpack への入力とし別途ファイルを出力する」というものであり、重複する snowpack による出力を自動で削除してくれたりまではしない。
また *.html に関しては上記にようにやや特殊な処理が走るため、これだけは snowpack による出力が上書きされる。
とこのように optimize による最適化はまだまだ既存の snowpack build による処理との統合が不完全で、最終的な出力ディレクトリの構成やパスの置換をうまく制御するには snowpack と webpack 双方の深い知識が必要でやや難易度が高くなる。
snowpack 自体の仕様が安定しておらずドキュメントも不十分である現状や、webpack の設定が複雑になるリスクを考えると、個人的には snowpack build は環境が安定するまで封印して、本番用のビルドは webpack だけでやるほうがよいと思う。
ちなみに optimize で不要なファイルが残るという挙動に関しては、ドキュメントにも少しだけ記述がある。
2020/8/25 追記
v2.9.0になりまたディレクトリの構成が変わったようで、テンプレートが移動していた。
2020/9/14 追記
v2.11.1で @snowpack/plugin-webpack がデフォルトで html-minifier を使って HTML を minify するようになった。 2021/5/25 追記
v3から、以前あった snowpack install コマンドが esinstall として切り出され提供されるようになった。 これと serve-handler とパスを書き換えるツールを合わせれば、単純だったころの snowpack が再現できるかもしれない。 パスの書き換えは CSS 周りの充実度で webpack をとるか、速度で esbuild や SWC をとるかという選択にはなりそう。 そもそも依存関係が減って高速化された parcel v2 や snowpack と似た思想の vite でも充分かも。