stin's Blog

Claude Code で Shiki のテーマを見比べられるサイトを作った (Jotai もあるよ)


Shiki というコードシンタックスハイライターライブラリがあります。

Shiki は多くのテーマバリエーションを提供しています。それらを並べて見比べたいと思い、Claude Code に作ってもらいました。

僕はつい最近(ここ1週間くらい)やっと Claude Code を使い始めました。まだまだ凝った設定とかできていませんが、すでにその便利さに感動しています。

この Web アプリ作りは Claude Code を手懐けるための良い練習になりました。

Claude Code を使うに当たって初心者ながら試していることや、その他純粋な技術的こだわりについて書いていこうと思います。

リポジトリはこちら

アプリの機能説明

Shiki のテーマをいくつか同時に表示して、見比べられる Web アプリです。

初期表示では、2つのコードブロックが異なるテーマでシンタックスハイライトされて表示されます。

コードブロックはシンタックスハイライトされたまま編集可能なテキストエリアです。入力されたコードの内容は全コードブロックで同期されています。

画面上部のドロップダウンで、コードブロックに入力するプログラミング言語の選択ができます。

また画面上部のボタンで、コードブロックの表示数(=同時に見比べるテーマの数)を増やすこともできます。

Claude Code で試したこと

Claude Code に /init コマンドは使わなかった

Claude Code に /init コマンドがあります。これは Claude Code にリポジトリ全体を調べてもらい、リポジトリで何を作っているとか、技術スタックは何だとか、テストの実行コマンドはなにかなどをまとめた CLAUDE.md を作ってもらうコマンドです。

ただ、今回は完全に一から作るリポジトリだったので、/init コマンドは使いませんでした。初手からプロンプトに実装依頼を投げて、作業開始してもらいました。既存リポジトリで途中から使い始める場合は、CLAUDE.md を手書きするより圧倒的に早いので使った方が良いでしょう。

仮にゼロから作るリポジトリでも、CLAUDE.md を手書きして、リポジトリの目的などを書いたほうが良かったかもしれません。何度もプロンプトで説明するのは面倒ですからね。

Plan Mode で始める

Claude Code に Plan Mode というのがあります。いきなり実装を始めるのではなく、実装計画を立ててもらい、それを人間がレビューして承認をして初めて作り始める機能です。

今回はアプリの規模がとても小さいので、全ての要件を書き出して、一発でどれくらいアウトプットが出せるか試してみました。Claude Code の Plan Mode で実装計画を立ててもらうため、実際に投げた要件プロンプトは以下です。

JavaScript 向けのシンタックスハイライターである Shiki の、テーマ比較ページを作成してください。

## 機能要件

Shiki のテーマを切り替えて見比べる Web サイトを作成します。

サイトを開くと、シンタックスハイライトされたコードブロックを表示します。
コードブロックの上には、テーマを選択するドロップダウンを配置します。選ばれたテーマによって、コードブロックが再レンダリングされて異なる見た目になります。

「テーマ選択 + コードブロック」のセットを複数表示できます。デフォルトでは 1 枚だけ表示しますが、ユーザーが明示的に追加ボタンをクリックすることで、さらにセットの枚数を増やせます。セットの最小幅は 360px とし、現在表示されている枚数で画面幅が埋まらない場合は 画面幅/枚数 のサイズを各セットに割り当てます。画面幅が 360px\*枚数 を下回る場合は、横スクロールで全体が見えるようにします。

コードブロックはシンタックスハイライトされたままテキスト編集が可能です。複数のコードブロックが表示されているとき、すべてのコードブロックの表示内容が同期されます。つまり、どれか 1 つのコードブロックを編集すると、他のコードブロックも同じ内容に更新されます。

「テーマ選択 + コードブロック」のセットを削除ボタンで減らすことも可能です。ただし、最低 1 つのセットは表示されるように保証してください。

コードブロックに入力されているプログラミング言語の選択も可能です。コードブロックの内容が同期されるため、言語の選択 UI は 1 つだけ設置します。言語を切り替えると、すべてのコードブロックのシンタックスハイライトが更新されます。

画面をリロードしても、コードの入力状態、それぞれのセットで選択されているテーマの状態、選択されているプログラミング言語の状態を維持します。

## デザイン要件

画面中に表示される自然言語はすべて英語とします。

コードブロック部分はそれぞれ選択された Shiki のテーマカラーが適用されるようにします。

コードブロックの外側のデザインについては、特段の指定はありません。操作に迷わないような UI の配置を心がけてください。

必要に応じて SVG アイコンなどを使用してください。

コードブロック外側の色使いはおまかせしますが、極力シンプルなものとします。テーマやプログラミング言語の選択状態に依存せず、常に同じ見た目にしてください。

styled な UI コンポーネントライブラリは使用しません。TailwindCSS を使用してデザインしてください。

## 技術要件

### ページ構成

サイトのページはトップページの 1 つだけです。ルーティングはありません。

