Skip to content

Commit 58c5c54

Browse files
feat: add Node.js as a sidecar guide (tauri-apps#2771)
Co-authored-by: Tillmann <[email protected]>
1 parent 25f4f06 commit 58c5c54

File tree

2 files changed

+238
-42
lines changed

2 files changed

+238
-42
lines changed

src/content/docs/develop/sidecar.mdx

Lines changed: 58 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,17 @@ You may need to embed external binaries to add additional functionality to your
99

1010
Binaries are executables written in any programming language. Common use cases are Python CLI applications or API servers bundled using `pyinstaller`.
1111

12-
To bundle the binaries of your choice, you can add the `externalBin` property to the `tauri > bundle` object in your `tauri.conf.json`. `externalBin` expects a list of strings targeting binaries either with absolute or relative paths.
12+
To bundle the binaries of your choice, you can add the `externalBin` property to the `tauri > bundle` object in your `tauri.conf.json`.
13+
The `externalBin` configuration expects a list of strings targeting binaries either with absolute or relative paths.
1314

14-
Here is a sample to illustrate the configuration. This is not a complete `tauri.conf.json` file:
15+
Here is a Tauri configuration snippet to illustrate a sidecar configuration:
1516

1617
```json title="src-tauri/tauri.conf.json"
1718
{
1819
"bundle": {
1920
"externalBin": [
2021
"/absolute/path/to/sidecar",
21-
"relative/path/to/binary",
22+
"../relative/path/to/binary",
2223
"binaries/my-sidecar"
2324
]
2425
}
@@ -27,13 +28,19 @@ Here is a sample to illustrate the configuration. This is not a complete `tauri.
2728

2829
:::note
2930

30-
The relative paths are relative to the `tauri.conf.json` file which is in the `src-tauri` directory. So `binaries/my-sidecar` would represent `<PROJECT ROOT>/src-tauri/binaries/my-sidecar`.
31+
The relative paths are relative to the `tauri.conf.json` file which is in the `src-tauri` directory.
32+
So `binaries/my-sidecar` would represent `<PROJECT ROOT>/src-tauri/binaries/my-sidecar`.
3133

3234
:::
3335

34-
To make the external binary work on each supported architecture, a binary with the same name and a `-$TARGET_TRIPLE` suffix must exist on the specified path. For instance, `"externalBin": ["binaries/my-sidecar"]` requires a `src-tauri/binaries/my-sidecar-x86_64-unknown-linux-gnu` executable on Linux or `src-tauri/binaries/my-sidecar-aarch64-apple-darwin` on Mac OS with Apple Silicon.
36+
To make the external binary work on each supported architecture, a binary with the same name and a `-$TARGET_TRIPLE` suffix must exist on the specified path.
37+
For instance, `"externalBin": ["binaries/my-sidecar"]` requires a `src-tauri/binaries/my-sidecar-x86_64-unknown-linux-gnu` executable on Linux or `src-tauri/binaries/my-sidecar-aarch64-apple-darwin` on Mac OS with Apple Silicon.
3538

36-
You can find your **current** platform's `-$TARGET_TRIPLE` suffix by looking at the `host:` property reported by the `rustc -Vv` command.
39+
You can find your **current** platform's `-$TARGET_TRIPLE` suffix by looking at the `host:` property reported by the following command:
40+
41+
```sh
42+
rustc -Vv
43+
```
3744

3845
If the `grep` and `cut` commands are available, as they should on most Unix systems, you can extract the target triple directly with the following command:
3946

@@ -50,37 +57,29 @@ rustc -Vv | Select-String "host:" | ForEach-Object {$_.Line.split(" ")[1]}
5057
Here's a Node.js script to append the target triple to a binary:
5158

5259
```javascript
53-
const execa = require('execa');
54-
const fs = require('fs');
60+
import { execSync } from 'child_process';
61+
import fs from 'fs';
5562

56-
let extension = '';
57-
if (process.platform === 'win32') {
58-
extension = '.exe';
59-
}
63+
const ext = process.platform === 'win32' ? '.exe' : '';
6064

61-
async function main() {
62-
const rustInfo = (await execa('rustc', ['-vV'])).stdout;
63-
const targetTriple = /host: (\S+)/g.exec(rustInfo)[1];
64-
if (!targetTriple) {
65-
console.error('Failed to determine platform target triple');
66-
}
67-
fs.renameSync(
68-
`src-tauri/binaries/sidecar${extension}`,
69-
`src-tauri/binaries/sidecar-${targetTriple}${extension}`
70-
);
65+
const rustInfo = execSync('rustc -vV');
66+
const targetTriple = /host: (\S+)/g.exec(rustInfo)[1];
67+
if (!targetTriple) {
68+
console.error('Failed to determine platform target triple');
7169
}
72-
73-
main().catch((e) => {
74-
throw e;
75-
});
70+
fs.renameSync(
71+
`src-tauri/binaries/sidecar${extension}`,
72+
`src-tauri/binaries/sidecar-${targetTriple}${extension}`
73+
);
7674
```
7775

78-
Note that this script will not work if you compile for a different architecture than the one its running on.
76+
Note that this script will not work if you compile for a different architecture than the one its running on,
77+
so only use it as a starting point for your own build scripts.
7978

8079
## Running it from Rust
8180

8281
:::note
83-
Please follow the shell [plugin guide](/plugin/shell/) first to set up and initialize the plugin correctly.
82+
Please follow the [shell plugin guide](/plugin/shell/) first to set up and initialize the plugin correctly.
8483
Without the plugin being initialized and configured the example won't work.
8584
:::
8685

@@ -90,7 +89,6 @@ On the Rust side, import the `tauri_plugin_shell::ShellExt` trait and call the `
9089
use tauri_plugin_shell::ShellExt;
9190
use tauri_plugin_shell::process::CommandEvent;
9291

93-
// `sidecar()` expects just the filename, NOT the whole path like in JavaScript
9492
let sidecar_command = app.shell().sidecar("my-sidecar").unwrap();
9593
let (mut rx, mut _child) = sidecar_command
9694
.spawn()
@@ -99,7 +97,8 @@ let (mut rx, mut _child) = sidecar_command
9997
tauri::async_runtime::spawn(async move {
10098
// read events such as stdout
10199
while let Some(event) = rx.recv().await {
102-
if let CommandEvent::Stdout(line) = event {
100+
if let CommandEvent::Stdout(line_bytes) = event {
101+
let line = String::from_utf8_lossy(&line_bytes);
103102
window
104103
.emit("message", Some(format!("'{}'", line)))
105104
.expect("failed to emit event");
@@ -110,6 +109,24 @@ tauri::async_runtime::spawn(async move {
110109
});
111110
```
112111

112+
:::note
113+
The `sidecar()` function expects just the filename, NOT the whole path configured in the `externalBin` array.
114+
115+
Given the following configuration:
116+
117+
```json title="src-tauri/tauri.conf.json"
118+
{
119+
"bundle": {
120+
"externalBin": ["binaries/app", "my-sidecar", "../scripts/sidecar"]
121+
}
122+
}
123+
```
124+
125+
The appropriate way to execute the sidecar is by calling `app.shell().sidecar(name)` where `name` is either `"app"`, `"my-sidecar"` or `"sidecar"`
126+
instead of `"binaries/app"` for instance.
127+
128+
:::
129+
113130
You can place this code inside a Tauri command to easily pass the AppHandle or you can store a reference to the AppHandle in the builder script to access it elsewhere in your application.
114131

115132
## Running it from JavaScript
@@ -118,14 +135,17 @@ In the JavaScript code, import the `Command` class from the `@tauri-apps/plugin-
118135

119136
```javascript
120137
import { Command } from '@tauri-apps/plugin-shell';
121-
// `binaries/my-sidecar` is the EXACT value specified on `tauri.conf.json > tauri > bundle > externalBin`
122-
const sidecar_command = Command.sidecar('binaries/my-sidecar');
123-
const output = await sidecar_command.execute();
138+
const command = Command.sidecar('binaries/my-sidecar');
139+
const output = await command.execute();
124140
```
125141

142+
:::note
143+
The string provided to `Command.sidecar` must match one of the strings defined in the `externalBin` configuration array.
144+
:::
145+
126146
## Passing arguments
127147

128-
You can pass arguments to Sidecar commands just like you would for running normal `Command`s.
148+
You can pass arguments to Sidecar commands just like you would for running normal [Command][std::process::Command].
129149

130150
Arguments can be either **static** (e.g. `-o` or `serve`) or **dynamic** (e.g. `<file_path>` or `localhost:<PORT>`). You define the arguments in the exact order in which you'd call them. Static arguments are defined as-is, while dynamic arguments can be defined using a regular expression.
131151

@@ -157,7 +177,6 @@ First, define the arguments that need to be passed to the sidecar command in `sr
157177
"validator": "\\S+"
158178
}
159179
],
160-
"cmd": "",
161180
"name": "binaries/my-sidecar",
162181
"sidecar": true
163182
}
@@ -180,7 +199,8 @@ In Rust:
180199
use tauri_plugin_shell::ShellExt;
181200
#[tauri::command]
182201
async fn call_my_sidecar(app: tauri::AppHandle) {
183-
let sidecar_command = app.shell()
202+
let sidecar_command = app
203+
.shell()
184204
.sidecar("my-sidecar")
185205
.unwrap()
186206
.args(["arg1", "-a", "--arg2", "any-string-that-matches-the-validator"]);
@@ -192,7 +212,6 @@ In JavaScript:
192212

193213
```javascript
194214
import { Command } from '@tauri-apps/plugin-shell';
195-
// `binaries/my-sidecar` is the EXACT value specified on `tauri.conf.json > tauri > bundle > externalBin`
196215
// notice that the args array matches EXACTLY what is specified on `tauri.conf.json`.
197216
const command = Command.sidecar('binaries/my-sidecar', [
198217
'arg1',
@@ -202,3 +221,5 @@ const command = Command.sidecar('binaries/my-sidecar', [
202221
]);
203222
const output = await command.execute();
204223
```
224+
225+
[std::process::Command]: https://doc.rust-lang.org/std/process/struct.Command.html

src/content/docs/learn/sidecar-nodejs.mdx

Lines changed: 180 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,186 @@
22
title: Node.js as a sidecar
33
sidebar:
44
order: 1
5-
badge:
6-
text: Stub
7-
variant: caution
85
---
96

10-
import Stub from '@components/Stub.astro';
7+
import CommandTabs from '@components/CommandTabs.astro';
8+
import { Tabs, TabItem, Steps } from '@astrojs/starlight/components';
9+
import CTA from '@fragments/cta.mdx';
1110

12-
<Stub />
11+
In this guide we are going to package a Node.js application to a self contained binary
12+
to be used as a sidecar in a Tauri application without requiring the end user to have a Node.js installation.
13+
This example tutorial is applicable for desktop operating systems only.
14+
15+
We recommend reading the general [sidecar guide] first for a deeper understanding of how Tauri sidecars work.
16+
17+
In this example we will create a Node.js application that reads input from the command line [process.argv]
18+
and writes output to stdout using [console.log]. <br/>
19+
You can leverage alternative inter-process communication systems such as a localhost server, stdin/stdout or local sockets.
20+
Note that each has their own advantages, drawbacks and security concerns.
21+
22+
## Prerequisites
23+
24+
An existing Tauri application set up with the shell plugin, that compiles and runs for you locally.
25+
26+
:::tip[Create a lab app]
27+
28+
If you are not an advanced user it's **highly recommended** that you use the options and frameworks provided here. It's just a lab, you can delete the project when you're done.
29+
30+
<CTA />
31+
32+
- Project name: `node-sidecar-lab`
33+
- Choose which language to use for your frontend: `Typescript / Javascript`
34+
- Choose your package manager: `pnpm`
35+
- Choose your UI template: `Vanilla`
36+
- Choose your UI flavor: `Typescript`
37+
- Would you like to setup the project for mobile as well? `yes`
38+
39+
:::
40+
41+
:::note
42+
Please follow the [shell plugin guide](/plugin/shell/) first to set up and initialize the plugin correctly.
43+
Without the plugin being initialized and configured the example won't work.
44+
:::
45+
46+
## Guide
47+
48+
<Steps>
49+
50+
1. ##### Initialize Sidecar Project
51+
52+
Let's create a new Node.js project to contain our sidecar implementation.
53+
Create a new directory **in your Tauri application root folder** (in this example we will call it `sidecar-app`)
54+
and run the `init` command of your preferred Node.js package manager inside the directory:
55+
56+
<CommandTabs npm="npm init" yarn="yarn init" pnpm="pnpm init" />
57+
58+
We will compile our Node.js application to a self container binary using [pkg].
59+
Let's install it as a development dependency:
60+
61+
<CommandTabs
62+
npm="npm add pkg --save-dev"
63+
yarn="yarn add pkg --dev"
64+
pnpm="pnpm add pkg --save-dev"
65+
/>
66+
67+
1. ##### Write Sidecar Logic
68+
69+
Now we can start writing JavaScript code that will be executed by our Tauri application.
70+
71+
In this example we will process a command from the command line argmuents and write output to stdout,
72+
which means our process will be short lived and only handle a single command at a time.
73+
If your application must be long lived, consider using alternative inter-process communication systems.
74+
75+
Let's create a `index.js` file in our `sidecar-app` directory and write a basic Node.js app:
76+
77+
```js title=sidecar-app/index.js
78+
const command = process.argv[2];
79+
80+
switch (command) {
81+
case 'ping':
82+
const message = process.argv[3];
83+
console.log(`pong, ${message}`);
84+
break;
85+
default:
86+
console.error(`unknown command ${command}`);
87+
process.exit(1);
88+
}
89+
```
90+
91+
1. ##### Package the Sidecar
92+
93+
To package our Node.js application to a self contained binary, we can run the following `pkg` command:
94+
95+
<CommandTabs
96+
npm="npm run pkg -- --output app"
97+
yarn="yarn pkg --output app"
98+
pnpm="pnpm pkg --output app"
99+
/>
100+
101+
This will create the `sidecar-app/app` binary on Linux and macOS, and a `sidecar-app/app.exe` executable on Windows.
102+
To rename this file to the expected Tauri sidecar filename, we can use the following Node.js script:
103+
104+
```js
105+
import { execSync } from 'child_process';
106+
import fs from 'fs';
107+
108+
const ext = process.platform === 'win32' ? '.exe' : '';
109+
110+
const rustInfo = execSync('rustc -vV');
111+
const targetTriple = /host: (\S+)/g.exec(rustInfo)[1];
112+
if (!targetTriple) {
113+
console.error('Failed to determine platform target triple');
114+
}
115+
fs.renameSync(
116+
`app${ext}`,
117+
`../src-tauri/binaries/app-${targetTriple}${ext}`
118+
);
119+
```
120+
121+
1. ##### Configure the Sidecar in the Tauri Application
122+
123+
Now that we have our Node.js application ready, we can connect it to our Tauri application
124+
by configuring the [`bundle > externalBin`] array:
125+
126+
```json title="src-tauri/tauri.conf.json"
127+
{
128+
"bundle": {
129+
"externalBin": ["binaries/app"]
130+
}
131+
}
132+
```
133+
134+
The Tauri CLI will handle the bundling of the sidecar binary as long as it exists as `src-tauri/binaries/app-<target-triple>`.
135+
136+
1. ##### Execute the Sidecar
137+
138+
We can run the sidecar binary either from Rust code or directly from JavaScript.
139+
140+
<Tabs>
141+
142+
<TabItem label="JavaScript">
143+
144+
Let's execute the `ping` command in the Node.js sidecar directly:
145+
146+
```javascript
147+
import { Command } from '@tauri-apps/plugin-shell';
148+
149+
const message = 'Tauri';
150+
151+
const command = Command.sidecar('binaries/app', ['ping', message]);
152+
const output = await command.execute();
153+
const response = output.stdout;
154+
```
155+
156+
</TabItem>
157+
158+
<TabItem label="Rust">
159+
160+
Let's pipe a `ping` Tauri command to the Node.js sidecar:
161+
162+
```rust
163+
#[tauri::command]
164+
async fn ping(app: tauri::AppHandle, message: String) -> String {
165+
let sidecar_command = app
166+
.shell()
167+
.sidecar("app")
168+
.unwrap()
169+
.arg("ping")
170+
.arg(message);
171+
let output = sidecar_command.output().unwrap();
172+
let response = String::from_utf8(output.stdout).unwrap();
173+
response
174+
}
175+
```
176+
177+
</TabItem>
178+
179+
</Tabs>
180+
181+
</Steps>
182+
183+
[sidecar guide]: /develop/sidecar
184+
[process.argv]: https://nodejs.org/docs/latest/api/process.html#processargv
185+
[console.log]: https://nodejs.org/api/console.html#consolelogdata-args
186+
[pkg]: https://github.com/vercel/pkg
187+
[`bundle > externalBin`]: /reference/config/#externalbin

0 commit comments

Comments
 (0)