SSkilltecabyclaudinhocode
Enviar skill
← Voltar para o catálogo

tauri-js-runtime

Design e Frontend

Adicione capacidades de backend de runtime JavaScript a aplicativos desktop Tauri v2, seja usando o plugin tauri-plugin-js ou construindo do zero. Isso permite integrar Bun, Node.js ou Deno como processos de backend, configurar RPC type-safe, criar arquiteturas semelhantes ao Electron e gerenciar processos filhos via comunicação stdio.

14estrelas
Ver no GitHub ↗Autor: HuakunShen

Tauri + JS Runtime Integration

Give Tauri apps full JS runtime backends (Bun, Node.js, Deno) with type-safe bidirectional RPC. This covers two approaches: using the tauri-plugin-js plugin, and building the integration from scratch.

When to Use

  • User wants to run JS/TS backend code from a Tauri desktop app
  • User asks about Electron alternatives or "Electron-like" features in Tauri
  • User needs to spawn/manage child processes (Bun, Node, Deno) from Rust
  • User wants type-safe RPC between a Tauri webview and a JS runtime
  • User needs stdio-based IPC between Rust and a child process
  • User asks about kkrpc integration with Tauri
  • User wants multi-window apps where windows share backend processes
  • User needs runtime detection (which runtimes are installed, paths, versions)
  • User wants to ship a Tauri app without requiring JS runtimes on user machines (compiled sidecars)
  • User asks about bun build --compile or deno compile with Tauri

Core Architecture

Frontend (Webview)  <-- Tauri Events -->  Rust Core  <-- stdio -->  JS Runtime
  • Rust spawns child processes, pipes their stdin/stdout/stderr, and relays data via Tauri events
  • Rust never parses RPC payloads — it forwards raw newline-delimited strings
  • kkrpc handles the RPC protocol on both ends (frontend webview + backend runtime)
  • Frontend IO adapter bridges Tauri events to kkrpc's IoInterface (read/write/on/off)
  • Multi-window works because all windows receive the same Tauri events; kkrpc request IDs handle routing

Approach A: Using tauri-plugin-js (Recommended)

The plugin handles process management, stdio relay, event emission, and provides a frontend npm package with typed wrappers and an IO adapter.

Step 1: Install

Rust — add to src-tauri/Cargo.toml:

[dependencies]
tauri-plugin-js = "0.1"

Frontend — install npm packages:

pnpm add tauri-plugin-js-api kkrpc

Step 2: Register the plugin

In src-tauri/src/lib.rs:

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_js::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Step 3: Add permissions

In src-tauri/capabilities/default.json:

{
  "permissions": [
    "core:default",
    "js:default"
  ]
}

Step 4: Define a shared API type

Create a type definition shared between frontend and backend workers:

// backends/shared-api.ts
export interface BackendAPI {
  add(a: number, b: number): Promise<number>;
  echo(message: string): Promise<string>;
  getSystemInfo(): Promise<{
    runtime: string;
    pid: number;
    platform: string;
    arch: string;
  }>;
}

Step 5: Write backend workers

Each runtime has its own IO adapter from kkrpc:

Bun (backends/bun-worker.ts):

import { RPCChannel, BunIo } from "kkrpc";
import type { BackendAPI } from "./shared-api";

const api: BackendAPI = {
  async add(a, b) { return a + b; },
  async echo(msg) { return `[bun] ${msg}`; },
  async getSystemInfo() {
    return { runtime: "bun", pid: process.pid, platform: process.platform, arch: process.arch };
  },
};

const io = new BunIo(Bun.stdin.stream());
const channel = new RPCChannel(io, { expose: api });

Node (backends/node-worker.mjs):

import { RPCChannel, NodeIo } from "kkrpc";

const api = {
  async add(a, b) { return a + b; },
  async echo(msg) { return `[node] ${msg}`; },
  async getSystemInfo() {
    return { runtime: "node", pid: process.pid, platform: process.platform, arch: process.arch };
  },
};

const io = new NodeIo(process.stdin, process.stdout);
const channel = new RPCChannel(io, { expose: api });

Deno (backends/deno-worker.ts):

