🧊

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

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

`compiler`がエクスポートしてる3つの主な関数のうち、`preprocess()`と`parse()`を読んだのが前回の記事。

今回は残りの1つであり、最大の山場でもある`compile()`を追っていく。

TL;DR

ざっと雰囲気をつかみたい場合は、Svelteのコントリビューターが書いたまとめ記事があるので、そちらを読むほうが絶対に良いです。

The Svelte Compiler Handbook | Tan Li Hau

動画がいい場合はこっち。

https://www.youtube.com/watch?v=e2pGS1eqja8

ここからは、この記事のアウトラインに沿ってコードを1行ずつ読んでいく・・ってことをやります。

コンパイル結果

先に、この単純なコンポーネントである`App.svlete`を、`compile()`に渡すとどうなるかを見ておく。

const { compile } = require("svelte/compiler");

const result = compile(
  `
  <script>
    let name = "svelte";
  </script>
  <p>Hello {name}</p>
`,
  {
    generate: "dom", // or "ssr"
  }
);

console.log(result);

というイメージのコード。

`dom`つまりCSR用の場合の結果はこうなる。

/* App.svelte generated by Svelte v3.23.2 */
import {
  SvelteComponent,
  detach,
  element,
  init,
  insert,
  noop,
  safe_not_equal,
} from "svelte/internal";

function create_fragment(ctx) {
  let p;

  return {
    c() {
      p = element("p");
      p.textContent = `Hello ${name}`;
    },
    m(target, anchor) {
      insert(target, p, anchor);
    },
    p: noop,
    i: noop,
    o: noop,
    d(detaching) {
      if (detaching) detach(p);
    },
  };
}

let name = "svelte";

class App extends SvelteComponent {
  constructor(options) {
    super();
    init(this, options, null, create_fragment, safe_not_equal, {});
  }
}

export default App;

コンポーネントのクラスがそのまま返る。
`fragment`は剥き身のコンポーネントのようなイメージで、コードでも度々出てくる。

`ssr`の場合はこうなる。

/* App.svelte generated by Svelte v3.23.2 */
import { create_ssr_component, escape } from "svelte/internal";

let name = "svelte";

const App = create_ssr_component(($$result, $$props, $$bindings, $$slots) => {
  return `<p>Hello ${escape(name)}</p>`;
});

export default App;

コンポーネントを返す関数を含んだオブジェクトが返る。DOMはもちろんただの文字列の表現。

というように、コンポーネントを実行可能なコードにコンパイルする処理なのである。

ちなみにこの様子は、公式のREPLでオプションをいじればすぐ見れるのでおすすめ。

https://svelte.dev/repl

svelte.compile()

さて本題のコードへ。

`parse()`で得たSvelte流のASTを元に、Svelteのランタイムに変換していくステップ。

コンパイラの肝であり、ここを読めばコンパイル時に何が成されているのかのすべてがわかるはず・・!

compiler/compile/index.ts

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

毎度のざっくりシグネチャーはこちら。

function compile(
  source: string,
  options: CompileOptions
): {
  js: { code: string };
  css: { code: string };
  ast: Ast;
  warnings: Warning[];
  vars: Var[];
  stats: Stats;
} {}
  • `source`は受け取った`*.svelte`ファイルの文字列そのもの
  • 返り値にある`js.code`と`css.code`が実際に実行されるコード
    • REPLで見れたやつ
    • バンドラー経由でファイルに書き出されるやつ
  • `warnings`はまんま注意
  • `vars`はプラグイン向けのメタデータ
  • `stats`は各ステップの実行時間などを取ってある

オプションの詳細は、APIのDocsを見るべし。

  • `dom`か`ssr`か対象とする環境
  • `esm`か`cjs`かフォーマット
  • CustomElementにするかどうか
  • コメントを残すか

などなど、コンパイラっぽい設定項目がある。

https://svelte.dev/docs#svelte_compile

