nodnarbnitram/claude-code-extensions

tauri-v2

Tauri v2+ cross-platform app development with Rust backend.

ソースを見る
リポジトリの原文

見出し、例、コード、表、リンク、参照画像を含む原文を表示しています。

Tauri v2+ Development Skill

Build cross-platform desktop and mobile apps with web frontends and Rust backends.

Before You Start

This skill prevents 8+ common errors and saves ~60% tokens.

MetricWithout SkillWith Skill
Setup Time~2 hours~30 min
Common Errors8+0
Token UsageHigh (exploration)Low (direct patterns)

Known Issues This Skill Prevents

  1. Permission denied errors from missing capabilities
  2. IPC failures from unregistered commands in generate_handler!
  3. State management panics from type mismatches
  4. Mobile build failures from missing Rust targets
  5. White screen issues from misconfigured dev URLs

Quick Start

Step 1: Create a Tauri Command

rust
// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: String) -> String {
    format!("Hello, {}!", name)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Why this matters: Commands not in generate_handler![] silently fail when invoked from frontend.

`main.rs` stays thin: src-tauri/src/main.rs should only be a thin passthrough — all application logic lives in lib.rs: ``rust // src-tauri/src/main.rs #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { app_lib::run(); } This split is required for mobile builds — Tauri replaces main() with mobileentrypoint` on mobile targets.

Step 2: Call from Frontend

typescript
import { invoke } from '@tauri-apps/api/core';

const greeting = await invoke<string>('greet', { name: 'World' });
console.log(greeting); // "Hello, World!"

Why this matters: Use @tauri-apps/api/core (not @tauri-apps/api/tauri - that's v1 API).

Step 3: Add Required Permissions

json
// src-tauri/capabilities/default.json
{
    "$schema": "../gen/schemas/desktop-schema.json",
    "identifier": "default",
    "windows": ["main"],
    "permissions": ["core:default"]
}

Why this matters: Tauri v2 denies everything by default - explicit permissions required for all operations.

Critical Rules

Always Do

  • Register every command in tauri::generate_handler![cmd1, cmd2, ...]
  • Return Result<T, E> from commands for proper error handling
  • Use Mutex<T> for shared state accessed from multiple commands
  • Add capabilities before using any plugin features
  • Use lib.rs for shared code (required for mobile builds)
  • Use #[cfg_attr(mobile, tauri::mobile_entry_point)] on pub fn run() in lib.rs for mobile compatibility

Never Do

  • Never use borrowed types (&str) in async commands - use owned types
  • Never block the main thread - use async for I/O operations
  • Never hardcode paths - use Tauri path APIs (app.path())
  • Never skip capability setup - even "safe" operations need permissions

Common Mistakes

Wrong - Borrowed type in async:

rust
#[tauri::command]
async fn bad(name: &str) -> String { // Compile error!
    name.to_string()
}

Correct - Owned type:

rust
#[tauri::command]
async fn good(name: String) -> String {
    name
}

Why: Async commands cannot borrow data across await points; Tauri requires owned types for async command parameters.

Known Issues Prevention

IssueRoot CauseSolution
"Command not found"Missing from generate_handler!Add command to handler macro
"Permission denied"Missing capabilityAdd to capabilities/default.json
Plugin feature silently failsPlugin installed but permission not in capabilityAdd plugin permission string to capabilities/default.json
Updater fails in productionUnsigned artifacts or HTTP endpointGenerate keys with cargo tauri signer generate, use HTTPS endpoint only
Sidecar not foundexternalBin not in tauri.conf.json or missing executableAdd path to bundle.externalBin, ensure binary is bundled
Feature works on desktop, breaks on mobileDesktop-only API usedCheck if API has mobile support — some plugins are desktop-only
State panic on accessType mismatch in State<T>Use exact type from .manage()
White screen on launchFrontend not buildingCheck beforeDevCommand in config
IPC timeoutBlocking async commandRemove blocking code or use spawn
Mobile build failsMissing Rust targetsRun rustup target add <target>

Deep-Dive References

Configuration Reference

tauri.conf.json

json
{
    "$schema": "./gen/schemas/desktop-schema.json",
    "productName": "my-app",
    "version": "1.0.0",
    "identifier": "com.example.myapp",
    "build": {
        "devUrl": "http://localhost:5173",
        "frontendDist": "../dist",
        "beforeDevCommand": "npm run dev",
        "beforeBuildCommand": "npm run build"
    },
    "app": {
        "windows": [{
            "label": "main",
            "title": "My App",
            "width": 800,
            "height": 600
        }],
        "security": {
            "csp": "default-src 'self'; img-src 'self' data:",
            "capabilities": ["default"]
        }
    },
    "bundle": {
        "active": true,
        "targets": "all",
        "icon": ["icons/icon.icns", "icons/icon.ico", "icons/icon.png"]
    }
}

Key settings:

  • build.devUrl: Must match your frontend dev server port
  • app.security.capabilities: Array of capability file identifiers

Plugin configuration — Some plugins require additional tauri.conf.json blocks (e.g., store, updater). Always check the specific plugin docs at v2.tauri.app/plugin/<plugin-name>/ for required config keys.

Project Structure

my-tauri-app/
├── src/                    # Frontend source
├── src-tauri/
│   ├── src/
│   │   ├── main.rs         # Thin passthrough — calls lib::run()
│   │   └── lib.rs          # ALL application logic lives here
│   ├── capabilities/
│   │   └── default.json    # Capability definitions (grant permissions here)
│   ├── tauri.conf.json     # App configuration (devUrl, bundle, security)
│   ├── Cargo.toml          # Rust dependencies
│   └── build.rs            # Build script (required for tauri-build)
└── package.json

Why `lib.rs` owns all logic: Tauri replaces main() with #[cfg_attr(mobile, tauri::mobile_entry_point)] on mobile. All commands, state, and builder setup must live in lib.rs::run().

Cargo.toml

toml
[package]
name = "app"
version = "0.1.0"
edition = "2021"

[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = { version = "2", features = [] }

[dependencies]
tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Key settings:

  • [lib] section: Required for mobile builds
  • crate-type: Must include all three types for cross-platform

Common Patterns

Error Handling Pattern

Use Result<T, E> and thiserror for type-safe error propagation across the IPC boundary. See `references/ipc-patterns.md` for full implementation details.

rust
use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Not found: {0}")]
    NotFound(String),
}

impl serde::Serialize for AppError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where S: serde::ser::Serializer {
        serializer.serialize_str(self.to_string().as_ref())
    }
}

