この続き。
概況について把握できたので、次はCLIのコマンドを読んでいく。
一ヶ月でバージョンが`1.0.0-beta.9`から、`1.0.0-beta.27`になっており・・・、この時点でのハッシュは`9ef92e0a3d604c479bb325eab073e7d3c896d388`でした。
さて、CLIで利用できる主なサブコマンドは次のとおり。
- `add`
- `docs`
- `dev`
- `build`
- `preview`
- `check`
- `telemetry`
`check`と`dev`と`build`以外は、自明なので割愛。`telemetry`は匿名での利用状況を集める設定をするやつで、現状ではどのコマンドが使われてるかだけを収集してるとのこと。
astro check
ビルドの前に実行すると、間違いやエラーに気づけて便利という立ち位置のコマンド。CIなんかでやるといいよとのこと。
やってることは単純で、
- プロジェクト内の`.astro`ファイルをすべて探す
- `@astrojs/language-server`をローカルに立てて、全部読ませてみる
- なんらかのエラーが検知されてれば、それを表示
ってだけ。
ちなみに、`@astrojs/language-server`は別のリポジトリになってる。
LSP方面のコードはまたいずれ。
astro dev
ローカルに開発用のサーバーを建てるコマンド。
やってることは、
- Viteの設定をまとめる
- 初期設定 + ユーザーの設定 + CLIでの設定
- Viteのサーバーを建てる
なんとこれだけ。
その都度で各フックを呼んだりして挙動を調整できるようになってるけど、基本的にはViteに丸投げしていくスタイル。
その分、Viteの自作プラグインなんかが仕事をしてるので、そういう意味でNodeのコンテキストと、ViteSSRのコンテキストは区別して考えないとハマる。
ということが、`CONTRIBUTING.md`にも書いてあった。
Viteプラグインの山
`dev`コマンドで使われるViteの自作プラグインは以下の通り。
- `vite-plugin-astro-server`
- `vite-plugin-astro`
- `vite-plugin-astro-postprocess`
- `vite-plugin-config-alias`
- `vite-plugin-env`
- `vite-plugin-integrations-container`
- `vite-plugin-jsx`
- `vite-plugin-markdown`
- `vite-plugin-scripts`
そういうわけで、ViteのプラグインおよびRollupのプラグイン機構について多少なりとも理解がないと、この先を進むのは相当に辛いものになりそう。
https://rollupjs.org/guide/en/#plugin-development
https://vitejs.dev/guide/api-plugin.html
vite-plugin-astro-server
`dev`コマンドでだけ使用されるプラグイン。
Viteの`configureServer`を使って、ローカルに建てる開発サーバーの挙動を変えるプラグイン。
大まかには、
- `RouteManifest`と呼ばれるプロジェクト構造を、ソースの更新にあわせて更新
- Viteのローカルサーバーへミドルウェアを追加して、リクエストをさばく
- その際、さっきのマニフェストを使って実際の処理へルーティング
- ファイルの更新時に、マニフェストを再生成
ってことをやってる。
Viteのデフォルトのミドルウェアを潰してたりもしてて、なかなか生々しい感じ。
createRouteManifest()
プロジェクトのページ構造を抽象化したマニフェスト。
この`RouteManifest`を作るために、`/pages`ディレクトリ中のファイルを走査していって、
- `.astro`と`.md`のファイルを見つけてページ判定
- `.js`と`.ts`のファイルを見つけてAPIのエンドポイント判定
- パスの`[...slug]`みたいなダイナミックなものも判断
ちなみにこのマニフェストは、serialize/deserialize可能になってた。
handleRequest()
ローカルサーバーに対するリクエストをさばく処理。
- SSR用のアダプタを利用してるかチェック
- Nodeの`http.IncomingMessage`を、Fetch APIの`Request`に変換
- ここで冒頭の`webapi`のポリフィルが効いてくる
- マニフェストからマッチするルートを探す
- 見つからなければ404を返す
- 独自の`404.astro`があれば、それを返す
- 該当するルートを表すコンポーネントを、SSRするために事前ロード(というかコンパイル)
- ルートがエンドポイント(`route.type = endpoint`)なら呼んでみて
- `Response`が返る(`res.type: response`)なら、それはAPIなので、そのままレスポンスを返す
- そうでない場合(`res.type: simple`)は、生成されるファイルを返す
- エンドポイントでないならページなので、SSRした結果を返す
- さっき事前ロードしたコンポーネントを使う
- ここまでで例外が出たら、500を返す
リクエストに対し、事前にパースしたマニフェストから対応するものを探し、レスポンスするのが仕事。
レスポンスしたいコンポーネントがわかったら、事前ロードするあたりの流れは、
- まずは`preload()`
- https://github.com/withastro/astro/blob/9ef92e0a3d604c479bb325eab073e7d3c896d388/packages/astro/src/core/render/dev/index.ts#L74
- 先にViteでコンポーネント自体をSSRする
- `viteServer.ssrLoadModule()`すると、対応するローダーが呼ばれる(つまり後述の`vite-plugin-astro`)
- そして`ssr()`
- https://github.com/withastro/astro/blob/9ef92e0a3d604c479bb325eab073e7d3c896d388/packages/astro/src/core/render/dev/index.ts#L88
- コンポーネントではなく、リクエストされたページそのものをSSRするという意味
- HMR用の`script`要素を埋め込んだり
- SFCで書かれたCSSなどを`link`要素で埋め込んだり
- `coreRender()`したものをレスポンスとして返す
- https://github.com/withastro/astro/blob/9ef92e0a3d604c479bb325eab073e7d3c896d388/packages/astro/src/core/render/core.ts#L85
- `renderPage(result, Component, pageProps, null)`がキモ
- この`Component`が、後述のプラグインで`.astro`を`.ts`にしてあるやつ
- Astroコンポーネントのfrontmatterで使えるグローバルスコープとかもこのへんで定義される
- https://github.com/withastro/astro/blob/9ef92e0a3d604c479bb325eab073e7d3c896d388/packages/astro/src/runtime/server/index.ts#L541
- ページであればHTMLが得られたはずなので、`Response`に載せて返す
と、ここまでが開発用のサーバーでリクエストをレスポンスする一連の流れ。
次は、`.astro`みたいなファイル自体を処理するローダープラグインを読む。
vite-plugin-astro
`.astro`ファイルを処理するプラグイン。Goで書かれたコンパイラを使ったりしてるのもココ。
プラグイン作法に則り、`load()`でやってる処理が本体。
- `style`と`script`はここで判別されて先に処理
- `.astro`内で定義されてるscoped-styleとか
- HTMLとして生成すべきもののコンパイルは2段構え
- `cachedCompilation()`: `.astro` > `.ts`
- `esbuild.transform()`: `.ts` > `.js`
- `cachedCompilation()`
- キャッシュしつつコンパイルするそのまんまのやつ
- `compile()`の本体はこっち
- https://github.com/withastro/astro/blob/9ef92e0a3d604c479bb325eab073e7d3c896d388/packages/astro/src/vite-plugin-astro/compile.ts#L37
- `compile()`
- https://github.com/withastro/compiler
- このリポジトリの、`packages/compiler`以下にあるJSの実装を通して、WASMを呼ぶ
- WASMはTinyGoで変換したもの
- Go(WASM)でやるコンパイルは3段構え
- Tokenize > Scan > Print
- ASTのパースには、 https://github.com/tdewolff/parse を使ってる
- `.astro`ファイルを、HTML文字列を返す`.ts`のコードに変換する
- Printでは、`$$renderComponent`みたいな中間コードがいっぱい埋め込まれる
- Svelteのコンパイル結果みたいな、別のところで定義してあるモジュールを後から使うやつ
- https://github.com/withastro/astro/blob/9ef92e0a3d604c479bb325eab073e7d3c896d388/packages/astro/src/runtime/server/index.ts
- 最後に、`dev`コマンドなので`import.meta.hot`のおまじないをくっつける
これらが全部、ViteのSSRのコンテキストで動いてる。
実際に変換されるコードたち
たとえばこんな`.astro`を変換するとする。
--- --- <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>Astro</title> </head> <body> <h1>Astro</h1> </body> </html>
Goのコンパイラで`.ts`に変換すると、こんな風に。
"import { Fragment, render as $$render, createAstro as $$createAstro, createComponent as $$createComponent, renderComponent as $$renderComponent, unescapeHTML as $$unescapeHTML, renderSlot as $$renderSlot, addAttribute as $$addAttribute, spreadAttributes as $$spreadAttributes, defineStyleVars as $$defineStyleVars, defineScriptVars as $$defineScriptVars, createMetadata as $$createMetadata } from "/@fs/Users/leader22/Codes/tryastro/node_modules/astro/dist/runtime/server/index.js"; export const $$metadata = $$createMetadata("/src/pages/index.astro", { modules: [], hydratedComponents: [], clientOnlyComponents: [], hydrationDirectives: new Set([]), hoisted: [] }); const $$Astro = $$createAstro("/src/pages/index.astro", 'https://astro.build', 'file:///Users/leader22/Codes/tryastro/'); const Astro = $$Astro; //@ts-ignore const $$Index = $$createComponent(async ($$result, $$props, $$slots) => { const Astro = $$result.createAstro($$Astro, $$props, $$slots); Astro.self = $$Index; return $$render`<html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>Astro</title> <!--astro:head--></head> <body> <h1>Astro</h1> </body></html>`; }); export default $$Index;
Astroコンポーネントのコードってわけで、これを`esbuild`で最適化しつつ`.js`にする。
import { render as $$render, createAstro as $$createAstro, createComponent as $$createComponent, createMetadata as $$createMetadata } from "/@fs/Users/leader22/Codes/tryastro/node_modules/astro/dist/runtime/server/index.js"; export const $$metadata = $$createMetadata("/src/pages/index.astro", { modules: [], hydratedComponents: [], clientOnlyComponents: [], hydrationDirectives: /* @__PURE__ */ new Set([]), hoisted: [] }); const $$Astro = $$createAstro("/src/pages/index.astro", "https://astro.build", "file:///Users/leader22/Codes/tryastro/"); const Astro = $$Astro; const $$Index = $$createComponent(async ($$result, $$props, $$slots) => { const Astro2 = $$result.createAstro($$Astro, $$props, $$slots); Astro2.self = $$Index; return $$render`<html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>Astro</title> <!--astro:head--></head> <body> <h1>Astro</h1> </body></html>`; }); export default $$Index;
で、これがページをSSRするときに利用されるというわけ。
vite-plugin-astro-postprocess
おまけみたいなもの。
`Astro.glob()`を使ってる`.astro`ファイルをよしなにする最後の一手間とのこと。
`Astro.glob()`が`.astro`でしかたたけないっていうissueがあった気がしたけど、そういうことっぽい。
まとめ
基本的には、Svelteのそれと一緒でコンパイル時に中間コードを生成して、それを別のコンテキストで利用するってところがトリッキーではあるが、まぁ納得って感じ。
Viteに依存してるとはいえ、綺麗に乗っかってるわけではないってのがよくわかる回だった。(SvelteKitのコード読んでる時も同じこと思ってたけど)
正直なところ、薄目で読んでるだけでも迷子になりまくるコードベースなので、ちゃんと全貌を理解するのは諦めたほうがいいかもなと悟った回でもあった。
ViteやRollupやら、バンドラ自体にそれなりに習熟してないと、コードベースに対してはほぼ何もできんな・・って思ったし、さすが`snowpack`作ってた人たちよなーっていう。
まぁ`.astro`ファイル内でだけ使える特殊な構文の裏側が、ある程度はわかってよかったかなと。
コマンドとしては`build`が残ってるので、それを次回に読んで終わりとしたい。