🧊

Astro のコードを読む Part.2

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

この続き。

概況について把握できたので、次は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`は別のリポジトリになってる。

GitHub - withastro/language-tools: Language tools for Astro

LSP方面のコードはまたいずれ。

astro dev

ローカルに開発用のサーバーを建てるコマンド。

やってることは、

  • Viteの設定をまとめる
    • 初期設定 + ユーザーの設定 + CLIでの設定
  • Viteのサーバーを建てる

なんとこれだけ。

その都度で各フックを呼んだりして挙動を調整できるようになってるけど、基本的にはViteに丸投げしていくスタイル。

その分、Viteの自作プラグインなんかが仕事をしてるので、そういう意味でNodeのコンテキストと、ViteSSRのコンテキストは区別して考えないとハマる。

ということが、`CONTRIBUTING.md`にも書いてあった。

https://github.com/withastro/astro/blob/9ef92e0a3d604c479bb325eab073e7d3c896d388/CONTRIBUTING.md#code-structure

Viteプラグインの山

https://github.com/withastro/astro/blob/9ef92e0a3d604c479bb325eab073e7d3c896d388/packages/astro/src/core/create-vite.ts

`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`コマンドでだけ使用されるプラグイン

https://github.com/withastro/astro/blob/9ef92e0a3d604c479bb325eab073e7d3c896d388/packages/astro/src/vite-plugin-astro-server/index.ts

Viteの`configureServer`を使って、ローカルに建てる開発サーバーの挙動を変えるプラグイン

大まかには、

  • `RouteManifest`と呼ばれるプロジェクト構造を、ソースの更新にあわせて更新
  • Viteのローカルサーバーへミドルウェアを追加して、リクエストをさばく
  • その際、さっきのマニフェストを使って実際の処理へルーティング
  • ファイルの更新時に、マニフェストを再生成

ってことをやってる。

Viteのデフォルトのミドルウェアを潰してたりもしてて、なかなか生々しい感じ。

createRouteManifest()

プロジェクトのページ構造を抽象化したマニフェスト

https://github.com/withastro/astro/blob/9ef92e0a3d604c479bb325eab073e7d3c896d388/packages/astro/src/core/routing/manifest/create.ts#L168-L169

この`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を返す

リクエストに対し、事前にパースしたマニフェストから対応するものを探し、レスポンスするのが仕事。

レスポンスしたいコンポーネントがわかったら、事前ロードするあたりの流れは、

と、ここまでが開発用のサーバーでリクエストをレスポンスする一連の流れ。

次は、`.astro`みたいなファイル自体を処理するローダープラグインを読む。

vite-plugin-astro

`.astro`ファイルを処理するプラグイン。Goで書かれたコンパイラを使ったりしてるのもココ。

https://github.com/withastro/astro/blob/9ef92e0a3d604c479bb325eab073e7d3c896d388/packages/astro/src/vite-plugin-astro/index.ts

プラグイン作法に則り、`load()`でやってる処理が本体。

これらが全部、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

おまけみたいなもの。

https://github.com/withastro/astro/blob/9ef92e0a3d604c479bb325eab073e7d3c896d388/packages/astro/src/vite-plugin-astro-postprocess/index.ts

`Astro.glob()`を使ってる`.astro`ファイルをよしなにする最後の一手間とのこと。

`Astro.glob()`が`.astro`でしかたたけないっていうissueがあった気がしたけど、そういうことっぽい。

まとめ

基本的には、Svelteのそれと一緒でコンパイル時に中間コードを生成して、それを別のコンテキストで利用するってところがトリッキーではあるが、まぁ納得って感じ。

Viteに依存してるとはいえ、綺麗に乗っかってるわけではないってのがよくわかる回だった。(SvelteKitのコード読んでる時も同じこと思ってたけど)

正直なところ、薄目で読んでるだけでも迷子になりまくるコードベースなので、ちゃんと全貌を理解するのは諦めたほうがいいかもなと悟った回でもあった。

ViteやRollupやら、バンドラ自体にそれなりに習熟してないと、コードベースに対してはほぼ何もできんな・・って思ったし、さすが`snowpack`作ってた人たちよなーっていう。

まぁ`.astro`ファイル内でだけ使える特殊な構文の裏側が、ある程度はわかってよかったかなと。

コマンドとしては`build`が残ってるので、それを次回に読んで終わりとしたい。