9月も半ばを過ぎましたが、なんとも動きづらい天気が続いてます。もうちょいシトシトとした雨が降ってはくれないものか。いや外出してるときに降られたくはないのだけども。
最近TypeScriptをひたすら触っていますが、pnpmでworkspaceにしたらなんか解決できないかなぁ…と思うことがあったので考えてみました。
TypeScriptでの困りごと
元々のJavaScriptにおける言語機能から考えれば当たり前といえば当たり前なんですが、Rust/Java/Kotlin/OCamlとか触ってると(OCamlはめんどくさいことも多いけど)、次のようなことができないのが結構ストレスです。
pacakge privateのようなものができない
例えばRustであればcrate privateだったりmodule privateだったり、この観点ではかなり柔軟だと思います。というかそこまでやるか?と思うときもまれにあります。 Javaであればpackage private/private/protectedとか、Kotlinであればinternalですね。JVM系列はJava9からmodule systemが入ったのは把握してるんですが、moduleでどういうことができるのかがよくわかってないもので…。
OCamlは公開範囲を規定とかできませんが、まぁJavaScriptよりはマシです。ホントに?
さて、ではこれができないことで何が困るんでしょうか?具体的に言うと、↓のようなことをやろうとした場合にめんどくさいな、というところです。
- importできるmoduleのlayerを制約したい
- 本来見えないはずのmoduleが見えてしまうのを避けたい
- これはindex作ればまぁなんとかなりますが
- 共通で利用する部分を整理しやすくしたい
大体、一個のファイルが大きくなってもいいんなら解決できるんですが、1000行とかいったりするのもそれはそれでやだよね、という思考からきてます。
そこまで大きくならないのであれば、exportしなけりゃいいだけなんで、二つめは問題にならないですね。
お仕事で使ってるJVMというかGradleならば、Gradleで適切にmulti projectで分割してやれば強制することができます。Rustはまぁ強烈すぎるんでやりかたは沢山あるでしょうが。
pnpmのworkspace
最初にnodeのパッケージマネージャでこれを作ったのはyarnじゃないのかな?という浅学っぷりなのですが、pnpmにはworkspaceという機能があります。
これはモノレポ構成を上手く扱うための機能の一つで、workspaceを利用することで、workspace内のpackageは workspace:*
のような形で、package間の依存関係を作成することができます。また、ルートにあるpackage.jsonと上手く利用することで、開発時に利用する同じ依存関係、たとえばviteやtypescriptといった開発用パッケージのバージョン管理をシンプルにすることができます。
Bitとは?
ところで、↑のページを見ていたら、pnpm本体から bit というツールの存在を示唆されました。なんじゃこれ?というところでちょっと試してみたのですが、以下のようなものでした。
- componentを1node packageという単位で管理することを容易にし、相互参照できるようにする
- 上記の依存を自動的に管理できる
- 増えたコンポーネントの数に関わらず、同じビルドパイプラインを利用できる
開発元はbit.cloudという、このbitを利用したコラボレーションやらなんやらを行うことができるプラットフォームを開発しているようです。お、これでいけるんじゃね…?と試してみましたが、ちょっと思っていたものと違いました。
- bitは別のバージョン管理の元に生きている
- 例えばビルドパイプラインで利用するtypescriptですが、 bitに依存を明示的に追加しないといけないです 。それって管理するものがただ増えただけなのでは…
- bitコマンドが非常に大きい
- なんかえらい時間かかるなーと思ったら、展開した結果が1.5GBくらいありました。大分ビックリ
- bitの世界でのビルドパイプラインを構築しなければならない
- 色々用意はされているようですが、どちらにせよbitという世界の中で作成する必要がありました
構成全部をここに全振りできるんならいいのだと思いますが、ちょっと個人でこれを全振りする勇気は出ませんでした。
pnpmのworkspaceでのpackage分割を考察してみる
さて、戻ってpnpmのworkspaceでpackage分割をしてみます。構成としては、
- packages // workspace
- lib-a
- package.json
- lib-b
- package.json
- app
- package.json
のような超シンプルなもので考えてみます。一旦考察だけなので。しかしちょっと考察するだけで色々としんどさが見えます。
- typesとmjsなどのビルド・提供が必要
- 真面目にやると、それぞれのpackageでこれが必要です
- アプリケーションのエントリポイントだったら最後viteとかにできますが、それ以外だとしんどさしかない
- ビルドの依存関係が必要になる
- 当然ですが、pnpm自体はビルドしてくれません。これについては turborepo でなんとかなりますが、↑のしんどさはまったくクリアされません
- 設定ファイルがそれぞれに分散する
- baseを云々したりが一般的だと思いますが、workspaceを利用する場合は、こちらのような方法 をとることができます。こっちの方がスマート
private packageかつtypescriptの場合のショートカット
真面目にやると超めんどくさいのですが、実はショートカットがあります。
https://turbo.build/blog/you-might-not-need-typescript-project-references
package.jsonは次のように書くことができます。
{
...,
"main": "./src/index.ts",
"types": "./src/index.ts",
...
}
What?って感じですが、これできちんと動きます。Language Serverも問題なく動作します。こうすることで、declarationを生成したりビルドしたり、を各パッケージで実行する必要がなくなります。が、当然エントリポイントではこれらをtranspileしないといけません。これについてはviteがハンドリングしてくれるため、viteを利用している場合には特に問題はありません。
だがしかし、禍福はあざなえる縄のごとし。これをやった場合一個欠点があり、 node_modulesからsrc/配下が全部見えるようになります 。これはnode_modulesがそういう構造になっちゃってるからなので、制約は大変難しいです。
結論
- monorepo構成は、中規模〜大規模か、もしくは非常に細分化されたpackage管理のとき以外は採用しない方がよし
- generatorとかを利用することで一定利用のハードルは下がる
- しかし、再利用とかをしないのであれば、その分割自体がコストにしかならない
- JavaScriptというかTypeScriptでの可視性は若干妥協した方がコストがかからなさそう
- packageを分割し、exportするものをきちんと絞って、かつindexでexportするものをまとめる、とかしていけば、恐らくきちんとすることはできる
- しかし、そこまでやるよりlintとかで制御できるし、
a/index.ts
とかを適切に使えば、補完とかでは問題ない- 手動でやると見えちゃうけど、それはもうどうしようもない気がする
ということで、個人的なプロジェクト程度では、これを利用するのは過剰だね、という気分でした。めんどくさくてもきちんとindex.tsとかで整理していくとか、なんでもかんでもexportしないとかが重要ですね。
後はファイルサイズに対するアレルギー的なものですが、実際にはある責務をmoduleに担当させているのであれば、それが一定のサイズになるのはままあることなので、まぁ気にしすぎない方がいいかな、と思ったり。特にReactでコンポーネント作ってるとめっちゃ細かく割りがちなのですが、後から考えるとそこ割らないでまとめた方が結局テストとか楽だよな、ってこともあります。
なんもまとまっていませんがこのへんで。