🧊

Astro のコードを読む Part.3

Astro のコードを読む Part.2 - console.lealog();

これの続きであり、最後の記事です。

2週間ちょいでバージョンが`1.0.0-beta.27`から、`1.0.0-beta.33`になり、この時点でのハッシュは`51db2b9b4efd899bdd7efc481a5f226b3b040614`でした。

まもなくメジャーバージョンがリリースされることもあり、これが最後のコードリーディング。

残してきたビルドコマンドまわりと、Partial Hydrationの仕組みまわりを読む。

astro build

CLIの`build`コマンドを見ていく。

このコマンドは、SSGとして成果物をディスクに書き出す処理、もしくは、SSRするためのランタイムを生成する処理になる。

AstroBuilder

https://github.com/withastro/astro/blob/51db2b9b4efd899bdd7efc481a5f226b3b040614/packages/astro/src/core/build/index.ts#L47

  • ビルドコマンドの実体
    • 以前にも見たポリフィルをロードしたあとは、これに丸投げ
  • コンストラク
    • 以前にみた`RouteManifest`の生成
  • `run()`
    • `setup()`からの`build()`
  • `setup()`
    • 以前にみた`createVite()`から、Viteの設定オブジェクトを用意
    • それを使ってViteのサーバーを立てる
  • `build()`
    • `collectPagesData()`: ページとそのアセットのリストを返す
    • `staticBuild()`: ページを生成する
    • アセットの書き出し

各所でインテグレーションのためにフックを呼んでたり、メトリクスをログに出したりはしてるけど、基本的にはこれだけ。ビルドでもViteを酷使していくスタイル。

collectPagesData()

https://github.com/withastro/astro/blob/51db2b9b4efd899bdd7efc481a5f226b3b040614/packages/astro/src/core/build/page-data.ts#L33

  • `RouteManifest`を頼りに、すべてのページ(コンポーネント)を探す
  • コンポーネントは、以前に見た`preload()`によってSSRする
    • `dev`ではリクエストに応じてやってたこと
    • `build`では一括でやってる
  • `getStaticPaths()`が存在してれば、その個別ページも同様にやる
    • `paginate()`の設定の単位でよしなに

ちなみに返り値は、`allPages`と`assets`という2つのオブジェクトで、どっちもファイル名がキーになってた。

staticBuild()

https://github.com/withastro/astro/blob/51db2b9b4efd899bdd7efc481a5f226b3b040614/packages/astro/src/core/build/static-build.ts#L26

  • ページの情報を回収
    • `.astro`で定義されたやつ
    • `client:`のディレクティブの有無の確認とか
    • hoistedされるものを含め、JSが必要なものの確認も
  • `clientBuild()`
    • クライアントサイドのJSをビルドする処理
    • さっき確認したときに見つからなかったなら、何もしない
    • SSR用にビルドしてる場合は、ファイルをコピーするだけ
    • ビルド専用のViteの設定オブジェクトを用意して、ここでも`vite.build()`を呼ぶ
    • プラグインも`preload()`のときとは違う顔ぶれ
    • `rollupOptions.input`に対して、クライアントサイドのJSを全部流す
    • `esbuild`でミニファイ
  • `ssrBuild()`
    • 本命のプロジェクト自体のビルド
    • ただし、まだ静的なファイルを生成するわけではない
    • Rollupのビルドアウトプットのチャンク集ができるまで
    • こっちもビルド専用のViteの設定オブジェクトを用意して、`vite.build()`を呼ぶ
    • `@astro/plugin-build-pages`がここで登場
    • SSRされたモジュールのエントリーポイント用のテンプレ
  • `generatePages()`
    • これまでに集めた`internals`オブジェクトのデータを活用
    • その中で`type: page`と判断されたものを、`generatePage()`
    • SSR済のエントリーファイルの中から、そのパスに合致するものを探す
    • あったらそれを`generatePath()`
  • `generatePath()`
    • `dev`コマンド時にもやってた`render()`がここで呼ばれてる
    • ただし結果はディスクに書き出されるという違いがある
    • これでやっと最終的なHTMLができる
  • あとはビルドに使った一時ファイルを削除して終わり

長かった。

Partial Hydrationまわり

Astroの存在意義でもあるこの部分を読まずには終われまい。

  • `.astro`内部で`import`してる別のコンポーネントを評価するのは、`preload()`でViteにSSRされるとき
    • `$$metadata`にそういうデータが詰まってる
  • `client:idle`などの`hydrationDirectives`も、この時点で判別される
    • そして`client:only`には特別なマーキング
  • `client:*`のディレクティブがあった = クライアントサイドでJSが必要ということ
    • なので`clientBuild()`時に、それ用のコードが出力される
  • `ssrBuild()`の各ページごとに`topLevelImports`を見つけるところで、挿入される
    • `client:idle`は、`astro/client/idle.js`のような命名ルールでランタイムのファイルが対応する
  • これらは別のディレクトリにある
  • ハイドレーションのスクリプト
    • 基本的にはよしななタイミングで`innerHTML`を更新したり関数を呼ぶだけ
    • `requestIdleCallback`や`IntersectionObserver`などにあわせて
  • `.svelte`などのUIフレームワークのコードももちろんViteでビルドする
    • インテグレーションを追加すると、Viteのプラグインリストに入ってくる
    • そして`clientBuild()`の時に使われる

なんとな〜く、わかったかな。

まとめ

  • URLに対応する`pages`をまとめたマニフェストをまず作る
  • それらルートのエントリーとなるコンポーネントをロードしていく
    • Viteで`ssrLoadModule()`する`preload()`が最初の関門
    • そのためのViteプラグインたち
  • 数多のフォーマットを、いったんJSにまとめるのがポイント
    • `.astro`をJS表現にして by WASM
    • `.svelte|vue|jsx`も全部JS表現にして
    • 依存関係やらCSSなどのアセットの相関をまとめあげ
  • `render()`でそのコンポーネント関数を実行して、HTMLを吐かせる
    • `dev`コマンドではそれをリクエスト時に実行して返す
    • `build`コマンドでは一括でやって書き出す

ViteでビルドしたものをViteでビルドしてViteでビルドする!って感じ。

SSRモードでビルドする実行パスは追えず終いだった。(つかれた)
ただしSSRモード、コードとしても後付した感がすごいので、v2に向けてコードベースをリライトしたりしそうやなって思った。

それなりに大変だったけど、Nodeのツールなおかげで実行時にもだいたい難読化されてたりしないし、デバッガー使ってコードリーディングができて楽でいいですね。

(実際にAstroでサイトを作ってみてる感想としては、動いてるし方向性としても間違いないけど、DXとしては正直まだまだと思ってるので、あと数日でほんとにメジャーバージョン出すの?って思ってたら、7月後半に延期するって記事が出てた!)