mcpField
T
tauri-app-dev

tauri-app-dev

github.com/xiaolai開発87876 回インストール今日更新
リポジトリ

tauri-app-dev について

Web フロントエンドと Rust バックエンドでデスクトップアプリを構築する Tauri 2.0 開発スキルです。IPC、権限、プラグイン、パッケージ化、テストを扱います。

主な機能

  1. 01Web フロントエンドと Rust バックエンドの構成
  2. 02コマンドとイベントによる IPC
  3. 03ケイパビリティと API 権限の設定
  4. 04ファイルシステムなどのプラグイン利用
  5. 05アプリのビルド、バンドル、テスト

活用シーン

  1. 01Web フロントエンドから Rust コマンドを呼び出す
  2. 02ウィンドウのファイルアクセス権限を設定
  3. 03ファイルダイアログと読み書きを実装
  4. 04実行中の Tauri アプリをテスト

MCP 対応

実行中のアプリの E2E テストに Tauri MCP ツールを使用し、chrome-devtools MCP はこの用途に使わないよう説明しています。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/xiaolai/vmark --skill tauri-app-dev

インストール後、Claude Code で/tauri-app-devを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
bundling.md
commands-and-ipc.md
patterns.md
plugins.md
security.md

詳細

Tauri 2.0 アプリ開発

Tauri は、Web フロントエンドと Rust バックエンドを使用して、小さく、高速で、安全なデスクトップアプリを構築するためのフレームワークです。

アーキテクチャの概要

┌─────────────────────────────────────────┐
│           Frontend (Webview)            │
│     HTML/CSS/JS • React/Vue/Svelte      │
└────────────────┬────────────────────────┘
                 │ IPC (invoke/events)
┌────────────────▼────────────────────────┐
│           Tauri Core (Rust)             │
│  Commands • State • Plugins • Events    │
└────────────────┬────────────────────────┘
                 │ TAO (windows) + WRY (webview)
┌────────────────▼────────────────────────┐
│          Operating System               │
│   macOS • Windows • Linux • Mobile      │
└─────────────────────────────────────────┘

プロジェクト構成

my-app/
├── src/                    # Frontend source
├── src-tauri/
│   ├── Cargo.toml          # Rust dependencies
│   ├── tauri.conf.json     # Tauri configuration
│   ├── capabilities/       # Security permissions (v2)
│   │   └── default.json
│   ├── src/
│   │   ├── main.rs         # Desktop entry point
│   │   └── lib.rs          # Main app logic + mobile entry
│   └── icons/
└── package.json

コマンド(フロントエンド → Rust)

Rust で #[tauri::command] を使ってコマンドを定義します。

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

#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
    std::fs::read_to_string(&path).map_err(|e| e.to_string())
}

pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet, read_file])
        .run(tauri::generate_context!())
        .expect("error running app");
}

フロントエンドから直接呼び出します。

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

const greeting = await invoke<string>('greet', { name: 'World' });
const content = await invoke<string>('read_file', { path: '/tmp/test.txt' });

プロジェクト規約: invoke() を TanStack Query でラップし、キャッシュと状態管理を行います。

