Lerna で webpack を内包したパッケージを開発する際の注意点
Next.jsは、webpackとwebpack.configを内包し、自身のソースをエントリポイントにして起動する風変わりなnpmパッケージである。非常にレアなケースで、多くの人がその気を起こすことなく一生を終えるケースだと思うが、私は同様のパッケージを作ってみたくなった。
この「webpack内包型」のパッケージは、ユーザ側のコードを適切に取り込み、アプリを起動するのが責務となる。開発するためには、実際にユーザが使用する際を想定し、ライブラリ側のコード「libDir」と、それに依存するユーザ側のコード「userDir」の、少なくとも2つのpackage.jsonをもつディレクトリが必要になる。
2つとも限らない。userDirはユーザーに使い方を示すサンプルプロジェクトとしても使える。様々な利用ケースに対応することを示すため、サンプルプロジェクトは今後増やすかも知れない。package.jsonの数が今後増えることを想定する必要が出た。
Lernaを使うことにした。Github上でのBabelやwebpackの開発はmonorepoと呼ばれ、1つのリポジトリで複数のnpmパッケージを開発している。これに利用されるのがLernaである。 lerna init
で生成される lerna.json
の設定にしたがって複数のパッケージを管理する。 lerna bootstrap
が有用で、リポジトリ内の対象package.jsonでお互いの依存があった場合、 node_modules
にsymlinkを貼りソースを直に参照できるようにする。それだけなら yarn link
と同じだが、おまけに .bin
にもsymlinkを貼ってくれる。今回私はユーザ側から bin
でwebpackを起動させるため、便利である。
注意点
この「Next.jsみたいなライブラリ」の開発が佳境だ。しかし、主に私のLernaとwebpackの不理解によって大いに時間を削られて来た。3週間前の私に送る警告があるとしたら、それはおよそ下のようなものになる。
- 君はLernaの動きが分かってない。
lerna bootstrap
で依存を貼るのは、package.json に依存が確認された時だけだ。期待した通りにsymlinkが貼られないので、libDirでpackしてuserDirでyarn add
したりしてる君、それではLernaを入れた意味がない。userDir/package.jsonのdevDependenciesに"libDir": "*"
を入れてlerna bootstrap
を叩き直せ。まだpublishしてなくてもだ。 - webpackの
resolve.modules
でハマりまくってる君。まずdocsをよく読め。resolve.modules
は絶対パスと相対パスを指定できる。絶対パスは期待する通りそこだけ探すが、相対パスは遡って全部参照される。ここではそれを「巻き上げ解決」と呼ぶとする。相対パスは通常Node.jsの流儀に従い混乱を避けるためnode_modules
が指定される。これもdocsに書いてあるが、webpack.configに渡された相対パスは原則context
が起点となる。相対パスnode_modules
は最初は${contextで指定されたディレクトリ}/node_modules
からsearchし、次に${context}/../../node_modules
,${context}/../../../node_modules
, と巻き上げ解決する。君がころころcontext
の値を変えたのも災いしたな。 - 前後するが、
stats.errorDetails: true
しておけ。モジュール解決が失敗した時、どこを探して見つからなかったのか教えてくれる。 - Lernaが貼るのはsymlinkに過ぎない。君が最初に期待するのは、contextを
libDir
にし、resolve.modules
を相対パスnode_modules
にすることで、始めにroot/packages/userDir/node_modules/libDir/node_modules
、次にroot/packages/userDir/node_modules
と巻き上げ解決させることだった。そうは動かない。libDirの実ディレクトリはroot/packages/libDir/node_modules
なので、次に巻き上げるのはroot/node_modules
だ。ここも動いたり動かなかったりして混乱したが、最初にroot/package.jsonを整理しなかったのが災いしたな。root/node_modules
にパッケージが残ってて、ビルドが成功したように見えたこともあった。でも実際は失敗してる。意図しない場所からモジュール解決させるな。 - 最後は
webpack-node-externals
だ。これはwebpackでバンドルしたソースを(ブラウザでなく)Node.jsで実行したい場合、除外して欲しいnpmパッケージを決定する関数を返す便利なツールだ。でもこれはお前向きじゃない。ソースを読んで分かったが、これが除外するパッケージを決定する仕組みは単純で、「${process.cwd()}/node_modules
にあるディレクトリ名を除外の対象とする。」、以上。あまりに質素。第一の問題は、resolve.modules
の巻き上げ解決を無視していること。除外したつもりで巻き上げ解決されたモジュールはバンドルされ、大体の場合に問題を起こす(十中八九パッケージの中でdynamic resolveしててバンドルが失敗する)。第二の問題は、「相対パスはcontext
を起点とする」というwebpackのルールを無視していること。おかげで「変な場所だけexternalsされる」が起こり混乱を極めた。このユーティリティは界隈のデファクトだがオフィシャルがホストしてない点に注意を払うべきだったな。1つ1つ順を追って積み上げろ、何一つお前の都合のいいようには作られていないんだから。
まとめ
2週間前の私に向けて書いたたつもりが、昨日の私への苦言になってしまった。でも知ったことか。およその仕組みは分かったし、もう過ぎたことだ。