mirinjs 0.0.1-alpha.13 → 0.0.1-alpha.15
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 +71 -0
- package/src/host.ts +12 -1
- package/src/index.ts +7 -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
|
@@ -94,9 +94,80 @@ export interface MirinConfig {
|
|
|
94
94
|
* support folder, so multiple channels can coexist at the same `baseUrl`.
|
|
95
95
|
*/
|
|
96
96
|
release?: ReleaseConfig;
|
|
97
|
+
/**
|
|
98
|
+
* macOS `.dmg` installer produced by `mirin release` (alongside the updater
|
|
99
|
+
* artifacts). `true`/omitted builds a default drag-to-Applications DMG; an
|
|
100
|
+
* object customizes it; `false` disables it. The DMG is codesigned and (when
|
|
101
|
+
* notary credentials are set) notarized + stapled like the `.app`.
|
|
102
|
+
*/
|
|
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>;
|
|
97
124
|
windows: Record<string, WindowConfig>;
|
|
98
125
|
}
|
|
99
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
|
+
|
|
145
|
+
export interface DmgConfig {
|
|
146
|
+
/** Volume name shown when the DMG is mounted. Default: the app name. */
|
|
147
|
+
volumeName?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Disk-image format. `ULFO` (lzfse, default) compresses large CEF frameworks
|
|
150
|
+
* best on modern macOS; `UDZO` (zlib) is the most broadly compatible; `UDBZ`
|
|
151
|
+
* (bzip2) is smallest but slowest.
|
|
152
|
+
*/
|
|
153
|
+
format?: "ULFO" | "UDZO" | "UDBZ";
|
|
154
|
+
/**
|
|
155
|
+
* Background image for the mounted Finder window (path relative to the project
|
|
156
|
+
* root). Setting this — or any position/size below — switches to the laid-out
|
|
157
|
+
* DMG (Finder window styled via AppleScript); best-effort, falling back to a
|
|
158
|
+
* plain DMG if Finder automation is unavailable.
|
|
159
|
+
*/
|
|
160
|
+
background?: string;
|
|
161
|
+
/** Mounted Finder window size, in points. Default 640×400. */
|
|
162
|
+
windowSize?: { width: number; height: number };
|
|
163
|
+
/** Icon size in the mounted window, in points. Default 128. */
|
|
164
|
+
iconSize?: number;
|
|
165
|
+
/** App icon position in the window. Default centered-left. */
|
|
166
|
+
appPosition?: { x: number; y: number };
|
|
167
|
+
/** `/Applications` symlink position. Default centered-right. */
|
|
168
|
+
applicationsPosition?: { x: number; y: number };
|
|
169
|
+
}
|
|
170
|
+
|
|
100
171
|
export interface ReleaseConfig {
|
|
101
172
|
/** Flat directory URL hosting `{channel}-{platform}-{arch}-*` update files. */
|
|
102
173
|
baseUrl: 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 {
|
|
@@ -47,6 +49,10 @@ export type {
|
|
|
47
49
|
WindowConfig,
|
|
48
50
|
WindowMaterial,
|
|
49
51
|
WindowMaterialOptions,
|
|
52
|
+
ReleaseConfig,
|
|
53
|
+
DmgConfig,
|
|
54
|
+
SidecarSpec,
|
|
55
|
+
SidecarEntitlement,
|
|
50
56
|
} from "./config.ts";
|
|
51
57
|
|
|
52
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
|
+
}
|