npm パッケージを使った既存の Node.js スクリプトや社内ツールを、Tauri アプリから動かしたいことがあります。WebView の中は Node.js ではないので fs などはそのまま使えず、利用者の PC に Node.js が入っているとも限りません。Node.js の Single Executable Applications(SEA)機能で Node.js 本体とスクリプトを 1 つの実行ファイルにまとめ、サイドカーとして同梱すれば、インストールなしで動かせます。ファイル名の規則や externalBin など、サイドカー自体の基本は 同梱したバイナリファイルを実行する にまとめています。
前提条件
npm run tauri add shell
npm install yaml
npm install --save-dev esbuild postject
postject は Node.js 22 / 24 で使う旧手順でだけ必要です。tauri.conf.json の bundle.externalBin に "binaries/nodetool" を登録し、capability で「1 番目は yaml2json 固定、2 番目は .yaml か .yml で終わる値だけ」を許可します。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "shell:allow-execute",
"allow": [
{ "name": "binaries/nodetool", "sidecar": true, "args": ["yaml2json", { "validator": ".+\\.ya?ml" }] }
]
}
]
}
Node.js スクリプトを書く
YAML ファイルを読み、JSON にして標準出力に書く例です。
// sidecar/tool.js
const fs = require('node:fs');
const YAML = require('yaml'); // npm パッケージ。esbuild で 1 ファイルに取り込む
// SEA でも、利用者が渡した引数は process.argv[2] から入る
const [command, file] = process.argv.slice(2);
if (command === 'yaml2json' && file) {
try {
const data = YAML.parse(fs.readFileSync(file, 'utf8'));
process.stdout.write(JSON.stringify(data) + '\n');
} catch (e) {
console.error(e instanceof Error ? e.message : String(e));
process.exitCode = 1; // process.exit() は使わない
}
} else {
console.error('usage: nodetool yaml2json <file.yaml>');
process.exitCode = 2;
}
- 終了コードは
process.exitCodeで返す:process.exit()を呼ぶと、書き終わっていない標準出力が捨てられることがあります。 - 文字コード: Node.js の標準出力は UTF-8 なので、Python のような Windows での文字化けは起きません。
- 1 ファイルにまとめる: SEA に入れたスクリプトの
require()は、node:fsのような組み込みモジュールしか読めません。npm パッケージは esbuild で取り込みます。
npx esbuild sidecar/tool.js --bundle --platform=node --format=cjs --outfile=sidecar/dist/tool.cjs
SEA で実行ファイルにする
設定ファイルを作ります。disableExperimentalSEAWarning を付けないと、起動のたびに実験的機能だという警告が標準エラー出力に出ます。Windows では output に .exe が必要です。
{
"main": "sidecar/dist/tool.cjs",
"output": "sidecar/dist/nodetool.exe",
"disableExperimentalSEAWarning": true
}
Node.js 25.5 以降: --build-sea
--build-sea 1 回で実行ファイルができます。できたらトリプル付きの名前でコピーします。
# Windows (PowerShell)
node --build-sea sea-config.json
$triple = (rustc -Vv | Select-String "host:").Line.Split(" ")[1]
Copy-Item sidecar/dist/nodetool.exe "src-tauri/binaries/nodetool-$triple.exe"
macOS では output を sidecar/dist/nodetool にし、ビルド後に codesign --sign - sidecar/dist/nodetool で署名し直してからコピーします。
Node.js 22 / 24: blob を作って postject で注入する
--build-sea がない版では、設定の output を blob の出力先(例: sidecar/dist/sea-prep.blob)にして、node 本体のコピーに注入します。blob を作る node と注入先の node は同じ版でなければならないので、実行中の node 自身をコピーします。
# macOS(Linux は codesign の 2 行と --macho-segment-name を除く)
node --experimental-sea-config sea-config.json
cp "$(command -v node)" sidecar/dist/nodetool
codesign --remove-signature sidecar/dist/nodetool
npx postject sidecar/dist/nodetool NODE_SEA_BLOB sidecar/dist/sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
--macho-segment-name NODE_SEA
codesign --sign - sidecar/dist/nodetool
cp sidecar/dist/nodetool "src-tauri/binaries/nodetool-$(rustc -Vv | grep host | cut -f2 -d' ')"
# Windows (PowerShell)
node --experimental-sea-config sea-config.json
node -e "require('fs').copyFileSync(process.execPath, 'sidecar/dist/nodetool.exe')"
npx postject sidecar/dist/nodetool.exe NODE_SEA_BLOB sidecar/dist/sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
$triple = (rustc -Vv | Select-String "host:").Line.Split(" ")[1]
Copy-Item sidecar/dist/nodetool.exe "src-tauri/binaries/nodetool-$triple.exe"
Windows では署名についての警告が出ますが、実行には影響しません。
1. フロントエンドから実装する (TypeScript)
import { Command } from '@tauri-apps/plugin-shell';
export async function yamlToJson(path: string): Promise<unknown> {
// 引数は capability の args と同じ並びで渡す
const output = await Command.sidecar('binaries/nodetool', ['yaml2json', path]).execute();
if (output.code !== 0) {
throw new Error(output.stderr.trim() || `nodetool exited with code ${output.code}`);
}
return JSON.parse(output.stdout);
}
呼ぶたびに Node.js ごと起動するので、その分の時間がかかります。設定に "useCodeCache": true を足すと起動が少し速くなります。頻繁に呼ぶなら、常駐させて標準入力で依頼を受ける作りにします(Python 版 の常駐の例と同じ形で、行ごとの受け取りは 出力をリアルタイムで受け取る)。
2. バックエンドから実装する (Rust)
Rust からはファイル名 nodetool だけを渡し、JSON は serde_json::Value で受けてそのまま JS に返します。JS 側の capability は不要です。
use tauri_plugin_shell::ShellExt;
#[tauri::command]
async fn yaml_to_json(app: tauri::AppHandle, path: String) -> Result<serde_json::Value, String> {
let output = app
.shell()
.sidecar("nodetool")
.map_err(|e| e.to_string())?
.args(["yaml2json", path.as_str()])
.output()
.await
.map_err(|e| e.to_string())?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).trim().to_string());
}
serde_json::from_slice(&output.stdout).map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![yaml_to_json])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
const data = await invoke<unknown>('yaml_to_json', { path: '/Users/me/config.yaml' });
console.log(data);
動作確認
組み込む前に、できた実行ファイルをターミナルで単体で動かしておくと、失敗したときに原因を切り分けやすくなります。
> sidecar\dist\nodetool.exe yaml2json sample.yaml
{"name":"demo","ports":[8080,8081]}
次に npm run tauri dev で起動し、YAML ファイルの絶対パスを渡して yamlToJson() を呼ぶと、同じ内容がオブジェクトで返ります。
よくあるエラーと対処法
- モジュールが見つからない趣旨のエラーで終わる: SEA の中では npm パッケージを読めません。
mainに、esbuild でまとめたtool.cjsを指定しているかを確かめます。 yaml2jsonというファイルが見つからない趣旨のエラーになる: 注入に失敗していて、ただの node として動いています(最初の引数をスクリプト名と解釈する)。--sentinel-fuseの値と postject の出力を確かめます。- 標準エラー出力に実験的機能の警告が出る:
disableExperimentalSEAWarning: trueを付けてビルドし直します。 - 出力の最後が欠ける:
process.exit()で終わらせています。process.exitCodeに置き換えます。 - 「Cannot define a sidecar with the same name as the Cargo package name」: サイドカー名が
src-tauri/Cargo.tomlのパッケージ名と同じです。appのようなありがちな名前は避けます。名前や権限まわりのほかのエラーは shell-012 の一覧を参照してください。
OS ごとの違いと注意点
- Windows: 実行ファイル名に
.exeが必要です。node 本体の署名は注入で無効になるので、配布時は自分の証明書で署名し直すと警告を減らせます。 - macOS: 署名の除去と再署名を忘れると起動できません。Node.js のドキュメントでは、SEA のテスト対象は arm64 だけで、x64(Intel)は現在対象外とされています。
- Linux: 追加の手順はありません。ただし Alpine は対象外です。
- OS ごとに作る: 注入先の node は配布先の OS 用が必要です。
--build-seaならexecutableに同じ版の別 OS 用 node を指定して作れますが、その場合はuseCodeCacheとuseSnapshotをfalseにします。 - サイズ: Node.js 本体が丸ごと入るので、スクリプトが小さくても数十 MB 以上になります。
- ネイティブアドオン:
.nodeファイルを使うパッケージは esbuild で 1 ファイルにまとめられません。SEA のassetsに入れて一時ファイルから読み込む手順が必要になるため、まずは純粋な JavaScript のパッケージで組むのが無難です。
