Tauri¶
デスクトップ Rust バリデーション
Vite+ でバンドルされた ReScript + React のレンダラーと、src-tauri/ 配下の小さな Rust クレートを組み合わせて動作する Tauri 2.x デスクトップアプリです。同梱サンプルは「Hello World」止まりではなく、レンダラーと Rust の間のリクエスト/レスポンス全往復を示します: 文字列を返す greet(name) コマンドと、Validation.res (zod または sury) でレンダラー側がパースしてから形を仮定する struct を返す get_info() コマンドの 2 本。Rust の struct と ReScript の record の間の差分は、React の奥深くで起きる TypeError ではなく UI エラーとして浮かび上がります。
デスクトップアプリを出荷するつもりで、Tauri の CSP、snake_case コマンドのアローリスト、そして Validation.res の境界がすでに配線済みのスタックから始めたい場合はこのテンプレートを選んでください。レンダラーは Vite + React や Electron テンプレートと同じく Vite 駆動の React サーフェスなので React の知識はそのまま流用できます。新しく学ぶ部分は Rust プロセスと tauri::generate_handler! アローリストです。
生成内容¶
my-project/
├── rescript.json # JSX enabled, depends on @rescript/react + @rescript-tauri/core
├── package.json # type: "module", @tauri-apps/cli + vite-plus dev deps
├── index.html # renderer entry — loads /src/Main.res.mjs as a module
├── vite.config.mjs # vite-plus + @vitejs/plugin-react, base: "./", strictPort 5173
├── src/
│ ├── Main.res # ReactDOM.Client.createRoot(rootEl) → <App />
│ ├── App.res # Buttons → greet / get_info → validate → render result/error
│ ├── Tauri.res # @rescript-tauri/core bindings (Core.Raw.invoke wrappers)
│ ├── Validation.res # zod or sury — parses get_info response into typed `info`
│ └── __tests__/App.test.mjs # vitest smoke test that imports App.res.mjs
├── src-tauri/
│ ├── Cargo.toml # tauri + serde + serde_json + tauri-build
│ ├── build.rs # tauri_build::build()
│ ├── tauri.conf.json # CSP, devUrl, frontendDist, beforeDev/Build (PM-aware)
│ ├── .gitignore # target/ and gen/
│ └── src/main.rs # greet / get_info commands + invoke_handler allowlist
├── README.md # IPC security model + Production Bundling section
├── LICENSE # MIT, holder = project name
├── .nvmrc # Node 24
├── .gitignore # node_modules + ReScript output + dist/ + src-tauri/target/
├── .editorconfig # 2-space indent, LF line endings
└── .github/
├── dependabot.yml # weekly npm updates
└── workflows/ci.yml # install + rescript build + vp build + vitest
ウィザードオプション¶
オプション |
効果 |
|---|---|
Project name |
npm の |
Package manager |
npm / pnpm / yarn / bun。 |
Validation library |
|
主要な依存¶
パッケージ |
用途 |
バージョン |
|---|---|---|
|
ReScript コンパイラ |
|
|
標準ライブラリ |
|
|
コンパイル済み |
|
|
React バインディング (JSX 有効化された |
|
|
|
|
|
Tauri の JS SDK ( |
|
|
レンダラーの UI ライブラリ |
|
|
バリデーションバックエンド (IPC ペイロードパーサー) |
|
|
|
|
|
レンダラー用に使われる Vite ベースのバンドラ |
|
|
Vite+ コアランタイム ( |
|
|
|
|
|
React refresh + JSX 変換プラグイン |
|
|
スモークテストランナー |
|
|
|
|
Rust の依存 (src-tauri/Cargo.toml) は tauri = "2"、tauri-build = "2"、serde、serde_json のみを pin しています — コマンドサーフェスに応じて随時追加してください。
tauri-plugin-* クレートはデフォルトでは含まれていません。必要になった時点で対応する ReScript バインディング (@rescript-tauri/plugin-dialog、@rescript-tauri/plugin-fs など、@rescript-tauri/core と並んで公開されています) と Rust プラグインクレートを合わせて追加してください。
主要なファイル¶
src-tauri/src/main.rs¶
Tauri のメインプロセス。2 つの #[tauri::command] 関数を定義します:
greet(name: &str) -> String— 「Say hello」ボタン用のプレーンな RPCget_info() -> AppInfo—serde::Serializeでシリアライズされた{ name, version, platform, arch }を返します
invoke_handler マクロが両方を IPC アローリストに登録します:
.invoke_handler(tauri::generate_handler![greet, get_info])
新しいコマンドを定義してこのリストを更新しないと、JS 側で静かに 404 になります。コマンド名は Rust 関数名の snake_case のままで、Core.Raw.invoke が JS から呼ぶ名前と完全に一致します。
src-tauri/tauri.conf.json¶
Tauri 2.x の設定ファイル。主なフィールド:
build.beforeDevCommandとbuild.beforeBuildCommand— スキャフォールド時にパッケージマネージャ固有の dev / build 起動 (pnpm dev/npm run dev/yarn dev/bun run dev) で置換されます。tauri devはウィンドウを開く前にレンダラーの dev サーバを起動し、tauri buildは先にvp buildを走らせてdist/を生成します。build.devUrl: "http://localhost:5173"—vite.config.mjsのstrictPort: trueと対になっています。別ポートが必要な場合は両方を一緒に変更してください。build.frontendDist: "../dist"—tauri build時に Tauri がこのパスから Vite+ の出力を読み込みます。app.security.csp—default-src 'self'に加えて、asset:/https://asset.localhost(Core.Raw.convertFileSrc用) とipc:/http://ipc.localhost(Windows / macOS で Tauri の invoke トランスポート用) を許可しています。本番アプリでは明示的な CSP を必ず定義してください — Tauri の CSP ガイド を参照。bundle.icon— 意図的に省略しています。アイコンソースなしのチェックアウトでもtauri devが動作するためです。tauri buildを実行する準備ができたらtauri icon path/to/source.pngでアイコンを生成し、出来上がったリストを書き戻してください。
src/Tauri.res¶
@rescript-tauri/core の薄い ReScript バインディング。同梱のラッパーは get_info を意図的に JSON.t 型として定義しています — 最終的な info レコードではありません。バリデーションは 1 つ深いレイヤーで行うためです:
let greet = (name: string): promise<string> =>
RescriptTauriCore.Core.Raw.invoke("greet", ~args={"name": name})
let getInfoRaw = (): promise<JSON.t> =>
RescriptTauriCore.Core.Raw.invoke("get_info", ~args=Js.Obj.empty())
getInfoRaw の Raw サフィックスは意図的な命名規約です: 「この値はまだバリデーションされていない」と示すことで、呼び出し側に分解前に Validation.res を通す必要があると伝えます。
src/Validation.res¶
parseInfo: JSON.t => result<info, string>。シグネチャは zod バリアントと sury バリアントで同一であるため、レンダラーの呼び出し側はどちらのライブラリが同梱されたかで分岐しない:
type info = {name: string, version: string, platform: string, arch: string}
let parseInfo: JSON.t => result<info, string>
なぜバリデーションするのか? レンダラーは Tauri のアローリストが未知のコマンドを拒否することは信頼できますが、Rust から戻ってくるstruct の形は証明されるまでただの JSON です。Rust 側で AppInfo にフィールドを足して parseInfo への追加を忘れたら? React が undefined を見る前にバリデータがパースエラーとして検出します。
src/App.res¶
インタラクティブなデモ。2 つのボタンを備えます:
Say hello は
Tauri.greet("ReScript")を実行し、返ってきた文字列を表示します。Get app info は
Tauri.getInfoRaw()を実行し、結果をバリデーションした上で、型付きレコードの<dl>か、赤いIPC validation error: ...バナーのどちらかをレンダリングします。
main.rs の get_info を書き換えて想定外のペイロード — フィールドの削除、名前変更、型変更 — を返すようにすると、React ツリーがクラッシュすることなくバリデータがそれを捕捉する様子を確認できます。
src/Main.res¶
index.html の <script type="module"> が読み込むレンダラーエントリ。<App /> を #root にマウントします:
switch ReactDOM.querySelector("#root") {
| Some(rootEl) =>
ReactDOM.Client.Root.render(ReactDOM.Client.createRoot(rootEl), <App />)
| None => Console.error("Could not find root element")
}
vite.config.mjs¶
最小限の Vite+ 設定 — vite ではなく vite-plus から defineConfig、React プラグイン、base: "./"、server.strictPort: true、server.port: 5173、clearScreen: false。相対 base は Tauri が file:// 経由で dist/index.html をロードしても本番バンドルが動くように保ちます。strictPort は 5173 番が埋まっていたときに Vite を即座に失敗させます — 黙って 5174 番にスライドすると tauri.conf.json の devUrl が誤った場所を指したままになるためです。
npm スクリプト¶
スクリプト |
説明 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
新しい Tauri コマンドを追加する¶
コマンド追加は常に 3 つのレイヤーに触れます — README に手順を載せているのと併せて、形を覚えておく価値があります。どこかを忘れると黙って失敗するためです (レンダラー側で "command set_window_title not found" になります)。
src-tauri/src/main.rsでコマンドを定義する:#[tauri::command] fn set_window_title(window: tauri::Window, title: String) -> Result<(), String> { window.set_title(&title).map_err(|e| e.to_string()) }
invoke_handlerに登録する (引き続きmain.rs内で):.invoke_handler(tauri::generate_handler![greet, get_info, set_window_title])
src/Tauri.resでバインドする:let setWindowTitle = (title: string): promise<unit> => RescriptTauriCore.Core.Raw.invoke( "set_window_title", ~args={"title": title}, )
ハンドラが UI で消費するデータを返すなら、
Validation.resでレスポンスをバリデーションします。fire-and-forget なコマンド (set_window_titleは()を返す) はこのステップを省略できます。
本番バンドル¶
tauri build には Rust ツールチェイン (rustup default stable) と、プラットフォーム固有の依存が必要です — Tauri 2 の前提条件ガイド を参照してください。Linux は webkit2gtk + libsoup、Windows は MSVC C++ ビルドツール、macOS は Xcode CLI ツールが必要です。
初回の tauri build の前にアイコンセットを生成します:
tauri icon path/to/source.png
# Writes 32x32.png / 128x128.png / icon.icns / icon.ico into src-tauri/icons/
生成されたファイル名で tauri.conf.json の bundle.icon を再度有効化してください。署名と自動更新はスターターの範囲外です — レシピへのポインタ (macOS の Developer ID notarisation、Windows の signtool、Tauri updater プラグイン) は README の "Production Bundling" セクションを参照してください。
このテンプレートでは CI で tauri build を走らせていません — OS ごとのランナーと署名情報が必要になり、プロジェクトごとに大きく異なるためです。配布戦略が固まったら自分でワークフローを追加してください。
2日目以降のレシピ¶
React コンポーネントの作成 — 同じ React 規約がレンダラーにも適用される
Import の最適化 — コマンド表面が広がっても
src/Tauri.resとsrc/Validation.resを整理された状態に保ちますデッドコードの検出 — どのレンダラーからも呼ばれなくなったコマンドを刈り取るのに便利です
プロジェクトを開いた後の ReScript 側のエディタワークフローについては 機能概要 を参照のこと。
補足¶
Tauri コマンドは
snake_caseを使います。 Rust の関数名は JS の呼び出し名にそのままマップされます —greet、get_info、set_window_title。マクロは camelCase に自動変換しません。invoke_handlerがアローリストです。 関数に#[tauri::command]を付けるだけでは何も起きません —generate_handler![...]にも追加する必要があります。これを忘れると黙って失敗します (JS の呼び出しが "command X not found" で reject されます)。レンダラーは
@tauri-apps/apiを直接 import しません。 すべての プロセス間呼び出しはTauri.res→RescriptTauriCore.Core.Raw.invoke(...)→#[tauri::command]を通ります。import { invoke } from "@tauri-apps/api"を書きたくなったときは、ほぼ確実に新しい ReScript バインディングを足すべきです。関数、クラスインスタンス、DOM ノードはブリッジを越えられません。 Tauri は引数と戻り値を JSON としてシリアライズするので、プリミティブ、owned な文字列、プレーンな struct (
serde::Serialize)、配列、マップだけが対象です。CSP の
connect-srcホワイトリストはfetch/XHR を制御するもので、invokeではありません。 Tauri の IPC は専用のipc:とhttp://ipc.localhostオリジンを使い、どちらも同梱の CSP に含まれています。削除しないでください。@rescript-tauri/coreは pre-1.0 (0.1.x) です。 1.0 が出るまでリリース間で軽い破壊的変更が入る可能性があります。バージョン pin は^0.1.xなので、破壊的な 0.minor 更新を自動採用することなく patch 更新だけが流入します。vite-plusも pre-1.0 です。 バージョン pin は^0.1.x。リリースがバンドラのワークフローを壊した場合に備えて、クラシック Vite へのスワップ手順を README に記載しています。スモークテストは意図的に小さくしています。
App.test.mjsはimport("../App.res.mjs")が resolve することだけを確認します — Tauri を起動したり IPC を動かしたりはしません。レンダラーが成長するにつれてドメイン固有のテストを追加してください。