#[tauri::command]
fn risky_operation() -> Result<String, AppError> {
    Ok("success".into())
}

Serde Boundary Rules

All command arguments must implement serde::Deserialize, and return types must implement serde::Serialize. This is how Tauri bridges JSON over the IPC boundary.

rust
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct CreateUserArgs {
    name: String,
    email: String,
    role: Option<String>,  // Optional fields use Option<T>
}

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
}

#[tauri::command]
fn create_user(args: CreateUserArgs) -> Result<User, String> {
    Ok(User { id: 1, name: args.name })
}

Common serde pitfalls:

  • Field names are camelCase in JS, snake_case in Rust — Tauri automatically converts between them
  • Option<T> maps to optional JS arguments (can be undefined or null)
  • Complex enums need #[serde(tag = "type")] or similar to be JSON-safe
  • Error types must also implement Serialize (see Error Handling Pattern above)

State Management Pattern

Tauri state manages application data across commands. See `references/ipc-patterns.md` for more complex state patterns.

rust
use std::sync::Mutex;
use tauri::State;

struct AppState {
    counter: u32,
}

#[tauri::command]
fn increment(state: State<'_, Mutex<AppState>>) -> u32 {
    let mut s = state.lock().unwrap();
    s.counter += 1;
    s.counter
}

// In builder:
tauri::Builder::default()
    .manage(Mutex::new(AppState { counter: 0 }))

Event Emission Pattern

Events are fire-and-forget notifications. See `references/ipc-patterns.md` for bidirectional examples.

rust
use tauri::Emitter;

#[tauri::command]
fn start_task(app: tauri::AppHandle) {
    std::thread::spawn(move || {
        app.emit("task-progress", 50).unwrap();
        app.emit("task-complete", "done").unwrap();
    });
}
typescript
import { listen } from '@tauri-apps/api/event';

const unlisten = await listen('task-progress', (e) => {
    console.log('Progress:', e.payload);
});
// Call unlisten() when done

Channel Streaming Pattern

