13 · Tauri
Tauri builds the desktop shell in Rust, but the frontend is a web app with its own JS toolchain, usually Vite plus Jest. Here 3va fills that role with a single binary: dev server, bundler and test runner.
Source: examples/13-tauri
index.html loads /src/main.ts as a module
src/
main.ts UI: volume slider, theme and mute buttons
state.ts typed "commands" + reducer (mirrors #[tauri::command])
state.test.ts 7 tests
package.json dev / build / test scripts pointing at 3vaThis example contains only the frontend. There’s no src-tauri/ folder.
Dev server
cd examples/13-tauri
3va dev --port 3000Open http://localhost:3000 to get a volume slider, a mute button and a
light/dark toggle. .ts files are transpiled per request and served as real
ES modules:
import { applyCommand, initialAppState } from "/src/state.ts";
const app = document.getElementById("app");
…Production bundle
3va bundle src/main.ts -o dist/bundle.js --minifyTests
3va test ✓ applyCommand > set-theme cambia el tema
✓ applyCommand > set-volume válido
✓ applyCommand > set-volume a 0 silencia
✓ applyCommand > set-volume inválido es rechazado y no muta el estado
✓ applyCommand > mute / unmute
✓ reduceCommands > aplica una secuencia en orden
✓ reduceCommands > un comando inválido detiene la escritura pero no rompe los anteriores
Tests: 7
Passed: 7Wiring it into Tauri v2
In a real project, point Tauri’s build hooks at 3va:
{
"build": {
"beforeDevCommand": "3va dev --port 3000",
"beforeBuildCommand": "3va bundle src/main.ts -o dist/bundle.js --minify",
"devUrl": "http://localhost:3000",
"frontendDist": "../dist"
}
}cargo tauri dev starts 3va dev, points the webview at devUrl, and
reloads on save.
The package.json scripts wrap the same commands, and because
3va <name> falls back to package.json scripts, 3va build runs the
bundle step:
"scripts": {
"dev": "3va dev --port 3000",
"build": "3va bundle src/main.ts -o dist/bundle.js --minify",
"test": "3va test",
"tauri": "cargo tauri dev"
}How it works
state.ts models the backend commands as a discriminated union and a
reducer:
export type VolumeChange =
| { kind: "set-volume"; value: number }
| { kind: "mute" }
| { kind: "unmute" };
export function applyCommand(state: AppState, cmd: ThemeChange | VolumeChange): CommandResultIn a real app these would be invoke("set_volume", { value }) calls handled
by Rust. Keeping a TypeScript version of the same contract lets you test
the UI logic (validation, 0 → muted, rejecting 150) without compiling the
shell or mocking IPC.
Things to keep in mind
frontendDistneeds anindex.html, not justbundle.js.3va bundlewrites only the JS file. The rootindex.htmlloads/src/main.tsfor dev and isn’t copied. For production, add an HTML file todist/that loadsbundle.jswith a classic<script>after#app, because the bundle is not an ES module (see 08 · Bundler). Otherwisecargo tauri buildhas nothing to open.- The TS reducer is a copy of the Rust logic, not the logic itself. The
tests prove the frontend’s model is consistent. They don’t prove the Rust
command behaves the same way. Keep the two in sync (e.g. generate the TS
types from Rust with
ts-rsorspecta) and still test the Rust side withcargo test. - CSP:
3va devsends its own Content-Security-Policy header, and Tauri applies the CSP fromtauri.conf.jsonin production. The example README suggests3va dev --port 3000 --no-cspso the dev policy doesn’t get in the way of the webview. Make sure your production CSP intauri.conf.jsonis the one you actually want. 3va buildand3va devare not the same kind of command.3va devis a built-in (built-ins always win over scripts), while3va buildruns thebuildscript through the package manager, outside the sandbox. It ran without a prompt here only becausepackage.jsonsets"no-prompt": true.⚠ Dependencies may be out of date — run 3va installappears because there’s apackage.jsonand nonode_modules. The example has no dependencies, so it’s harmless.cargo tauri devitself is Rust tooling. 3va doesn’t replace it.