で、この関数でやってる処理は大きく4つ。

  • `parse()`を使ったAST化
  • ASTを元に、`Component`クラスへの変換
    • 変数の参照や、依存関係の洗い出し
  • その`Component`をターゲット環境向けにレンダリング
    • ランタイムコードのレンダリング
    • 必要なときに必要な部分がアップデートされるように
  • コード文字列として生成

察するに、`Component`とレンダリングまわりの2本柱って感じ。

最初の`parse()`は前回見たやつなので割愛して、2つ目から見ていく。

compiler/compile/Component.ts

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

`*.svelte`ファイルはそれぞれがコンポーネントという単位で扱われて、その実態がこのクラス。
コンストラクタでASTを受け取って、それを再帰で歩きながら精査する。

コンストラクタでやってることはこんな感じ。

まずはCSSまわりからはじまる。

つぎに、`markup`と`script`の部分。

  • `svelte:option`要素があればそれのパース
    • コンポーネント全体へ関係するオプションなので
    • `CustomElement`としての指定があるかどうかも
  • `context=module`な`script`部の精査
    • 変数名に`$`を使ってたらエラーにする
    • Writableな変数の宣言のマーキング
  • 通常の`script`部の精査
    • 同様に変数名の精査や、`writable`な変数のマーキング
    • リアクティブな変数のマーキングも
    • 再代入による値の更新部分のマーキング
  • `markup`部を表現する`Fragment`クラスの初期化
    • `html`のASTを受け取る
    • 再帰でASTの`type`からそれぞれのクラスに仕分けていく
    • Svelteで利用できるテンプレートの構文は、すべてここで定義されてる
    • 不正な記述があったらここで弾かれる(a11y的にこの属性をつけろとかもここであわせてみてる)
    • `{#if}`とか`{#each}`とか、ブロックごとにスコープを区切りながら
    • 参照されてたら、変数に`referenced`とマーク

そして最後にまとめの処理。

  • 再び`script`部の精査
    • この時点ですべての変数やその用途がわかってる
    • `markup`部で新たに参照されてたりするのでそれをアップデート
    • 存在しない変数を参照してたら警告するなども
    • リアクティブな宣言を抽出して、依存グラフを作る
    • `import`してるものがあればそれもココでチェックしておく
  • scopedなCSSの`class`属性の付与
    • 最初の方で用意したハッシュ
  • unusedなCSSセレクタの警告

まずは`script`部と`markup`部のASTを掘っていって、リアクティブな依存関係や参照の関係を明らかにして、最後に最適化するってのがメインの処理だった。

これでASTを元に必要な情報を持った`Component`のインスタンスを作るところまでできたので、次はそれをレンダリングしていく。
この時点ではまだ書き出しできる文字列にはなってない。

プロパティ / メソッド

そのほか、`Component`クラスに生えてるめぼしいプロパティたち。

  • `stylesheet`
    • CSSに関するもの
  • `vars` / `var_lookup`
    • コンポーネント内で使われてる変数のリスト
    • 実際に使われてるかとか、Writableかとか、`props`として受け取るかなどのフラグがある
  • `imports`
    • `import`してるもの
  • `fragment`
    • `markup`部を表すツリー
  • `instance_scope` / `instance_scope_map`
    • `script`部
  • `reactive_declarations` / `reactive_declarations_nodes`
    • リアクティブな定義
  • `slots`
    • `slot`要素を使ってれば
  • `used_names` / `globally_used_names`
    • 使用された変数名、新たに割り振った変数名
    • `t0`とか`h1`とか

メソッドとしては、`get_unique_name_maker()`みたく、一時的な変数名を衝突しないように割り当てるやつとかもある。

まとめ

  • `compile()`を経てJavaScriptCSSは実行可能な文字列になる
  • `compile()`は、まず`parse()`を使ってSvelteのASTを用意する
  • そのASTを使って、`Component`クラスを初期化する
    • ここでやってるのは、変数とその依存関係の精査
    • ASTを使って、ASTを変換しながらそれをプロパティに持つクラスに置き換えてってる感じ

さて、この`Component`を使ってランタイムのコードをレンダリングするのが次にやること。

次回へ続く。