import { DenoIo, RPCChannel } from "npm:kkrpc/deno";
import type { BackendAPI } from "./shared-api.ts";  // .ts extension required by Deno

const api: BackendAPI = {
  async add(a, b) { return a + b; },
  async echo(msg) { return `[deno] ${msg}`; },
  async getSystemInfo() {
    return { runtime: "deno", pid: Deno.pid, platform: Deno.build.os, arch: Deno.build.arch };
  },
};

const io = new DenoIo(Deno.stdin.readable);
const channel = new RPCChannel(io, { expose: api });

Step 6: Frontend — spawn and call

import { spawn, createChannel, onStdout, onStderr, onExit } from "tauri-plugin-js-api";
import type { BackendAPI } from "../backends/shared-api";

// Spawn
const cwd = await resolve("..", "backends");  // from @tauri-apps/api/path
await spawn("my-worker", { runtime: "bun", script: "bun-worker.ts", cwd });

// Events
onStdout("my-worker", (data) => console.log(data));
onStderr("my-worker", (data) => console.error(data));
onExit("my-worker", (code) => console.log("exited", code));

// Type-safe RPC
const { api } = await createChannel<Record<string, never>, BackendAPI>("my-worker");
const result = await api.add(5, 3);  // compile-time checked

Step 7: Compiled binary sidecars (no runtime on user machine)

Both Bun and Deno can compile TS workers into standalone executables. The compiled binaries preserve stdin/stdout behavior, so kkrpc works unchanged.

Compile with target triple suffix:

TARGET=$(rustc -vV | grep host | cut -d' ' -f2)

# Bun — compile directly from the project directory
bun build --compile --minify backends/bun-worker.ts --outfile src-tauri/binaries/bun-worker-$TARGET

# Deno — MUST compile from a separate Deno package (see pitfall #8 below)
deno compile --allow-all --output src-tauri/binaries/deno-worker-$TARGET path/to/deno-package/main.ts

Configure Tauri to bundle sidecars in src-tauri/tauri.conf.json:

{
  "bundle": {
    "externalBin": ["binaries/bun-worker", "binaries/deno-worker"]
  }
}

Tauri automatically appends the current platform's triple when resolving externalBin paths, so the binary is included in the app bundle and runs on the user's machine without any runtime installed.

Spawn with sidecar instead of runtime:

import { spawn, createChannel } from "tauri-plugin-js-api";

await spawn("compiled-worker", { sidecar: "bun-worker" });

// RPC works identically
const { api } = await createChannel<Record<string, never>, BackendAPI>("compiled-worker");
await api.add(5, 3); // => 8

Key points:

  • config.sidecar resolves the binary via Tauri's sidecar mechanism — looks next to the app executable, tries both plain name (production) and {name}-{triple} (development)
  • The same worker TS source compiles into a binary that runs identically to the runtime-based version
  • getSystemInfo() still reports runtime: "bun" or runtime: "deno" — the runtime is embedded in the binary
  • No filesystem path resolution needed on the frontend — just pass the sidecar name

Step 8: Runtime detection (optional)

import { detectRuntimes, setRuntimePath } from "tauri-plugin-js-api";

const runtimes = await detectRuntimes();
// [{ name: "bun", available: true, version: "1.2.0", path: "/usr/local/bin/bun" }, ...]

// Override path for a specific runtime
await setRuntimePath("node", "/custom/path/to/node");

Plugin API Summary

CommandDescription
spawn(name, config)Start a named process
kill(name)Kill by name
killAll()Kill all
restart(name, config?)Restart with optional new config
listProcesses()List running processes
getStatus(name)Get process status
writeStdin(name, data)Write raw string to stdin
detectRuntimes()Detect bun/node/deno availability
setRuntimePath(rt, path)Set custom executable path
getRuntimePaths()Get custom path overrides
EventPayload
js-process-stdout{ name: string, data: string }
js-process-stderr{ name: string, data: string }
js-process-exit{ name: string, code: number | null }

Approach B: Buildi

Como adicionar

/plugin marketplace add HuakunShen/tauri-plugin-js

O comando exato pode variar conforme o repositório. Confira o README no GitHub.

Comentários · Nenhum comentário

Entre para comentar. Entrar

  • Ainda não há comentários. Seja o primeiro.