Add definitive Tauri 2.0 guide, agent prompts, and WebSocket docs
- tauri2-guide/definitive-guide.md: Comprehensive Tauri 2.0 reference covering project structure, configuration, capabilities/permissions, commands/IPC, events/channels, window management, plugin ecosystem, WebSocket server, script injection, state management, v1->v2 migration, anti-patterns, and quick reference cheat sheet. - tauri2-guide/agent-prompts.md: Copy-paste ready prompts for instructing AI agents to build Tauri 2.0 apps. Includes universal system prompt with sandbox constraints, 10 task-specific prompts, verification checklist, and v1->v2 API map. - tauri2-docs/ws_server_client_inject_guide.md: Complete WebSocket server/client/script injection guide based on the moxie-app reference implementation. Covers the native Tokio WebSocket server pattern, frontend client connections, and script injection into external webview windows. All code validated against Tauri 2.0 official docs (https://v2.tauri.app).
This commit is contained in:
parent
7f8eb17292
commit
04c128f509
@ -0,0 +1,554 @@
|
|||||||
|
# WebSocket Server, Client & Script Injection Guide
|
||||||
|
|
||||||
|
> Based on the working `moxie-app` reference implementation in this repository.
|
||||||
|
> This guide documents the complete pattern for running a native WebSocket server inside Tauri 2.0, connecting frontend clients, and injecting scripts into external webview windows.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide covers three interrelated patterns used in the reference `moxie-app`:
|
||||||
|
|
||||||
|
1. **Native WebSocket Server** — A Tokio-backed WebSocket server running inside the Tauri process
|
||||||
|
2. **Frontend WebSocket Client** — JavaScript clients connecting to the local server for bidirectional communication
|
||||||
|
3. **Script Injection** — Injecting JavaScript into dynamically created webview windows (including external URLs) to bridge them into the WebSocket network
|
||||||
|
|
||||||
|
### Architecture Diagram
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Tauri Process │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────────────────────┐ │
|
||||||
|
│ │ lib.rs │ │ Tokio WebSocket Server │ │
|
||||||
|
│ │ │────▶│ (ws://127.0.0.1:8080) │ │
|
||||||
|
│ │ .setup() │ │ │ │
|
||||||
|
│ │ spawns │ │ broadcast channel (16 msg) │ │
|
||||||
|
│ │ server │ │ │ │
|
||||||
|
│ └──────────────┘ └───────────┬───────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────────────────────┼─────────────────┐ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ┌──────▼──────┐ ┌───────────────▼──────────┐ │ │
|
||||||
|
│ │ Main Window │ │ Remote Window │ │ │
|
||||||
|
│ │ (index.html) │ │ (external URL) │ │ │
|
||||||
|
│ │ │ │ + injected script │ │ │
|
||||||
|
│ │ WebSocket │ │ + WebSocket client │ │ │
|
||||||
|
│ │ Client (JS) │ │ Client (injected JS) │ │ │
|
||||||
|
│ └──────┬──────┘ └───────────────┬──────────┘ │ │
|
||||||
|
└─────────┼─────────────────────────┼────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ WebSocket (local) │ │
|
||||||
|
└─────────────────────────┘ │
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1: Native WebSocket Server (Rust)
|
||||||
|
|
||||||
|
### Why Run a WebSocket Server Inside Tauri?
|
||||||
|
|
||||||
|
Standard Tauri IPC (`invoke`, events, channels) only works between Tauri's own windows and its Rust backend. When you need to:
|
||||||
|
|
||||||
|
- Communicate with **external applications** running on the same machine
|
||||||
|
- Bridge **external URLs** loaded in webview windows back to the Tauri backend
|
||||||
|
- Enable **cross-window communication** at scale (beyond Tauri's event system)
|
||||||
|
- Create a **local API endpoint** for mobile apps or other tools to connect to
|
||||||
|
|
||||||
|
...a native WebSocket server running on localhost is the solution.
|
||||||
|
|
||||||
|
### Dependencies (`Cargo.toml`)
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tokio = { version = "1", features = ["net", "rt", "sync", "macros"] }
|
||||||
|
tokio-tungstenite = "0.24"
|
||||||
|
futures-util = "0.3"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key dependencies explained:**
|
||||||
|
- `tokio` — The async runtime Tauri uses internally. The `net` feature provides `TcpListener`, `sync` provides `broadcast` channels, `rt` provides the runtime, and `macros` provides `tokio::select!`.
|
||||||
|
- `tokio-tungstenite` — Industry-standard WebSocket implementation for Tokio. Provides `accept_async` for accepting WS connections and the `Message` type for frame construction.
|
||||||
|
- `futures-util` — Provides `StreamExt` (for `.next()` on streams) and `SinkExt` (for `.send()` on sinks).
|
||||||
|
|
||||||
|
### Server Implementation (`src-tauri/src/server.rs`)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tokio_tungstenite::accept_async;
|
||||||
|
use futures_util::stream::StreamExt;
|
||||||
|
use futures_util::sink::SinkExt;
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
|
pub async fn start_websocket_server() {
|
||||||
|
// Bind to localhost only (security: not exposed to network)
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
|
||||||
|
println!("WebSocket Server listening on ws://127.0.0.1:8080");
|
||||||
|
|
||||||
|
// Broadcast channel: fans out messages to ALL connected clients
|
||||||
|
// Queue depth of 16 means if a slow client hasn't read 16 messages,
|
||||||
|
// the oldest undelivered message is dropped (lagging policy)
|
||||||
|
let (tx, _rx) = broadcast::channel::<String>(16);
|
||||||
|
|
||||||
|
// Accept connections in a loop
|
||||||
|
while let Ok((stream, _addr)) = listener.accept().await {
|
||||||
|
// Clone the broadcast transmitter for this connection's task
|
||||||
|
let tx = tx.clone();
|
||||||
|
// Create a dedicated receiver for this connection
|
||||||
|
let mut rx = tx.subscribe();
|
||||||
|
|
||||||
|
// Spawn a separate async task per client connection
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
// Perform the WebSocket handshake
|
||||||
|
if let Ok(ws_stream) = accept_async(stream).await {
|
||||||
|
println!("New WebSocket client connected");
|
||||||
|
let (mut ws_sender, mut ws_receiver) = ws_stream.split();
|
||||||
|
|
||||||
|
// Main message loop using tokio::select!
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
// Branch A: Message FROM this specific client
|
||||||
|
incoming = ws_receiver.next() => {
|
||||||
|
match incoming {
|
||||||
|
Some(Ok(msg)) if msg.is_text() => {
|
||||||
|
let text = msg.to_text().unwrap().to_string();
|
||||||
|
println!("Received from client: {}", text);
|
||||||
|
|
||||||
|
// Broadcast to ALL other connected clients
|
||||||
|
let _ = tx.send(text);
|
||||||
|
}
|
||||||
|
Some(Ok(msg)) if msg.is_binary() => {
|
||||||
|
// Handle binary data if needed
|
||||||
|
let _ = tx.send(format!("[binary:{}bytes]", msg.len()));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// None = client disconnected, Err = error
|
||||||
|
println!("Client disconnected");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Branch B: Broadcast message FROM another client
|
||||||
|
broadcast_msg = rx.recv() => {
|
||||||
|
if let Ok(payload) = broadcast_msg {
|
||||||
|
// Construct a WebSocket text frame and send it
|
||||||
|
let frame = tokio_tungstenite::tungstenite::Message::Text(
|
||||||
|
payload.into()
|
||||||
|
);
|
||||||
|
if let Err(e) = ws_sender.send(frame).await {
|
||||||
|
eprintln!("Failed to send to client: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("WebSocket handshake failed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### How the Broadcast Pattern Works
|
||||||
|
|
||||||
|
```text
|
||||||
|
Client A sends "hello" ─────▶ Rust Server ─────▶ broadcast("hello") ─────▶ Client B receives "hello"
|
||||||
|
─────▶ Client C receives "hello"
|
||||||
|
─────▶ Client D receives "hello"
|
||||||
|
|
||||||
|
Client B sends "world" ─────▶ Rust Server ─────▶ broadcast("world") ─────▶ Client A receives "world"
|
||||||
|
─────▶ Client C receives "world"
|
||||||
|
─────▶ Client D receives "world"
|
||||||
|
```
|
||||||
|
|
||||||
|
Each client:
|
||||||
|
1. Gets its own `rx = tx.subscribe()` receiver
|
||||||
|
2. The main loop uses `tokio::select!` to wait on BOTH incoming messages AND broadcast messages simultaneously
|
||||||
|
3. When a client sends a message, it goes through `tx.send()` which broadcasts to all OTHER clients (the sender's own receiver won't get its own message because `subscribe()` only receives messages sent after subscription)
|
||||||
|
|
||||||
|
### Spawning the Server from `lib.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src-tauri/src/lib.rs
|
||||||
|
mod server;
|
||||||
|
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.setup(|_app| {
|
||||||
|
// Spawn the WebSocket server as a background task
|
||||||
|
// This runs on Tauri's managed Tokio runtime
|
||||||
|
tauri::async_runtime::spawn(server::start_websocket_server());
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![greet])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key details:**
|
||||||
|
- `tauri::async_runtime::spawn()` is used instead of `tokio::spawn()` to ensure the task runs on Tauri's managed async runtime
|
||||||
|
- The server task runs for the entire lifetime of the application
|
||||||
|
- The `.setup()` closure returns immediately — the server runs in the background
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2: Frontend WebSocket Client
|
||||||
|
|
||||||
|
### Main Window Connection (`src/main.js`)
|
||||||
|
|
||||||
|
The main application window connects to the WebSocket server using the standard browser `WebSocket` API:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { invoke } = window.__TAURI__.core;
|
||||||
|
|
||||||
|
// Connect to the local WebSocket server
|
||||||
|
const mainSocket = new WebSocket('ws://127.0.0.1:8080');
|
||||||
|
|
||||||
|
mainSocket.onopen = () => {
|
||||||
|
console.log('Main Control Window connected to the broadcast hub!');
|
||||||
|
};
|
||||||
|
|
||||||
|
mainSocket.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
console.log('Received broadcast:', data);
|
||||||
|
|
||||||
|
// Handle specific message types
|
||||||
|
if (data.localsession) {
|
||||||
|
document.querySelector('#session-display').textContent =
|
||||||
|
`Session: ${data.localsession}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mainSocket.onerror = (err) => {
|
||||||
|
console.error('WebSocket connection error:', err);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sending data to other connected windows
|
||||||
|
function sendToAllWindows(payload) {
|
||||||
|
mainSocket.send(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: button click sends data
|
||||||
|
document.querySelector('#broadcast-btn').addEventListener('click', () => {
|
||||||
|
sendToAllWindows({
|
||||||
|
sender: 'main-window',
|
||||||
|
type: 'notification',
|
||||||
|
content: 'Hello from the main window!'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message Protocol
|
||||||
|
|
||||||
|
While you can send any format over WebSocket, using JSON with a consistent structure is recommended:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Standard message envelope
|
||||||
|
{
|
||||||
|
"sender": "main-window", // Origin identifier
|
||||||
|
"type": "action-type", // Message category
|
||||||
|
"content": "payload data", // Message body
|
||||||
|
"timestamp": 1234567890 // Optional: for ordering
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Lifecycle Management
|
||||||
|
|
||||||
|
For production apps, handle reconnection and cleanup:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class TauriSocket {
|
||||||
|
constructor(url) {
|
||||||
|
this.url = url;
|
||||||
|
this.ws = null;
|
||||||
|
this.reconnectDelay = 1000;
|
||||||
|
this.maxReconnectDelay = 30000;
|
||||||
|
this.listeners = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.ws = new WebSocket(this.url);
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
console.log('Connected to local WS server');
|
||||||
|
this.reconnectDelay = 1000; // Reset on successful connect
|
||||||
|
this.emit('connected');
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
this.emit('message', data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse WS message:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onclose = () => {
|
||||||
|
console.log('Disconnected, reconnecting...');
|
||||||
|
this.emit('disconnected');
|
||||||
|
setTimeout(() => this.connect(), this.reconnectDelay);
|
||||||
|
this.reconnectDelay = Math.min(
|
||||||
|
this.reconnectDelay * 1.5,
|
||||||
|
this.maxReconnectDelay
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = (err) => {
|
||||||
|
console.error('WebSocket error:', err);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data) {
|
||||||
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event, callback) {
|
||||||
|
this.listeners.set(event, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event, data) {
|
||||||
|
this.listeners.get(event)?.(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.ws?.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const socket = new TauriSocket('ws://127.0.0.1:8080');
|
||||||
|
socket.connect();
|
||||||
|
socket.on('message', (data) => console.log('Got:', data));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 3: Script Injection into Webview Windows
|
||||||
|
|
||||||
|
### The Use Case
|
||||||
|
|
||||||
|
When your Tauri app opens windows with **external URLs** (e.g., a web-based admin panel, a third-party dashboard, or any remote page), those pages have no knowledge of Tauri or your local WebSocket server. Script injection allows you to:
|
||||||
|
|
||||||
|
1. **Bridge the external page** into your local communication network
|
||||||
|
2. **Extract data** from the page (localStorage, DOM, cookies) and relay it to Tauri
|
||||||
|
3. **Modify the page** (UI changes, injected controls, overlays)
|
||||||
|
4. **Share sessions** between windows (e.g., pass auth tokens)
|
||||||
|
|
||||||
|
### Injection via `initialization_script`
|
||||||
|
|
||||||
|
The `WebviewWindowBuilder::initialization_script()` method injects JavaScript that runs when the webview's page loads. Combined with `include_str!()`, you can load scripts from external files at compile time:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src-tauri/src/lib.rs
|
||||||
|
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn open_remote_session(app_handle: AppHandle, url: String) -> Result<String, String> {
|
||||||
|
let target_url = url.parse()
|
||||||
|
.map(WebviewUrl::External)
|
||||||
|
.map_err(|_| format!("Invalid URL: {}", url))?;
|
||||||
|
|
||||||
|
let window_label = "remote-session";
|
||||||
|
|
||||||
|
// Check if window already exists — focus it instead of duplicating
|
||||||
|
if let Some(existing) = app_handle.get_webview_window(window_label) {
|
||||||
|
let _ = existing.set_focus();
|
||||||
|
return Ok(format!("Focused existing window for {}", url));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the injection script at compile time
|
||||||
|
let script_to_inject = include_str!("../extWebview.js");
|
||||||
|
|
||||||
|
let app_clone = app_handle.clone();
|
||||||
|
let url_clone = target_url.clone();
|
||||||
|
|
||||||
|
// Window creation MUST happen on the main thread
|
||||||
|
app_handle.run_on_main_thread(move || {
|
||||||
|
let _window = WebviewWindowBuilder::new(&app_clone, window_label, url_clone)
|
||||||
|
.title("Remote Session")
|
||||||
|
.inner_size(1024.0, 768.0)
|
||||||
|
.resizable(true)
|
||||||
|
.focused(true)
|
||||||
|
.initialization_script(script_to_inject) // ← Inject JS here
|
||||||
|
.build();
|
||||||
|
}).map_err(|e| format!("Failed to dispatch to main thread: {}", e))?;
|
||||||
|
|
||||||
|
Ok(format!("Dispatched window spawn for: {}", url))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Injected Script (`src-tauri/extWebview.js`)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// extWebview.js — This script runs automatically when the webview loads
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
console.log('[TAURI] Initializing injected socket link...');
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://127.0.0.1:8080');
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('[TAURI] Injected socket connected to backend!');
|
||||||
|
|
||||||
|
// Announce this window's identity to the Tauri backend
|
||||||
|
const payload = {
|
||||||
|
sender: 'remote-webview',
|
||||||
|
type: 'identify',
|
||||||
|
url: window.location.href,
|
||||||
|
title: document.title
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(payload));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
console.log('[TAURI] RECEIVED FROM BACKEND:', data);
|
||||||
|
|
||||||
|
// Respond with session/token data if requested
|
||||||
|
if (data.type === 'request-session') {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
sender: 'remote-webview',
|
||||||
|
type: 'session-response',
|
||||||
|
localsession: window.localStorage.getItem('auth-token') || 'no-session'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle commands from the backend
|
||||||
|
if (data.type === 'execute-action') {
|
||||||
|
// Perform actions on the remote page as instructed by Tauri
|
||||||
|
console.log('[TAURI] Executing action:', data.action);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (err) => {
|
||||||
|
console.error('[TAURI] Injected socket error:', err);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Expose the socket on window for the page's own code to use
|
||||||
|
window.localAppSocket = ws;
|
||||||
|
|
||||||
|
// Periodically send status updates
|
||||||
|
setInterval(() => {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
sender: 'remote-webview',
|
||||||
|
type: 'heartbeat',
|
||||||
|
url: window.location.href
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSP Considerations
|
||||||
|
|
||||||
|
Content Security Policy (CSP) can block script injection on external pages. If you need injection on external URLs:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// tauri.conf.json — disable CSP (use with caution!)
|
||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For better security, craft a specific CSP that allows your injection patterns while blocking other sources:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"security": {
|
||||||
|
"csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src ws://127.0.0.1:* http://127.0.0.1:* https:;"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Working Example Flow
|
||||||
|
|
||||||
|
### Step-by-step data flow through the entire system:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. App starts → lib.rs setup() → spawns WebSocket server on ws://127.0.0.1:8080
|
||||||
|
|
||||||
|
2. Main window (index.html) loads → main.js creates WebSocket client
|
||||||
|
→ connects to ws://127.0.0.1:8080
|
||||||
|
→ onopen fires → ready to communicate
|
||||||
|
|
||||||
|
3. User enters URL and clicks button → main.js calls invoke('greet', { name: url })
|
||||||
|
|
||||||
|
4. Rust greet() command:
|
||||||
|
→ parses URL as WebviewUrl::External
|
||||||
|
→ checks if window already exists (focus if so)
|
||||||
|
→ loads extWebview.js via include_str!()
|
||||||
|
→ dispatches window creation to main thread
|
||||||
|
→ creates new WebviewWindow with external URL + injected script
|
||||||
|
|
||||||
|
5. Remote window loads external page → extWebview.js runs on DOMContentLoaded
|
||||||
|
→ creates WebSocket client
|
||||||
|
→ connects to ws://127.0.0.1:8080
|
||||||
|
→ sends identification message
|
||||||
|
|
||||||
|
6. Server broadcasts identification to all clients
|
||||||
|
→ Main window receives it → displays session info
|
||||||
|
→ Other remote windows receive it (if any)
|
||||||
|
|
||||||
|
7. Main window user clicks "Send" → main.js sends JSON via WebSocket
|
||||||
|
→ Server broadcasts to all clients
|
||||||
|
→ All remote windows receive and log the message
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Choosing Between Approaches
|
||||||
|
|
||||||
|
| Requirement | Recommended Approach |
|
||||||
|
|-------------|---------------------|
|
||||||
|
| Connect to external WebSocket server | `tauri-plugin-websocket` (client plugin) |
|
||||||
|
| App IS the WebSocket server | Native Tokio server (this guide) |
|
||||||
|
| Tauri-to-frontend communication | Tauri events (`emit`/`listen`) |
|
||||||
|
| High-throughput streaming data | Tauri channels (`Channel<T>`) |
|
||||||
|
| Cross-window messaging (internal) | Tauri events or native WS server |
|
||||||
|
| External page needs to talk to Tauri | Native WS server + script injection |
|
||||||
|
| Multiple external pages sharing state | Native WS server + script injection |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Address already in use" (port 8080)
|
||||||
|
- Another process is using port 8080
|
||||||
|
- Solution: Change the port in both `server.rs` and all frontend connection code
|
||||||
|
- Or kill the process using port 8080: `lsof -i :8080` / `netstat -ano | findstr :8080`
|
||||||
|
|
||||||
|
### WebSocket connection fails from injected scripts
|
||||||
|
- CSP may be blocking the connection → set `"csp": null` in config
|
||||||
|
- The injected script runs before the page's own scripts → ensure DOM is ready via `DOMContentLoaded`
|
||||||
|
- External HTTPS pages may refuse unencrypted WebSocket connections → page must allow mixed content
|
||||||
|
|
||||||
|
### Messages not broadcasting to other windows
|
||||||
|
- Each client needs its own `rx = tx.subscribe()` receiver
|
||||||
|
- The `tx.send()` call broadcasts to all subscribers EXCEPT the sender's own subscription
|
||||||
|
- Ensure `tokio::select!` has both branches (incoming AND broadcast)
|
||||||
|
|
||||||
|
### Window creation fails from async command
|
||||||
|
- Window creation MUST happen on the main thread
|
||||||
|
- Use `app_handle.run_on_main_thread(move || { ... })` for async contexts
|
||||||
|
- Not doing this will cause a panic or silent failure
|
||||||
509
tauri2-guide/agent-prompts.md
Normal file
509
tauri2-guide/agent-prompts.md
Normal file
@ -0,0 +1,509 @@
|
|||||||
|
# Tauri 2.0 Agent Prompts — Definitive Collection
|
||||||
|
|
||||||
|
> **Purpose**: Copy-paste ready prompts for instructing AI agents to build Tauri 2.0 applications.
|
||||||
|
> **Constraint**: These prompts are designed for sandbox environments where code compilation is NOT available. Agents must verify correctness using logic, API references, and pattern matching.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Universal System Prompt (Required Prefix)](#1-universal-system-prompt-required-prefix)
|
||||||
|
2. [Prompt: Scaffold a New Tauri 2.0 Project](#2-prompt-scaffold-a-new-tauri-20-project)
|
||||||
|
3. [Prompt: Add a Tauri Plugin](#3-prompt-add-a-tauri-plugin)
|
||||||
|
4. [Prompt: Create a Custom Command (IPC)](#4-prompt-create-a-custom-command-ipc)
|
||||||
|
5. [Prompt: Add State Management](#5-prompt-add-state-management)
|
||||||
|
6. [Prompt: Implement Event-Based Communication](#6-prompt-implement-event-based-communication)
|
||||||
|
7. [Prompt: Create Multi-Window App](#7-prompt-create-multi-window-app)
|
||||||
|
8. [Prompt: Build a WebSocket Server in Rust](#8-prompt-build-a-websocket-server-in-rust)
|
||||||
|
9. [Prompt: Implement Script Injection](#9-prompt-implement-script-injection)
|
||||||
|
10. [Prompt: Set Up Capabilities & Permissions](#10-prompt-set-up-capabilities--permissions)
|
||||||
|
11. [Prompt: Migrate a v1 App to v2](#11-prompt-migrate-a-v1-app-to-v2)
|
||||||
|
12. [Prompt: Full App Build (End-to-End)](#12-prompt-full-app-build-end-to-end)
|
||||||
|
13. [Verification Checklist for Agents](#13-verification-checklist-for-agents)
|
||||||
|
14. [Quick Reference: v1 vs v2 API Map](#14-quick-reference-v1-vs-v2-api-map)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Universal System Prompt (Required Prefix)
|
||||||
|
|
||||||
|
> **Always include this as the system context before any task-specific prompt.**
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a Tauri 2.0 expert developer. You build desktop and mobile applications using the Tauri 2.0 framework (Rust backend + web frontend).
|
||||||
|
|
||||||
|
CRITICAL ENVIRONMENT CONSTRAINTS:
|
||||||
|
- You CANNOT compile or run code in this sandbox. Do NOT attempt compilation.
|
||||||
|
- Verify correctness using logic, API pattern matching, and reference knowledge only.
|
||||||
|
- Cross-reference every code snippet against known Tauri 2.0 API patterns before outputting.
|
||||||
|
- If you are uncertain about an API or pattern, say so explicitly rather than guessing.
|
||||||
|
|
||||||
|
MANDATORY TAURI 2.0 RULES — NEVER VIOLATE:
|
||||||
|
1. This is Tauri 2.0 (NOT v1). The APIs are fundamentally different.
|
||||||
|
2. Use `tauri::WebviewWindow` (NOT `tauri::Window`).
|
||||||
|
3. Use `tauri::WebviewWindowBuilder` (NOT `tauri::WindowBuilder`).
|
||||||
|
4. Use `tauri::WebviewUrl` (NOT `tauri::WindowUrl`).
|
||||||
|
5. Use `Manager::get_webview_window()` (NOT `Manager::get_window()`).
|
||||||
|
6. Use `tauri::Emitter` trait for `emit()` and `emit_to()` (NOT the old window-only emit).
|
||||||
|
7. Use `tauri::Listener` trait for `listen()` and `once()` on the Rust side.
|
||||||
|
8. JS import `invoke` from `@tauri-apps/api/core` (NOT `@tauri-apps/api/tauri`).
|
||||||
|
9. JS import window utilities from `@tauri-apps/api/webviewWindow` (NOT `@tauri-apps/api/window`).
|
||||||
|
10. JS plugins import from `@tauri-apps/plugin-<name>` (NOT `@tauri-apps/api/<name>`).
|
||||||
|
11. Config uses `app` section (NOT `tauri` section), `build.frontendDist` (NOT `build.distDir`).
|
||||||
|
12. Security uses Capabilities + Permissions (NOT `allowlist`).
|
||||||
|
13. Every plugin requires: (a) Rust crate in Cargo.toml, (b) `.plugin()` registration in Builder, (c) npm package, (d) permissions in capabilities.
|
||||||
|
14. Use the `lib.rs` + `main.rs` pattern. Core logic goes in `lib.rs`. `main.rs` only calls `lib::run()`.
|
||||||
|
15. `#[cfg_attr(mobile, tauri::mobile_entry_point)]` attribute is REQUIRED on the `pub fn run()` function.
|
||||||
|
16. `invoke_handler` can only be called ONCE — all commands go in a single `tauri::generate_handler![]`.
|
||||||
|
17. Async commands cannot use borrowed references (`&str`, `&Path`) directly — use owned types or wrap return in `Result`.
|
||||||
|
18. Window creation from async contexts MUST use `app_handle.run_on_main_thread()`.
|
||||||
|
19. Always clean up event listeners (call `unlisten()`) on the frontend.
|
||||||
|
20. Use `tauri = { version = "2" }` and `tauri-build = { version = "2" }` (NOT version 1).
|
||||||
|
|
||||||
|
FILE STRUCTURE:
|
||||||
|
- Frontend source: `src/`
|
||||||
|
- Rust backend: `src-tauri/`
|
||||||
|
- Rust entry-point: `src-tauri/src/main.rs` (minimal)
|
||||||
|
- Core app logic: `src-tauri/src/lib.rs`
|
||||||
|
- Extra modules: `src-tauri/src/<module>.rs`
|
||||||
|
- Capabilities: `src-tauri/capabilities/default.json`
|
||||||
|
- Config: `src-tauri/tauri.conf.json`
|
||||||
|
- Rust deps: `src-tauri/Cargo.toml`
|
||||||
|
- Build script: `src-tauri/build.rs`
|
||||||
|
|
||||||
|
OFFICIAL DOCUMENTATION:
|
||||||
|
- Main docs: https://v2.tauri.app
|
||||||
|
- Rust API: https://docs.rs/tauri/latest/tauri/
|
||||||
|
- Migration guide: https://v2.tauri.app/start/migrate/from-tauri-1/
|
||||||
|
- Capabilities: https://v2.tauri.app/security/capabilities/
|
||||||
|
- Permissions: https://v2.tauri.app/security/permissions/
|
||||||
|
- Configuration: https://v2.tauri.app/develop/configuration-files/
|
||||||
|
- Calling Rust: https://v2.tauri.app/develop/calling-rust/
|
||||||
|
- Calling Frontend: https://v2.tauri.app/develop/calling-frontend/
|
||||||
|
- Plugin index: https://v2.tauri.app/plugin/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Prompt: Scaffold a New Tauri 2.0 Project
|
||||||
|
|
||||||
|
```
|
||||||
|
Create a new Tauri 2.0 project from scratch with vanilla JavaScript (no frontend framework).
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- App name: [APP_NAME]
|
||||||
|
- Window title: [WINDOW_TITLE]
|
||||||
|
- Default window size: [WIDTH]x[HEIGHT]
|
||||||
|
- withGlobalTauri enabled (for window.__TAURI__ access without npm package)
|
||||||
|
|
||||||
|
Generate ALL of the following files with complete, correct Tauri 2.0 code:
|
||||||
|
|
||||||
|
1. `package.json` — with @tauri-apps/cli v2 devDependency and scripts (dev, build, tauri)
|
||||||
|
2. `src/index.html` — basic HTML5 boilerplate
|
||||||
|
3. `src/main.js` — demonstrate invoking a Rust command
|
||||||
|
4. `src/styles.css` — basic styling
|
||||||
|
5. `src-tauri/tauri.conf.json` — correct v2 structure (app section, build section, bundle section)
|
||||||
|
6. `src-tauri/Cargo.toml` — with tauri v2, tauri-build v2, serde, serde_json. Include [lib] with crate-type ["staticlib", "cdylib", "rlib"]
|
||||||
|
7. `src-tauri/build.rs` — standard tauri_build::build()
|
||||||
|
8. `src-tauri/capabilities/default.json` — with core:default permission
|
||||||
|
9. `src-tauri/src/main.rs` — minimal entry-point calling lib::run()
|
||||||
|
10. `src-tauri/src/lib.rs` — with Builder pattern, mobile_entry_point attribute, a sample "greet" command, and the command registered in invoke_handler
|
||||||
|
|
||||||
|
Verify every line follows Tauri 2.0 conventions. Cross-check config structure against the v2 schema.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Prompt: Add a Tauri Plugin
|
||||||
|
|
||||||
|
```
|
||||||
|
Add the [PLUGIN_NAME] plugin to an existing Tauri 2.0 project.
|
||||||
|
|
||||||
|
Plugin to add: [tauri-plugin-STORE/turi-plugin-FS/etc.]
|
||||||
|
JS package name: [@tauri-apps/plugin-store/etc.]
|
||||||
|
|
||||||
|
Instructions:
|
||||||
|
1. Add the Rust crate to `src-tauri/Cargo.toml` as `tauri-plugin-[name] = "2"`
|
||||||
|
2. Register the plugin in `src-tauri/src/lib.rs` using `.plugin(tauri_plugin_[name]::init())` or the Builder pattern if the plugin requires configuration
|
||||||
|
3. Add the npm package: `@tauri-apps/plugin-[name]`
|
||||||
|
4. Update `src-tauri/capabilities/default.json` to add the appropriate permission (usually `[name]:default` or specific allow permissions)
|
||||||
|
5. Provide a frontend usage example showing the correct v2 import from `@tauri-apps/plugin-[name]`
|
||||||
|
|
||||||
|
Show the exact changes needed for each file. Do NOT use v1 import paths or API patterns.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Prompt: Create a Custom Command (IPC)
|
||||||
|
|
||||||
|
```
|
||||||
|
Create a Tauri 2.0 command called "[COMMAND_NAME]" that [DESCRIBE WHAT IT DOES].
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Define the command using `#[tauri::command]` in a separate module file `src-tauri/src/commands.rs`
|
||||||
|
- The command must be `pub` (required for separate modules)
|
||||||
|
- Include proper error handling with `Result<T, String>` or a custom error type
|
||||||
|
- Register the command in `src-tauri/src/lib.rs` using `mod commands;` and including it in `generate_handler![commands::COMMAND_NAME]`
|
||||||
|
- Provide the frontend JavaScript code to invoke the command using `invoke()` from `@tauri-apps/api/core` or `window.__TAURI__.core`
|
||||||
|
- If the command is async, ensure all arguments are owned types (not borrowed references)
|
||||||
|
|
||||||
|
Show complete, correct code for both the Rust side and the JS side.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Prompt: Add State Management
|
||||||
|
|
||||||
|
```
|
||||||
|
Add shared state management to an existing Tauri 2.0 application.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
1. Define a state struct in `src-tauri/src/lib.rs` using `std::sync::Mutex` for thread safety
|
||||||
|
2. Manage the state in the Builder using `.manage(MyState { ... })`
|
||||||
|
3. Create at least two commands that read and write the managed state using `tauri::State<'_, MyState>`
|
||||||
|
4. Show the frontend code for invoking these commands
|
||||||
|
|
||||||
|
If persistent storage is also needed:
|
||||||
|
- Add `tauri-plugin-store = "2"` to Cargo.toml
|
||||||
|
- Register the plugin
|
||||||
|
- Add `store:default` to capabilities
|
||||||
|
- Show frontend usage with `@tauri-apps/plugin-store`
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Prompt: Implement Event-Based Communication
|
||||||
|
|
||||||
|
```
|
||||||
|
Implement bidirectional event communication in a Tauri 2.0 application.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
Rust side:
|
||||||
|
- Use `use tauri::{Emitter, AppHandle}` to emit events from Rust
|
||||||
|
- Demonstrate both global emit (`app.emit()`) and targeted emit (`app.emit_to()`)
|
||||||
|
- If listening for events on the Rust side, use `use tauri::Listener` and `app.listen()` or `app.once()`
|
||||||
|
|
||||||
|
Frontend side:
|
||||||
|
- Import from `@tauri-apps/api/event` (NOT `@tauri-apps/api/tauri`)
|
||||||
|
- Show `listen()`, `once()`, `emit()`, and `emitTo()` usage
|
||||||
|
- Demonstrate proper cleanup with `unlisten()`
|
||||||
|
- Show targeted listening using `getCurrentWebviewWindow().listen()`
|
||||||
|
|
||||||
|
Show complete code for:
|
||||||
|
1. A Rust command that emits a progress event
|
||||||
|
2. A Rust command that emits a completion event to a specific window
|
||||||
|
3. Frontend listeners that handle these events
|
||||||
|
4. Frontend emitting an event back to Rust
|
||||||
|
|
||||||
|
Make sure `core:event:default` (or specific `core:event:allow-*` permissions) is included in capabilities.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Prompt: Create Multi-Window App
|
||||||
|
|
||||||
|
```
|
||||||
|
Create a Tauri 2.0 application with multiple windows.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
1. Main window defined in `tauri.conf.json` with label "main"
|
||||||
|
2. A command `open_secondary` that creates a new window dynamically using `WebviewWindowBuilder`
|
||||||
|
3. The secondary window should load a local HTML file (`WebviewUrl::App("secondary.html".into())`)
|
||||||
|
4. If creating the window from an async context, use `app_handle.run_on_main_thread()`
|
||||||
|
5. Check for existing windows with the same label using `app_handle.get_webview_window(label)` and focus it if it exists
|
||||||
|
6. Show frontend code for creating windows using `new WebviewWindow(label, options)` from `@tauri-apps/api/webviewWindow`
|
||||||
|
|
||||||
|
CRITICAL: Use `tauri::WebviewWindowBuilder` and `tauri::WebviewUrl` (NOT the v1 `WindowBuilder`/`WindowUrl`).
|
||||||
|
CRITICAL: Use `app_handle.get_webview_window()` (NOT `app_handle.get_window()`).
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Prompt: Build a WebSocket Server in Rust
|
||||||
|
|
||||||
|
```
|
||||||
|
Build a native WebSocket server that runs inside a Tauri 2.0 application using the Tokio async runtime.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
Dependencies (Cargo.toml):
|
||||||
|
- `tokio = { version = "1", features = ["net", "rt", "sync", "macros"] }`
|
||||||
|
- `tokio-tungstenite = "0.24"`
|
||||||
|
- `futures-util = "0.3"`
|
||||||
|
|
||||||
|
Server implementation (`src-tauri/src/server.rs`):
|
||||||
|
- Bind a TcpListener to `127.0.0.1:[PORT]`
|
||||||
|
- Use `tokio::sync::broadcast` channel for message fan-out to all connected clients
|
||||||
|
- Accept WebSocket connections using `tokio_tungstenite::accept_async`
|
||||||
|
- Use `tokio::select!` to handle both incoming messages from clients AND broadcast messages from other clients
|
||||||
|
- Handle disconnections gracefully
|
||||||
|
- Spawn the server in `src-tauri/src/lib.rs` using `tauri::async_runtime::spawn(server::start_websocket_server())` inside the `.setup()` closure
|
||||||
|
|
||||||
|
Frontend client:
|
||||||
|
- Connect using standard `new WebSocket('ws://127.0.0.1:[PORT]')`
|
||||||
|
- Show send and receive handlers
|
||||||
|
|
||||||
|
Key pattern: Each client connection gets its own broadcast receiver via `tx.subscribe()`, enabling fan-out to all connected clients.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Prompt: Implement Script Injection
|
||||||
|
|
||||||
|
```
|
||||||
|
Implement JavaScript injection into a Tauri 2.0 WebviewWindow.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
1. Create a JavaScript file (e.g., `src-tauri/inject.js`) that will be injected into webview windows
|
||||||
|
2. The injected script should:
|
||||||
|
- Connect to a local WebSocket server (ws://127.0.0.1:[PORT])
|
||||||
|
- Send data from the webview page (e.g., localStorage contents) back to the server
|
||||||
|
- Receive data from the server and process it
|
||||||
|
- Expose the socket connection on `window.localAppSocket` for the page's own code to use
|
||||||
|
3. Use `include_str!("../inject.js")` to load the script at compile time
|
||||||
|
4. Inject it using `WebviewWindowBuilder::initialization_script(script_content)`
|
||||||
|
5. Show a complete working example of creating a window with an external URL and injecting the script
|
||||||
|
|
||||||
|
IMPORTANT: Set `"csp": null` in the app security config if injecting scripts into external URLs, otherwise CSP may block the injection.
|
||||||
|
|
||||||
|
Show both the Rust window creation code with injection and the JavaScript injection script.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Prompt: Set Up Capabilities & Permissions
|
||||||
|
|
||||||
|
```
|
||||||
|
Set up the Tauri 2.0 Capabilities and Permissions system for an application.
|
||||||
|
|
||||||
|
The application needs access to:
|
||||||
|
- [LIST REQUIRED FEATURES: file system, dialogs, HTTP requests, clipboard, notifications, etc.]
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
1. Create `src-tauri/capabilities/default.json` with proper v2 structure
|
||||||
|
2. Include the `$schema` pointing to `../gen/schemas/desktop-schema.json`
|
||||||
|
3. Set the `identifier`, `description`, `windows` (list of window labels), and `platforms` fields
|
||||||
|
4. Add the correct permission identifiers for each required feature:
|
||||||
|
- File system: `fs:default` or specific `fs:allow-*`
|
||||||
|
- Dialogs: `dialog:default` or specific `dialog:allow-*`
|
||||||
|
- HTTP: `http:default` or specific `http:allow-*`
|
||||||
|
- Events: `core:event:default`
|
||||||
|
- Window management: `core:window:default`
|
||||||
|
5. If fine-grained scopes are needed (e.g., restricting file access to specific directories), show scoped permissions with `allow` and `deny` arrays using path variables like `$APPDATA`, `$HOME`, `$RESOURCE`
|
||||||
|
|
||||||
|
CRITICAL: Do NOT use the v1 `allowlist` in `tauri.conf.json`. In v2, all permissions are defined via capability files.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Prompt: Migrate a v1 App to v2
|
||||||
|
|
||||||
|
```
|
||||||
|
Migrate the following Tauri v1 code to Tauri 2.0.
|
||||||
|
|
||||||
|
[PASTE v1 CODE HERE]
|
||||||
|
|
||||||
|
Migration checklist — verify ALL of these changes:
|
||||||
|
|
||||||
|
Config (tauri.conf.json):
|
||||||
|
- [ ] `tauri` section renamed to `app`
|
||||||
|
- [ ] `build.distDir` renamed to `build.frontendDist`
|
||||||
|
- [ ] `build.devPath` renamed to `build.devUrl`
|
||||||
|
- [ ] `build.withGlobalTauri` moved to `app.withGlobalTauri`
|
||||||
|
- [ ] `tauri.allowlist` REMOVED → replaced by capabilities
|
||||||
|
- [ ] `tauri.bundle` promoted to top-level `bundle`
|
||||||
|
- [ ] `bundle.identifier` promoted to top-level `identifier`
|
||||||
|
- [ ] `tauri.updater` moved to `plugins.updater`
|
||||||
|
|
||||||
|
Project structure:
|
||||||
|
- [ ] `main.rs` logic moved to `lib.rs` with `pub fn run()`
|
||||||
|
- [ ] New minimal `main.rs` calling `lib::run()`
|
||||||
|
- [ ] `Cargo.toml` has `[lib]` section with `crate-type = ["staticlib", "cdylib", "rlib"]`
|
||||||
|
- [ ] `#[cfg_attr(mobile, tauri::mobile_entry_point)]` added to `run()`
|
||||||
|
|
||||||
|
Rust code:
|
||||||
|
- [ ] All `tauri::api::*` imports REMOVED (replaced by plugins)
|
||||||
|
- [ ] `tauri::Window` → `tauri::WebviewWindow`
|
||||||
|
- [ ] `tauri::WindowBuilder` → `tauri::WebviewWindowBuilder`
|
||||||
|
- [ ] `tauri::WindowUrl` → `tauri::WebviewUrl`
|
||||||
|
- [ ] `get_window()` → `get_webview_window()`
|
||||||
|
- [ ] `tauri::Emitter` trait imported for `emit()` / `emit_to()`
|
||||||
|
- [ ] `tauri::Listener` trait imported for `listen()` / `once()`
|
||||||
|
- [ ] All plugins registered with `.plugin()` in Builder
|
||||||
|
- [ ] `tauri` and `tauri-build` version set to "2"
|
||||||
|
|
||||||
|
JavaScript code:
|
||||||
|
- [ ] `@tauri-apps/api/tauri` → `@tauri-apps/api/core`
|
||||||
|
- [ ] `@tauri-apps/api/window` → `@tauri-apps/api/webviewWindow`
|
||||||
|
- [ ] All plugin imports → `@tauri-apps/plugin-<name>`
|
||||||
|
- [ ] `@tauri-apps/cli` version set to "^2"
|
||||||
|
|
||||||
|
Capabilities:
|
||||||
|
- [ ] Created `src-tauri/capabilities/default.json`
|
||||||
|
- [ ] All necessary permissions listed
|
||||||
|
- [ ] `core:default` included at minimum
|
||||||
|
|
||||||
|
Output the complete migrated code for every file that changed.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Prompt: Full App Build (End-to-End)
|
||||||
|
|
||||||
|
```
|
||||||
|
Build a complete Tauri 2.0 application with the following requirements:
|
||||||
|
|
||||||
|
App Name: [APP_NAME]
|
||||||
|
Purpose: [DESCRIBE APP PURPOSE]
|
||||||
|
|
||||||
|
Features needed:
|
||||||
|
- [FEATURE 1: e.g., File system read/write]
|
||||||
|
- [FEATURE 2: e.g., HTTP API client]
|
||||||
|
- [FEATURE 3: e.g., Persistent settings storage]
|
||||||
|
- [FEATURE 4: e.g., System tray icon]
|
||||||
|
- [FEATURE 5: e.g., Multi-window support]
|
||||||
|
- [FEATURE 6: e.g., Custom Rust commands]
|
||||||
|
|
||||||
|
Frontend: [Vanilla JS / React / Vue / Svelte / etc.]
|
||||||
|
|
||||||
|
Generate ALL files for a complete, working Tauri 2.0 application:
|
||||||
|
1. `package.json`
|
||||||
|
2. `src/index.html` (or appropriate entry point for the chosen framework)
|
||||||
|
3. Frontend source files (JavaScript/TypeScript/CSS)
|
||||||
|
4. `src-tauri/tauri.conf.json` — v2 structure with all necessary config
|
||||||
|
5. `src-tauri/Cargo.toml` — all dependencies including required plugins
|
||||||
|
6. `src-tauri/build.rs`
|
||||||
|
7. `src-tauri/capabilities/default.json` — all required permissions
|
||||||
|
8. `src-tauri/src/main.rs` — minimal entry-point
|
||||||
|
9. `src-tauri/src/lib.rs` — Builder with all plugins, commands, setup
|
||||||
|
10. Additional Rust module files as needed
|
||||||
|
11. Any injected scripts if required
|
||||||
|
|
||||||
|
For every file:
|
||||||
|
- Use ONLY valid Tauri 2.0 code
|
||||||
|
- Use the correct v2 import paths
|
||||||
|
- Use v2 type names (WebviewWindow, WebviewUrl, etc.)
|
||||||
|
- Include proper error handling
|
||||||
|
- Add appropriate comments explaining the v2 patterns used
|
||||||
|
|
||||||
|
Do NOT skip any file. Generate complete, production-ready code.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Verification Checklist for Agents
|
||||||
|
|
||||||
|
> Agents should run through this checklist mentally for EVERY code snippet they generate.
|
||||||
|
|
||||||
|
### Config Verification
|
||||||
|
- [ ] `tauri.conf.json` uses `app` section (not `tauri`)
|
||||||
|
- [ ] `build.frontendDist` (not `build.distDir`)
|
||||||
|
- [ ] `build.devUrl` (not `build.devPath`)
|
||||||
|
- [ ] `withGlobalTauri` is under `app` (not `build`)
|
||||||
|
- [ ] `identifier` is at top level (not under `bundle`)
|
||||||
|
- [ ] `bundle` is at top level (not under `tauri`)
|
||||||
|
- [ ] No `allowlist` present anywhere
|
||||||
|
|
||||||
|
### Rust Code Verification
|
||||||
|
- [ ] `tauri` version is `"2"` (not `"1"`)
|
||||||
|
- [ ] `tauri-build` version is `"2"` (not `"1"`)
|
||||||
|
- [ ] `Cargo.toml` has `[lib]` with `crate-type = ["staticlib", "cdylib", "rlib"]`
|
||||||
|
- [ ] `main.rs` only contains `#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]` and `fn main() { lib_name::run() }`
|
||||||
|
- [ ] `lib.rs` has `#[cfg_attr(mobile, tauri::mobile_entry_point)]` on `pub fn run()`
|
||||||
|
- [ ] No `tauri::api::*` imports anywhere
|
||||||
|
- [ ] `WebviewWindow` used (not `Window`)
|
||||||
|
- [ ] `WebviewWindowBuilder` used (not `WindowBuilder`)
|
||||||
|
- [ ] `WebviewUrl` used (not `WindowUrl`)
|
||||||
|
- [ ] `get_webview_window()` used (not `get_window()`)
|
||||||
|
- [ ] `Emitter` trait imported when using `emit()` or `emit_to()`
|
||||||
|
- [ ] `Listener` trait imported when using `listen()` or `once()`
|
||||||
|
- [ ] `invoke_handler` called only ONCE with ALL commands
|
||||||
|
- [ ] Async commands use owned types or Result wrapping (not bare `&str`)
|
||||||
|
- [ ] Window creation in async contexts uses `run_on_main_thread()`
|
||||||
|
- [ ] All plugins registered with `.plugin()` before `.run()`
|
||||||
|
|
||||||
|
### Frontend Code Verification
|
||||||
|
- [ ] `invoke` imported from `@tauri-apps/api/core` (not `@tauri-apps/api/tauri`)
|
||||||
|
- [ ] `WebviewWindow` imported from `@tauri-apps/api/webviewWindow` (not `@tauri-apps/api/window`)
|
||||||
|
- [ ] Event functions from `@tauri-apps/api/event` (not core or tauri)
|
||||||
|
- [ ] Plugin imports from `@tauri-apps/plugin-<name>` (not `@tauri-apps/api/<name>`)
|
||||||
|
- [ ] `@tauri-apps/cli` is version `^2`
|
||||||
|
- [ ] Event listeners cleaned up with `unlisten()`
|
||||||
|
- [ ] Argument names in `invoke()` calls match Rust command parameter names (camelCase by default)
|
||||||
|
|
||||||
|
### Capabilities Verification
|
||||||
|
- [ ] Capability files exist in `src-tauri/capabilities/`
|
||||||
|
- [ ] `$schema` points to correct schema path
|
||||||
|
- [ ] All used plugins have corresponding permissions
|
||||||
|
- [ ] Window labels in capabilities match actual window labels
|
||||||
|
- [ ] No `allowlist` in config files
|
||||||
|
|
||||||
|
### Plugin Verification
|
||||||
|
- [ ] Rust crate: `tauri-plugin-<name> = "2"` in Cargo.toml
|
||||||
|
- [ ] Registration: `.plugin(tauri_plugin_<name>::init())` in Builder
|
||||||
|
- [ ] JS package: `@tauri-apps/plugin-<name>` in dependencies
|
||||||
|
- [ ] Permissions: Listed in capabilities file
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Quick Reference: v1 vs v2 API Map
|
||||||
|
|
||||||
|
> Keep this table handy when migrating or reviewing code.
|
||||||
|
|
||||||
|
### Rust Type Renames
|
||||||
|
| v1 | v2 |
|
||||||
|
|----|----|
|
||||||
|
| `tauri::Window` | `tauri::WebviewWindow` |
|
||||||
|
| `tauri::WindowBuilder` | `tauri::WebviewWindowBuilder` |
|
||||||
|
| `tauri::WindowUrl` | `tauri::WebviewUrl` |
|
||||||
|
| `Manager::get_window()` | `Manager::get_webview_window()` |
|
||||||
|
| `tauri::WindowEvent` | `tauri::WebviewWindowEvent` |
|
||||||
|
| `tauri::Manager::fs_scope()` | `tauri_plugin_fs::FsExt` |
|
||||||
|
|
||||||
|
### Rust API Removals (replaced by plugins)
|
||||||
|
| v1 | v2 Plugin |
|
||||||
|
|----|-----------|
|
||||||
|
| `tauri::api::dialog` | `tauri-plugin-dialog` |
|
||||||
|
| `tauri::api::fs` | `std::fs` / `tauri-plugin-fs` |
|
||||||
|
| `tauri::api::http` | `tauri-plugin-http` |
|
||||||
|
| `tauri::api::path` | `tauri::Manager::path()` |
|
||||||
|
| `tauri::api::process::Command` | `tauri-plugin-shell` |
|
||||||
|
| `tauri::api::clipboard` | `tauri-plugin-clipboard-manager` |
|
||||||
|
| `tauri::api::notification` | `tauri-plugin-notification` |
|
||||||
|
| `tauri::api::global_shortcut` | `tauri-plugin-global-shortcut` |
|
||||||
|
| `tauri::api::updater` | `tauri-plugin-updater` |
|
||||||
|
| `tauri::api::cli` | `tauri-plugin-cli` |
|
||||||
|
| `tauri::api::os` | `tauri-plugin-os` |
|
||||||
|
| `tauri::api::version` | `semver` crate |
|
||||||
|
|
||||||
|
### JavaScript Import Path Changes
|
||||||
|
| v1 | v2 |
|
||||||
|
|----|----|
|
||||||
|
| `@tauri-apps/api/tauri` | `@tauri-apps/api/core` |
|
||||||
|
| `@tauri-apps/api/window` | `@tauri-apps/api/webviewWindow` |
|
||||||
|
| `@tauri-apps/api/dialog` | `@tauri-apps/plugin-dialog` |
|
||||||
|
| `@tauri-apps/api/fs` | `@tauri-apps/plugin-fs` |
|
||||||
|
| `@tauri-apps/api/http` | `@tauri-apps/plugin-http` |
|
||||||
|
| `@tauri-apps/api/notification` | `@tauri-apps/plugin-notification` |
|
||||||
|
| `@tauri-apps/api/clipboard` | `@tauri-apps/plugin-clipboard-manager` |
|
||||||
|
| `@tauri-apps/api/shell` | `@tauri-apps/plugin-shell` |
|
||||||
|
| `@tauri-apps/api/global-shortcut` | `@tauri-apps/plugin-global-shortcut` |
|
||||||
|
| `@tauri-apps/api/updater` | `@tauri-apps/plugin-updater` |
|
||||||
|
| `@tauri-apps/api/os` | `@tauri-apps/plugin-os` |
|
||||||
|
| `@tauri-apps/api/process` | `@tauri-apps/plugin-process` |
|
||||||
|
| `@tauri-apps/api/cli` | `@tauri-apps/plugin-cli` |
|
||||||
|
|
||||||
|
### Config Path Changes
|
||||||
|
| v1 | v2 |
|
||||||
|
|----|----|
|
||||||
|
| `build.distDir` | `build.frontendDist` |
|
||||||
|
| `build.devPath` | `build.devUrl` |
|
||||||
|
| `build.withGlobalTauri` | `app.withGlobalTauri` |
|
||||||
|
| `tauri.*` | `app.*` |
|
||||||
|
| `tauri.allowlist.*` | Capabilities system |
|
||||||
|
| `tauri.windows.fileDropEnabled` | `app.windows.dragDropEnabled` |
|
||||||
|
| `tauri.bundle` | `bundle` (top-level) |
|
||||||
|
| `bundle.identifier` | `identifier` (top-level) |
|
||||||
|
| `tauri.updater` | `plugins.updater` |
|
||||||
|
| `tauri.systemTray` | `app.trayIcon` |
|
||||||
|
| `tauri.cli` | `plugins.cli` |
|
||||||
1816
tauri2-guide/definitive-guide.md
Normal file
1816
tauri2-guide/definitive-guide.md
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user