Tauriでドキュメントのラッパーアプリを作ってみた感想
概要
~/.claude/doc/にzudo-doc(Astroベース)で作ったドキュメントサイトがある。Claude Codeのリソース(CLAUDE.mdファイル、コマンド、スキル、エージェント)から自動生成されるブラウズ可能なドキュメントサイトで、これを以前はElectronでスタンドアロンのmacOSアプリとしてラップしていた。今回、軽量な代替としてTauri v2で同じことを試した。そのメモ。
以前TauriとElectronの比較を調べた記事がある。
そのときは「複雑なWebアプリにはTauriは厳しそう」という結論だったが、シンプルなドキュメントビューアなら試す価値がありそうだということで、やってみた。
背景
~/.claude/doc/にはAstroベースのドキュメントサイトがあって、pnpm dev:stableでビルドしてlocalhostで配信できる。これをmacOSアプリとして開けるようにしたかった。
以前Electronでやったときはかなりスムーズだった。BrowserWindowでlocalhostを表示するだけで、ほぼ問題なく動いた。Tauriでも同じぐらい簡単にいくだろうと思っていた。
ファイル構成
~/.claude/
├── doc/ # zudo-docベースのサイト
│ ├── scripts/
│ │ └── dev-stable.js # ビルドしてからサーブするスクリプト
│ └── src/ # Astroソース
└── app/ # Tauri v2ラッパー
├── Cargo.toml
├── tauri.conf.json
├── frontend/
│ └── index.html # バンドルされたローディングページ(IPCポーリング付き)
└── src/
└── main.rs # サイドカー管理 + メニュー + IPCコマンド
動作の流れ
最終的に動くようになったアーキテクチャはこうなっている。
- アプリ起動 → Rustが
/bin/zsh -l -c "pnpm dev:stable"でサイドカープロセスを起動(PATHの継承のためにログインシェルを使う) - dev-stable.jsがHTTPサーバーを即座に起動(ポート4892)。ビルド中はローディングページを配信する
- dev-stable.jsがAstroサイトを
dist_tmp/にビルドし、完了後にdist/にアトミックにスワップ - Tauriウィンドウはバンドルされた
frontend/index.html(ローディングスピナー)を表示 - ローディングページのJSがRustの
check_readyコマンドをwindow.__TAURI__.core.invoke()経由で呼ぶ - Rustの
check_readyがlocalhost:4892/___readyにTCPでHTTPリクエストを送る - サーバーが200を返したら、JSが
navigate_to_docsというRustコマンドを呼ぶ - Rustが
window.navigate("http://localhost:4892/docs/claude")を実行 - ドキュメントページが表示される
こう書くとシンプルに見えるが、ここに至るまでにかなりの試行錯誤があった。
遭遇した問題
白い画面
起動すると白い画面のまま何も表示されない。WebViewがhttp://localhostのURLを直接読み込めなかった。

