mirinjs 0.0.1-alpha.14 → 0.0.1-alpha.16
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/package.json +1 -1
- package/src/app.ts +10 -0
- package/src/config.ts +38 -0
- package/src/host.ts +12 -1
- package/src/index.ts +5 -1
- package/src/runtime.ts +21 -0
- package/src/sidecar.ts +107 -0
package/package.json
CHANGED
package/src/app.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type { Router, EventProc } from "./rpc.ts";
|
|
|
8
8
|
import type { WindowConfig, WindowMaterial, WindowMaterialOptions } from "./config.ts";
|
|
9
9
|
import { updater } from "./updater.ts";
|
|
10
10
|
import type { Updater } from "./updater.ts";
|
|
11
|
+
import { sidecar as spawnSidecar } from "./sidecar.ts";
|
|
12
|
+
import type { SidecarOptions, SidecarProcess } from "./sidecar.ts";
|
|
11
13
|
|
|
12
14
|
/** Which native backend a window's `material` resolved to. */
|
|
13
15
|
export type WindowMaterialInfo = {
|
|
@@ -313,6 +315,14 @@ class MirinApp extends Emitter<AppEvents> {
|
|
|
313
315
|
return rpcEmitters((method, payload) => runtime().rpc.broadcast(method, payload));
|
|
314
316
|
}
|
|
315
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Spawn a bundled sidecar binary (declared in `mirin.config.ts` `sidecars`).
|
|
320
|
+
* Resolves the in-bundle path and tracks the child so it's killed on quit.
|
|
321
|
+
*/
|
|
322
|
+
sidecar(name: string, opts?: SidecarOptions): SidecarProcess {
|
|
323
|
+
return spawnSidecar(name, opts);
|
|
324
|
+
}
|
|
325
|
+
|
|
316
326
|
quit(): void {
|
|
317
327
|
runtime().core.quit();
|
|
318
328
|
}
|
package/src/config.ts
CHANGED
|
@@ -101,9 +101,47 @@ export interface MirinConfig {
|
|
|
101
101
|
* notary credentials are set) notarized + stapled like the `.app`.
|
|
102
102
|
*/
|
|
103
103
|
dmg?: boolean | DmgConfig;
|
|
104
|
+
/**
|
|
105
|
+
* External binaries bundled into the `.app` and spawned at runtime with
|
|
106
|
+
* `app.sidecar(name)`. Maps a logical name to a path (relative to the project
|
|
107
|
+
* root) or a {@link SidecarSpec}. Each binary is copied into
|
|
108
|
+
* `Contents/Resources/sidecars/<name>`, codesigned (hardened runtime), and —
|
|
109
|
+
* when notary credentials are set — notarized with the rest of the app.
|
|
110
|
+
*
|
|
111
|
+
* Prefer a binary already on the user's PATH (`Bun.spawn("git", …)`) or a
|
|
112
|
+
* download-on-first-run when size/licensing matter; bundle only when you need
|
|
113
|
+
* a pinned version offline.
|
|
114
|
+
*/
|
|
115
|
+
sidecars?: Record<string, string | SidecarSpec>;
|
|
116
|
+
/**
|
|
117
|
+
* Extra Bun Worker entry files (relative to the project root) bundled next to
|
|
118
|
+
* the main worker. Resolve one at runtime with `resolveWorker(name)` and pass
|
|
119
|
+
* it to `new Worker(...)` from `node:worker_threads`. These run off the main
|
|
120
|
+
* thread for CPU/IO offload and must NOT call window/native APIs — only the
|
|
121
|
+
* app worker owns AppKit/CEF (see docs/architecture.md).
|
|
122
|
+
*/
|
|
123
|
+
workers?: Record<string, string>;
|
|
104
124
|
windows: Record<string, WindowConfig>;
|
|
105
125
|
}
|
|
106
126
|
|
|
127
|
+
/** A bundled sidecar binary with optional hardened-runtime entitlements. */
|
|
128
|
+
export interface SidecarSpec {
|
|
129
|
+
/** Path to the binary, relative to the project root. */
|
|
130
|
+
bin: string;
|
|
131
|
+
/**
|
|
132
|
+
* Extra codesign entitlements for this binary under the hardened runtime.
|
|
133
|
+
* Most plain CLIs need none. Use `disable-library-validation` for a binary
|
|
134
|
+
* that loads dylibs not signed by your Team ID, or the JIT entitlements for a
|
|
135
|
+
* binary that runs its own JIT (another Bun/V8/Node).
|
|
136
|
+
*/
|
|
137
|
+
entitlements?: SidecarEntitlement[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type SidecarEntitlement =
|
|
141
|
+
| "allow-jit"
|
|
142
|
+
| "allow-unsigned-executable-memory"
|
|
143
|
+
| "disable-library-validation";
|
|
144
|
+
|
|
107
145
|
export interface DmgConfig {
|
|
108
146
|
/** Volume name shown when the DMG is mounted. Default: the app name. */
|
|
109
147
|
volumeName?: string;
|
package/src/host.ts
CHANGED
|
@@ -24,6 +24,10 @@ const resourcesDir = join(exeDir, "..", "Resources");
|
|
|
24
24
|
|
|
25
25
|
const corePath = process.env.MIRIN_CORE ?? join(exeDir, "libmirin_core.dylib");
|
|
26
26
|
const workerPath = process.env.MIRIN_WORKER ?? join(resourcesDir, "worker.js");
|
|
27
|
+
// Bundled-asset dirs for sidecars (Bun.spawn binaries) and extra workers. Dev
|
|
28
|
+
// overrides via env (dev.ts stages them under .mirin); prod resolves in-bundle.
|
|
29
|
+
const sidecarDir = process.env.MIRIN_SIDECAR_DIR ?? join(resourcesDir, "sidecars");
|
|
30
|
+
const workersDir = process.env.MIRIN_WORKERS_DIR ?? join(resourcesDir, "workers");
|
|
27
31
|
|
|
28
32
|
if (!existsSync(corePath) || !existsSync(workerPath)) {
|
|
29
33
|
console.error(`[mirin host] missing core (${corePath}) or worker (${workerPath})`);
|
|
@@ -48,7 +52,14 @@ const coreConfig = JSON.parse(
|
|
|
48
52
|
const core = new Core(corePath);
|
|
49
53
|
|
|
50
54
|
const worker = new Worker(workerPath, {
|
|
51
|
-
workerData: {
|
|
55
|
+
workerData: {
|
|
56
|
+
corePath,
|
|
57
|
+
manifest,
|
|
58
|
+
devUrl: process.env.MIRIN_DEV_URL,
|
|
59
|
+
resourcesDir,
|
|
60
|
+
sidecarDir,
|
|
61
|
+
workersDir,
|
|
62
|
+
},
|
|
52
63
|
});
|
|
53
64
|
worker.on("error", (err) => console.error("[mirin worker]", err));
|
|
54
65
|
|
package/src/index.ts
CHANGED
|
@@ -24,7 +24,8 @@ export { clipboard } from "./clipboard.ts";
|
|
|
24
24
|
export { globalShortcut } from "./shortcut.ts";
|
|
25
25
|
export { logger, setLogLevel, getLogLevel, Logger } from "./logger.ts";
|
|
26
26
|
export { updater, Updater } from "./updater.ts";
|
|
27
|
-
export {
|
|
27
|
+
export { sidecar } from "./sidecar.ts";
|
|
28
|
+
export { NotAttachedError, resolveWorker } from "./runtime.ts";
|
|
28
29
|
|
|
29
30
|
export type {
|
|
30
31
|
WindowEvents,
|
|
@@ -40,6 +41,7 @@ export type {
|
|
|
40
41
|
export type { MenuItemTemplate, MenuRole } from "./menu.ts";
|
|
41
42
|
export type { LogLevel } from "./logger.ts";
|
|
42
43
|
export type { UpdaterStatus, UpdateInfo, UpdateProgress, UpdaterEvents } from "./updater.ts";
|
|
44
|
+
export type { SidecarOptions, SidecarProcess } from "./sidecar.ts";
|
|
43
45
|
export type { TrayOptions } from "./tray.ts";
|
|
44
46
|
export type { OpenDialogOptions, SaveDialogOptions, MessageDialogOptions } from "./dialog.ts";
|
|
45
47
|
export type {
|
|
@@ -49,6 +51,8 @@ export type {
|
|
|
49
51
|
WindowMaterialOptions,
|
|
50
52
|
ReleaseConfig,
|
|
51
53
|
DmgConfig,
|
|
54
|
+
SidecarSpec,
|
|
55
|
+
SidecarEntitlement,
|
|
52
56
|
} from "./config.ts";
|
|
53
57
|
|
|
54
58
|
wireAppEvents();
|
package/src/runtime.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { workerData } from "node:worker_threads";
|
|
8
|
+
import { join } from "node:path";
|
|
8
9
|
import { Core } from "./native.ts";
|
|
9
10
|
import { RpcServer } from "./rpc-server.ts";
|
|
10
11
|
import type { WindowConfig } from "./config.ts";
|
|
@@ -22,6 +23,10 @@ export interface Runtime {
|
|
|
22
23
|
resourcesDir?: string;
|
|
23
24
|
/** Path to libmirin_core (for the updater codec). */
|
|
24
25
|
corePath?: string;
|
|
26
|
+
/** Dir holding bundled sidecar binaries (for `app.sidecar`). */
|
|
27
|
+
sidecarDir?: string;
|
|
28
|
+
/** Dir holding bundled extra-worker JS (for `resolveWorker`). */
|
|
29
|
+
workersDir?: string;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export class NotAttachedError extends Error {
|
|
@@ -84,6 +89,8 @@ export function boot(): void {
|
|
|
84
89
|
manifest?: { windows?: Record<string, WindowConfig> };
|
|
85
90
|
devUrl?: string;
|
|
86
91
|
resourcesDir?: string;
|
|
92
|
+
sidecarDir?: string;
|
|
93
|
+
workersDir?: string;
|
|
87
94
|
};
|
|
88
95
|
const corePath = data.corePath ?? process.env.MIRIN_CORE;
|
|
89
96
|
if (!corePath) return; // not under the host; the API stays detached
|
|
@@ -104,6 +111,20 @@ export function boot(): void {
|
|
|
104
111
|
devUrl: data.devUrl,
|
|
105
112
|
resourcesDir: data.resourcesDir,
|
|
106
113
|
corePath,
|
|
114
|
+
sidecarDir: data.sidecarDir,
|
|
115
|
+
workersDir: data.workersDir,
|
|
107
116
|
};
|
|
108
117
|
core.onEvent(dispatch);
|
|
109
118
|
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve a bundled extra-worker (declared in `mirin.config.ts` `workers`) to an
|
|
122
|
+
* absolute path, for `new Worker(resolveWorker(name))` from `node:worker_threads`.
|
|
123
|
+
* Works in `mirin dev` and the built `.app`. Extra workers run off the main
|
|
124
|
+
* thread and must not call window/native APIs.
|
|
125
|
+
*/
|
|
126
|
+
export function resolveWorker(name: string): string {
|
|
127
|
+
const dir = current?.workersDir;
|
|
128
|
+
if (!dir) throw new NotAttachedError("resolveWorker");
|
|
129
|
+
return join(dir, `${name}.js`);
|
|
130
|
+
}
|
package/src/sidecar.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidecars — spawn a binary bundled into the `.app` (declared in
|
|
3
|
+
* `mirin.config.ts` `sidecars`). A thin wrapper over `Bun.spawn` that resolves
|
|
4
|
+
* the bundled path (dev or prod, via `runtime().sidecarDir`) and tracks the
|
|
5
|
+
* child so it's killed when the app quits — the two things hand-rolled
|
|
6
|
+
* `Bun.spawn` gets wrong (path resolution + orphaned processes).
|
|
7
|
+
*
|
|
8
|
+
* Sidecars run as separate OS processes, so they may do anything a CLI can; they
|
|
9
|
+
* just can't touch the app's AppKit/CEF UI (nothing off the main thread can).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { runtime, onNativeEvent } from "./runtime.ts";
|
|
14
|
+
|
|
15
|
+
export interface SidecarOptions {
|
|
16
|
+
/** Arguments passed after the binary path. */
|
|
17
|
+
args?: string[];
|
|
18
|
+
cwd?: string;
|
|
19
|
+
/** Extra env on top of the inherited environment. */
|
|
20
|
+
env?: Record<string, string>;
|
|
21
|
+
/** Defaults: stdin "pipe", stdout "pipe", stderr "inherit". */
|
|
22
|
+
stdin?: Bun.SpawnOptions.Writable;
|
|
23
|
+
stdout?: Bun.SpawnOptions.Readable;
|
|
24
|
+
stderr?: Bun.SpawnOptions.Readable;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A running sidecar. Mirrors the subset of `Bun.Subprocess` apps need. */
|
|
28
|
+
export interface SidecarProcess {
|
|
29
|
+
readonly pid: number;
|
|
30
|
+
readonly stdin: Bun.Subprocess["stdin"];
|
|
31
|
+
readonly stdout: Bun.Subprocess["stdout"];
|
|
32
|
+
readonly stderr: Bun.Subprocess["stderr"];
|
|
33
|
+
/** Resolves with the exit code when the process ends. */
|
|
34
|
+
readonly exited: Promise<number>;
|
|
35
|
+
/** Send a signal (default SIGTERM) and stop tracking it. */
|
|
36
|
+
kill(signal?: number | NodeJS.Signals): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const children = new Set<Bun.Subprocess>();
|
|
40
|
+
let hooked = false;
|
|
41
|
+
|
|
42
|
+
function killAll(): void {
|
|
43
|
+
for (const child of children) {
|
|
44
|
+
try {
|
|
45
|
+
child.kill();
|
|
46
|
+
} catch {
|
|
47
|
+
// already gone
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
children.clear();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Register shutdown hooks once, so sidecars don't outlive the app. */
|
|
54
|
+
function ensureHooks(): void {
|
|
55
|
+
if (hooked) return;
|
|
56
|
+
hooked = true;
|
|
57
|
+
// App fully quit (last window closed / Cmd-Q path surfaces here).
|
|
58
|
+
onNativeEvent("window.all-closed", killAll);
|
|
59
|
+
// Worker-process shutdown backstops (normal exit / signals). `terminate()` is
|
|
60
|
+
// abrupt and won't fire these, hence the native-event hook above as well.
|
|
61
|
+
for (const sig of ["exit", "SIGINT", "SIGTERM"] as const) {
|
|
62
|
+
process.on(sig, killAll);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Spawn a bundled sidecar binary by its config name. Throws if the runtime
|
|
68
|
+
* isn't attached or no `sidecarDir` is known (e.g. run outside the host).
|
|
69
|
+
*/
|
|
70
|
+
export function sidecar(name: string, opts: SidecarOptions = {}): SidecarProcess {
|
|
71
|
+
const dir = runtime().sidecarDir;
|
|
72
|
+
if (!dir) {
|
|
73
|
+
throw new Error(`app.sidecar("${name}"): no sidecar dir (is this running under the mirin host?)`);
|
|
74
|
+
}
|
|
75
|
+
ensureHooks();
|
|
76
|
+
|
|
77
|
+
const bin = join(dir, name);
|
|
78
|
+
const child = Bun.spawn([bin, ...(opts.args ?? [])], {
|
|
79
|
+
cwd: opts.cwd,
|
|
80
|
+
env: opts.env ? { ...process.env, ...opts.env } : process.env,
|
|
81
|
+
stdin: opts.stdin ?? "pipe",
|
|
82
|
+
stdout: opts.stdout ?? "pipe",
|
|
83
|
+
stderr: opts.stderr ?? "inherit",
|
|
84
|
+
});
|
|
85
|
+
children.add(child);
|
|
86
|
+
void child.exited.then(() => children.delete(child));
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
get pid() {
|
|
90
|
+
return child.pid;
|
|
91
|
+
},
|
|
92
|
+
get stdin() {
|
|
93
|
+
return child.stdin;
|
|
94
|
+
},
|
|
95
|
+
get stdout() {
|
|
96
|
+
return child.stdout;
|
|
97
|
+
},
|
|
98
|
+
get stderr() {
|
|
99
|
+
return child.stderr;
|
|
100
|
+
},
|
|
101
|
+
exited: child.exited,
|
|
102
|
+
kill(signal) {
|
|
103
|
+
children.delete(child);
|
|
104
|
+
child.kill(signal as number | undefined);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|