🧊

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

気になるもののコードは読むべし、ということで。

ちなみにコードを読み始めた時点でのバージョンは、`v3.23.2`です。

自分のコードリーディング用のメモ記事なので、他の人が読んでわかりやすいかは保証できません!

読みすすめ方

GitHub - sveltejs/svelte: Cybernetically enhanced web apps

Svelteには、`runtime`と`compiler`のコードがある。

いずれはどっちにも目を通すつもりでいるけど、まずは`compiler`から読んでいく。

ただ`compiler`のコードをそのまま使うことはほぼないと思うので、実際にモジュールとしてどういう使われ方をするのかが知りたい場合は、Rollupなどバンドラーのプラグインのコードも見ることになりそう。

この記事ではコードの中身を1行ずつ(もちろん端折るけど)読んでいく。

svelte/compiler

まずOverviewで、APIとして`export`してるのはこんな感じ。

export { default as compile } from './compile/index';
export { default as parse } from './parse/index';
export { default as preprocess } from './preprocess/index';
export { walk } from 'estree-walker';

export const VERSION = '__VERSION__';

さすが、コンパイラっぽいですね。

ちなみにここまではドキュメントにも記載があって、TL;DRならこれで十分だったりもする・・!

https://svelte.dev/docs#Compile_time

それはさておき、この記事ではこの3つの関数の中身を追っていく。

  • `compile()`
  • `parse()`
  • `preprocess()`

まずは名前から察して`preprocess()`から。

svelte.preprocess()

`preprocess`はその名の通り、事前に何かしらを処理するということ。

Svelteのコンテキストでいう`markup` / `style` / `script`の3つのパーツを、それぞれ事前に処理するということで、つまりはTypeScriptやPostCSSなどを使いたい場合に必要になる。

実際にどうやって導入するかみたいな話は、この記事では公式に丸投げする・・・。

GitHub - sveltejs/integrations: Ways to incorporate Svelte into your stack

というわけで、いざ`preprocess()`のコードへ。

compiler/preprocess/index.ts

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

長さにして70行くらいで、関数のシグネチャーはこんな感じ。

function preprocess(
  source: string,
  preprocessor: PreprocessorGroup | PreprocessorGroup[],
  options?: { filename?: string }
): Promise<{
  code: string,
  dependencies: string[],
  toString(): string,
}> {}
  • 引数の`source`は、`*.svelte`ファイルの中身そのままの文字列
  • `preprocessor`は、バンドラーの設定から渡される`markup` / `script` / `style`それぞれの処理
    • だいたいはそういうモジュールが渡ってくるはず
    • これらは単一である必要はなく、同じターゲットに対して複数の処理をつなげることもできる
  • `source`の中の3部位に対して、それぞれ渡された`preprocessor`の処理を通す
    • それぞれの部位は、正規表現で切り出してる
    • やってることはただの文字列変換だが、`replace_async()`という非同期に複数の変換を行う最適化が入ってる
  • 変換後の`source`を、`Promise`で返す

ということをやってる。

GitHub - sveltejs/svelte-preprocess: A ✨ magical ✨ Svelte preprocessor with sensible defaults and support for: PostCSS, SCSS, Less, Stylus, Coffeescript, TypeScript, Pug and much more.

`svelte-preprocess`に渡されるであろう野生のPreprocessorに必要なものを渡すための層という感じで、それ以上の情報はなさそう。

というわけで次。

svelte.parse()

`parse()`は、`*.svelte`ファイルをその名の通りパースして、ASTにして返す処理。

これは実は次に見ていく予定の`compile()`の中で使われてる関数で、バリデーションなどは行わず、単にASTに置換するだけのことをやるらしい。

compiler/parse/index.ts

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

またもシグネチャーはこんな感じ。

function parse(
  template: string,
  options: ParserOptions = {}
): {
  html: TemplateNode,
  css: Style,
  instance: Script,
  module: Script,
} {}
  • 内部的に`Parser`というクラスを使ってて、処理の本体はそっちにある
  • バリデーションは行わないって書いてあったけど、`style`部が複数あったりするとエラーにしたりはする
  • 最終的に返すSvelteのASTは、4つのパーツから成る
    • html: `markup`部
    • css: `style`部
    • instance: `script`部
    • module: `script(context=module)`部 = Svelteの概念

Parserクラス

  • パースした結果は自身のプロパティに溜め込んでいく
    • `html`と`css`と`js`というプロパティに積んでいく
  • まずはHTMLのフラグメントとして読みはじめる
    • `script`と`style`に当たったら、それ用の特別な処理
    • それ以外はいっしょくたに扱う
  • Svelte独自の構文も当然ここで解析する
    • `svelte:`要素とか
    • `{#if}`の独自テンプレート構文とか
    • 独自のバインディング属性とか
  • ASTを歩くためのライブラリは以下
    • js: `acorn` + `estree-walker`
    • css: `css-tree`
    • html: 独自の実装
  • 独自のHTMLパーサーでは独自の構文をさばく
  • `Parser`のコンストラクタ内でパース処理はすべて終わってる

そしてこの4つのパートからなるASTを、次の`compile()`が処理するという流れ。

SvelteのASTの様子

元気なASTの様子を見たい場合は、ここで試せるようになってる。

AST explorer

プルダウンで`HTML`を選んでから、`svelte`を選ぶ。

まとめ

  • `svelte/compiler`は主に3つの関数を`export`してる
  • 1. `preprocess()`
    • TypeScriptなど変換が必要なコードを事前に処理する
    • 処理の実態はプリプロセッサがやるので、そのIN/OUTを調整してる
  • 2. `parse()`
  • 3. `compile()`
    • 次回以降に

つまりコンパイラのすべては、ほぼ`compile()`に集約されているということ。

それでは次回へ続く。