searchParams を持ちます。

- `themes`: 表示する Shiki theme の識別子を指定する配列。表示されたセットの順番に対応します。
- `language`: 選択されているプログラミング言語の識別子。
- `code`: コードブロックに入力されているコードの内容を base64 エンコードし、CompressionStream で圧縮した文字列。

### ライブラリ

Vite + React + TailwindCSS の環境構築済みです。これをベースに開発してください。

- Shiki: シンタックスハイライター
- nuqs: searchParams のステート管理
- valibot: searchParams のバリデーションが必要なら使用する。なくてもいいならインストールしない。
- アイコンライブラリ: スタイリングに合ったものを自由に選んでください

### 転送量の配慮

Shiki のテーマデータは大きいため、画面表示に必要な分だけ動的インポートされるようにしてください。言語データも同様に、選択されて初めてブラウザにダウンロードされるように実装してください。

動的インポートは React の Suspense を使用して、ローディング中であることがわかるようにしてください。

### SEO

Shiki のテーマプレビューサイトであることが伝わるように、meta タグを設定してください。meta タグの内容はすべて英語で記述してください。

機能要件とデザイン要件、そして技術要件(非機能要件)に分けて提示しました。

デザイン要件についてはザックリした依頼になっていますが、自分がふんわりイメージしていた UI のど真ん中を作り上げてくれました。完成品のデザイン面は、一発目の依頼で作ったまま変わっていません。

機能要件についても、1度で完璧に作り上げてくれました。特に、シンタックスハイライトエディターの作り方を伝えていませんでしたが、シンタックスハイライトエリアに透明な textarea を重ねるやり方で上手く作ってくれたことは驚きでした。

非機能要件についてもかなりの精度で実装してくれました。React の use / Suspense を駆使して Shiki のロード中を適切に表現してくれました。ただ一つだけバグがあって、コードブロックを編集すると1文字ずつサスペンドが起きてしまっていました。

再び Plan Mode で1文字ずつサスペンドが起きてしまうことを伝えたら、コードから原因を発見して自力で直してくれました。割とトリッキーな不具合だと思いますが、現在の挙動を伝えただけで修正できることに驚きました。Claude Code の自走力やばい。

外部パッケージは指示した分しかインストールしようとしないのも良いですね。それ以外は自力のコードで何とかしようとします。ただ、Jotai は jotai-family とか jotai-eager みたいなユーティリティが細かく別パッケージになっており、それらも勝手にインストールしようとはしないのがちょっと面倒でした。人間がパッケージ選択込みで設計しなければならないということなのでしょう。

rules を都度メンテする

Claude Code が書くコードには納得がいかないことがあります。例えば僕は React.FC 信者ですが、Claude Code は普通の function でコンポーネントを定義します。他には、non-null-assertion (!) を平気で使ってきます。

