← ブログ一覧

Tauri + xterm.js アプリの本番ビルドで vi がフリーズするバグの修正方法

はじめに

Tauri v2 + xterm.js でターミナルアプリを開発していたところ、厄介なバグに遭遇しました。

開発モードでは正常に動くのに、本番ビルドでviを起動するとフリーズする。

この記事では、原因の特定から修正までの過程を紹介します。

症状

  • npm run tauri:dev(開発モード)→ viは正常に動作する
  • npm run tauri build(本番ビルド)→ viを起動した瞬間フリーズする

同じコード、同じマシンなのに、ビルド方法だけで挙動が変わる。最初は全く見当がつきませんでした。

調査

ターミナルアプリのデータフローはこうなっています。

ユーザー入力 → xterm.js → Rust(PTY) → シェル/vi

画面表示   ← xterm.js ← Rust(PTY) ← シェル/vi

「どこでデータが止まっているのか?」を特定するため、各ステージにログを仕込みました。

  • Rust側: PTYからの読み取り、PTYへの書き込みをファイルに記録
  • フロントエンド側: WebViewのconsole.logでデータの送受信を記録

ログからわかったこと

本番ビルドでviを起動したときのログを見ると、意外なことがわかりました。

[FE-RECV] msg#49 len=65 **DSR(\e[6n)** data=...
[FE-WRITE] len=6 **CPR(\e[..R)** data=\e[2;2R
[FE-WRITE] len=6 **CPR(\e[..R)** data=\e[3;1R
[Error] ReferenceError: Can't find variable: n
    requestMode → parse → parse → _innerWrite

データの流れ自体は正常でした。 viが送った\e[6n(カーソル位置の問い合わせ)に対して、xterm.jsはちゃんと応答を返しています。

しかしその直後、xterm.js自身がJavaScriptエラーでクラッシュしていました。

原因

esbuildのminifyがコードを壊していました。 エラーの発生箇所は、xterm.js v6.0.0のrequestModeというメソッドでした。

// xterm.js のソースコード(簡略化)
requestMode(e, i) {
  let r;
  (P => (P[P.NOT_RECOGNIZED = 0] = "NOT_RECOGNIZED", ...))(r ||= {});
  let n = this._coreService.decPrivateModes;  // ← この n が消える
  ...
}

ここで使われている ||= は「Logical OR assignment」というES2021で追加された構文です。

ビルドターゲットが原因だった

プロジェクトの vite.config.ts を見ると、こう書かれていました。

build: {
  target: process.env.TAURI_ENV_PLATFORM === "windows"
    ? "chrome105"
    : "safari13",  // ← これが原因
}

Tauriのテンプレートから生成されたデフォルト設定で、ビルドターゲットがかなり古いsafari13になっていました。

||=はSafari 14.1以降の機能なので、esbuildはsafari13向けに古い構文へ変換しようとします。このトランスパイル + minify(圧縮)の組み合わせで変数nが消失し、ReferenceErrorが発生していたのです。

なぜ開発モードでは動くのか?

build: {
  minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
}

開発モードでは minify: false なので、コードが圧縮されません。トランスパイルだけなら問題なく動作するため、開発時には気づけなかったのです。

修正方法

- target: "safari13",
+ target: "safari26",

ターゲットを現在のmacOSに合わせたsafari26に変更するだけで解決しました。

||=がネイティブでサポートされているため、トランスパイルが不要になり、minifyしても壊れなくなります。

学んだこと

1. 「本番でだけ起きるバグ」はビルド設定を疑う

開発と本番で異なる設定は意外と多いです。

設定

開発モード

本番ビルド

minify

無効

有効

sourcemap

あり

なし

ターゲット

同じだが影響が異なる

トランスパイル+minifyの組み合わせで問題発生

2. ビルドターゲットは「動けばOK」ではない

safari13は「Safari 13でも動くコードを生成する」という意味です。古いターゲットを指定すると、esbuildはモダンな構文を古い書き方に変換します。この変換処理にバグがあると、本番ビルドでだけ壊れるという再現困難な問題になります。

サポートするOSに合わせて、できるだけ新しいターゲットを指定しましょう。

3. ログは「全ステージ」に仕込む

今回は「PTYの問題」「IPC通信の問題」「xterm.jsの問題」のどれかわからない状態からスタートしました。データフローの各ステージにログを仕込んだことで、「データは流れているがxterm.jsがクラッシュしている」と素早く特定できました。

おわりに

原因は「Tauriテンプレートのデフォルト設定」+「xterm.js v6のモダン構文」+「esbuildのminifyバグ」という3つの組み合わせでした。どれか1つだけでは発生せず、3つが揃って初めて起きるバグです。

こういった組み合わせ起因のバグは、優秀なAIエージェントをもってしても推測で修正しようとすると沼にハマりがちです。ログを仕込んで事実を確認し、根本原因を特定してから修正するというアプローチが、結局は最短ルートでした。