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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirinjs",
3
- "version": "0.0.1-alpha.14",
3
+ "version": "0.0.1-alpha.16",
4
4
  "description": "Build desktop apps with Bun, TypeScript, and Chromium (CEF). The Bun-native Electron alternative.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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: { corePath, manifest, devUrl: process.env.MIRIN_DEV_URL, resourcesDir },
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 { NotAttachedError } from "./runtime.ts";
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
+ }