🧊

Svelteコンパイラのコードを読む Part.3

Svelteコンパイラソースコードリーディング記事の続編の続編。

`svelte/compiler`の`compile()`を追っている途中。

前回までのあらすじ

  • `compiler.parse()`によって得られるのがSvelteのAST
  • それを使って`Component`クラスを初期化した

そしてその`Component`のインスタンスを使って、環境ごとのレンダリングをやっていくのが今回。

render_dom() と render_ssr()

https://github.com/sveltejs/svelte/tree/master/src/compiler/compile/render_dom
https://github.com/sveltejs/svelte/tree/master/src/compiler/compile/render_ssr

DOMとSSRという2つのレンダリング方法があって、引数に`Component`のインスタンスを渡す。
どっちが使われるかは、`compile()`に渡された`generate`オプション次第。

どちらも返り値は、`js`と`css`という2つのプロパティがあるオブジェクト。

  • js: ASTで表現された、Svelteのランタイムに変換されたあとのコード
  • css: ScopedになったCSSのテキストと、ソースマップ

`css`は同じものが返ってくるみたい。
`js`のほうはDOMとSSRで結果が異なる。(具体的な違いは、前回の記事を参照)

というわけで繰り返しになるけど、ランタイムで動くコードをレンダリングするのが、この関数たちの仕事。

render_dom()

やってることの流れ。

  • `Renderer`クラスの初期化
    • (後述)
  • `body`という配列の用意
    • ここに、最終的な実行コードをASTで組み上げたものを積んでいく
  • cssコンパイル
    • `Component`の`Stylesheet`から
    • ここでScopedになったCSS文字列になる
  • `Block`の適用
    • `{#if}`とか`{#each}`とかで生まれる概念
    • ブロックごとにコンテキスト(スコープ)が異なる
    • ブロックは、いくつかのノードにまたがる
  • 上層のコンポーネントから`props`として受け取るもののハンドリング
    • 後に`$$props`に置き換え
  • ASTの`instance`(`script`部)の精査
    • `AssignmentExpression`か`UpdateExpression`があったら、`$$invalidate()`するよう置き換え
  • `create_fragment()`の挿入
  • `context=module`部の挿入
  • コンポーネントの実態である`instance()`の組み上げ
    • ランタイムでも使われるやつ
  • リアクティブな変数のアップデート時に再レンダリングなどするよう紐付け
  • エクスポートするクラスのテンプレートの組み上げ
    • CustomElementの場合、`SvelteElement`
    • それ以外の場合、`SvelteComponent`
  • `js`はAST、`css`は実行可能な文字列になったものを返す

理解するのも難しければ、説明するのも難しい!

`*.svelte`コンポーネントを`parse()`して得たASTを元に得られた`Component`クラスを使って、ランタイムで動くコードのASTを、JavaScriptで動的に作ることをやってる。

ASTをJavaScriptで作る

ASTをJavaScriptで作るとはどういうことかは、その処理のコアであるこのライブラリを見たほうがはやい。

GitHub - Rich-Harris/code-red: Experimental toolkit for writing x-to-JavaScript compilers

READMEから転載したコードはこんな感じ。

import { b, x } from "code-red";

const expression = x`i + j`;

assert.equal(expression.type, "AssignmentExpression");
assert.equal(expression.operator, "+");
assert.equal(expression.left.name, "i");
assert.equal(expression.right.name, "j");

const body = b`
  const i = 1;
  const j = 2;
  const k = i + j;
`;

assert.equal(body.length, 3);
assert.equal(body[0].type, "VariableDeclaration");

Tag付きのテンプレートリテラルで書いたコード片が、ESTreeのBodyになるという仕組み。
ASTを制するものはすべてを制す!という心意気を感じる・・・。

なんしかこれを使って、動的にランタイムのコードを組み上げていく。

  • 参照だけされてる変数や純関数なら巻き上げてしまう
  • 更新されてるならインスタンスのスコープに動かす

みたいな最適化(実際はもっとたくさんのことを、あらゆる条件でやってる)を、動的にやってるというわけ。