Channels provide high-frequency, typed streaming from Rust to Frontend. See `references/ipc-patterns.md` for full implementation details.

rust
use tauri::ipc::Channel;

#[derive(Clone, serde::Serialize)]
#[serde(tag = "event", content = "data")]
enum DownloadEvent {
    Progress { percent: u32 },
    Complete { path: String },
}

#[tauri::command]
async fn download(url: String, on_event: Channel<DownloadEvent>) {
    for i in 0..=100 {
        on_event.send(DownloadEvent::Progress { percent: i }).unwrap();
    }
    on_event.send(DownloadEvent::Complete { path: "/downloads/file".into() }).unwrap();
}
typescript
import { invoke, Channel } from '@tauri-apps/api/core';

const channel = new Channel<DownloadEvent>();
channel.onmessage = (msg) => console.log(msg.event, msg.data);
await invoke('download', { url: 'https://...', onEvent: channel });

Window Access Pattern

Tauri v2 uses WebviewWindow for unified window and webview management.

rust
use tauri::Manager;

#[tauri::command]
fn focus_window(app: tauri::AppHandle) {
    if let Some(window) = app.get_webview_window("main") {
        let _ = window.set_focus();
    }
}

Why this matters: Use tauri::WebviewWindow and app.get_webview_window("label") in v2 — the v1 app.get_window() API is removed in v2.

Bundled Resources

References

Located in references/:

Note: For deep dives on specific topics, see the reference files above.

Dependencies

Required

PackageVersionPurpose
@tauri-apps/cli^2 (v2+)CLI tooling
@tauri-apps/api^2 (v2+)Frontend APIs
tauri^2 (v2+)Rust core
tauri-build^2 (v2+)Build scripts

\Last verified: 2026-04-02. Always check official changelog for feature timing.*

Optional (Plugins)

PackageVersionPurposeKey Permission
tauri-plugin-fs^2 (v2+)File system accessfs:default
tauri-plugin-dialog^2 (v2+)Native dialogsdialog:default
tauri-plugin-shell^2 (v2+)Shell commands, open URLsshell:default
tauri-plugin-http^2 (v2+)HTTP clienthttp:default
tauri-plugin-store^2 (v2+)Key-value storagestore:default
Plugin permissions are mandatory. Installing a plugin without adding its permission string to a capability file causes silent runtime failures. See `references/plugin-reference.md` for full install + permission details for all official plugins.

Official Documentation

Troubleshooting

White Screen on Launch

Symptoms: App launches but shows blank white screen

Solution:

  1. Verify devUrl matches your frontend dev server port
  2. Check beforeDevCommand runs your dev server
  3. Open DevTools (Cmd+Option+I / Ctrl+Shift+I) to check for errors

Command Returns Undefined

Symptoms: invoke() returns undefined instead of expected value

Solution:

  1. Verify command is in generate_handler![]
  2. Check Rust command actually returns a value
  3. Ensure argument names match (camelCase in JS, snake_case in Rust by default)

Mobile Build Failures

Symptoms: Android/iOS build fails with missing target

Solution:

bash
# Android targets
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android

# iOS targets (macOS only)
rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim

Desktop vs Mobile Behavioral Differences

Not all Tauri APIs and plugins support mobile (iOS/Android). Before using any plugin or API in a mobile build:

  1. Check the plugin page at v2.tauri.app/plugin/<name>/ for platform support matrix
  2. Common desktop-only items: System tray (TrayIconBuilder), window labels/multi-window, some shell plugin features
  3. Mobile-safe patterns: IPC commands/events/channels work on all platforms; tauri::AppHandle is mobile-safe
  4. Conditional compilation: Use #[cfg(desktop)] / #[cfg(mobile)] for platform-specific Rust logic
rust
#[tauri::command]
fn platform_info() -> String {
    #[cfg(desktop)]
    return "desktop".to_string();
    #[cfg(mobile)]
    return "mobile".to_string();
}

Setup Checklist

Before using this skill, verify:

  • [ ] npx tauri info shows correct Tauri v2 versions
  • [ ] src-tauri/capabilities/default.json exists with at least core:default
  • [ ] All commands registered in generate_handler![]
  • [ ] lib.rs contains shared code (for mobile support)
  • [ ] Required Rust targets installed for target platforms