running-nodejs-sidecar-in-tauri について
running-nodejs-sidecar-in-tauriは、Node.jsアプリをTauriデスクトップアプリのサイドカープロセスとしてパッケージ化するガイドスキルです。Node.jsツールの同梱、クロスプラットフォームビルド、Tauri Shellプラグインを使った通信を支援します。
主な機能
- 01Node.jsアプリのサイドカー化
- 02Tauri外部バイナリの設定
- 03Shellプラグイン経由のサイドカー呼び出し
- 04標準出力とストリーミング出力の処理
- 05HTTPによる長時間実行プロセスとの通信
活用シーン
- 01Tauriデスクトップアプリで既存のNode.jsツールを実行
- 02ユーザーにNode.jsの別途インストールを求めずに配布
- 03Rustに相当機能がないnpmパッケージをサイドカー経由で利用
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dchuk/ claude-code-tauri-skills --skill running-nodejs-sidecar-in-tauriインストール後、Claude Code で/running-nodejs-sidecar-in-tauriを入力して読み込まれたか確認します。
詳細
Tauri で Node.js をサイドカーとして実行する
Node.js アプリケーションを Tauri デスクトップアプリケーションのサイドカープロセスとしてパッケージ化して実行することで、ユーザーに Node.js のインストールを求めずに Node.js エコシステムを活用できます。
Node.js サイドカーを使用する理由
- 既存の Node.js ツールやライブラリを Tauri アプリケーションにバンドルできます
- エンドユーザーに外部の Node.js ランタイムが不要です
- Rust に相当するものがない npm パッケージを活用できます
- Node.js のロジックをメインの Tauri プロセスから分離できます
- クロスプラットフォームに対応しています(Windows、macOS、Linux)
前提条件
- 既存の Tauri v2 アプリケーション
- Shell プラグインがインストールされ、設定済みであること
- 開発マシンに Node.js と npm がインストールされていること
- Rust ツールチェーン(1.84.0 以降を推奨)
Shell プラグインをインストールする
npm install @tauri-apps/plugin-shell
cargo add tauri-plugin-shell --manifest-path src-tauri/Cargo.toml
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");
}
プロジェクト構成
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/
セットアップ手順
1. サイドカーディレクトリを作成する
mkdir sidecar && cd sidecar
npm init -y
npm add @yao-pkg/pkg --save-dev
2. サイドカーのロジックを記述する
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. 名前変更スクリプトを作成する
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. ビルドスクリプトを設定する
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. Tauri を設定する
src-tauri/tauri.conf.json に追加します。
{
"bundle": {
"externalBin": ["binaries/my-sidecar"]
}
}
6. 権限を設定する
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
}]
}
]
}
セキュリティのために引数を制限します。
{
"identifier": "shell:allow-execute",
"allow": [
{
"args": ["hello", { "validator": "\\w+" }],
"name": "binaries/my-sidecar",
"sidecar": true
}
]
}
通信パターン
フロントエンドからサイドカーへ(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;
}
バックエンドからサイドカーへ(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())
}
}
コマンドを登録します。
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");
}
ストリーミング出力
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();
}
長時間実行する HTTP サイドカー
プロセスを永続的に実行する場合は、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)));
フロントエンドとの通信:
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;
}
}
本番環境向けのビルド
ルートの package.json を更新します。
{
"scripts": {
"build:sidecar": "cd sidecar && npm run build",
"dev": "npm run build:sidecar && tauri dev",
"build": "npm run build:sidecar && tauri build"
}
}
クロスプラットフォームのターゲット:
| プラットフォーム | pkg ターゲット | Rust トリプル |
|---|---|---|
| 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 |
セキュリティ
"args": trueの代わりにバリデーターを使用します- HTTP サーバーは
127.0.0.1のみにバインドします - Tauri とサイドカーの両方で入力を検証します
- アプリの終了時にサイドカーが終了することを確認します
トラブルシューティング
バイナリが見つからない場合: ターゲットトリプルが一致していることを確認します。
ls -la src-tauri/binaries/
rustc --print host-tuple
権限が拒否された場合(Unix):
chmod +x src-tauri/binaries/my-sidecar-*
クラッシュが発生しても出力がない場合: stderr を確認します。
const output = await command.execute();
if (output.code !== 0) console.error(output.stderr);