compiler/compile/render_dom/Renderer.ts

https://github.com/sveltejs/svelte/blob/master/src/compiler/compile/render_dom/Renderer.ts

ツリーは`Fragment`で、そこに各種ラッパーに包まれたノードがぶら下がってる。
Svelte的な各ノードの要素の実態は`compiler/compile/nodes`にあって、`dom`用のラッパーと、`ssr`用のラッパーが別々に存在してるというわけ。

各ラッパは`Block`への参照を持ってて、処理の単位を協調させてるっぽい。
で、各ノード(ラッパー)がそれぞれ`render()`関数を持ってて、ランタイムで必要なときに呼ばれてアップデートされるという仕組み。

render_ssr()

  • `dom`とは別の`Renderer`を使う
  • ラッパの代わりにハンドラという層があって、各ノードを包んでる
  • 各ノードはクラスではなく、文字列を返すようになってる
    • `Renderer`の`add_string()`を呼んで、文字列を組み上げてる
    • その時点でわからないものは、`add_expression()`
  • `code-red`を使って、同様の流れでASTを組み上げるところは同じ

`render_dom()`に比べると、やはり圧倒的にコード量が少ない・・・!

component.generate()

`compile()`で行われる最後の処理。
`render_dom()`か`render_ssr()`で得られた`{ js, css }`な結果を引数に実行される。(正確には`null`が渡されることもあるけど)

ちなみにこの`Component.generate()`の返り値が、`compile()`自体の返り値になっており、ついに実行可能なJavaScriptの文字列が得られるところ。

やってることはこの通りで単純。

  • バナーの挿入
    • Svelteバージョンいくつで生成しました的な
  • ASTを歩いて、`helpers`プロパティに関数を抽出(`@`ではじまる関数を探してる)
    • これらがランタイムで呼ばれる関数名たち
  • `create_module()`
    • 引数は、AST本体、モジュールのフォーマット(`esm` or `cjs`)、`helpers`、バナー、などなど
    • (後述)
  • `css`の文字列化(既にそうなってるので代入だけ)
  • `js`プロパティの文字列化
    • `code-red`の`print()`を使ってる

ついに総仕上げで、モジュールとして書き出すところをやってる。

create_module()

https://github.com/sveltejs/svelte/blob/master/src/compiler/compile/create_module.ts

  • `esm`か`cjs`かどちらかのフォーマット
    • `import`か`require()`かで微妙に完成するコードが変わる
  • どちらも単一のモジュールを返すようになってる

ここは関数名からイメージできるそのまんまの処理だった。

まとめ

  • `render_dom()`
    • ランタイムのためのASTを、`Component`のインスタンスを使って組み上げる
    • 必要なときにブロックごとに`render()`できるように
    • `render_ssr()`も基本的には同様だが、文字列と関数を組み上げる
  • `component.generate()`
    • ASTから実行可能な文字列に変換する処理
    • モジュールとして使えるように

というわけでこれでコンパイラは一通り読めたことになる・・・はず・・。

ただコンパイル結果には、ランタイムで読み出す変数や関数がいっぱい含まれてる(`SvelteComponent`とか)ので、近いうちにそっちも読みたい。

コンパイラ総まとめ

Svelteのコンパイラのコードを読むシリーズは、ひとまずこれで完結。

  • `preprocess()`
    • Svelteの構文はそのままに、事前に処理が必要なら行う
  • `parse()`
    • SvelteのASTを生成する
  • `compile()`
    • ASTを解析し、環境ごとのASTを生成し、最終的に実行可能文字列へ

という3つの棲み分けはわかったし、Issueの読解も捗る予感がする。(コントリビュートできるかは別問題ではある)

コードを読んでみての感想は、

  • 想像してた以上に複雑だった
  • ASTに慣れてないとつらい
    • というか、基本的にAST芸である
  • コメントはそんなに書かれてないので、コード読むのそこそこ大変
  • コード内に`TODO`も結構残ってた

まあ現世におけるモダンなフレームワークの中身を読むの、どれを取っても大変なんやろうし、やはり我々は生かされておるのじゃ・・。