Claude Media
Claude CodeでSvelteKitを開発する — Runes記法の規約化とsv check

Claude CodeでSvelteKitを開発する — Runes記法の規約化とsv check

Svelte 5のRunesと旧記法の混在をClaude Codeに起こさせない手順。CLAUDE.mdとpath指定ルール、runesモードの強制、sv checkのhook連携までを設定例つきで示します。

Claude CodeにSvelteKitアプリを書かせると、Svelte 5のRunes($state や $props)と、Svelte 3/4の旧記法(export let や on:click)が同じプロジェクトに混ざりがちです。旧記法はSvelte 5でも動くため、動作確認では気づけません。

この混在は3層で止めます。

手順

混在を止める3層

  1. 1

    書き方を伝える

    CLAUDE.mdと.claude/rules/に、禁止する記法と代わりに使う記法を並べます。

  2. 2

    旧記法を書けなくする

    コンパイラーのrunesモードで、export letや$:をコンパイルエラーにします。

  3. 3

    結果をClaudeに返す

    sv checkをhookで走らせ、エラーを行番号つきでClaudeへ戻します。

2層目だけではon:clickと<slot />が通ってしまいます。その穴を埋める設定も、sv checkの節で扱います。

なぜ旧記法が混ざるのか

Svelte 5は旧記法を廃止していません。Legacy APIsの節には、Svelte 3/4の一部の機能は非推奨で、明記がない限り今は動くが、いずれ削除される、と書かれています。そしてコンポーネントは、runesを使うかコンパイラーオプションrunes: trueを明示した時点でrunesモードになり、その中では旧機能が使えなくなります。

つまり何も指定しない状態では、コンポーネントごとに旧記法モードとrunesモードが決まります。Claudeは学習データ中の旧記法も、新しい記法も知っています。周辺のコードが旧記法なら旧記法に寄せて書きますし、新規ファイルでどちらを選ぶかも指示がなければ揺れます。

旧記法とRunesの対応は、移行ガイドに沿うと次のとおりです。CLAUDE.mdに書く禁止リストの元になります。

