mirinjs 0.1.0-alpha.1 → 0.1.0-alpha.11
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 +19 -0
- package/src/config.ts +83 -3
- package/src/host.ts +23 -6
- package/src/index.ts +3 -0
- package/src/native.ts +3 -3
- package/src/rpc-server.ts +2 -0
- package/src/runtime.ts +8 -1
- package/src/updater.ts +56 -9
package/package.json
CHANGED
package/src/app.ts
CHANGED
|
@@ -289,6 +289,25 @@ class MirinApp extends Emitter<AppEvents> {
|
|
|
289
289
|
/** Auto-updater (macOS). Inert unless the app was built with `release` set. */
|
|
290
290
|
readonly updater: Updater = updater;
|
|
291
291
|
|
|
292
|
+
/** This app's bundle id (mirin.config `id`). `undefined` when run detached. */
|
|
293
|
+
get id(): string | undefined {
|
|
294
|
+
try {
|
|
295
|
+
return runtime().id;
|
|
296
|
+
} catch {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** True under `mirin dev`; false in a packaged build (and when detached). Use it
|
|
302
|
+
* to keep dev and installed data (caches, databases) separate. */
|
|
303
|
+
get isDev(): boolean {
|
|
304
|
+
try {
|
|
305
|
+
return runtime().isDev;
|
|
306
|
+
} catch {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
292
311
|
/** macOS Dock-icon controls. Hiding suits resident, hotkey-summoned apps. */
|
|
293
312
|
readonly dock: Dock = {
|
|
294
313
|
hide: () => this.#setDock(false),
|
package/src/config.ts
CHANGED
|
@@ -42,6 +42,9 @@ export interface WindowConfig {
|
|
|
42
42
|
title?: string;
|
|
43
43
|
width?: number;
|
|
44
44
|
height?: number;
|
|
45
|
+
/** Minimum window size; the OS prevents resizing below it. */
|
|
46
|
+
minWidth?: number;
|
|
47
|
+
minHeight?: number;
|
|
45
48
|
/** Screen position (bottom-left origin, points). Centered when absent. */
|
|
46
49
|
x?: number;
|
|
47
50
|
y?: number;
|
|
@@ -78,12 +81,20 @@ export interface MirinConfig {
|
|
|
78
81
|
/** Reverse-DNS app id, e.g. "dev.peje.hello". */
|
|
79
82
|
id: string;
|
|
80
83
|
name: string;
|
|
84
|
+
/**
|
|
85
|
+
* Publisher / company name (e.g. "Netko Labs"). Shown in the Windows installer,
|
|
86
|
+
* the exe's file properties (CompanyName), and Add/Remove Programs. Defaults to
|
|
87
|
+
* the app `name`.
|
|
88
|
+
*/
|
|
89
|
+
publisher?: string;
|
|
81
90
|
/** Main-process entry, relative to the project root (runs in the Bun Worker). */
|
|
82
91
|
main: string;
|
|
83
92
|
/**
|
|
84
|
-
* App icon, relative to the project root
|
|
85
|
-
*
|
|
86
|
-
*
|
|
93
|
+
* App icon, relative to the project root. Accepts a `.icns`, a `.iconset`
|
|
94
|
+
* directory, or a single square `.png` (≥512px). macOS renders it into an
|
|
95
|
+
* `.icns` embedded in the bundle for the Dock and Finder; Linux resolves a PNG
|
|
96
|
+
* and sets it as the window's `_NET_WM_ICON` (taskbar/dock); Windows uses the
|
|
97
|
+
* `.ico` for the exe and window.
|
|
87
98
|
*/
|
|
88
99
|
icon?: string;
|
|
89
100
|
/**
|
|
@@ -109,6 +120,23 @@ export interface MirinConfig {
|
|
|
109
120
|
* `.zip`. Requires `makensis` (NSIS) on the build machine.
|
|
110
121
|
*/
|
|
111
122
|
nsis?: boolean | NsisConfig;
|
|
123
|
+
/**
|
|
124
|
+
* Windows installer via **Inno Setup** — a modern flat wizard
|
|
125
|
+
* (`WizardStyle=modern`, the installer VS Code ships). Preferred over `nsis`
|
|
126
|
+
* when both are available; `true`/omitted builds the default installer, an
|
|
127
|
+
* object customizes it, `false` falls back to NSIS / the portable `.zip`.
|
|
128
|
+
* Requires `iscc` (Inno Setup 6+) on the build machine.
|
|
129
|
+
*/
|
|
130
|
+
inno?: boolean | InnoConfig;
|
|
131
|
+
/**
|
|
132
|
+
* Linux distributable packages produced by `mirin release` (and by `mirin build`
|
|
133
|
+
* when packaging is requested): an **AppImage** (single-file, run-anywhere), a
|
|
134
|
+
* **.deb** (Debian/Ubuntu), and an **.rpm** (Fedora/RHEL/openSUSE). `true`/omitted
|
|
135
|
+
* builds all three with sensible defaults; an object customizes them (which
|
|
136
|
+
* `formats`, maintainer, runtime `depends`, …); `false` disables Linux packaging.
|
|
137
|
+
* Requires `fpm` (deb + rpm) and `appimagetool` (AppImage) on the build machine.
|
|
138
|
+
*/
|
|
139
|
+
linux?: boolean | LinuxConfig;
|
|
112
140
|
/**
|
|
113
141
|
* External binaries bundled into the `.app` and spawned at runtime with
|
|
114
142
|
* `app.sidecar(name)`. Maps a logical name to a path (relative to the project
|
|
@@ -138,6 +166,12 @@ export interface MirinConfig {
|
|
|
138
166
|
*/
|
|
139
167
|
urlSchemes?: string[];
|
|
140
168
|
windows: Record<string, WindowConfig>;
|
|
169
|
+
/**
|
|
170
|
+
* Single-instance app (default true): a second launch focuses the running
|
|
171
|
+
* window and exits, instead of opening another window. Set false to allow
|
|
172
|
+
* multiple instances (each gets its own CEF cache dir).
|
|
173
|
+
*/
|
|
174
|
+
singleInstance?: boolean;
|
|
141
175
|
}
|
|
142
176
|
|
|
143
177
|
/** A bundled sidecar binary with optional hardened-runtime entitlements. */
|
|
@@ -222,6 +256,52 @@ export interface NsisConfig {
|
|
|
222
256
|
include?: string;
|
|
223
257
|
}
|
|
224
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Inno Setup installer options — the same knobs as {@link NsisConfig}
|
|
261
|
+
* (perMachine, oneClick, shortcuts, license, publisher, runAfterFinish,
|
|
262
|
+
* installerIcon, and a raw `include` injected as `.iss` instead of NSIS script).
|
|
263
|
+
*/
|
|
264
|
+
export type InnoConfig = NsisConfig;
|
|
265
|
+
|
|
266
|
+
/** A Linux distributable package format `mirin` can emit. */
|
|
267
|
+
export type LinuxPackageFormat = "appimage" | "deb" | "rpm";
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Linux packaging options. Every format ships the same relocatable app payload
|
|
271
|
+
* (host binary + libcef.so + the CEF runtime + `resources/`) under a single
|
|
272
|
+
* prefix, with a launcher and a freedesktop `.desktop` entry + hicolor icon.
|
|
273
|
+
*/
|
|
274
|
+
export interface LinuxConfig {
|
|
275
|
+
/** Which package formats to emit. Default: all three (`appimage`, `deb`, `rpm`). */
|
|
276
|
+
formats?: LinuxPackageFormat[];
|
|
277
|
+
/**
|
|
278
|
+
* The `/usr/bin` launcher command name and the deb/rpm package name. Defaults to
|
|
279
|
+
* the last dot-segment of the app `id` (e.g. `dev.netko.anko` → `anko`),
|
|
280
|
+
* lowercased and sanitized to a valid package name.
|
|
281
|
+
*/
|
|
282
|
+
binName?: string;
|
|
283
|
+
/** Package maintainer for deb/rpm (`"Name <email>"`). Defaults to `publisher`. */
|
|
284
|
+
maintainer?: string;
|
|
285
|
+
/** One-line package description / `.desktop` `Comment`. Defaults to the app name. */
|
|
286
|
+
description?: string;
|
|
287
|
+
/** License identifier for deb/rpm metadata (e.g. `"MIT"`). Omitted when unset. */
|
|
288
|
+
license?: string;
|
|
289
|
+
/** Homepage URL for deb/rpm metadata. Omitted when unset. */
|
|
290
|
+
homepage?: string;
|
|
291
|
+
/** Freedesktop `.desktop` category (e.g. `"Development"`). Default `"Utility"`. */
|
|
292
|
+
category?: string;
|
|
293
|
+
/**
|
|
294
|
+
* Debian runtime dependency package names, replacing the built-in CEF/GTK
|
|
295
|
+
* defaults (`libgtk-3-0`, `libnss3`, `libasound2`, …). Advanced.
|
|
296
|
+
*/
|
|
297
|
+
debDepends?: string[];
|
|
298
|
+
/**
|
|
299
|
+
* RPM runtime dependency package names, replacing the built-in CEF/GTK defaults
|
|
300
|
+
* (`gtk3`, `nss`, `alsa-lib`, …). Advanced.
|
|
301
|
+
*/
|
|
302
|
+
rpmDepends?: string[];
|
|
303
|
+
}
|
|
304
|
+
|
|
225
305
|
export interface ReleaseConfig {
|
|
226
306
|
/** Flat directory URL hosting `{channel}-{platform}-{arch}-*` update files. */
|
|
227
307
|
baseUrl: string;
|
package/src/host.ts
CHANGED
|
@@ -20,15 +20,20 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
20
20
|
import { Core } from "./native.ts";
|
|
21
21
|
|
|
22
22
|
// Bundle layout differs by platform: macOS `.app` puts the host in
|
|
23
|
-
// `Contents/MacOS` with resources in `../Resources`; Windows
|
|
24
|
-
// where the host
|
|
23
|
+
// `Contents/MacOS` with resources in `../Resources`; Windows and Linux are flat app
|
|
24
|
+
// folders where the host binary sits beside the core lib (`mirin_core.dll` /
|
|
25
|
+
// `libmirin_core.so`) with resources in `resources/`.
|
|
25
26
|
const isWindows = process.platform === "win32";
|
|
27
|
+
const isMac = process.platform === "darwin";
|
|
26
28
|
const exeDir = dirname(process.execPath);
|
|
27
|
-
const resourcesDir =
|
|
29
|
+
const resourcesDir = isMac ? join(exeDir, "..", "Resources") : join(exeDir, "resources");
|
|
28
30
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
const coreFileName = isWindows
|
|
32
|
+
? "mirin_core.dll"
|
|
33
|
+
: isMac
|
|
34
|
+
? "libmirin_core.dylib"
|
|
35
|
+
: "libmirin_core.so";
|
|
36
|
+
const corePath = process.env.MIRIN_CORE ?? join(exeDir, coreFileName);
|
|
32
37
|
const workerPath = process.env.MIRIN_WORKER ?? join(resourcesDir, "worker.js");
|
|
33
38
|
// Bundled-asset dirs for sidecars (Bun.spawn binaries) and extra workers. Dev
|
|
34
39
|
// overrides via env (dev.ts stages them under .mirin); prod resolves in-bundle.
|
|
@@ -50,6 +55,17 @@ const coreConfig = JSON.parse(
|
|
|
50
55
|
process.env.MIRIN_DEV_URL ? { dev: true } : { resources_path: resourcesDir },
|
|
51
56
|
),
|
|
52
57
|
);
|
|
58
|
+
// Opt out of single-instance (the core default) when the app allows multiple.
|
|
59
|
+
if (manifest.singleInstance === false) coreConfig.single_instance = false;
|
|
60
|
+
// The app's bundle id keys the per-app CEF cache dir (Windows has no OS bundle id).
|
|
61
|
+
if (typeof manifest.id === "string") coreConfig.identifier = manifest.id;
|
|
62
|
+
// Linux prod: the CLI stages the resolved app icon at resources/icon.png; the core
|
|
63
|
+
// stamps it onto each window as `_NET_WM_ICON` (taskbar/dock). Dev supplies this via
|
|
64
|
+
// MIRIN_CONFIG_JSON instead. macOS/Windows take the icon from the bundle.
|
|
65
|
+
if (process.platform === "linux" && !coreConfig.icon_path) {
|
|
66
|
+
const iconPng = join(resourcesDir, "icon.png");
|
|
67
|
+
if (existsSync(iconPng)) coreConfig.icon_path = iconPng;
|
|
68
|
+
}
|
|
53
69
|
|
|
54
70
|
// Load the native core on the main thread FIRST. The Worker also dlopens the
|
|
55
71
|
// same dylib in its boot; doing the main-thread dlopen before spawning the
|
|
@@ -61,6 +77,7 @@ const worker = new Worker(workerPath, {
|
|
|
61
77
|
workerData: {
|
|
62
78
|
corePath,
|
|
63
79
|
manifest,
|
|
80
|
+
id: typeof manifest.id === "string" ? manifest.id : undefined,
|
|
64
81
|
devUrl: process.env.MIRIN_DEV_URL,
|
|
65
82
|
resourcesDir,
|
|
66
83
|
sidecarDir,
|
package/src/index.ts
CHANGED
package/src/native.ts
CHANGED
|
@@ -24,7 +24,7 @@ const symbols = {
|
|
|
24
24
|
mirin_window_control: { args: [FFIType.u32, FFIType.ptr], returns: FFIType.void },
|
|
25
25
|
mirin_window_set_material: { args: [FFIType.u32, FFIType.ptr], returns: FFIType.void },
|
|
26
26
|
mirin_window_set_position: { args: [FFIType.u32, FFIType.f64, FFIType.f64], returns: FFIType.void },
|
|
27
|
-
mirin_window_maybe_start_drag: { args: [FFIType.u32, FFIType.i32, FFIType.i32, FFIType.i32], returns: FFIType.void },
|
|
27
|
+
mirin_window_maybe_start_drag: { args: [FFIType.u32, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.i32], returns: FFIType.void },
|
|
28
28
|
mirin_app_quit: { args: [], returns: FFIType.void },
|
|
29
29
|
mirin_app_set_dock_visible: { args: [FFIType.i32], returns: FFIType.void },
|
|
30
30
|
mirin_set_app_menu: { args: [FFIType.ptr], returns: FFIType.void },
|
|
@@ -114,8 +114,8 @@ export class Core {
|
|
|
114
114
|
this.#lib.symbols.mirin_window_set_position(id, x, y);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
windowMaybeStartDrag(id: number, x: number, y: number, detail: number): void {
|
|
118
|
-
this.#lib.symbols.mirin_window_maybe_start_drag(id, x, y, detail);
|
|
117
|
+
windowMaybeStartDrag(id: number, x: number, y: number, detail: number, ht: number): void {
|
|
118
|
+
this.#lib.symbols.mirin_window_maybe_start_drag(id, x, y, detail, ht);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
quit(): void {
|
package/src/rpc-server.ts
CHANGED
|
@@ -29,6 +29,8 @@ interface ControlFrame {
|
|
|
29
29
|
verb?: string;
|
|
30
30
|
/** Click count for pointer actions (2 = double-click → maximize). */
|
|
31
31
|
detail?: number;
|
|
32
|
+
/** Win32 resize hit-test code when the mousedown is on a window edge/corner. */
|
|
33
|
+
ht?: number;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
type IncomingFrame = RequestFrame | ControlFrame;
|
package/src/runtime.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface Runtime {
|
|
|
18
18
|
core: Core;
|
|
19
19
|
rpc: RpcServer;
|
|
20
20
|
manifestWindows: ManifestWindowConfig[];
|
|
21
|
+
/** App bundle id (mirin.config `id`); undefined when detached. */
|
|
22
|
+
id?: string;
|
|
23
|
+
/** True under `mirin dev`; false in a packaged build. */
|
|
24
|
+
isDev: boolean;
|
|
21
25
|
devUrl?: string;
|
|
22
26
|
/** Contents/Resources of the running .app (for the updater). Absent in dev. */
|
|
23
27
|
resourcesDir?: string;
|
|
@@ -87,6 +91,7 @@ export function boot(): void {
|
|
|
87
91
|
const data = (workerData ?? {}) as {
|
|
88
92
|
corePath?: string;
|
|
89
93
|
manifest?: { windows?: Record<string, WindowConfig> };
|
|
94
|
+
id?: string;
|
|
90
95
|
devUrl?: string;
|
|
91
96
|
resourcesDir?: string;
|
|
92
97
|
sidecarDir?: string;
|
|
@@ -107,7 +112,7 @@ export function boot(): void {
|
|
|
107
112
|
rpc.setControlHandler((frame, webview) => {
|
|
108
113
|
switch (frame.action) {
|
|
109
114
|
case "window.maybeStartDrag":
|
|
110
|
-
core.windowMaybeStartDrag(webview, frame.x ?? 0, frame.y ?? 0, frame.detail ?? 1);
|
|
115
|
+
core.windowMaybeStartDrag(webview, frame.x ?? 0, frame.y ?? 0, frame.detail ?? 1, frame.ht ?? 0);
|
|
111
116
|
break;
|
|
112
117
|
case "window.control":
|
|
113
118
|
if (frame.verb) core.windowControl(webview, frame.verb);
|
|
@@ -126,6 +131,8 @@ export function boot(): void {
|
|
|
126
131
|
core,
|
|
127
132
|
rpc,
|
|
128
133
|
manifestWindows,
|
|
134
|
+
id: data.id,
|
|
135
|
+
isDev: !!data.devUrl,
|
|
129
136
|
devUrl: data.devUrl,
|
|
130
137
|
resourcesDir: data.resourcesDir,
|
|
131
138
|
corePath,
|
package/src/updater.ts
CHANGED
|
@@ -13,9 +13,17 @@
|
|
|
13
13
|
* packaged app with `release` set; in `mirin dev` the updater is idle.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { homedir } from "node:os";
|
|
16
|
+
import { homedir, tmpdir } from "node:os";
|
|
17
17
|
import { join } from "node:path";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
renameSync,
|
|
24
|
+
rmSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from "node:fs";
|
|
19
27
|
import { $ } from "bun";
|
|
20
28
|
import { runtime } from "./runtime.ts";
|
|
21
29
|
import { loadCodec } from "./codec.ts";
|
|
@@ -213,20 +221,59 @@ export class Updater {
|
|
|
213
221
|
// to exit (the exe/bundle is locked while running), swaps the folder, relaunches.
|
|
214
222
|
if (IS_WINDOWS) {
|
|
215
223
|
const runningApp = join(this.#resourcesDir()!, "..");
|
|
216
|
-
const
|
|
224
|
+
const name = this.#version()!.name;
|
|
225
|
+
const exe = join(runningApp, `${name}.exe`);
|
|
226
|
+
const script = join(tmpdir(), `mirin-apply-${name}.ps1`);
|
|
227
|
+
const applyVbs = join(tmpdir(), `mirin-apply-${name}.vbs`);
|
|
228
|
+
const launchVbs = join(tmpdir(), `mirin-launch-${name}.vbs`);
|
|
217
229
|
const ps = [
|
|
218
230
|
`$ErrorActionPreference='SilentlyContinue'`,
|
|
231
|
+
// Never let the app dir be this process's cwd (Windows can't delete a cwd).
|
|
232
|
+
`Set-Location $env:TEMP`,
|
|
233
|
+
// Wait for the host to exit (it locks the exe), then a beat for CEF's
|
|
234
|
+
// helper subprocesses to release their file locks (libcef.dll etc.).
|
|
219
235
|
`while (Get-Process -Id ${process.pid} -ErrorAction SilentlyContinue) { Start-Sleep -Milliseconds 200 }`,
|
|
220
|
-
`Start-Sleep -Milliseconds
|
|
221
|
-
|
|
236
|
+
`Start-Sleep -Milliseconds 700`,
|
|
237
|
+
// Replace the app folder, retrying ~10s while any lingering lock clears.
|
|
238
|
+
`for ($i=0; $i -lt 50; $i++) { Remove-Item -Recurse -Force ${psq(runningApp)}; if (-not (Test-Path ${psq(runningApp)})) { break }; Start-Sleep -Milliseconds 200 }`,
|
|
222
239
|
`Move-Item -Force ${psq(this.#staged)} ${psq(runningApp)}`,
|
|
223
|
-
`Start-Process ${psq(exe)}`,
|
|
224
|
-
|
|
225
|
-
|
|
240
|
+
`Start-Process -FilePath ${psq(exe)} -WorkingDirectory ${psq(runningApp)}`,
|
|
241
|
+
// self-clean the temp launch scripts (this .ps1 + the two .vbs shims)
|
|
242
|
+
`Remove-Item -Force ${psq(script)},${psq(applyVbs)},${psq(launchVbs)}`,
|
|
243
|
+
].join("\n");
|
|
244
|
+
writeFileSync(script, ps, "utf8");
|
|
245
|
+
|
|
246
|
+
// No terminal window may appear during the update. powershell.exe is a
|
|
247
|
+
// console-subsystem program: launching it directly (even with
|
|
248
|
+
// -WindowStyle Hidden) flashes a console, because the OS allocates the
|
|
249
|
+
// console before powershell can hide it. wscript.exe is a GUI-subsystem
|
|
250
|
+
// host with no console; its Shell.Run(cmd, 0) starts powershell with
|
|
251
|
+
// SW_HIDE in the initial STARTUPINFO, so no window is ever shown.
|
|
252
|
+
writeFileSync(
|
|
253
|
+
applyVbs,
|
|
254
|
+
`CreateObject("WScript.Shell").Run "powershell -NoProfile -NonInteractive ` +
|
|
255
|
+
`-ExecutionPolicy Bypass -WindowStyle Hidden -File ""${script}""", 0, False\n`,
|
|
256
|
+
"utf8",
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// The swap MUST outlive this process. A Bun.spawn child is in our job
|
|
260
|
+
// object and is killed when we quit — even with unref/detached (verified).
|
|
261
|
+
// WMI's Win32_Process.Create has the WMI service spawn it, outside our job,
|
|
262
|
+
// so it survives. Issue that WMI call from a console-less wscript host too,
|
|
263
|
+
// so the launcher itself never flashes a window. `await` the short launcher
|
|
264
|
+
// so the swap process exists before core.quit() (no race), then quit.
|
|
265
|
+
writeFileSync(
|
|
266
|
+
launchVbs,
|
|
267
|
+
`GetObject("winmgmts:Win32_Process").Create ` +
|
|
268
|
+
`"wscript.exe //B //Nologo ""${applyVbs}""", Null, Null, pid\n`,
|
|
269
|
+
"utf8",
|
|
270
|
+
);
|
|
271
|
+
const launcher = Bun.spawn(["wscript.exe", "//B", "//Nologo", launchVbs], {
|
|
226
272
|
stdin: "ignore",
|
|
227
273
|
stdout: "ignore",
|
|
228
274
|
stderr: "ignore",
|
|
229
|
-
})
|
|
275
|
+
});
|
|
276
|
+
await launcher.exited;
|
|
230
277
|
} else {
|
|
231
278
|
const runningApp = join(this.#resourcesDir()!, "..", "..");
|
|
232
279
|
const script = [
|