import { useQuery, useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';

// Query (read operations)
const { data: content } = useQuery({
  queryKey: ['file', path],
  queryFn: () => invoke<string>('read_file', { path }),
});

// Mutation (write operations)
const { mutate: saveFile } = useMutation({
  mutationFn: (content: string) => invoke('write_file', { path, content }),
});

重要なルール:

  • 引数は serde::Deserialize を実装する必要があります
  • 戻り値の型は serde::Serialize を実装する必要があります
  • 失敗する可能性のある操作には Result<T, E> を使用します
  • 非同期コマンドはスレッドプールで実行されます(ノンブロッキング)
  • Rust のスネークケースは、JS の引数ではキャメルケースになります

状態管理

コマンド間で状態を共有します。

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

struct AppState {
    counter: Mutex<i32>,
    db: Mutex<Option<Database>>,
}

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

pub fn run() {
    tauri::Builder::default()
        .manage(AppState {
            counter: Mutex::new(0),
            db: Mutex::new(None),
        })
        .invoke_handler(tauri::generate_handler![increment])
        .run(tauri::generate_context!())
        .expect("error running app");
}

AppHandle 経由でのアクセス(バックグラウンドスレッド用):

use tauri::Manager;

#[tauri::command]
async fn background_task(app: tauri::AppHandle) {
    let state = app.state::<AppState>();
    // use state...
}

イベント(Rust → フロントエンド)

Rust からイベントを発行します。

use tauri::Emitter;

#[tauri::command]
fn start_process(app: tauri::AppHandle) {
    std::thread::spawn(move || {
        for i in 0..100 {
            app.emit("progress", i).unwrap();
            std::thread::sleep(std::time::Duration::from_millis(50));
        }
        app.emit("complete", "Done!").unwrap();
    });
}

フロントエンドでリッスンします。

import { listen } from '@tauri-apps/api/event';

const unlisten = await listen<number>('progress', (event) => {
    console.log(`Progress: ${event.payload}%`);
});

// Clean up when done
unlisten();

必須プラグイン

プラグインをインストールします。src-tauri で cargo add <plugin>、フロントエンドで pnpm add <package> を実行します。

プラグインCargo クレートNPM パッケージ用途
ファイルシステムtauri-plugin-fs@tauri-apps/plugin-fsファイルの読み書き
ダイアログtauri-plugin-dialog@tauri-apps/plugin-dialog開く/保存ダイアログ
クリップボードtauri-plugin-clipboard-manager@tauri-apps/plugin-clipboard-managerコピー/貼り付け
シェルtauri-plugin-shell@tauri-apps/plugin-shell外部コマンドの実行
ストアtauri-plugin-store@tauri-apps/plugin-storeキーと値の永続化
アップデーターtauri-plugin-updater@tauri-apps/plugin-updater自動アップデート

Rust で登録します。

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_fs::init())
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_clipboard_manager::init())
        .run(tauri::generate_context!())
        .expect("error running app");
}

セキュリティ:ケイパビリティと権限

Tauri 2.0 では、ケイパビリティ(src-tauri/capabilities/ 内)を使用して、各ウィンドウがアクセスできる API を制御します。

src-tauri/capabilities/default.json:

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-capability",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "fs:default",
    "fs:allow-read-text-file",
    "dialog:default",
    {
      "identifier": "fs:scope",
      "allow": [{ "path": "$APPDATA/**" }, { "path": "$DOCUMENT/**" }]
    }
  ]
}

スコープ変数: $APPDATA、$APPCONFIG、$DOCUMENT、$DOWNLOAD、$HOME、$TEMP など。

ファイル操作(エディターパターン)

import { open, save } from '@tauri-apps/plugin-dialog';
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';

// Open file dialog
const path = await open({
    filters: [{ name: 'Markdown', extensions: ['md'] }],
    multiple: false,
});

if (path) {
    const content = await readTextFile(path);
    // Edit content...
    await writeTextFile(path, modifiedContent);
}

// Save as dialog
const savePath = await save({
    filters: [{ name: 'Markdown', extensions: ['md'] }],
    defaultPath: 'untitled.md',
});

if (savePath) {
    await writeTextFile(savePath, content);
}

ウィンドウ管理

実行時にウィンドウを作成します。

use tauri::{WebviewUrl, WebviewWindowBuilder};

#[tauri::command]
async fn open_settings(app: tauri::AppHandle) -> Result<(), String> {
    WebviewWindowBuilder::new(&app, "settings", WebviewUrl::App("settings.html".into()))
        .title("Settings")
        .inner_size(600.0, 400.0)
        .build()
        .map_err(|e| e.to_string())?;
    Ok(())
}

tauri.conf.json で設定します。

{
  "app": {
    "windows": [
      {
        "label": "main",
        "title": "My App",
        "width": 1200,
        "height": 800,
        "decorations": true,
        "resizable": true
      }
    ]
  }
}

カスタムタイトルバー

設定で decorations: false を指定し、次のようにします。

<div data-tauri-drag-region class="titlebar">
    <span>My App</span>
    <button id="minimize">−</button>
    <button id="maximize">□</button>
    <button id="close">×</button>
</div>
import { getCurrentWindow } from '@tauri-apps/api/window';

const appWindow = getCurrentWindow();
document.getElementById('minimize')?.addEventListener('click', () => appWindow.minimize());
document.getElementById('maximize')?.addEventListener('click', () => appWindow.toggleMaximize());
document.getElementById('close')?.addEventListener('click', () => appWindow.close());

