tauri-plugin-js-api 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +321 -0
- package/dist-js/index.cjs +158 -0
- package/dist-js/index.d.ts +66 -0
- package/dist-js/index.js +142 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# tauri-plugin-js
|
|
2
|
+
|
|
3
|
+
A Tauri v2 plugin that spawns and manages JavaScript runtime processes (Bun, Node.js, Deno) from your desktop app. Rust manages process lifecycles and relays stdio via Tauri events. The frontend communicates with backend JS processes through type-safe RPC powered by [kkrpc](https://github.com/nicepkg/kkrpc).
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Tauri gives you a tiny, fast, secure desktop shell — but sometimes you need a full JS runtime for things the webview can't do: filesystem watchers, native modules, long-running compute, local AI inference, dev servers, etc. This plugin bridges that gap without the weight of Electron.
|
|
8
|
+
|
|
9
|
+
**What it enables:**
|
|
10
|
+
- Run Bun/Node/Deno workers from a Tauri app with full process lifecycle management
|
|
11
|
+
- Type-safe bidirectional RPC between frontend and backend JS processes
|
|
12
|
+
- Multiple concurrent named processes with independent stdio streams
|
|
13
|
+
- Runtime auto-detection (discovers installed runtimes, paths, versions)
|
|
14
|
+
- Custom runtime executable paths via settings
|
|
15
|
+
- Compiled binary sidecars — compile TS workers into standalone executables, no runtime needed on user machines
|
|
16
|
+
- Clean shutdown on app exit
|
|
17
|
+
- Multi-window support — all windows can communicate with the same backend processes
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
```mermaid
|
|
22
|
+
graph LR
|
|
23
|
+
subgraph Tauri App
|
|
24
|
+
FE["Frontend (Webview)<br/>kkrpc RPCChannel"]
|
|
25
|
+
RS["Rust Plugin<br/>tauri-plugin-js"]
|
|
26
|
+
end
|
|
27
|
+
subgraph Child Processes
|
|
28
|
+
B["Bun Worker<br/>kkrpc BunIo"]
|
|
29
|
+
N["Node Worker<br/>kkrpc NodeIo"]
|
|
30
|
+
D["Deno Worker<br/>kkrpc DenoIo"]
|
|
31
|
+
end
|
|
32
|
+
FE <-->|"Tauri Events<br/>(js-process-stdout/stderr)"| RS
|
|
33
|
+
RS <-->|"stdin / stdout"| B
|
|
34
|
+
RS <-->|"stdin / stdout"| N
|
|
35
|
+
RS <-->|"stdin / stdout"| D
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Rust never parses RPC payloads — it forwards raw newline-delimited strings between the webview and child processes. The RPC protocol layer (kkrpc) runs entirely in JS on both sides.
|
|
39
|
+
|
|
40
|
+
### Message flow
|
|
41
|
+
|
|
42
|
+
```mermaid
|
|
43
|
+
sequenceDiagram
|
|
44
|
+
participant FE as Frontend (Webview)
|
|
45
|
+
participant RS as Rust Plugin
|
|
46
|
+
participant RT as JS Runtime
|
|
47
|
+
|
|
48
|
+
Note over FE,RT: Frontend → Runtime (RPC request)
|
|
49
|
+
FE->>RS: writeStdin(name, jsonMessage)
|
|
50
|
+
RS->>RT: stdin.write(jsonMessage + \n)
|
|
51
|
+
|
|
52
|
+
Note over FE,RT: Runtime → Frontend (RPC response)
|
|
53
|
+
RT->>RS: stdout line (BufReader::lines)
|
|
54
|
+
RS->>FE: emit("js-process-stdout", {name, data})
|
|
55
|
+
Note over FE: JsRuntimeIo re-appends \n<br/>kkrpc parses response
|
|
56
|
+
|
|
57
|
+
Note over FE,RT: Process lifecycle
|
|
58
|
+
FE->>RS: spawn(name, config)
|
|
59
|
+
RS->>RT: Command::new(runtime).spawn()
|
|
60
|
+
RT-->>RS: process exits
|
|
61
|
+
RS->>FE: emit("js-process-exit", {name, code})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
### Rust side
|
|
67
|
+
|
|
68
|
+
Add to your `src-tauri/Cargo.toml`:
|
|
69
|
+
|
|
70
|
+
```toml
|
|
71
|
+
[dependencies]
|
|
72
|
+
tauri-plugin-js = { path = "../path/to/tauri-plugin-js" }
|
|
73
|
+
# or from git:
|
|
74
|
+
# tauri-plugin-js = { git = "https://github.com/user/tauri-plugin-js" }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Register in `src-tauri/src/lib.rs`:
|
|
78
|
+
|
|
79
|
+
```rust
|
|
80
|
+
pub fn run() {
|
|
81
|
+
tauri::Builder::default()
|
|
82
|
+
.plugin(tauri_plugin_js::init())
|
|
83
|
+
.run(tauri::generate_context!())
|
|
84
|
+
.expect("error while running tauri application");
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Frontend side
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pnpm add tauri-plugin-js-api kkrpc
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Permissions
|
|
95
|
+
|
|
96
|
+
Add to `src-tauri/capabilities/default.json`:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"permissions": [
|
|
101
|
+
"core:default",
|
|
102
|
+
"js:default"
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`js:default` grants all 10 commands: spawn, kill, kill-all, restart, list-processes, get-status, write-stdin, detect-runtimes, set-runtime-path, get-runtime-paths.
|
|
108
|
+
|
|
109
|
+
## Usage
|
|
110
|
+
|
|
111
|
+
### 1. Define a shared API type
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// backends/shared-api.ts
|
|
115
|
+
export interface BackendAPI {
|
|
116
|
+
add(a: number, b: number): Promise<number>;
|
|
117
|
+
echo(message: string): Promise<string>;
|
|
118
|
+
getSystemInfo(): Promise<{ runtime: string; pid: number; platform: string; arch: string }>;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 2. Write a backend worker
|
|
123
|
+
|
|
124
|
+
**Bun** (`backends/bun-worker.ts`):
|
|
125
|
+
```typescript
|
|
126
|
+
import { RPCChannel, BunIo } from "kkrpc";
|
|
127
|
+
import type { BackendAPI } from "./shared-api";
|
|
128
|
+
|
|
129
|
+
const api: BackendAPI = {
|
|
130
|
+
async add(a, b) { return a + b; },
|
|
131
|
+
async echo(msg) { return `[bun] ${msg}`; },
|
|
132
|
+
async getSystemInfo() {
|
|
133
|
+
return { runtime: "bun", pid: process.pid, platform: process.platform, arch: process.arch };
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const io = new BunIo(Bun.stdin.stream());
|
|
138
|
+
const channel = new RPCChannel(io, { expose: api });
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Node** (`backends/node-worker.mjs`):
|
|
142
|
+
```javascript
|
|
143
|
+
import { RPCChannel, NodeIo } from "kkrpc";
|
|
144
|
+
|
|
145
|
+
const api = { /* same methods */ };
|
|
146
|
+
const io = new NodeIo(process.stdin, process.stdout);
|
|
147
|
+
const channel = new RPCChannel(io, { expose: api });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Deno** (`backends/deno-worker.ts`):
|
|
151
|
+
```typescript
|
|
152
|
+
import { DenoIo, RPCChannel } from "npm:kkrpc/deno";
|
|
153
|
+
import type { BackendAPI } from "./shared-api.ts";
|
|
154
|
+
|
|
155
|
+
const api: BackendAPI = { /* same methods, using Deno.pid, Deno.build.os, etc. */ };
|
|
156
|
+
const io = new DenoIo(Deno.stdin.readable);
|
|
157
|
+
const channel = new RPCChannel(io, { expose: api });
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 3. Spawn and call from the frontend
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { spawn, createChannel, onStdout, onStderr, onExit } from "tauri-plugin-js-api";
|
|
164
|
+
import type { BackendAPI } from "../backends/shared-api";
|
|
165
|
+
|
|
166
|
+
// Spawn a worker
|
|
167
|
+
await spawn("my-worker", { runtime: "bun", script: "bun-worker.ts", cwd: "/path/to/backends" });
|
|
168
|
+
|
|
169
|
+
// Listen to stdio events
|
|
170
|
+
onStdout("my-worker", (data) => console.log("[stdout]", data));
|
|
171
|
+
onStderr("my-worker", (data) => console.error("[stderr]", data));
|
|
172
|
+
onExit("my-worker", (code) => console.log("exited with", code));
|
|
173
|
+
|
|
174
|
+
// Create a typed RPC channel
|
|
175
|
+
const { api } = await createChannel<Record<string, never>, BackendAPI>("my-worker");
|
|
176
|
+
|
|
177
|
+
// Type-safe calls — checked at compile time
|
|
178
|
+
const sum = await api.add(5, 3); // => 8
|
|
179
|
+
const info = await api.getSystemInfo(); // => { runtime: "bun", pid: 1234, ... }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 4. Spawn a compiled binary sidecar (no runtime needed)
|
|
183
|
+
|
|
184
|
+
Both Bun and Deno can compile TS workers into standalone executables. Use `sidecar` instead of `runtime` to spawn them via Tauri's sidecar resolution:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
TARGET=$(rustc -vV | grep host | cut -d' ' -f2)
|
|
188
|
+
|
|
189
|
+
# Bun — compile directly from the project
|
|
190
|
+
bun build --compile --minify backends/bun-worker.ts --outfile src-tauri/binaries/bun-worker-$TARGET
|
|
191
|
+
|
|
192
|
+
# Deno — MUST compile from a separate Deno package (see note below)
|
|
193
|
+
deno compile --allow-all --output src-tauri/binaries/deno-worker-$TARGET path/to/deno-package/main.ts
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
> **Important: `deno compile` and `node_modules`**
|
|
197
|
+
>
|
|
198
|
+
> `deno compile` will crash with a stack overflow if run from a directory that contains `node_modules` — it attempts to traverse and compile everything in the directory tree. **Deno worker source must live in a separate directory** set up as a standalone Deno package (with its own `deno.json` listing dependencies like kkrpc). See [the example app](examples/tauri-app/) for the full setup using `examples/deno-compile/`.
|
|
199
|
+
|
|
200
|
+
Add `externalBin` to `src-tauri/tauri.conf.json` so Tauri bundles the sidecars:
|
|
201
|
+
```json
|
|
202
|
+
{ "bundle": { "externalBin": ["binaries/bun-worker", "binaries/deno-worker"] } }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { spawn, createChannel } from "tauri-plugin-js-api";
|
|
207
|
+
|
|
208
|
+
// Spawn the sidecar — no bun/deno/node needed at runtime
|
|
209
|
+
await spawn("my-compiled-worker", { sidecar: "bun-worker" });
|
|
210
|
+
|
|
211
|
+
// RPC works identically — same worker code, same API
|
|
212
|
+
const { api } = await createChannel<Record<string, never>, BackendAPI>("my-compiled-worker");
|
|
213
|
+
const sum = await api.add(5, 3); // => 8
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The compiled binaries preserve stdin/stdout behavior, so kkrpc works unchanged. This is the recommended approach for production — end users don't need any JS runtime installed.
|
|
217
|
+
|
|
218
|
+
The plugin resolves sidecars by looking next to the app executable, trying both plain names (production) and target-triple-suffixed names (development).
|
|
219
|
+
|
|
220
|
+
### 5. Bundle scripts as resources (for runtime-based spawning in production)
|
|
221
|
+
|
|
222
|
+
Worker scripts that import `kkrpc` need `node_modules` at runtime. In production, bundle them into self-contained JS files first:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
# Bundle for bun target (inlines kkrpc dependency)
|
|
226
|
+
bun build backends/bun-worker.ts --target bun --outfile src-tauri/workers/bun-worker.js
|
|
227
|
+
|
|
228
|
+
# Bundle for node target
|
|
229
|
+
bun build backends/node-worker.mjs --target node --outfile src-tauri/workers/node-worker.mjs
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Then add the bundled files as Tauri resources in `tauri.conf.json`:
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"bundle": {
|
|
236
|
+
"resources": {
|
|
237
|
+
"workers/bun-worker.js": "workers/bun-worker.js",
|
|
238
|
+
"workers/node-worker.mjs": "workers/node-worker.mjs"
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Resolve the script path at runtime:
|
|
245
|
+
```typescript
|
|
246
|
+
import { resolveResource } from "@tauri-apps/api/path";
|
|
247
|
+
|
|
248
|
+
const script = await resolveResource("workers/bun-worker.js");
|
|
249
|
+
await spawn("my-worker", { runtime: "bun", script });
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Note: Deno workers use `npm:kkrpc/deno` which Deno resolves natively — no bundling needed, just copy the source file.
|
|
253
|
+
|
|
254
|
+
### 6. Runtime detection
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { detectRuntimes, setRuntimePath, getRuntimePaths } from "tauri-plugin-js-api";
|
|
258
|
+
|
|
259
|
+
const runtimes = await detectRuntimes();
|
|
260
|
+
// => [{ name: "bun", path: "/usr/local/bin/bun", version: "1.2.0", available: true }, ...]
|
|
261
|
+
|
|
262
|
+
// Override a runtime's executable path
|
|
263
|
+
await setRuntimePath("node", "/usr/local/nvm/versions/node/v22.0.0/bin/node");
|
|
264
|
+
|
|
265
|
+
// Get all custom path overrides
|
|
266
|
+
const paths = await getRuntimePaths();
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## API Reference
|
|
270
|
+
|
|
271
|
+
### Commands
|
|
272
|
+
|
|
273
|
+
| Function | Description |
|
|
274
|
+
|----------|-------------|
|
|
275
|
+
| `spawn(name, config)` | Start a named process |
|
|
276
|
+
| `kill(name)` | Kill a named process |
|
|
277
|
+
| `killAll()` | Kill all managed processes |
|
|
278
|
+
| `restart(name, config?)` | Restart a process (optionally with new config) |
|
|
279
|
+
| `listProcesses()` | List all running processes |
|
|
280
|
+
| `getStatus(name)` | Get status of a named process |
|
|
281
|
+
| `writeStdin(name, data)` | Write raw string to a process's stdin |
|
|
282
|
+
| `detectRuntimes()` | Detect installed runtimes (bun, node, deno) |
|
|
283
|
+
| `setRuntimePath(rt, path)` | Override executable path for a runtime |
|
|
284
|
+
| `getRuntimePaths()` | Get all custom path overrides |
|
|
285
|
+
|
|
286
|
+
### Events
|
|
287
|
+
|
|
288
|
+
| Event | Payload | Description |
|
|
289
|
+
|-------|---------|-------------|
|
|
290
|
+
| `js-process-stdout` | `{ name, data }` | Line from process stdout |
|
|
291
|
+
| `js-process-stderr` | `{ name, data }` | Line from process stderr |
|
|
292
|
+
| `js-process-exit` | `{ name, code }` | Process exited |
|
|
293
|
+
|
|
294
|
+
### RPC Helper
|
|
295
|
+
|
|
296
|
+
`createChannel<LocalAPI, RemoteAPI>(processName, localApi?)` — creates a kkrpc channel over the process's stdio, returns `{ channel, api, io }`. The `api` proxy is fully typed against `RemoteAPI`.
|
|
297
|
+
|
|
298
|
+
### SpawnConfig
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
interface SpawnConfig {
|
|
302
|
+
runtime?: "bun" | "deno" | "node"; // Managed runtime
|
|
303
|
+
command?: string; // Direct binary path
|
|
304
|
+
sidecar?: string; // Tauri sidecar binary name (as in externalBin)
|
|
305
|
+
script?: string; // Script file to run
|
|
306
|
+
args?: string[]; // Additional arguments
|
|
307
|
+
cwd?: string; // Working directory
|
|
308
|
+
env?: Record<string, string>; // Environment variables
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Key Design Decisions
|
|
313
|
+
|
|
314
|
+
- **Rust is a thin relay.** It spawns processes, pipes stdio, emits events. It never parses or transforms RPC messages.
|
|
315
|
+
- **RPC is end-to-end JS.** kkrpc runs in both the frontend webview and the backend runtime. Rust just forwards the bytes.
|
|
316
|
+
- **Newline framing.** Rust's `BufReader::lines()` strips `\n`. The frontend `JsRuntimeIo` adapter re-appends it so kkrpc's message parser works correctly.
|
|
317
|
+
- **`isDestroyed` guard.** kkrpc's listen loop continues on null reads. The IO adapter exposes `isDestroyed` and returns a never-resolving promise from `read()` when destroyed, preventing spin loops.
|
|
318
|
+
|
|
319
|
+
## Example App
|
|
320
|
+
|
|
321
|
+
See [`examples/tauri-app/`](examples/tauri-app/) for a full working demo with all three runtimes, compiled binary sidecars, type-safe RPC, runtime detection, and a settings dialog.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@tauri-apps/api/core');
|
|
4
|
+
var event = require('@tauri-apps/api/event');
|
|
5
|
+
|
|
6
|
+
// ── A) Command wrappers ──
|
|
7
|
+
async function spawn(name, config) {
|
|
8
|
+
return core.invoke("plugin:js|spawn", { name, config });
|
|
9
|
+
}
|
|
10
|
+
async function kill(name) {
|
|
11
|
+
return core.invoke("plugin:js|kill", { name });
|
|
12
|
+
}
|
|
13
|
+
async function killAll() {
|
|
14
|
+
return core.invoke("plugin:js|kill_all");
|
|
15
|
+
}
|
|
16
|
+
async function restart(name, config) {
|
|
17
|
+
return core.invoke("plugin:js|restart", { name, config: config ?? null });
|
|
18
|
+
}
|
|
19
|
+
async function listProcesses() {
|
|
20
|
+
return core.invoke("plugin:js|list_processes");
|
|
21
|
+
}
|
|
22
|
+
async function getStatus(name) {
|
|
23
|
+
return core.invoke("plugin:js|get_status", { name });
|
|
24
|
+
}
|
|
25
|
+
async function writeStdin(name, data) {
|
|
26
|
+
return core.invoke("plugin:js|write_stdin", { name, data });
|
|
27
|
+
}
|
|
28
|
+
async function detectRuntimes() {
|
|
29
|
+
return core.invoke("plugin:js|detect_runtimes");
|
|
30
|
+
}
|
|
31
|
+
async function setRuntimePath(runtime, path) {
|
|
32
|
+
return core.invoke("plugin:js|set_runtime_path", { runtime, path });
|
|
33
|
+
}
|
|
34
|
+
async function getRuntimePaths() {
|
|
35
|
+
return core.invoke("plugin:js|get_runtime_paths");
|
|
36
|
+
}
|
|
37
|
+
// ── B) Event helpers ──
|
|
38
|
+
function onStdout(name, callback) {
|
|
39
|
+
return event.listen("js-process-stdout", (event) => {
|
|
40
|
+
if (event.payload.name === name) {
|
|
41
|
+
callback(event.payload.data);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function onStderr(name, callback) {
|
|
46
|
+
return event.listen("js-process-stderr", (event) => {
|
|
47
|
+
if (event.payload.name === name) {
|
|
48
|
+
callback(event.payload.data);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function onExit(name, callback) {
|
|
53
|
+
return event.listen("js-process-exit", (event) => {
|
|
54
|
+
if (event.payload.name === name) {
|
|
55
|
+
callback(event.payload.code);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
class JsRuntimeIo {
|
|
60
|
+
constructor(processName) {
|
|
61
|
+
this.queue = [];
|
|
62
|
+
this.waitResolve = null;
|
|
63
|
+
this.listeners = new Set();
|
|
64
|
+
this.unlisten = null;
|
|
65
|
+
this._isDestroyed = false;
|
|
66
|
+
this.processName = processName;
|
|
67
|
+
this.name = `tauri-js-runtime:${processName}`;
|
|
68
|
+
}
|
|
69
|
+
get isDestroyed() {
|
|
70
|
+
return this._isDestroyed;
|
|
71
|
+
}
|
|
72
|
+
async initialize() {
|
|
73
|
+
this.unlisten = await event.listen("js-process-stdout", (event) => {
|
|
74
|
+
if (event.payload.name !== this.processName)
|
|
75
|
+
return;
|
|
76
|
+
if (this._isDestroyed)
|
|
77
|
+
return;
|
|
78
|
+
// Re-append the newline that BufReader::lines() strips
|
|
79
|
+
const data = event.payload.data + "\n";
|
|
80
|
+
// Dispatch to message listeners
|
|
81
|
+
for (const listener of this.listeners) {
|
|
82
|
+
listener(data);
|
|
83
|
+
}
|
|
84
|
+
// Feed the read queue
|
|
85
|
+
if (this.waitResolve) {
|
|
86
|
+
const resolve = this.waitResolve;
|
|
87
|
+
this.waitResolve = null;
|
|
88
|
+
resolve(data);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.queue.push(data);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async write(data) {
|
|
96
|
+
await writeStdin(this.processName, data);
|
|
97
|
+
}
|
|
98
|
+
async read() {
|
|
99
|
+
if (this._isDestroyed) {
|
|
100
|
+
// Return a never-resolving promise so kkrpc's listen loop hangs
|
|
101
|
+
return new Promise(() => { });
|
|
102
|
+
}
|
|
103
|
+
if (this.queue.length > 0) {
|
|
104
|
+
return this.queue.shift();
|
|
105
|
+
}
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
this.waitResolve = resolve;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
on(event, listener) {
|
|
111
|
+
if (event === "message") {
|
|
112
|
+
this.listeners.add(listener);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
off(event, listener) {
|
|
116
|
+
if (event === "message") {
|
|
117
|
+
this.listeners.delete(listener);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async destroy() {
|
|
121
|
+
this._isDestroyed = true;
|
|
122
|
+
if (this.unlisten) {
|
|
123
|
+
this.unlisten();
|
|
124
|
+
this.unlisten = null;
|
|
125
|
+
}
|
|
126
|
+
if (this.waitResolve) {
|
|
127
|
+
this.waitResolve(null);
|
|
128
|
+
this.waitResolve = null;
|
|
129
|
+
}
|
|
130
|
+
this.listeners.clear();
|
|
131
|
+
this.queue = [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ── D) Channel helper (dynamic kkrpc import) ──
|
|
135
|
+
async function createChannel(processName, localApi) {
|
|
136
|
+
const { RPCChannel } = await import('kkrpc/browser');
|
|
137
|
+
const io = new JsRuntimeIo(processName);
|
|
138
|
+
await io.initialize();
|
|
139
|
+
const channel = new RPCChannel(io, { expose: localApi ?? {} });
|
|
140
|
+
const api = channel.getAPI();
|
|
141
|
+
return { channel, api: api, io };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
exports.JsRuntimeIo = JsRuntimeIo;
|
|
145
|
+
exports.createChannel = createChannel;
|
|
146
|
+
exports.detectRuntimes = detectRuntimes;
|
|
147
|
+
exports.getRuntimePaths = getRuntimePaths;
|
|
148
|
+
exports.getStatus = getStatus;
|
|
149
|
+
exports.kill = kill;
|
|
150
|
+
exports.killAll = killAll;
|
|
151
|
+
exports.listProcesses = listProcesses;
|
|
152
|
+
exports.onExit = onExit;
|
|
153
|
+
exports.onStderr = onStderr;
|
|
154
|
+
exports.onStdout = onStdout;
|
|
155
|
+
exports.restart = restart;
|
|
156
|
+
exports.setRuntimePath = setRuntimePath;
|
|
157
|
+
exports.spawn = spawn;
|
|
158
|
+
exports.writeStdin = writeStdin;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type UnlistenFn } from "@tauri-apps/api/event";
|
|
2
|
+
export interface SpawnConfig {
|
|
3
|
+
runtime?: "bun" | "deno" | "node";
|
|
4
|
+
command?: string;
|
|
5
|
+
sidecar?: string;
|
|
6
|
+
script?: string;
|
|
7
|
+
args?: string[];
|
|
8
|
+
cwd?: string;
|
|
9
|
+
env?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export interface ProcessInfo {
|
|
12
|
+
name: string;
|
|
13
|
+
pid: number | null;
|
|
14
|
+
running: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface StdioEventPayload {
|
|
17
|
+
name: string;
|
|
18
|
+
data: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ExitEventPayload {
|
|
21
|
+
name: string;
|
|
22
|
+
code: number | null;
|
|
23
|
+
}
|
|
24
|
+
export interface RuntimeInfo {
|
|
25
|
+
name: string;
|
|
26
|
+
path: string | null;
|
|
27
|
+
version: string | null;
|
|
28
|
+
available: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function spawn(name: string, config: SpawnConfig): Promise<ProcessInfo>;
|
|
31
|
+
export declare function kill(name: string): Promise<void>;
|
|
32
|
+
export declare function killAll(): Promise<void>;
|
|
33
|
+
export declare function restart(name: string, config?: SpawnConfig): Promise<ProcessInfo>;
|
|
34
|
+
export declare function listProcesses(): Promise<ProcessInfo[]>;
|
|
35
|
+
export declare function getStatus(name: string): Promise<ProcessInfo>;
|
|
36
|
+
export declare function writeStdin(name: string, data: string): Promise<void>;
|
|
37
|
+
export declare function detectRuntimes(): Promise<RuntimeInfo[]>;
|
|
38
|
+
export declare function setRuntimePath(runtime: string, path: string): Promise<void>;
|
|
39
|
+
export declare function getRuntimePaths(): Promise<Record<string, string>>;
|
|
40
|
+
export declare function onStdout(name: string, callback: (data: string) => void): Promise<UnlistenFn>;
|
|
41
|
+
export declare function onStderr(name: string, callback: (data: string) => void): Promise<UnlistenFn>;
|
|
42
|
+
export declare function onExit(name: string, callback: (code: number | null) => void): Promise<UnlistenFn>;
|
|
43
|
+
type MessageListener = (data: string) => void;
|
|
44
|
+
export declare class JsRuntimeIo {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
private processName;
|
|
47
|
+
private queue;
|
|
48
|
+
private waitResolve;
|
|
49
|
+
private listeners;
|
|
50
|
+
private unlisten;
|
|
51
|
+
private _isDestroyed;
|
|
52
|
+
constructor(processName: string);
|
|
53
|
+
get isDestroyed(): boolean;
|
|
54
|
+
initialize(): Promise<void>;
|
|
55
|
+
write(data: string): Promise<void>;
|
|
56
|
+
read(): Promise<string | null>;
|
|
57
|
+
on(event: "message" | "error", listener: MessageListener): void;
|
|
58
|
+
off(event: "message" | "error", listener: Function): void;
|
|
59
|
+
destroy(): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
export declare function createChannel<LocalAPI extends Record<string, any> = Record<string, never>, RemoteAPI extends Record<string, any> = Record<string, any>>(processName: string, localApi?: LocalAPI): Promise<{
|
|
62
|
+
channel: any;
|
|
63
|
+
api: RemoteAPI;
|
|
64
|
+
io: JsRuntimeIo;
|
|
65
|
+
}>;
|
|
66
|
+
export {};
|
package/dist-js/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { invoke } from '@tauri-apps/api/core';
|
|
2
|
+
import { listen } from '@tauri-apps/api/event';
|
|
3
|
+
|
|
4
|
+
// ── A) Command wrappers ──
|
|
5
|
+
async function spawn(name, config) {
|
|
6
|
+
return invoke("plugin:js|spawn", { name, config });
|
|
7
|
+
}
|
|
8
|
+
async function kill(name) {
|
|
9
|
+
return invoke("plugin:js|kill", { name });
|
|
10
|
+
}
|
|
11
|
+
async function killAll() {
|
|
12
|
+
return invoke("plugin:js|kill_all");
|
|
13
|
+
}
|
|
14
|
+
async function restart(name, config) {
|
|
15
|
+
return invoke("plugin:js|restart", { name, config: config ?? null });
|
|
16
|
+
}
|
|
17
|
+
async function listProcesses() {
|
|
18
|
+
return invoke("plugin:js|list_processes");
|
|
19
|
+
}
|
|
20
|
+
async function getStatus(name) {
|
|
21
|
+
return invoke("plugin:js|get_status", { name });
|
|
22
|
+
}
|
|
23
|
+
async function writeStdin(name, data) {
|
|
24
|
+
return invoke("plugin:js|write_stdin", { name, data });
|
|
25
|
+
}
|
|
26
|
+
async function detectRuntimes() {
|
|
27
|
+
return invoke("plugin:js|detect_runtimes");
|
|
28
|
+
}
|
|
29
|
+
async function setRuntimePath(runtime, path) {
|
|
30
|
+
return invoke("plugin:js|set_runtime_path", { runtime, path });
|
|
31
|
+
}
|
|
32
|
+
async function getRuntimePaths() {
|
|
33
|
+
return invoke("plugin:js|get_runtime_paths");
|
|
34
|
+
}
|
|
35
|
+
// ── B) Event helpers ──
|
|
36
|
+
function onStdout(name, callback) {
|
|
37
|
+
return listen("js-process-stdout", (event) => {
|
|
38
|
+
if (event.payload.name === name) {
|
|
39
|
+
callback(event.payload.data);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function onStderr(name, callback) {
|
|
44
|
+
return listen("js-process-stderr", (event) => {
|
|
45
|
+
if (event.payload.name === name) {
|
|
46
|
+
callback(event.payload.data);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function onExit(name, callback) {
|
|
51
|
+
return listen("js-process-exit", (event) => {
|
|
52
|
+
if (event.payload.name === name) {
|
|
53
|
+
callback(event.payload.code);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
class JsRuntimeIo {
|
|
58
|
+
constructor(processName) {
|
|
59
|
+
this.queue = [];
|
|
60
|
+
this.waitResolve = null;
|
|
61
|
+
this.listeners = new Set();
|
|
62
|
+
this.unlisten = null;
|
|
63
|
+
this._isDestroyed = false;
|
|
64
|
+
this.processName = processName;
|
|
65
|
+
this.name = `tauri-js-runtime:${processName}`;
|
|
66
|
+
}
|
|
67
|
+
get isDestroyed() {
|
|
68
|
+
return this._isDestroyed;
|
|
69
|
+
}
|
|
70
|
+
async initialize() {
|
|
71
|
+
this.unlisten = await listen("js-process-stdout", (event) => {
|
|
72
|
+
if (event.payload.name !== this.processName)
|
|
73
|
+
return;
|
|
74
|
+
if (this._isDestroyed)
|
|
75
|
+
return;
|
|
76
|
+
// Re-append the newline that BufReader::lines() strips
|
|
77
|
+
const data = event.payload.data + "\n";
|
|
78
|
+
// Dispatch to message listeners
|
|
79
|
+
for (const listener of this.listeners) {
|
|
80
|
+
listener(data);
|
|
81
|
+
}
|
|
82
|
+
// Feed the read queue
|
|
83
|
+
if (this.waitResolve) {
|
|
84
|
+
const resolve = this.waitResolve;
|
|
85
|
+
this.waitResolve = null;
|
|
86
|
+
resolve(data);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
this.queue.push(data);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async write(data) {
|
|
94
|
+
await writeStdin(this.processName, data);
|
|
95
|
+
}
|
|
96
|
+
async read() {
|
|
97
|
+
if (this._isDestroyed) {
|
|
98
|
+
// Return a never-resolving promise so kkrpc's listen loop hangs
|
|
99
|
+
return new Promise(() => { });
|
|
100
|
+
}
|
|
101
|
+
if (this.queue.length > 0) {
|
|
102
|
+
return this.queue.shift();
|
|
103
|
+
}
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
this.waitResolve = resolve;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
on(event, listener) {
|
|
109
|
+
if (event === "message") {
|
|
110
|
+
this.listeners.add(listener);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
off(event, listener) {
|
|
114
|
+
if (event === "message") {
|
|
115
|
+
this.listeners.delete(listener);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async destroy() {
|
|
119
|
+
this._isDestroyed = true;
|
|
120
|
+
if (this.unlisten) {
|
|
121
|
+
this.unlisten();
|
|
122
|
+
this.unlisten = null;
|
|
123
|
+
}
|
|
124
|
+
if (this.waitResolve) {
|
|
125
|
+
this.waitResolve(null);
|
|
126
|
+
this.waitResolve = null;
|
|
127
|
+
}
|
|
128
|
+
this.listeners.clear();
|
|
129
|
+
this.queue = [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// ── D) Channel helper (dynamic kkrpc import) ──
|
|
133
|
+
async function createChannel(processName, localApi) {
|
|
134
|
+
const { RPCChannel } = await import('kkrpc/browser');
|
|
135
|
+
const io = new JsRuntimeIo(processName);
|
|
136
|
+
await io.initialize();
|
|
137
|
+
const channel = new RPCChannel(io, { expose: localApi ?? {} });
|
|
138
|
+
const api = channel.getAPI();
|
|
139
|
+
return { channel, api: api, io };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { JsRuntimeIo, createChannel, detectRuntimes, getRuntimePaths, getStatus, kill, killAll, listProcesses, onExit, onStderr, onStdout, restart, setRuntimePath, spawn, writeStdin };
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tauri-plugin-js-api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"author": "Huakun",
|
|
5
|
+
"description": "",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"types": "./dist-js/index.d.ts",
|
|
8
|
+
"main": "./dist-js/index.cjs",
|
|
9
|
+
"module": "./dist-js/index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
"types": "./dist-js/index.d.ts",
|
|
12
|
+
"import": "./dist-js/index.js",
|
|
13
|
+
"require": "./dist-js/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist-js",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@tauri-apps/api": "^2.0.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"kkrpc": ">=0.6.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependenciesMeta": {
|
|
26
|
+
"kkrpc": {
|
|
27
|
+
"optional": true
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@rollup/plugin-typescript": "^12.0.0",
|
|
32
|
+
"rollup": "^4.9.6",
|
|
33
|
+
"typescript": "^5.3.3",
|
|
34
|
+
"tslib": "^2.6.2"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "rollup -c",
|
|
38
|
+
"pretest": "pnpm build"
|
|
39
|
+
}
|
|
40
|
+
}
|