旧記法(Svelte 3/4)Runes(Svelte 5)
トップレベルの let count = 0Runes(Svelte 5)let count = $state(0)
$: double = count * 2Runes(Svelte 5)let double = $derived(count * 2)
$: による副作用Runes(Svelte 5)$effect(() => { ... })
export let nameRunes(Svelte 5)let { name } = $props()
on:click={handler}Runes(Svelte 5)onclick={handler}
createEventDispatcherRunes(Svelte 5)コールバックprop
<slot />Runes(Svelte 5){@render children()} と {#snippet}
$app/storesRunes(Svelte 5)$app/state

最後の行はSvelteKit側の変更です。sv migrateのサブコマンドapp-stateが、.svelteファイル内の$app/storesを$app/stateへ書き換えます。sveltekit-3移行のタスクの1つとしても実行されます。

CLAUDE.mdに書く規約

Claude Codeは、CLAUDE.mdをセッション開始時にコンテキストへ読み込みます。ただしCLAUDE.mdはシステムプロンプトの一部ではなく、その後にユーザーメッセージとして渡される文脈です。厳密な遵守は保証されず、曖昧な指示や矛盾する指示ほど守られにくくなります。

書き方の指針も公式に示されています。1ファイル200行以内、確認できるほど具体的に書く、の2点です。「コードを整えて」ではなく「インデントは2スペース」と書く、という例が挙がっています。Svelteの規約なら次の形です。

# SvelteKit プロジェクト規約
 
- このプロジェクトは Svelte 5 の runes モード。旧記法は書かない
- 状態は `$state`、算出値は `$derived`、props は `$props()` で受ける
- 副作用の `$effect` 内で state を更新しない。まず `$derived` を検討する
- DOM イベントは `onclick` などのイベント属性。`on:click` は使わない
- 子から親への通知はコールバック prop。`createEventDispatcher` は使わない
- 子コンテンツは `{@render children()}`。`<slot />` は使わない
- ページ状態は `$app/state`。`$app/stores` は使わない
- rune を使うモジュールは `.svelte.ts` に置く(通常の `.ts` では使えない)
 
## 検証コマンド
 
- 変更後は `npm run check:strict` を実行し、エラー 0 を確認する

「使わない」だけでなく「代わりに何を使うか」を並べているのがポイントです。禁止だけを書くと、Claudeは何に置き換えるかを自分で選び、そこでまた揺れます。

runesが使える場所の制約も1行入れておきます。Svelteの説明では、runesは.svelteファイルと.svelte.js/.svelte.tsファイルで使うものです。ストアの代わりに共有状態をモジュールへ切り出すとき、Claudeが普通の.tsに$stateを書いてしまう失敗を防げます。

規約が長くなったら.claude/rules/に分ける

Svelte以外の規約も増えてくると、CLAUDE.mdが200行に近づきます。.claude/rules/にトピックごとのmarkdownを置く構成もあります。pathsフロントマターを付けたルールは、ClaudeがRead・Write・Editツールで一致するファイルを扱ったときに読み込まれます。すべてのツール呼び出しが対象ではありません。pathsのないルールは起動時に無条件で読み込まれます。

Svelteの規約をSvelteファイルだけに効かせる例です。

---
paths:
  - "src/**/*.svelte"
  - "src/**/*.svelte.ts"
---
 
# Svelte 5 規約
 
- runes モード前提。`export let` / `$:` / `on:` / `<slot />` は書かない
- `$derived` には式を渡す。関数が要るときは `$derived.by`
- 大きな API レスポンスは `$state.raw`

$state.rawと$derived.byは、Svelteのベストプラクティスにある指針です。オブジェクトや配列を$stateに入れると深くリアクティブになり、プロキシのオーバーヘッドがかかります。再代入しかしない大きなレスポンスには$state.rawが向く、と説明されています。

CLAUDE.mdの書き方そのものはClaude CodeのCLAUDE.mdを実用に引き上げる10のパターンにまとめています。

コンパイラーで旧記法を書けなくする

文章による指示は、最終的にはClaudeの判断に任されます。Svelte側の仕組みで縛れるなら、そちらが確実です。

1つ目は、プロジェクト全体をrunesモードにするコンパイラーオプションです。SvelteKit 2ではsvelte.config.jsのcompilerOptionsにrunes: trueを渡します。移行ガイドのcompatibility.componentApiの例と同じく、compilerOptionsの下に置きます。

// svelte.config.js
export default {
  compilerOptions: {
    runes: true
  }
};

SvelteKit 3ではsvelte.config.jsが使われなくなり、設定はvite.config.jsのsveltekitプラグインに渡します。compilerOptionsはそのオプションと並ぶ最上位の項目です。

// vite.config.js(SvelteKit 3)
import { sveltekit } from '@sveltejs/kit/vite';
 
export default {
  plugins: [sveltekit({ compilerOptions: { runes: true } })]
};

SvelteKit 2.62以降は、2系でもvite.config.jsに書けます。

runes: trueはnode_modulesの中のコンポーネントにも及びます。旧記法で書かれたサードパーティ製コンポーネントを使っていると、依存側がコンパイルエラーになりかねません。Viteを使うなら、dynamicCompileOptionsで対象を絞る方法がSvelteのドキュメントで案内されています。

runesモードのコンポーネントでexport letや$:を書くと、コンパイルエラーになります。Svelteのコンパイラーエラー一覧にあるメッセージは次のとおりです。

Cannot use `export let` in runes mode — use `$props()` instead
`$:` is not allowed in runes mode, use `$derived` or `$effect` instead

どちらも代わりに使う記法まで書いてあるので、Claudeが書き直す手がかりになります。ただし、エラーになるのはこの種の旧機能だけです。on:clickと<slot />の扱いは、次のsv checkの節で扱います。

2つ目は、コンポーネント単位の指定です。<svelte:options runes={true} />でそのコンポーネントをrunesモードに固定できます。逆にrunes={false}で旧記法モードに固定もできます。既存プロジェクトの移行中は、書き換え済みのファイルにrunes={true}を付けて、後戻りを防ぐ使い方ができます。

sv checkをClaudeの作業ループに組み込む

sv checkは、Svelteプロジェクトのエラーと警告を洗い出すコマンドです。使うにはsvelte-checkパッケージをdevDependenciesに入れておきます。

npm i -D svelte-check
npx sv check --threshold error

sv checkの説明にある検査は、未使用のCSS、アクセシビリティのヒント、JavaScript/TypeScriptのコンパイラーエラーです。旧記法を専用に探す項目はなく、落とせるのはコンパイラーがエラーにした記法だけです。--threshold errorを付けると、警告を出さずエラーだけを表示します。警告でも失敗させたいときは--fail-on-warningsを使います。

sv checkは変更したファイルだけを対象にできません。FAQによれば、コンポーネントのpropの名前を変えたとき、使用側のエラーは変更していないファイルに出るため、変更ファイルだけの検査では見逃すからです。Claudeが1ファイルを編集するたびにプロジェクト全体が検査される前提で組みます。

on:clickと<slot />は警告止まり

runesモードでも、旧記法のすべてがエラーになるわけではありません。--threshold errorで拾えるものと拾えないものは、Svelteのエラー一覧と警告一覧で分かれています。

くらべる

runesモードの旧記法の扱い

--threshold errorで検出

エラーになる

export let(legacy_export_invalid)、$:(legacy_reactive_statement_invalid)、$$props(legacy_props_invalid)、$$restProps(legacy_rest_props_invalid)。

--threshold errorでは出ない

警告止まり

on:clickなどのイベントディレクティブ(event_directive_deprecated)と、<slot />(slot_element_deprecated)。どちらも非推奨の警告です。

移行ガイドは、on:eventの構文を後方互換のために今もサポートしていると書いています。警告を見逃さないために、--compiler-warningsで警告コードをエラーへ格上げします。code:behaviourの組で、behaviourはignoreかerrorです。

package.jsonにスクリプトとして置くと、CLAUDE.mdにもhookにも同じ名前で書けます。

{
  "scripts": {
    "check:strict": "npx sv check --threshold error --compiler-warnings \"event_directive_deprecated:error,slot_element_deprecated:error\""
  }
}

エラー扱いにした診断が--output machineでERROR行として出るかは、手元の出力で一度確かめてから、hookの判定に使います。

PostToolUse hookで自動実行する

CLAUDE.mdに「変更後はsv checkを実行する」と書いても、Claudeが必ず実行するとは限りません。ドキュメントでも、特定のタイミングで必ず走らせたい処理は指示文ではなくhookで書くよう勧められています。

編集直後に走らせるには、PostToolUse hookを使います。Prettierの自動整形例と同じ形で、Edit|Writeでマッチさせ、標準入力のJSONから編集されたファイルのパスを取り出します。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/sv-check.sh"
          }
        ]
      }
    ]
  }
}

