Read OSS

サポートシステム:リアクティブユーティリティ、マイグレーション、開発ツール、テスト

中級

前提知識

  • 第1回:アーキテクチャとコードベース全体像
  • 第3回:リアクティビティエンジン(ソースとエフェクトの理解)
  • マイグレーションを理解するための Svelte 4 構文の基礎知識

サポートシステム:リアクティブユーティリティ、マイグレーション、開発ツール、テスト

コンパイラとリアクティビティエンジンは Svelte の中核ですが、フレームワークの使いやすさはその周辺エコシステムにも大きく依存します。この記事では以下のサポートサブシステムを取り上げます。シグナルベースのリアクティビティで JavaScript 組み込み型を拡張するリアクティブユーティリティクラス、Svelte 4 ストアとの後方互換ブリッジ、マイグレーションツール、開発モードのツール群、トランジションランタイム、そしてテストインフラです。

パブリックなリアクティブユーティリティ:svelte/reactivity

svelte/reactivity モジュールは、JavaScript の組み込み型をシグナル対応でラップしたクラスを提供します。これらは reactivity/index-client.js からエクスポートされています:

export { SvelteDate } from './date.js';
export { SvelteSet } from './set.js';
export { SvelteMap } from './map.js';
export { SvelteURL } from './url.js';
export { SvelteURLSearchParams } from './url-search-params.js';
export { MediaQuery } from './media-query.js';
export { createSubscriber } from './create-subscriber.js';

各クラスはネイティブ型をラップし、すべての変更メソッドをシグナルグラフに接続します。たとえば SvelteSetSet を継承し、add()delete()clear() をオーバーライドして内部のソースシグナルに対して set() を呼び出します。一方、has() やイテレーション用の forEach().size などの読み取り系メソッドは get() を呼び出して依存関係を登録します。

このパターンはすべてのユーティリティで一貫しています:

  1. ネイティブクラスを継承する(またはネイティブインスタンスに処理を委譲する)
  2. トラッキング対象の状態に対してソースシグナルを作成する
  3. 変更系メソッドをオーバーライドしてシグナルに書き込む
  4. 読み取り系メソッド・ゲッターをオーバーライドしてシグナルから読み出す

MediaQuery は少し異なります。window.matchMedia() をラップし、メディアクエリの一致状態が変化したときに自動更新されるリアクティブな .current プロパティを公開します。createSubscriber ユーティリティは低レベルの構成要素で、外部(Svelte 以外)のリアクティビティシステムからサブスクライブできるソースシグナルを作成します。

classDiagram
    class SvelteSet~T~ {
        -#source: Source~number~
        +add(value: T): this
        +delete(value: T): boolean
        +has(value: T): boolean
        +size: number
    }
    class SvelteMap~K,V~ {
        -#source: Source~number~
        +set(key: K, value: V): this
        +get(key: K): V
        +delete(key: K): boolean
    }
    class SvelteDate {
        -#source: Source~number~
        +setTime(ms: number): number
        +getTime(): number
    }
    class MediaQuery {
        -#source: Source~boolean~
        +current: boolean
    }
    SvelteSet --|> Set : extends
    SvelteMap --|> Map : extends
    SvelteDate --|> Date : extends

ヒント: 組み込み型に対してきめ細かいリアクティビティが必要な場面では、これらのリアクティブユーティリティを活用しましょう。$state(new Map()) は Map を Proxy でラップしますが、new SvelteMap() を使えば Proxy のオーバーヘッドなしにキー単位のリアクティビティを得られます。

ストア互換性:Svelte 4 と 5 のブリッジ

.subscribe() メソッドを持つオブジェクトという Svelte 4 のストアの契約は、svelte/store モジュールを通じて今も維持されています。クライアント版の store/index-client.js は、共有実装からクラシックな writablereadablederived ストアを再エクスポートしつつ、Svelte 5 のシグナルをストアの契約に変換するブリッジ関数 toStore() も追加しています:

export function toStore(get, set) {
    var init_value = get();
    const store = writable(init_value, (set) => {
        // render_effect を通じてシグナルの変化をサブスクライブ
        // ...
    });
    return store;
}

$ プレフィックスによる自動サブスクリプション($storeName)は Svelte 5 コンポーネントでも引き続き動作します。コンパイラはストア参照を検出し、内部クライアントランタイムの $.store_get() / $.store_set() 呼び出しを生成します。legacy_mode_flag(第1回で解説)は、Svelte 4 コンポーネントが存在する場合にランタイムの追加互換コードパスを有効にします。

メインパッケージエントリと同様に、svelte/storepackage.json でクライアント/サーバー分割されています:

"./store": {
    "worker": "./src/store/index-server.js",
    "browser": "./src/store/index-client.js",
    "default": "./src/store/index-server.js"
}

マイグレーションツール:Svelte 4 から 5 へ

svelte/compiler からエクスポートされる migrate() 関数は、Svelte 4 コンポーネントを Svelte 5 構文へ自動変換します。コンパイラのパースフェーズと解析フェーズを再利用し、ソースマップを保持しつつ外科的な文字列操作を行うライブラリ MagicString を使ってテキストレベルの変換を適用します。

処理パイプラインは次のとおりです:

flowchart TD
    Source["Svelte 4 ソース"] --> Parse["parse() → AST"]
    Parse --> Analyze["analyze_component() → メタデータ"]
    Analyze --> Walk["マイグレーションビジターで AST をウォーク"]
    Walk --> Magic["MagicString で変換"]
    Magic --> Output["Svelte 5 ソース"]

主な変換内容:

  • export let proplet { prop } = $props()
  • $: derived = exprconst derived = $derived(expr)
  • $: { sideEffect() }$effect(() => { sideEffect() })
  • <slot>{@render children()}
  • on:click={handler}onclick={handler}
  • CSS :global() の記述の更新

自動マイグレーションが不可能なケース(変換の判断が難しいパターンなど)に対しては、MigrationError クラスを定義しています。そのような場合は、開発者が手動で対応すべき内容を説明する TODO コメントをソースに挿入します。

MagicString が重要な理由は、AST ではなく元のソース文字列を直接操作するからです。これにより、AST のラウンドトリップでは失われてしまう空白・コメント・フォーマットを保持しながら、export letlet { ... } = $props() へ正確に置換できます。.snip().overwrite().appendLeft() といったメソッドにより、周囲のコードに影響を与えない局所的な編集が可能になります。

開発モード機能

コンパイラオプションで dev: true を設定すると、Svelte は追加のチェックとデバッグサポートをコンパイル出力に組み込みます。

ルーングローバル

クライアントエントリーポイント index-client.js は、開発モード時にグローバルの $state$effect$derived$inspect$props ゲッターを登録します。これらは .svelte ファイル外でルーンが使用された場合に、わかりやすいエラーをスローします:

if (DEV) {
    function throw_rune_error(rune) {
        if (!(rune in globalThis)) {
            Object.defineProperty(globalThis, rune, {
                get: () => { e.rune_outside_svelte(rune); }
            });
        }
    }
    throw_rune_error('$state');
    throw_rune_error('$effect');
    // ...
}

所有権トラッキング

内部クライアントインデックスにエクスポートされている create_ownership_validator は、子コンポーネントが自分の所有でない props を変更しようとしたときに検出します。開発モードでは各エフェクトがどのコンポーネント関数によって作られたかを追跡し、不正なコンテキストからリアクティブな状態が変更された場合に警告を出します。

シグナルトレース

$inspect.trace() が使用され tracing_mode_flag が有効な場合、ランタイムはソースシグナルに生成時と更新時のスタックトレースを付加します。tagtag_proxy 関数はシグナルにデバッグラベル(ソースコード上の変数名)を追加し、開発ツールでの識別を容易にします。