ビルドと配布

# Development
pnpm tauri dev

# Production build
pnpm tauri build

# Build specific targets
pnpm tauri build --target universal-apple-darwin  # macOS universal
pnpm tauri build --bundles deb,appimage           # Linux only
pnpm tauri build --bundles nsis                   # Windows NSIS

出力先:

  • macOS:target/release/bundle/macos/*.app、*.dmg
  • Windows:target/release/bundle/nsis/*-setup.exe、msi/*.msi
  • Linux:target/release/bundle/deb/*.deb、appimage/*.AppImage

クイックリファレンス

タスクリソース
コマンド、IPC、チャネルreferences/commands-and-ipc.md を参照
プラグインの使用と開発references/plugins.md を参照
セキュリティ設定references/security.md を参照
バンドルと配布references/bundling.md を参照
一般的なアプリのパターンreferences/patterns.md を参照

テスト駆動開発(TDD)

重要: 必ず TDD に従い、実装の前にテストを書いてください。

TDD ワークフロー
1. RED    → Write failing test first
2. GREEN  → Write minimal code to pass
3. REFACTOR → Clean up, keep tests green
テストスタック
レイヤーツール用途
Rust 単体テストcargo testコマンドとビジネスロジックのテスト
React 単体テストVitestコンポーネント、フック、ストアのテスト
統合テストVitest + MSWモック IPC を使用したフロントエンドのテスト
E2ETauri MCP実行中のアプリのテスト(Chrome DevTools は使用しない)
Tauri MCP による E2E テスト

重要: 実行中のアプリをテストする際は、必ず tauri_* MCP ツールを使用してください。chrome-devtools MCP は使用しないでください。ブラウザページ専用です。

// Tauri MCP workflow for E2E tests:

// 1. Start session (connect to running Tauri app)
tauri_driver_session({ action: 'start', port: 9223 })

// 2. Take snapshot (get DOM state)
tauri_webview_screenshot()
tauri_webview_find_element({ selector: '.editor-content' })

// 3. Interact with app
tauri_webview_interact({ action: 'click', selector: '#save-button' })
tauri_webview_keyboard({ action: 'type', selector: 'input', text: 'hello' })

// 4. Wait for results
tauri_webview_wait_for({ type: 'selector', value: '.success-toast' })

// 5. Verify IPC calls
tauri_ipc_monitor({ action: 'start' })
tauri_ipc_get_captured({ filter: 'save_file' })

// 6. Check backend state
tauri_ipc_execute_command({ command: 'get_app_state' })

// 7. Read logs for debugging
tauri_read_logs({ source: 'console', lines: 50 })
Rust 単体テスト
// src-tauri/src/lib.rs

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_greet() {
        let result = greet("World".to_string());
        assert_eq!(result, "Hello, World!");
    }

    #[test]
    fn test_parse_markdown() {
        let input = "# Hello";
        let result = parse_markdown(input);
        assert!(result.is_ok());
        assert_eq!(result.unwrap().title, "Hello");
    }

    #[tokio::test]
    async fn test_async_command() {
        let result = read_file("/tmp/test.txt".to_string()).await;
        // Test with temp files or mocks
    }
}

実行:cd src-tauri && cargo test

React コンポーネントテスト(Vitest)
// src/components/Editor.test.tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Editor } from './Editor'

// Mock Tauri invoke
vi.mock('@tauri-apps/api/core', () => ({
  invoke: vi.fn()
}))

describe('Editor', () => {
  it('should render editor content', () => {
    render(<Editor initialValue="# Hello" />)
    expect(screen.getByText('Hello')).toBeInTheDocument()
  })

  it('should call save on Ctrl+S', async () => {
    const { invoke } = await import('@tauri-apps/api/core')
    render(<Editor initialValue="test" />)

    await userEvent.keyboard('{Control>}s{/Control}')

    expect(invoke).toHaveBeenCalledWith('save_file', expect.any(Object))
  })
})
Zustand ストアのテスト
// src/stores/editorStore.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { useEditorStore } from './editorStore'

describe('editorStore', () => {
  beforeEach(() => {
    // Reset store before each test
    useEditorStore.setState({
      content: '',
      isDirty: false,
      filePath: null
    })
  })

  it('should update content and mark dirty', () => {
    const { setContent } = useEditorStore.getState()

    setContent('new content')

    const state = useEditorStore.getState()
    expect(state.content).toBe('new content')
    expect(state.isDirty).toBe(true)
  })

  it('should clear dirty flag after save', () => {
    useEditorStore.setState({ isDirty: true })
    const { markSaved } = useEditorStore.getState()

    markSaved()

    expect(useEditorStore.getState().isDirty).toBe(false)
  })
})
モック IPC を使用した統合テスト
// src/features/file/useFileOperations.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useFileOperations } from './useFileOperations'

vi.mock('@tauri-apps/api/core', () => ({
  invoke: vi.fn()
}))

vi.mock('@tauri-apps/plugin-dialog', () => ({
  open: vi.fn(),
  save: vi.fn()
}))

describe('useFileOperations', () => {
  let queryClient: QueryClient

  beforeEach(() => {
    queryClient = new QueryClient({
      defaultOptions: { queries: { retry: false } }
    })
    vi.clearAllMocks()
  })

  it('should open file and load content', async () => {
    const { invoke } = await import('@tauri-apps/api/core')
    const { open } = await import('@tauri-apps/plugin-dialog')

    vi.mocked(open).mockResolvedValue('/path/to/file.md')
    vi.mocked(invoke).mockResolvedValue('# File Content')

    const { result } = renderHook(() => useFileOperations(), {
      wrapper: ({ children }) => (
        <QueryClientProvider client={queryClient}>
          {children}
        </QueryClientProvider>
      )
    })

    await result.current.openFile()

    await waitFor(() => {
      expect(invoke).toHaveBeenCalledWith('read_file', { path: '/path/to/file.md' })
    })
  })
})
TDD の例:新機能の追加
// Step 1: RED - Write failing test first
// src/features/wordcount/useWordCount.test.ts
describe('useWordCount', () => {
  it('should count words in content', () => {
    const { result } = renderHook(() => useWordCount('hello world'))
    expect(result.current.words).toBe(2)
  })

  it('should handle empty content', () => {
    const { result } = renderHook(() => useWordCount(''))
    expect(result.current.words).toBe(0)
  })

  it('should count characters', () => {
    const { result } = renderHook(() => useWordCount('hello'))
    expect(result.current.characters).toBe(5)
  })
})

// Step 2: GREEN - Minimal implementation
// src/features/wordcount/useWordCount.ts
export function useWordCount(content: string) {
  return {
    words: content.trim() ? content.trim().split(/\s+/).length : 0,
    characters: content.length
  }
}

// Step 3: REFACTOR - Add memoization, types, etc.
export function useWordCount(content: string): WordCountResult {
  return useMemo(() => ({
    words: content.trim() ? content.trim().split(/\s+/).length : 0,
    characters: content.length,
    charactersNoSpaces: content.replace(/\s/g, '').length
  }), [content])
}
テストの実行
# All tests
pnpm test

# Watch mode
pnpm test:watch

# Coverage
pnpm test:coverage

# Rust tests only
cd src-tauri && cargo test

# Type check + lint + test
pnpm check:all

デバッグのヒント

  • 開発者ツール: 右クリックして「検証」を選択するか、Cmd+Option+I(macOS)/Ctrl+Shift+I(Windows/Linux)を押します
  • Rust のログ: log クレートと tauri-plugin-log を使用するか、println! を使用します(ターミナルに表示されます)
  • ケイパビリティの確認: 「許可されていません」というエラーは、ケイパビリティに権限が不足していることを意味します
  • IPC エラー: 引数名が一致していることを確認します(Rust の snake_case → JS の camelCase)
  • E2E デバッグ: tauri_read_logs({ source: 'console' }) を使用して、WebView のコンソールを確認します

関連するスキル

  • tauri-v2-integration — VMark 固有の Tauri IPC パターン(invoke/emit ブリッジ、メニューアクセラレーター)
  • tauri-mcp-testing — MCP ツールを使用した、実行中の Tauri アプリの E2E テスト
  • rust-tauri-backend — VMark の Rust バックエンド(コマンド、メニュー項目、ファイルシステム)

コメント