pig's diary

何でも忘れるので万年初心者ね

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週間前の私に向けて書いたたつもりが、昨日の私への苦言になってしまった。でも知ったことか。およその仕組みは分かったし、もう過ぎたことだ。