スクリプト側では、Svelte関連のファイルだけを対象に絞ります。--output machineを指定すると、出力が行ごとの記録になり、エラー行にはERRORが付きます。これを判定に使います。

#!/bin/bash
# .claude/hooks/sv-check.sh
file=$(jq -r '.tool_input.file_path')
case "$file" in
  *.svelte|*.svelte.ts|*.svelte.js|*.ts) ;;
  *) exit 0 ;;
esac
 
out=$(npm run check:strict -- --output machine 2>&1)
if echo "$out" | grep -q ' ERROR '; then
  echo "$out" | grep ' ERROR ' >&2
  exit 2
fi

スクリプトにはchmod +x .claude/hooks/sv-check.shで実行権限を付けます。分岐は、npmを公式ドキュメントのmachine出力例(START・ERROR・WARNING・COMPLETEDの行)を返す仮のコマンドに差し替えると確かめられます。

echo '{"tool_input":{"file_path":"src/lib/Counter.svelte"}}' | .claude/hooks/sv-check.sh; echo "exit=$?"
# 1590680326283 ERROR "codeactions.svelte" 1:16 "Cannot find module 'blubb' or its corresponding type declarations."
# exit=2
echo '{"tool_input":{"file_path":"README.md"}}' | .claude/hooks/sv-check.sh; echo "exit=$?"
# exit=0

WARNING行は出力に混ざらず、Svelte以外のファイルではsv check自体が走りません。終了コード2は、hookが処理をブロックする合図です。PostToolUseではツールがすでに実行済みなので、ブロックはできません。代わりに標準エラー出力の内容がClaudeへ渡されます。エラーの行番号つきのメッセージを受け取ったClaudeは、そのまま修正に進めます。

hookの全イベントと設定はClaude Code Hooks完全ガイドに、Lintを編集ごとに自動修正する型はPostToolUse hookのLint自動修正にあります。

終了時にまとめて検査する

プロジェクトが大きく、編集のたびに全体検査が重いなら、Stop hookに移す手もあります。Claudeが応答を終えるタイミングで1回だけ走らせる形です。ただしStop hookは終了そのものを止めて作業を続けさせます。続行が8回連続すると、Claude Codeは次のブロックを上書きしてターンを終えます。上限は環境変数CLAUDE_CODE_STOP_HOOK_BLOCK_CAPで変えられますが、stop_hook_activeを見て、解消しない条件では素通りさせる処理も要ります。使い分けはClaude Code hooksでテストを自動実行するが詳しいです。

既存プロジェクトをRunesへ移す

旧記法のコードが大量にあるなら、先に移行します。Svelteは移行スクリプトを用意しています。

npx sv migrate svelte-5

このスクリプトは、依存関係の更新、letから$stateへの書き換え、on:clickからonclickへの書き換え、<slot />から{@render}への書き換えなどを自動で行います。ただしsv migrateの説明では、svelte-5は「レガシーの移行」に分類され、新しく見つかったコードパターンへの修正は受けないとされています。コミットしてから実行し、差分を確かめます。