試したこと。
- 外部URLを指定 → 空白
data:URL → クラッシュ(フィーチャーフラグが必要)- バンドルHTMLから
fetchでサーバーの準備を確認 → CORSでブロック(tauri://オリジンからHTTPへのfetchは不可)


- バンドルHTMLからTauri IPCで確認 →
__TAURI__がundefined(withGlobalTauriの設定が必要だった) - IPCで
check_readyが動いた → だがlocation.hrefでのナビゲーションがtauri://からhttp://へはブロックされる - 最終的にRust側の
window.navigate()で遷移 → 動いた
1つの問題を解決するたびに次の問題が出てきて、Claude Codeが順番に潰していく感じだった。
Finderから起動できない
cargo tauri devで開発中は動くのに、ビルドした.appをFinderからダブルクリックすると起動しない。pnpmが見つからないというエラー。
GUIアプリはシェルのPATHを継承しない。ターミナルから起動すると.zshrcのPATHが効くが、Finderからの起動では素のPATHしかない。/bin/zsh -l -cでログインシェルとして起動することで解決した。
ウィンドウを閉じてもアプリが終了しない
macOSのデフォルト動作として、赤い閉じるボタンはウィンドウを隠すだけで、アプリはDockに残る。RunEvent::WindowEvent::Destroyedハンドラでアプリを終了するように書く必要があった。
ポートが使用中
前回のインスタンスのサーバープロセスが生き残っていて、新しいインスタンスがポートをバインドできない。起動時にlsof -ti :PORT | xargs killで古いプロセスを殺すようにした。
リビルド中に404
Astroはビルド時にdist/をクリーンしてから書き込む。ビルド中にリクエストが来ると、中途半端な状態のファイルが配信される。dist_tmp/にビルドしてからdist → dist_old、dist_tmp → distとアトミックにスワップすることで解決した。
サイドカープロセスの孤児化
アプリがクラッシュしたり強制終了したりすると、サイドカーのNode.jsプロセスが残る。process_group(0)でプロセスグループを作り、終了時にSIGTERM → SIGKILLでクリーンアップするようにした。
主要なコードの概要
main.rs
Rustのコードは約250行。主な部分は以下。
spawn_dev_server()—/bin/zsh -l -c "pnpm dev:stable"でprocess_group(0)付きで起動check_ready()—localhost:4892/___readyにTCPでHTTPリクエストを送り、boolを返すnavigate_to_docs()— Rust側からwindow.navigate()を呼ぶ- メニュー — リフレッシュ(Cmd+R)、DevTools(Cmd+Option+I)、ズーム操作
RunEvent::WindowEvent::Destroyedハンドラ — ウィンドウを閉じたらアプリを終了RunEvent::Exitハンドラ — サイドカーのプロセスツリーをkill(SIGTERM → SIGKILL)
dev-stable.js
- HTTPサーバーを即座に起動(ビルド中はローディングHTMLを配信)
___readyエンドポイント — ビルド完了なら200、ビルド中なら503を返すdist_tmp/にビルドしてからアトミックにリネーム:dist → dist_old、dist_tmp → distsrc/やpublic/の変更をウォッチしてリビルドを自動実行- 起動時に
lsof -ti :PORT | xargs killで古いプロセスを殺す
frontend/index.html
バンドルされたローディングページ。スピナーアニメーション付き。JavaScriptが1秒ごとにTauri IPCでcheck_readyをポーリングする。準備完了になったらnavigate_to_docsを呼ぶ。
fetch()は使えない(tauri://オリジンからHTTPへのCORSでブロック)。location.hrefも使えない(tauri://からhttp://へのナビゲーションがブロック)。すべてRust経由でやる必要がある。
Q&A
開発中にClaude Codeに聞いた質問をまとめておく。フロントエンドの人間がTauriを初めて触るときに気になるであろうことが含まれている。
Q: アプリを起動するたびにcargo tauri devコマンドを実行する必要がある?
いいえ。cargo tauri buildで一度.appバンドルをビルドすれば、/Applications/にコピーしてダブルクリックで起動できる。ビルドの流れはこう。
cd app && cargo tauri build
cp -r target/release/bundle/macos/Claude\ Resources.app /Applications/
Q: Tauriはアプリの中にNode.jsを含んでいるの?
いいえ。TauriはNode.jsをバンドルしない。Rustバイナリが/bin/zsh -l -c "pnpm dev:stable"を呼び出して、ホストマシンにインストールされているpnpm/Node.jsを使う。つまり、このアプリを動かすにはNode.jsがマシンにインストールされている必要がある。配布用のスタンドアロンアプリにするなら、Tauriの「sidecar」機能でNode.jsバイナリをバンドルする必要があるが、それだとElectronの軽量さという利点がなくなる。
Q: TauriにDevToolsはある?
ある。Cmd+Option+Iで切り替えられる(Viewメニューに追加済み)。Cargo.tomlでfeatures = ["devtools"]を有効にしているため、リリースビルドでも使える。
Q: 起動時に何が起きるのか、具体的には?
Rustがサイドカープロセスを起動 → dev-stable.jsがHTTPサーバーを即座に起動(ビルド中はローディングページを配信) → Astroサイトをビルド → ビルド完了後にdist/をアトミックにスワップ → TauriウィンドウのローディングページがIPC経由でRustに「準備完了?」とポーリング → 200が返ったらRustのwindow.navigate()でドキュメントページに遷移。
Q: このアプリはWindowsの.exeも作れる?
Tauriは仕組み上Windowsビルドをサポートしているが、このアプリはWindowsでは動かない。理由は以下。
/bin/zshをハードコード(Unixシェル)- Unixシグナル(SIGTERM)と
process_groupを使用 ~/.claude/doc/パスはmacOS固有- シェルPATH経由で
pnpm/nodeが利用可能であることを前提
これは個人用のmacOS開発ツールとして設計されている。クロスプラットフォーム対応には大幅な書き換えが必要。
Q: そもそもTauriとElectronの違いは?
- Tauri — 5-10MB程度。OSのWebKit(macOS)を使用。Rustバックエンド。軽量だがプラットフォーム差異は自分で対処する。Node.jsをバンドルしない
- Electron — 150MB程度。Chromium + Node.jsをバンドル。重いがどこでも同じ動作。
個人開発ツール(ホストにNode.jsがある前提)ならTauri、配布アプリならElectronが向いている。
Q: クロスプラットフォーム対応は自分でやる必要がある?それがElectronとの違い?
その通り。Tauriは軽量なネイティブシェルを提供するが、プラットフォーム差異はすべて自分で処理する。Electronはバッテリー込み(Chromium + Node.jsを同梱)なので、どこでも同じ動作が保証される。Tauriのトレードオフは「ハッピーパスは軽量、エッジケースは手動作業」ということになる。
Q: スタンドアロンアプリとして配布するなら、Node.jsバイナリをアプリに含める必要がある?
そう。Tauriには外部バイナリをバンドルする「sidecar」機能がある。Node.jsバイナリ + node_modules + ドキュメントソースをバンドルすることになる。ただ、そうすると軽量という利点がなくなり、「追加の手間がかかるElectron」のような状態になる。個人開発ツールならホストのNode.jsに依存するのが正解だろう。
まとめ
動いた。この用途には十分なのでこのまま使う。メモリ使用量が少ないかもしれないが、要検証。
ただし問題が多かった。Electronで同じことをやったときは、ほぼ問題なく起動してすぐアプリとして動いた。BrowserWindowでlocalhost表示するだけなので、シンプルだった。
Tauriでは、Claude Codeが大量のトライ&エラーを繰り返した。RustとNode.jsプロセスの連携をデバッグしていた。白い画面が表示される、Finderから起動しない、アプリを閉じてもプロセスが死なない、リビルドで404が出る。こういった問題が多数あった。最終的にはポーリングでNode.jsのビルドとプロセスの準備を確認する仕組みになった。これがベストかどうかはわからない。
配布を考えると、Node.jsバイナリのバンドルが必要で、WebViewはOS間で異なる。AIを使ってコーディングしても時間がかかることは想像に難くない。
個人用のWebViewランチャーとしては、メモリを食わないのであればまぁ良いんじゃ無いかなと感じた。ただ、仕事でこれをやりたいかと言われるとかなり微妙。Electronのほうが簡単で信頼できる選択肢に見える。そもそも今コレを試したのは、AIがコードを書いてくれるからという前提ががあってのこと。AIがなかったら、初手の段階でデバッグに時間がかかりすぎて諦めていたと思うし、コレをクロスプラットフォームで、さらにOSのバージョン違いみたいなところを詰めていくのはかなり泥臭い部類のデバッグになると想像された。