こういった流派の違いや型安全を疎かにするコードを書いたら、プロンプトで伝えるのではなく .claude/rules/*.md をすぐメンテするようにしました。こうしておけば次から書くコードは自分のスタイルと近いものを出してくれるようになります。

型チェックや ESLint チェックを必ずしてから完了とする旨も書いておきました。これで、コード修正後に必ずチェックを実行し、エラーや警告を直し切るようになりました。eslint warning なら放置してもいいと思ってる人間とは大違いだよ!

rules を書く前にコミットしてしまったコードも、「rules を再確認してリファクタリングして」と伝えれば直してくれます。素直でいい子ですね。

通知音を鳴らす

Claude Code の作業は数十分かかることもあり、その間に何度かコマンド実行許可を要求してくることもあります。Claude Code が作業しているときは他事をしていたいので、Claude Code の作業が終わったり許可を要求したいときに人間が気付けるように、hooks 機能で通知音を鳴らすようにしました。

{
  "language": "Japanese",
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "afplay /System/Library/Sounds/Glass.aiff"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "afplay /System/Library/Sounds/Glass.aiff"
          }
        ]
      }
    ]
  }
}

これはリポジトリに含める settings.json ではなく、OS ユーザーのホームディレクトリにある ~/.claude/settings.json に書いてます。

これを設定してから、Claude Code の作業中にターミナルを見守ってあげる必要がなくなってとても体験が良いですね。

試行錯誤が大切

Claude Code は自走力があって便利ですが、完璧なコードを出力するとは限りません。表面的には動くけどコードはいまいちということがあります。

そのため、コードをしっかりレビューして、設計イメージをちゃんと伝えて作り直してもらうことが大事だと感じました。

実際この挑戦でも、動くアプリ自体は一発で出してくれましたが、コード品質を納得いくまで指示することに時間をかけました。そして何度か繰り返せば、自分も納得がいくコードに仕上げてくれることがわかりました。

技術的なこだわり

Claude Code に作ってもらったとはいえ、ちゃんと自分なりのこだわりも盛り込んでいます。

Shiki のテーマ・言語の動的インポート

Shiki は多数のビルトインテーマとプログラミング言語のシンタックスを提供します。すべてをバンドルしてしまうと、サイト閲覧時の初回ロードが重くなってしまいます。

そこで、テーマや言語が選択されたときに初めてそれらの定義モジュールがロードされるようにしました。Shiki では bundledThemesbundledLanguages というオブジェクトがエクスポートされていて、これらにぶら下がる関数を実行すると対応するテーマ・言語が動的インポートされる仕組みになっています。

import { bundledLanguages } from "shiki";

// 内部的には `await import("@shikijs/langs/javascript")` を実行している
const javascript = await bundledLanguages.javascript();

アプリ画面で言語が選択されたとき、上記のようなコードで取得した言語定義モジュールの取得を Jotai の Atom Family で行います(theme も同様)。シンタックスハイライト結果は computed Atom で計算していて、言語定義モジュールとテーマ定義モジュールが変更されたら自動で再計算されます。

Jotai

最初は Jotai なしで作ってもらっていたんですが、後から Jotai を使ってリファクタリングしてもらいました。Recoil は使ったことがあるのですが、先日 Jotai の記事を読んだこともあって使ってみたいと思っていました。

非同期が絡むステートのある Web アプリだったので、Jotai のちょうどいい練習になりました。

Jotai を導入する直前のコード (133bae40) では、グローバルな Map オブジェクトにロード済みのテーマ・言語定義の識別子を保存して、シンタックスハイライト時にロード済みかどうかチェックして、、、みたいな追いにくいコードになっていました。Jotai を導入し、シンタックスハイライターインスタンスの初期化もテーマ・言語定義のロードも入力されたコードの状態もすべて Atom に管理するようにしたことで、自前キャッシュ処理もロード済みチェックも不要になり、コードが非常に読みやすくなりました。

余分なサスペンドが発生しないように、先程の記事でも紹介されている jotai-eager も導入しました。例えばハイライト済みコードを計算する Atom では、「シンタックスハイライターインスタンスの Atom」「テーマ定義モジュールの Atom」「言語定義モジュールの Atom」「入力コードの Atom」の4つに依存しています。これらのうち「入力コードの Atom」だけが同期的に変化し、それ以外は非同期処理の結果です。jotai-eager を使わない場合、入力コードを同期的に変更しても、他の3つの Atom が Promise 故にサスペンドしてしまい、1文字入力するたびに画面がローディング中になってしまいます。jotai-eager を使えば、非同期 Atom の中身を同期的に取得できるため、入力コードの変更に伴うシンタックスハイライトの再計算も同期的に行われ、サスペンドも発生しません。

export const highlightedCodeAtomFamily = atomFamily((theme: BundledTheme) =>
  eagerAtom((get) => {
    const highlighter = get(highlighterAtom); // 非同期 Atom だが await 不要で値が取れる
    const code = get(codeAtom);
    const lang = get(languageAtom);

    const themeObj = get(themeObjectAtomFamily(theme)); // 非同期 Atom だが await 不要で値が取れる
    const langObj = get(languageObjectAtomFamily(lang)); // 非同期 Atom だが await 不要で値が取れる

    highlighter.loadThemeSync(themeObj);
    highlighter.loadLanguageSync(langObj);

    return highlighter.codeToHtml(code, { theme, lang });
  }),
);

Your URL Is Your State

ブラウザをリロードしても同じ画面が復元できることは Web アプリの必須要件です。今回のアプリでは、コードの入力状態、プログラミング言語の選択状態、表示しているテーマの組み合わせを URL に含めています。

入力しているコードは URL の上限に引っかかりにくいように CompressionStream で gzip 圧縮しています。このあたりについては、以前記事にしました。

URL と Jotai の同期は、jotai-location という Jotai 公式提供のライブラリを使いました。わざわざ URL と Atom をつなぎこむコードを書かなくて良いので便利です。

入力されたコードを URL に反映させるために jotai-effect も使用しています。useEffect の Jotai 版みたいなもので、effect 中に別の Atom に書き込みをしたりできます。コードの Atom が変化したら jotai-location のほうに書き込めば、後は jotai-location が URL に反映してくれます。

Jotai を導入する前は nuqs というライブラリを使って URL を状態として扱っていました。nuqs 自体は非常にシンプルで良いと思ったんですが、Jotai を使おうと思ったときに nuqs と Jotai の同期を取るコードを書くのが冗長に感じたので、jotai-location に乗り換えました。

まとめ

Claude Code を使って Shiki のテーマを見比べられるサイトを作りました。

Claude Code も Plan Mode でかなり精度の良いコードを出力してくれました。バグ調査やリファクタリングも自力でやってくれて、早くも Claude Code なしでは生きてゆけない 状態になりつつあります。

Jotai も初めて使ってみましたが、非同期処理を吸収して状態や依存グラフをシンプルに保てるのが気持ちいいですね。今後も状態管理が必要なアプリケーションでは最初に選択することになると思います。

それでは良い Claude Code ライフを!