HMR サポート

hmr エクスポートはホットモジュールリプレースメント (HMR) をサポートします。開発中にコンポーネントのソースが変更されると、HMR システムはページ全体をリロードせずにコンポーネントをその場で更新します。コンポーネント関数は実装を差し替えられる HMR 対応コンテナにラップされます。

flowchart TD
    subgraph "開発モード機能"
        Runes["ルーングローバル<br/>.svelte 外での使用時にエラー"]
        Ownership["所有権トラッキング<br/>props の不正変更を検出"]
        Tracing["シグナルトレース<br/>$inspect.trace() スタックトレース"]
        HMR["HMR サポート<br/>ホットコンポーネント置換"]
    end
    DEV["DEV フラグ<br/>(esm-env)"] --> Runes
    DEV --> Ownership
    Tracing_Flag["tracing_mode_flag"] --> Tracing
    DevOption["dev: true<br/>(コンパイラオプション)"] --> Ownership
    DevOption --> HMR

トランジション・アニメーションのランタイム

Svelte の transition:in:out:animate: ディレクティブは、内部クライアントランタイムの transition()animation() の呼び出しにコンパイルされます。

トランジションシステムは CSS アニメーションとエフェクトのライフサイクルを連携させます。ブロックエフェクトが新しい DOM を生成(入場)するとき、ランタイムはトランジション関数を呼び出してキーフレーム・duration・イージングといった CSS アニメーション設定を取得します。ブランチエフェクトが一時停止(退場)するとき、DOM を削除する前にアウトロトランジションが再生されます。

トランジションは要素上でカスタムイベント(introstartintroendoutrostartoutroend)をディスパッチし、コンポーネントがトランジション状態と連携できるようにします。dispatch_event 関数は without_reactive_context() を使用して、トランジションのイベントハンドラが誤ってリアクティブな依存関係を作ってしまわないようにします。

トランジション関数自体(svelte/transitionfadeflyslide など)は CSS または tick 関数を含む AnimationConfig を返します。CSS トランジションが返された場合、ランタイムは Web Animations API(Element.animate())を使用し、キーフレーム形式に合わせて css_property_to_camelcase() で変換します。