自動で移せない箇所も残ります。$:の文を$derivedへ安全に変換できないとき、スクリプトはsvelte/legacyのrunに置き換えます。$:はサーバー側でも実行されますが$effectは実行されないので、そのまま$effectにはできないためです。Claudeに手作業の残りを頼むときは、このrunを入口にします。

svelte/legacy から import している run を全部洗い出して。
1 ファイルずつ $derived か $effect に直し、直すたびに npm run check:strict を
実行してエラー 0 を確認してから次へ進んで。

SvelteKit 2から3へ上げるならsv migrate sveltekit-3があり、手作業が要る箇所には@migration-taskというコメントが付きます。移行を終える前に、この文字列を検索します。

一括で全ファイルを頼むより、1ファイル単位で検査を挟むほうが、崩れた箇所を特定しやすくなります。

Svelte公式のAIツールを足す

Svelteは、AIエージェント向けの公式ツールを別に配布しています。npx sv add ai-toolsで導入でき、MCPサーバー、Svelte 5を正しく書かせるためのskills、Svelteファイル編集用のサブエージェントが入ります。Claude Codeでは、コミットした.claude/settings.json経由でプラグインが有効になります。プロジェクトを開くとワークスペースを信頼するか聞かれ、承認するとインストールされます。

手動で入れるなら、Claude Codeのプラグインマーケットプレイスとして追加します。

/plugin marketplace add sveltejs/ai-tools
/plugin install svelte

MCPサーバーにはSvelteコードを解析して問題を返すツール(svelte-autofixer)があり、Svelteの案内では、コードを書いたら指摘がなくなるまでこのツールを呼ばせる使い方が示されています。案内には、CLAUDE.mdやAGENTS.mdに貼るプロンプトの例も載っています。MCPサーバーを入れない構成でも、同じ解析をコマンドで呼べます。導入手順とツールの使い分けはSvelte MCPをClaude Codeに入れる手順にあります。

npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte

Svelte 4のコードを対象にするなら--svelte-version 4を付けます。

CLAUDE.mdの規約、runesモード、sv checkのhookは、ここまでに設定した3層です。MCPツールはその上に足す4層目という位置づけです。AGENTS.mdだけを置いているプロジェクトでは注意点があります。Claude Codeは、作業ディレクトリかその上位にCLAUDE.mdもCLAUDE.local.mdも無いときにだけ、AGENTS.mdを読みます。両方を効かせたいなら、CLAUDE.mdからAGENTS.mdを@で取り込むか、/configのProject instructionsをclaude-md-and-agents-mdにします。なお、この設定で直接読まれたAGENTS.mdではInstructionsLoaded hookが発火しません。CLAUDE.mdから取り込んだAGENTS.mdは、いつもどおり発火します。

よくあるつまずき

  • runesを普通の.tsに書いてしまう: $stateは.svelte.tsか.svelte.jsでしか使えません。CLAUDE.mdに拡張子の制約を書いておくと防げます。
  • $effectで値を更新して無限ループになる: ベストプラクティスは、effectを最後の手段と位置づけ、state更新を避けるよう求めています。算出は$derivedを使います。
  • $effect内にブラウザ判定を書く: effectはサーバーで実行されないため、if (browser)で囲む必要はありません。
  • 旧記法モードで配列のpushが反映されない: 旧記法では代入がリアクティブの起点なので、pushのあとに再代入が要りました。runesモードの$stateなら変更がそのまま追跡されます。
  • hookが重くて編集が遅くなる: sv checkは全体を見る仕様です。重いときは--ignoreでビルド成果物を除くか、Stop hookに移します。--ignoreは--no-tsconfigと併用したときだけ診断対象に効き、--tsconfigを使う場合はtsconfig.jsonの指定が対象を決めます。

まとめ

旧記法の混在は、指示だけでは止まりません。コンパイラーがエラーにする旧機能と、警告止まりの旧機能を分け、後者は警告コードをエラーへ格上げしてからhookに渡すと、規約は文章ではなく検査の合否で守られます。

Go言語のプロジェクトで同じ考え方を適用した例はClaude CodeでGoアプリを開発する手順にあります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/memory
  2. svelte.dev/docs/cli/sv-check
  3. svelte.dev/docs/svelte/v5-migration-guide
  4. svelte.dev/docs/svelte/legacy-overview
  5. code.claude.com/docs/en/hooks-guide
  6. code.claude.com/docs/en/hooks
  7. svelte.dev/docs/cli/sv-migrate
  8. svelte.dev/docs/svelte/compiler-errors
  9. svelte.dev/docs/svelte/compiler-warnings
  10. svelte.dev/docs/ai/overview
この記事を共有:XはてブLinkedIn