running-nodejs-sidecar-in-tauri
1
总安装量
1
周安装量
#50331
全站排名
安装命令
npx skills add https://github.com/beshkenadze/claude-code-tauri-skills --skill running-nodejs-sidecar-in-tauri
Agent 安装分布
cursor
1
codex
1
claude-code
1
gemini-cli
1
Skill 文档
Running Node.js as a Sidecar in Tauri
Package and run Node.js applications as sidecar processes in Tauri desktop applications, leveraging the Node.js ecosystem without requiring users to install Node.js.
Why Use a Node.js Sidecar
- Bundle existing Node.js tools and libraries with your Tauri application
- No external Node.js runtime dependency for end users
- Leverage npm packages that have no Rust equivalent
- Isolate Node.js logic from the main Tauri process
- Cross-platform support (Windows, macOS, Linux)
Prerequisites
- Existing Tauri v2 application
- Shell plugin installed and configured
- Node.js and npm on the development machine
- Rust toolchain (1.84.0+ recommended)
Install the Shell Plugin
npm install @tauri-apps/plugin-shell
cargo add tauri-plugin-shell --manifest-path src-tauri/Cargo.toml
Register in src-tauri/src/lib.rs:
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Project Structure
my-tauri-app/
âââ package.json
âââ src-tauri/
â âââ binaries/
â â âââ my-sidecar-<target-triple>[.exe]
â âââ capabilities/default.json
â âââ tauri.conf.json
â âââ src/lib.rs
âââ sidecar/
â âââ package.json
â âââ index.js
â âââ rename.js
âââ src/
Step-by-Step Setup
1. Create the Sidecar Directory
mkdir sidecar && cd sidecar
npm init -y
npm add @yao-pkg/pkg --save-dev
2. Write Sidecar Logic
Create sidecar/index.js:
const command = process.argv[2];
const args = process.argv.slice(3);
switch (command) {
case 'hello':
console.log(`Hello ${args[0] || 'World'}!`);
break;
case 'add':
const [a, b] = args.map(Number);
if (isNaN(a) || isNaN(b)) {
console.error('Error: Both arguments must be numbers');
process.exit(1);
}
console.log(JSON.stringify({ result: a + b }));
break;
default:
console.error(`Unknown command: ${command}`);
process.exit(1);
}
3. Create the Rename Script
Create sidecar/rename.js:
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
const ext = process.platform === 'win32' ? '.exe' : '';
let targetTriple;
try {
targetTriple = execSync('rustc --print host-tuple').toString().trim();
} catch {
const rustInfo = execSync('rustc -vV').toString();
const match = rustInfo.match(/host: (.+)/);
targetTriple = match ? match[1] : null;
if (!targetTriple) {
console.error('Could not determine Rust target triple');
process.exit(1);
}
}
const destDir = path.join('..', 'src-tauri', 'binaries');
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
fs.renameSync(`my-sidecar${ext}`, path.join(destDir, `my-sidecar-${targetTriple}${ext}`));
4. Configure Build Scripts
Update sidecar/package.json:
{
"name": "my-sidecar",
"type": "module",
"scripts": {
"build": "pkg index.js --output my-sidecar --targets node18",
"postbuild": "node rename.js"
},
"devDependencies": {
"@yao-pkg/pkg": "^5.0.0"
}
}
5. Configure Tauri
Add to src-tauri/tauri.conf.json:
{
"bundle": {
"externalBin": ["binaries/my-sidecar"]
}
}
6. Configure Permissions
Update src-tauri/capabilities/default.json:
{
"identifier": "default",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "shell:allow-execute",
"allow": [{
"args": true,
"name": "binaries/my-sidecar",
"sidecar": true
}]
}
]
}
Restrict arguments for security:
{
"identifier": "shell:allow-execute",
"allow": [
{
"args": ["hello", { "validator": "\\w+" }],
"name": "binaries/my-sidecar",
"sidecar": true
}
]
}
Communication Patterns
Frontend to Sidecar (TypeScript)
import { Command } from '@tauri-apps/plugin-shell';
async function sayHello(name: string): Promise<string> {
const command = Command.sidecar('binaries/my-sidecar', ['hello', name]);
const output = await command.execute();
if (output.code !== 0) throw new Error(output.stderr);
return output.stdout.trim();
}
async function addNumbers(a: number, b: number): Promise<number> {
const command = Command.sidecar('binaries/my-sidecar', ['add', String(a), String(b)]);
const output = await command.execute();
if (output.code !== 0) throw new Error(output.stderr);
return JSON.parse(output.stdout).result;
}
Backend to Sidecar (Rust)
use tauri_plugin_shell::ShellExt;
#[tauri::command]
async fn call_sidecar(
app: tauri::AppHandle,
command: String,
args: Vec<String>,
) -> Result<String, String> {
let mut sidecar = app.shell().sidecar("my-sidecar").map_err(|e| e.to_string())?;
sidecar = sidecar.arg(&command);
for arg in args {
sidecar = sidecar.arg(&arg);
}
let output = sidecar.output().await.map_err(|e| e.to_string())?;
if output.status.success() {
String::from_utf8(output.stdout).map_err(|e| e.to_string())
} else {
Err(String::from_utf8_lossy(&output.stderr).to_string())
}
}
Register the command:
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![call_sidecar])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Streaming Output
import { Command } from '@tauri-apps/plugin-shell';
async function runWithStreaming(args: string[]): Promise<void> {
const command = Command.sidecar('binaries/my-sidecar', args);
command.on('close', (data) => console.log(`Finished: ${data.code}`));
command.on('error', (error) => console.error(`Error: ${error}`));
command.stdout.on('data', (line) => console.log(`stdout: ${line}`));
command.stderr.on('data', (line) => console.error(`stderr: ${line}`));
await command.spawn();
}
Long-Running HTTP Sidecar
For persistent processes, use HTTP:
sidecar/index.js:
import http from 'http';
const PORT = process.env.SIDECAR_PORT || 3333;
const server = http.createServer((req, res) => {
let body = '';
req.on('data', (chunk) => (body += chunk));
req.on('end', () => {
res.setHeader('Content-Type', 'application/json');
try {
const data = body ? JSON.parse(body) : {};
if (req.url === '/hello') {
res.end(JSON.stringify({ message: `Hello ${data.name || 'World'}!` }));
} else if (req.url === '/health') {
res.end(JSON.stringify({ status: 'ok' }));
} else {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Not found' }));
}
} catch (err) {
res.statusCode = 400;
res.end(JSON.stringify({ error: err.message }));
}
});
});
server.listen(PORT, '127.0.0.1', () => console.log(`Listening on ${PORT}`));
process.on('SIGTERM', () => server.close(() => process.exit(0)));
Frontend communication:
import { Command } from '@tauri-apps/plugin-shell';
import { fetch } from '@tauri-apps/plugin-http';
let sidecarProcess: any = null;
const PORT = 3333;
async function startSidecar(): Promise<void> {
if (sidecarProcess) return;
const command = Command.sidecar('binaries/my-sidecar', [], {
env: { SIDECAR_PORT: String(PORT) },
});
sidecarProcess = await command.spawn();
for (let i = 0; i < 10; i++) {
try {
const res = await fetch(`http://127.0.0.1:${PORT}/health`);
if (res.ok) return;
} catch {
await new Promise((r) => setTimeout(r, 100));
}
}
throw new Error('Sidecar failed to start');
}
async function callSidecar(endpoint: string, data?: object): Promise<any> {
const res = await fetch(`http://127.0.0.1:${PORT}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: data ? JSON.stringify(data) : undefined,
});
return res.json();
}
async function stopSidecar(): Promise<void> {
if (sidecarProcess) {
await sidecarProcess.kill();
sidecarProcess = null;
}
}
Building for Production
Update root package.json:
{
"scripts": {
"build:sidecar": "cd sidecar && npm run build",
"dev": "npm run build:sidecar && tauri dev",
"build": "npm run build:sidecar && tauri build"
}
}
Cross-platform targets:
| Platform | pkg Target | Rust Triple |
|---|---|---|
| Windows x64 | node18-win-x64 |
x86_64-pc-windows-msvc |
| macOS x64 | node18-macos-x64 |
x86_64-apple-darwin |
| macOS ARM | node18-macos-arm64 |
aarch64-apple-darwin |
| Linux x64 | node18-linux-x64 |
x86_64-unknown-linux-gnu |
Security
- Use validators instead of
"args": true - Bind HTTP servers to
127.0.0.1only - Validate input in both Tauri and sidecar
- Ensure sidecars terminate when the app closes
Troubleshooting
Binary not found: Check target triple matches:
ls -la src-tauri/binaries/
rustc --print host-tuple
Permission denied (Unix):
chmod +x src-tauri/binaries/my-sidecar-*
Silent crashes: Check stderr:
const output = await command.execute();
if (output.code !== 0) console.error(output.stderr);