FLIP アニメーション用の animate: ディレクティブは動作が異なります。並び替えの前後で要素の位置を記録し、その2つの状態間のアニメーションを作成します。animate: がキー付き {#each} ブロック内の要素にしか使えない理由はここにあります。

sequenceDiagram
    participant Block as ブロックエフェクト
    participant Transition as transition()
    participant Element as DOM 要素
    participant WAA as Web Animations API

    Block->>Element: ブランチ生成(入場)
    Block->>Transition: transition(element, config, TRANSITION_IN)
    Transition->>Transition: キーフレームと duration を取得
    Transition->>WAA: element.animate(keyframes, options)
    Transition->>Element: 'introstart' をディスパッチ
    WAA-->>Transition: アニメーション完了
    Transition->>Element: 'introend' をディスパッチ

    Note over Block: 後で:条件が変化
    Block->>Transition: transition(element, config, TRANSITION_OUT)
    Transition->>Element: 'outrostart' をディスパッチ
    Transition->>WAA: element.animate(keyframes, options)
    WAA-->>Transition: アニメーション完了
    Transition->>Element: 'outroend' をディスパッチ
    Block->>Element: DOM から削除

ヒント: TRANSITION_GLOBAL フラグは、トランジションが直接の親ブロックが変化したときだけ再生されるか(ローカル)、いずれかの祖先ブロックが変化したときに再生されるか(グローバル)を制御します。transition:fadetransition:fade|global の違いはここにあります。

テストインフラとコントリビューション

Svelte のテストスイートは Vitest を使用しており、設定は vitest.config.js に記述されています。設定内のカスタムリゾルバが重要な役割を果たしており、テストコンテキストに応じて svelte/* のインポートを適切なソースファイルに解決し、条件付きエクスポートの動作を再現します。

テストは packages/svelte/tests/ 以下のカテゴリに整理されています:

ディレクトリ テスト対象
runtime-runes/ Svelte 5 コンポーネントの動作(デフォルトモード)
runtime-legacy/ Svelte 4 互換モード
compiler-errors/ 想定されるコンパイルエラー — 各サンプルに .errors.json を含む
compiler-warnings/ 想定される診断警告
hydration/ SSR 出力とクライアントハイドレーションの整合性
signals/ 低レベルのリアクティビティ(ソース・derived・エフェクト)
snapshot/ スナップショットファイルによるコンパイル出力の安定性
css/ CSS スコーピング、プルーニング、出力

ほとんどのテストカテゴリはサンプルパターンに従っています。各テストケースは .svelte コンポーネント、テストアサーションを含む _config.js、そして任意の期待出力ファイルを格納したディレクトリです。_config.js はコンポーネントをマウントし、操作を加え、DOM の状態をアサートするテスト関数をエクスポートします。

flowchart LR
    Sample["tests/runtime-runes/samples/my-test/"]
    Sample --> Component["main.svelte<br/>テスト対象のコンポーネント"]
    Sample --> Config["_config.js<br/>マウント・操作・アサート"]
    Sample --> Expected["_expected.html(任意)<br/>期待される DOM 出力"]
    Vitest["vitest"] --> Sample
    Sample --> Result["Pass / Fail"]

コントリビューションガイド

新しいエラーや警告を追加する場合、第1回で紹介した Markdown メッセージパイプラインを活用したワークフローになります:

  1. messages/ 内の適切なファイルにメッセージを追加する
  2. node scripts/process-messages を実行して JavaScript を生成する
  3. 生成された関数(例:e.my_new_error(node))をコンパイラまたはランタイム内で呼び出す
  4. 対応するテストカテゴリにテストケースを追加する

新しいコンパイラ機能を追加する場合、通常は3つのフェーズすべてに手を入れる必要があります:

  1. パーサー — 新しい構文を含む場合は状態処理を追加する
  2. 解析ビジター — 使用方法を検証してメタデータを収集する
  3. 変換ビジター — 適切なランタイム呼び出しを生成する(クライアント版とサーバー版の両方)
  4. ランタイム関数internal/client/ または internal/server/ に実際の動作を実装する

tests/snapshot/ のスナップショットテストは特に有用です。さまざまなコンポーネントパターンに対するコンパイル出力を正確にキャプチャしているため、コンパイラを変更したらこれらのテストを実行して、変更が生成コードにどう影響するかを確認しましょう。差分を確認した上で問題なければ、vitest --update でスナップショットを更新できます。

シリーズのまとめ

5本の記事を通じて、Svelte 5 コードベースの全体像を追ってきました:

  1. アーキテクチャ — コンパイラとランタイムのデュアル設計、条件付きエクスポート、内部 ABI
  2. コンパイラ — 最適化された JavaScript を生成する3フェーズ(パース → 解析 → 変換)
  3. リアクティビティ — ビット演算フラグを用いたプルベースのシグナル、遅延評価される derived、バッチスケジューリング
  4. DOM レンダリング — テンプレートのクローン、制御フローブロック、ハイドレーションプロトコル、SSR
  5. サポートシステム — リアクティブユーティリティ、マイグレーション、開発ツール、トランジション、テスト

Svelte のコードベースは、その複雑さに対して驚くほど整然と整理されています。コンパイラとランタイムの明確な分離、一貫したモジュールレベルの状態パターン、Markdown 駆動のエラーパイプライン、充実したテストスイートは、パフォーマンスと保守性の両立を念頭に設計されたコードベースの証です。バグ修正のコントリビューション、ツール開発、あるいは純粋なフレームワークへの好奇心、どんな動機で読んでいたとしても、これで Svelte 5 という領域の地図が手に入ったはずです。