JavaScript や TypeScript のモノレポを便利に使えるようにしてくれる Turborepo というツールのドキュメントを読んでて、↓この部分で???ってなった。ので、将来の自分用にブログにまとめておくことにした。
topo ってなんだー!?
add、subtract、multiply という workspace があって
- subtract は add に依存している
- multiply も add に依存している
という場合に、こういう書き方をすると↓
{ "$schema": "https://turbo.build/schema.json", "pipeline": { "topo": { "dependsOn": ["^topo"] }, "test": { "dependsOn": ["topo"] } } }
↓こういうことが実現できる、と書いてある。
- add、subtract、multiply の
test
は、すべて同時に実行される - add に変更が入った場合は、subtract と multiply の
test
実行時にキャッシュが使われずに再度実行される
なるほど?←なにも分かってない。
まずは dependsOn
について再確認しておく。
workspace と task
その前に、workspace と task という言葉について。
- workspace:モノレポの場合は、複数の workspace がひとつのリポジトリに入ってる。上の例の add、subtract、multiply がそれぞれ workspace。
- task:npm scripts に定義される task のこと。
build
やlint
やtest
など。
それぞれの workspace 内の task の依存関係
それぞれの workspace の中の task に依存関係がある場合がある。deploy
の前に build
を実行する必要がある、みたいなの。Turborepo を使う場合は、ルートディレクトリに置いた turbo.json
で、一括でそういう依存関係を定義できる。
↓Turborepo のドキュメントより
{ "$schema": "https://turbo.build/schema.json", "pipeline": { "build": {}, "deploy": { // A workspace's `deploy` task depends on the `build` task of the same workspace. "dependsOn": ["build"] } } }
これは「それぞれの workspace 内の deploy
タスクを実行するときには、その前に build
タスクを実行する必要がある」と Turborepo に伝えることになる。
これはわかる。
モノレポ内の workspace 間の依存関係
モノレポなのでだいたい workspace 間に依存関係がある。さっきの add や subtract のように。だから、workspace 同士の依存関係に気をつけて build
などのタスクを実行する必要がある。Turborepo はこのあたりの依存関係を扱えるようになってる。便利。ただ、ややこしいことに、これも dependsOn
で定義する。さっきのと違って ^
を使う。
{ "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { // A workspace's `build` command depends on its dependencies' // and devDependencies' `build` commands being completed first "dependsOn": ["^build"], } } }
ちょっと説明のために書き換える。
"foo": { "dependsOn": ["^bar"], }
この場合 foo
を実行するためには、その workspace の依存先 workspace の bar
タスクを先に実行する必要がある。という意味になる。
元の設定に戻ると↓は「build
を実行するためには、その workspace の依存先 workspace の build
を先に実行する必要がある」ということになる。こう書くことで、それが再帰的に連鎖して、モノレポ内のすべての依存関係がいい感じに build
される。
"build": { "dependsOn": ["^build"], }
すこしややこしいけど、落ち着いて考えたら、これもまぁ分かる。
さて本題
で、最初の定義の部分に戻るんだけど、やっぱりいまいち意味が分からなくて、ぼーっと探してたら、この Issue を見つけて、なるほどーってなった。
実現したいこと
実現したいのはシンプルにこういうこと↓
add、subtract、multiply という workspace があって
- subtract は add に依存している
- multiply も add に依存している
というときに
- add、subtract、multiply の
test
を、すべて同時に実行したい
dependsOn
を書かなければ add、subtract、multiply の test
はふつうに同時実行される。問題は Turborepo のキャッシュ。
Turborepo のキャッシュ
Turborepo は task 実行の高速化のために、コードに変更がなければ前回実行時のキャッシュを使う。実際にはコード以外にも環境変数など色々なものを input として考慮してるけど、ここでは説明を簡単にするためにコードだけに注目する。
コードに変更がなければ、test
は実行せずに、前回実行時の test
の結果をキャッシュから返す。
workspace に依存関係がある場合の問題
dependsOn
を書いていない場合は、Turborepo はそれらの workspace に依存関係があることを知らない。そのため「add が更新されているから subtract の test
を再実行してほしいんだけど、subtract が更新されていないからキャッシュから返される」という動きになる。これはダメだ。
じゃあ、依存関係を見るようにするかー!と思ってこういう風に定義をすると
"test": { "dependsOn": ["^test"], }
subtract の test
を実行する前に依存先の add の test
を先に実行してね!となる。これで、Turborepo は workspace の依存関係に気づけるので add に更新があった場合には、subtract の test
実行時にはキャッシュを使わずに再度実行してくれるようになる。
でも、この設定を入れたことにより、同時実行されなくなってしまった。
workspace の依存関係は直接コードを見ている
状況を整理すると、add に変更があった場合には、subtract と multiply は、キャッシュを使わないで再度 test
を実行したい。モノレポの workspace 参照なので、npm パッケージへの依存と違って add のビルドなどを先にする必要はない。単純にキャッシュを使わずに再実行したいだけ。
だから、実現したいことはこういうことになる。やっと最初の要望の意味がわかった。
- add、subtract、multiply の
test
は、すべて同時に実行したい - add に変更が入った場合は、subtract と multiply の
test
実行時にキャッシュを使わずに再実行したい
ダミータスク topo
つまり、キャッシュの判断のために依存関係は見てほしいけど、task の実行自体には依存関係はないから同時に実行したい。という状況。これを解決するのがダミータスク topo
になる。モノレポ内に存在しない task であればいいので名前はなんでもいいけど、topology を意味する topo を使うみたい。
{ "$schema": "https://turbo.build/schema.json", "pipeline": { "topo": { "dependsOn": ["^topo"] }, "test": { "dependsOn": ["topo"] } } }
topo
自体は何もしない。test
は「その workspace 内の」topo
を先に実行する必要があるtopo
は「依存先 workspaceの」topo
を先に実行する必要がある
これにより、Turborepo は test
実行時に、workspace 間の依存関係を認識して、キャッシュの利用判断をしてくれる。
さらに、topo
は何もしないので、test
はすべて同時に実行される。
ということでやりたいことが実現できた!なるほどー。(ヽ´ω`)
質問
test
の dependsOn
に ^topo
を書くだけでいいのでは?
それだと、1つ先の依存までしか見てくれないからダメ。
この場合 test
は「依存先 workspaceの」 topo
を先に実行する必要がある、という意味になる。これで、add と subtract の依存関係に気づけるので、add に更新があった場合に、キャッシュを使わない状態になる。ここまではいい。
でも、この定義だと、再帰的にはならない。どういうことかというと、もうひとつ別の workspace として number が存在して、add が number に依存している場合を考える。
subtract の test
を実行するときには add の topo
を見るだけで、add の topo
はその先の依存関係を見に行かない。なので、number だけが更新された場合には、subtract はキャッシュを利用してしまう。
だから、topo に再帰的な依存を入れておく必要があるのだ。
"topo": { "dependsOn": ["^topo"] },
ややこしい!
dependsOn
に書いてなくても依存関係をキャッシュのキーとして考慮すればいいのでは?
そもそも、dependencies
や devDependencies
に workspace の依存関係が書いてあるので、Turborepo がそれを最初からキャッシュのキーとして考慮すればいいのでは?ということなんだけど、そういうわけにはいかない。
今回の場合は「workspace の依存がキャッシュに影響を与える」というケースだったが、他のケースとしてあるのが「workspace は依存しているがキャッシュには影響を与えない」という依存関係もある。例えば ESLint の workspace。
ESLint の設定を workspace としてくくりだしている場合、subtract から ESLint workspace への依存は存在するが、ESLint が更新されても、それは test
の実行には関係ないので、キャッシュから返しても問題がない。
だから、依存関係を自動的にキャッシュのキーとして扱うわけにはいかない。
ややこしい!
ということで
topo
についてこんがらがってた頭の中を整理した。1ヶ月後に覚えてる自信が全くないので、書き残しておいた。
簡単なコードを使って動作を確認しておいた↓
https://github.com/bufferings/turbo-topo
おもしろかった!