@mirinjs/cli 0.0.1-alpha.8 → 0.1.0-alpha.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mirinjs/cli",
3
- "version": "0.0.1-alpha.8",
3
+ "version": "0.1.0-alpha.0",
4
4
  "description": "CLI for mirin apps: dev, build, init.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,10 +27,12 @@
27
27
  "bun": ">=1.2.0"
28
28
  },
29
29
  "dependencies": {
30
- "create-mirinjs": "0.0.1-alpha.8"
30
+ "create-mirinjs": "0.1.0-alpha.0",
31
+ "mirinjs": "0.1.0-alpha.0"
31
32
  },
32
33
  "optionalDependencies": {
33
- "@mirinjs/darwin-arm64": "0.0.1-alpha.8"
34
+ "@mirinjs/darwin-arm64": "0.1.0-alpha.0",
35
+ "@mirinjs/win32-x64": "0.1.0-alpha.0"
34
36
  },
35
37
  "publishConfig": {
36
38
  "access": "public"
package/src/artifacts.ts CHANGED
@@ -49,8 +49,8 @@ export async function resolveArtifacts(opts: { release: boolean }): Promise<Arti
49
49
  await $`cargo build -p mirin-core -p mirin-helper ${flags}`.cwd(REPO_ROOT);
50
50
  const target = join(REPO_ROOT, "target", profile);
51
51
  return {
52
- coreDylib: join(target, "libmirin_core.dylib"),
53
- helperBin: join(target, "mirin-helper"),
52
+ coreDylib: join(target, coreFileName()),
53
+ helperBin: join(target, helperFileName()),
54
54
  hostEntry: join(REPO_ROOT, "packages", "mirin", "src", "host.ts"),
55
55
  cefPath,
56
56
  };
@@ -58,17 +58,30 @@ export async function resolveArtifacts(opts: { release: boolean }): Promise<Arti
58
58
 
59
59
  const nativeDir = resolveNativeDir();
60
60
  return {
61
- coreDylib: join(nativeDir, "libmirin_core.dylib"),
62
- helperBin: join(nativeDir, "mirin-helper"),
61
+ coreDylib: join(nativeDir, coreFileName()),
62
+ helperBin: join(nativeDir, helperFileName()),
63
63
  hostEntry: resolvePackageFile("mirinjs/host"),
64
64
  cefPath,
65
65
  };
66
66
  }
67
67
 
68
+ /** The native core library file name for the host platform (MSVC has no `lib` prefix). */
69
+ function coreFileName(): string {
70
+ return process.platform === "win32" ? "mirin_core.dll" : "libmirin_core.dylib";
71
+ }
72
+
73
+ /** The CEF subprocess binary name for the host platform. */
74
+ function helperFileName(): string {
75
+ return process.platform === "win32" ? "mirin-helper.exe" : "mirin-helper";
76
+ }
77
+
68
78
  function assertSupportedPlatform(): void {
69
- if (process.platform !== "darwin" || process.arch !== "arm64") {
79
+ const supported =
80
+ (process.platform === "darwin" && process.arch === "arm64") ||
81
+ (process.platform === "win32" && process.arch === "x64");
82
+ if (!supported) {
70
83
  throw new Error(
71
- `mirin alpha supports macOS arm64 only (got ${process.platform}/${process.arch}).`,
84
+ `mirin alpha supports macOS arm64 and Windows x64 (got ${process.platform}/${process.arch}).`,
72
85
  );
73
86
  }
74
87
  }
@@ -99,10 +112,16 @@ function cliVersion(): string {
99
112
  return pkg.version as string;
100
113
  }
101
114
 
115
+ /** A file/dir that, if present in a CEF dir, means the distribution is unpacked.
116
+ * macOS ships the framework bundle; Windows ships a flat dir with libcef.dll. */
117
+ function cefMarker(): string {
118
+ return process.platform === "win32" ? "libcef.dll" : "Chromium Embedded Framework.framework";
119
+ }
120
+
102
121
  async function ensureCef(): Promise<string> {
103
122
  if (IN_REPO) {
104
123
  const vendor = join(REPO_ROOT, "vendor", "cef");
105
- if (existsSync(join(vendor, "Chromium Embedded Framework.framework"))) return vendor;
124
+ if (existsSync(join(vendor, cefMarker()))) return vendor;
106
125
  console.error(
107
126
  "[mirin] vendor/cef missing — run `bun scripts/fetch-cef.ts` in the monorepo first.",
108
127
  );
@@ -111,7 +130,7 @@ async function ensureCef(): Promise<string> {
111
130
 
112
131
  const version = cliVersion();
113
132
  const cacheDir = join(homedir(), ".mirinjs", "cef", `${version}-${platformTag()}`);
114
- if (existsSync(join(cacheDir, "Chromium Embedded Framework.framework"))) return cacheDir;
133
+ if (existsSync(join(cacheDir, cefMarker()))) return cacheDir;
115
134
 
116
135
  mkdirSync(cacheDir, { recursive: true });
117
136
  const asset = `cef-${platformTag()}.tar.gz`;
package/src/build.ts CHANGED
@@ -16,23 +16,65 @@
16
16
  */
17
17
 
18
18
  import { $ } from "bun";
19
- import { mkdirSync, rmSync } from "node:fs";
19
+ import { mkdirSync, rmSync, readFileSync, existsSync } from "node:fs";
20
20
  import { join } from "node:path";
21
21
  import { buildAppBundle } from "./bundle.ts";
22
+ import { buildWindowsBundle } from "./bundle-win.ts";
22
23
  import { resolveArtifacts } from "./artifacts.ts";
24
+ import { sweepBuildTemps } from "./temps.ts";
25
+ import { normalizeSidecars, compileWorkers } from "./extras.ts";
23
26
 
24
- export async function build(projectDir = process.cwd()): Promise<number> {
27
+ const IS_WINDOWS = process.platform === "win32";
28
+
29
+ export interface BuildResult {
30
+ /** Path to the assembled .app. */
31
+ app: string;
32
+ appName: string;
33
+ bundleId: string;
34
+ /** App version (from the project's package.json). */
35
+ version: string;
36
+ /** Update channel (config.release.channel ?? "stable"). */
37
+ channel: string;
38
+ /** Update baseUrl, if `release` is configured. */
39
+ baseUrl?: string;
40
+ /** libmirin_core path (for the updater codec at release time). */
41
+ coreDylib: string;
42
+ /** Project root (so `mirin release` can resolve relative asset paths). */
43
+ projectDir: string;
44
+ /** DMG config from mirin.config.ts (`true`/object/`false`); default `true`. */
45
+ dmg: boolean | import("mirinjs").DmgConfig;
46
+ /** Codesign identity used for the bundle, if any (MIRIN_SIGN_IDENTITY). */
47
+ signIdentity?: string;
48
+ }
49
+
50
+ /** Read the project's package.json version (the single source of app version). */
51
+ function appVersion(projectDir: string): string {
52
+ const pkgPath = join(projectDir, "package.json");
53
+ if (!existsSync(pkgPath)) return "0.0.0";
54
+ try {
55
+ return JSON.parse(readFileSync(pkgPath, "utf8")).version ?? "0.0.0";
56
+ } catch {
57
+ return "0.0.0";
58
+ }
59
+ }
60
+
61
+ export async function build(projectDir = process.cwd()): Promise<BuildResult> {
25
62
  const outDir = join(projectDir, "build");
26
63
  const work = join(projectDir, ".mirin");
27
64
  mkdirSync(outDir, { recursive: true });
28
65
  mkdirSync(work, { recursive: true });
66
+ sweepBuildTemps(projectDir);
29
67
 
30
68
  const config = (await import(join(projectDir, "mirin.config.ts"))).default;
31
69
  const appName: string = config.name ?? "Mirin App";
32
70
  const bundleId: string = config.id ?? "dev.mirin.app";
33
71
  const mainEntry = join(projectDir, config.main ?? "main/main.ts");
72
+ const version = appVersion(projectDir);
73
+ const channel: string = config.release?.channel ?? "stable";
74
+ const baseUrl: string | undefined = config.release?.baseUrl;
75
+ const dmg: boolean | import("mirinjs").DmgConfig = config.dmg ?? true;
34
76
 
35
- console.log(`[mirin build] ${appName}`);
77
+ console.log(`[mirin build] ${appName} ${version}`);
36
78
 
37
79
  // 1. production UI
38
80
  console.log("[mirin build] vite build…");
@@ -43,32 +85,67 @@ export async function build(projectDir = process.cwd()): Promise<number> {
43
85
 
44
86
  // 3 + 4. host + worker (minified)
45
87
  console.log("[mirin build] compiling host + bundling main process…");
46
- const hostExe = join(work, "host-release");
88
+ const signIdentity = process.env.MIRIN_SIGN_IDENTITY;
89
+ const hostExe = join(work, IS_WINDOWS ? "host-release.exe" : "host-release");
47
90
  const workerJs = join(work, "worker.release.js");
48
91
  await $`bun build --compile --minify ${artifacts.hostEntry} --outfile ${hostExe}`.cwd(projectDir);
49
92
  await $`bun build ${mainEntry} --target=bun --minify --outfile ${workerJs}`.cwd(projectDir);
50
93
 
51
- // 5. assemble + sign
52
- console.log("[mirin build] assembling .app…");
53
- rmSync(join(outDir, `${appName}.app`), { recursive: true, force: true });
54
- const { app } = await buildAppBundle({
94
+ // Extra assets: resolve sidecar binaries + compile any extra worker entries.
95
+ const sidecars = normalizeSidecars(projectDir, config.sidecars);
96
+ const extraWorkers = await compileWorkers(projectDir, config.workers, join(work, "workers"), true);
97
+
98
+ // 5. assemble (+ sign on macOS)
99
+ console.log(`[mirin build] assembling ${IS_WINDOWS ? "app folder" : ".app"}…`);
100
+ // version.json embeds the running app's update identity (read by app.updater).
101
+ // Only when `release` is configured — otherwise the app has no updater.
102
+ const versionJson = baseUrl
103
+ ? JSON.stringify({ version, channel, baseUrl, name: appName, identifier: bundleId })
104
+ : undefined;
105
+ const resources = {
106
+ uiDir: join(projectDir, "dist"),
107
+ workerJs,
108
+ manifestJson: JSON.stringify({ windows: config.windows }),
109
+ versionJson,
110
+ workers: extraWorkers,
111
+ };
112
+ const { app } = IS_WINDOWS
113
+ ? await buildWindowsBundle({
114
+ appName,
115
+ outDir,
116
+ hostExe,
117
+ coreDll: artifacts.coreDylib,
118
+ helperExe: artifacts.helperBin,
119
+ cefPath: artifacts.cefPath,
120
+ icon: config.icon ? join(projectDir, config.icon) : undefined,
121
+ resources: { ...resources, sidecars: sidecars.map((s) => ({ name: s.name, src: s.src })) },
122
+ })
123
+ : await buildAppBundle({
124
+ appName,
125
+ bundleId,
126
+ outDir,
127
+ hostExe,
128
+ coreDylib: artifacts.coreDylib,
129
+ helperBin: artifacts.helperBin,
130
+ cefPath: artifacts.cefPath,
131
+ version,
132
+ icon: config.icon ? join(projectDir, config.icon) : undefined,
133
+ signIdentity,
134
+ urlSchemes: config.urlSchemes,
135
+ resources: { ...resources, sidecars },
136
+ });
137
+
138
+ console.log(`\n[mirin build] done → ${app}`);
139
+ return {
140
+ app,
55
141
  appName,
56
142
  bundleId,
57
- outDir,
58
- hostExe,
143
+ version,
144
+ channel,
145
+ baseUrl,
59
146
  coreDylib: artifacts.coreDylib,
60
- helperBin: artifacts.helperBin,
61
- cefPath: artifacts.cefPath,
62
- icon: config.icon ? join(projectDir, config.icon) : undefined,
63
- signIdentity: process.env.MIRIN_SIGN_IDENTITY,
64
- resources: {
65
- uiDir: join(projectDir, "dist"),
66
- workerJs,
67
- manifestJson: JSON.stringify({ windows: config.windows }),
68
- },
69
- });
70
-
71
- console.log(`\n[mirin build] done → ${app}`);
72
- console.log(` open "${app}"`);
73
- return 0;
147
+ projectDir,
148
+ dmg,
149
+ signIdentity,
150
+ };
74
151
  }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Assemble a Windows app folder for a mirin app. Windows has no `.app` bundle and
3
+ * no codesign step in the MVP — just a flat directory the OS loader can resolve
4
+ * libcef.dll + the CEF runtime from, next to the host exe:
5
+ *
6
+ * <App>/
7
+ * <App>.exe compiled Bun host (process.execPath)
8
+ * mirin_core.dll loaded by the host via bun:ffi
9
+ * mirin-helper.exe CEF subprocess (browser_subprocess_path)
10
+ * libcef.dll, *.pak, icudtl.dat, v8_context_snapshot.bin, locales/, … (CEF runtime)
11
+ * resources/ (production only — dev serves from the Vite URL)
12
+ * ui/, worker.js, mirin.manifest.json, version.json, sidecars/, workers/
13
+ *
14
+ * The CEF subprocess name (`mirin-helper.exe`) and the `resources/` layout match
15
+ * what mirin-core derives at runtime (engine::derive_subprocess_path, host.ts).
16
+ */
17
+
18
+ import { cpSync, mkdirSync, rmSync, writeFileSync, existsSync, readdirSync } from "node:fs";
19
+ import { join, extname } from "node:path";
20
+ import { makeWindowsIcon } from "./icon-win.ts";
21
+
22
+ /** Source dir (vendor/cef on Windows) must contain libcef.dll. */
23
+ const CEF_MARKER = "libcef.dll";
24
+
25
+ /** CEF runtime files copied next to the host exe. Everything else in the Windows
26
+ * distribution (libcef.lib, include/, cmake/, archive.json, bootstrap*.exe) is
27
+ * build-time only and intentionally left out. */
28
+ const CEF_RUNTIME_EXTS = new Set([".dll", ".bin", ".pak", ".dat"]);
29
+ const CEF_RUNTIME_EXTRAS = ["vk_swiftshader_icd.json", "locales"];
30
+
31
+ export interface WinBundleOptions {
32
+ appName: string; // also the host exe stem → <appName>.exe
33
+ outDir: string;
34
+ hostExe: string; // compiled Bun host (.exe)
35
+ coreDll: string; // mirin_core.dll
36
+ helperExe: string; // mirin-helper.exe
37
+ cefPath: string; // dir containing the Windows CEF runtime (vendor/cef)
38
+ /** App icon source (.ico / .iconset / .png) → <App>/icon.ico, set as the window icon. */
39
+ icon?: string;
40
+ /** Production-only resources placed under <App>/resources. */
41
+ resources?: {
42
+ uiDir?: string; // Vite dist/ → resources/ui (served via app://ui)
43
+ workerJs?: string; // bundled main-process Worker → resources/worker.js
44
+ manifestJson?: string; // serialized manifest → resources/mirin.manifest.json
45
+ versionJson?: string; // serialized version.json → resources/version.json (updater)
46
+ sidecars?: { name: string; src: string }[]; // → resources/sidecars/<name>
47
+ workers?: Record<string, string>; // name → abs path → resources/workers/<name>.js
48
+ };
49
+ }
50
+
51
+ /** Build the Windows app folder and return its path + the host exe to launch. */
52
+ export async function buildWindowsBundle(
53
+ opts: WinBundleOptions,
54
+ ): Promise<{ app: string; exe: string }> {
55
+ const { appName, cefPath } = opts;
56
+ if (!existsSync(join(cefPath, CEF_MARKER))) {
57
+ throw new Error(`CEF runtime not found at ${cefPath} — run: bun scripts/fetch-cef.ts`);
58
+ }
59
+
60
+ const app = join(opts.outDir, appName);
61
+ rmSync(app, { recursive: true, force: true });
62
+ mkdirSync(app, { recursive: true });
63
+
64
+ const exe = join(app, `${appName}.exe`);
65
+ cpSync(opts.hostExe, exe);
66
+ cpSync(opts.coreDll, join(app, "mirin_core.dll"));
67
+ cpSync(opts.helperExe, join(app, "mirin-helper.exe"));
68
+
69
+ copyCefRuntime(cefPath, app);
70
+
71
+ // App icon → <App>/icon.ico, loaded at runtime by the core and set as the window
72
+ // icon (taskbar / Alt-Tab / title). Sits beside the exe so the core finds it.
73
+ if (opts.icon) makeWindowsIcon(opts.icon, join(app, "icon.ico"));
74
+
75
+ // Production resources (dev passes none — paths come from env + the Vite URL).
76
+ if (opts.resources) {
77
+ const res = join(app, "resources");
78
+ mkdirSync(res, { recursive: true });
79
+ if (opts.resources.uiDir) cpSync(opts.resources.uiDir, join(res, "ui"), { recursive: true });
80
+ if (opts.resources.workerJs) cpSync(opts.resources.workerJs, join(res, "worker.js"));
81
+ if (opts.resources.manifestJson != null) {
82
+ writeFileSync(join(res, "mirin.manifest.json"), opts.resources.manifestJson);
83
+ }
84
+ if (opts.resources.versionJson != null) {
85
+ writeFileSync(join(res, "version.json"), opts.resources.versionJson);
86
+ }
87
+ if (opts.resources.workers && Object.keys(opts.resources.workers).length) {
88
+ const workersDir = join(res, "workers");
89
+ mkdirSync(workersDir, { recursive: true });
90
+ for (const [name, src] of Object.entries(opts.resources.workers)) {
91
+ cpSync(src, join(workersDir, `${name}.js`));
92
+ }
93
+ }
94
+ if (opts.resources.sidecars?.length) {
95
+ const sidecarsDir = join(res, "sidecars");
96
+ mkdirSync(sidecarsDir, { recursive: true });
97
+ for (const sc of opts.resources.sidecars) {
98
+ if (!existsSync(sc.src)) throw new Error(`sidecar "${sc.name}" not found: ${sc.src}`);
99
+ cpSync(sc.src, join(sidecarsDir, sc.name));
100
+ }
101
+ }
102
+ }
103
+
104
+ return { app, exe };
105
+ }
106
+
107
+ /** Copy the CEF runtime (dlls, paks, snapshot, icu data, locales) into `dest`. */
108
+ function copyCefRuntime(cefPath: string, dest: string): void {
109
+ for (const entry of readdirSync(cefPath)) {
110
+ if (CEF_RUNTIME_EXTS.has(extname(entry).toLowerCase())) {
111
+ cpSync(join(cefPath, entry), join(dest, entry));
112
+ }
113
+ }
114
+ for (const extra of CEF_RUNTIME_EXTRAS) {
115
+ const src = join(cefPath, extra);
116
+ if (existsSync(src)) cpSync(src, join(dest, extra), { recursive: true });
117
+ }
118
+ }
package/src/bundle.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { $ } from "bun";
15
- import { cpSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
15
+ import { cpSync, mkdirSync, rmSync, writeFileSync, existsSync, readdirSync, chmodSync } from "node:fs";
16
16
  import { join } from "node:path";
17
17
 
18
18
  const FRAMEWORK = "Chromium Embedded Framework.framework";
@@ -33,18 +33,57 @@ export interface BundleOptions {
33
33
  coreDylib: string; // libmirin_core.dylib
34
34
  helperBin: string; // compiled mirin-helper binary
35
35
  cefPath: string; // dir containing the CEF framework (vendor/cef)
36
+ /** App version → Info.plist CFBundleShortVersionString/CFBundleVersion. */
37
+ version?: string;
36
38
  /** App icon source: a .icns, a .iconset dir, or a square .png. Optional. */
37
39
  icon?: string;
38
40
  /** Codesign identity; "-" (default) is ad-hoc. Set to a Developer ID to ship. */
39
41
  signIdentity?: string;
42
+ /** Custom URL schemes -> Info.plist CFBundleURLTypes (deep links). */
43
+ urlSchemes?: string[];
40
44
  /** Production-only resources placed under Contents/Resources. */
41
45
  resources?: {
42
46
  uiDir?: string; // Vite dist/, copied to Resources/ui (served via app://ui)
43
47
  workerJs?: string; // bundled main-process Worker entry -> Resources/worker.js
44
48
  manifestJson?: string; // serialized manifest -> Resources/mirin.manifest.json
49
+ versionJson?: string; // serialized version.json -> Resources/version.json (updater)
50
+ /** Bundled sidecar binaries -> Resources/sidecars/<name> (copied + signed). */
51
+ sidecars?: SidecarBundle[];
52
+ /** Compiled extra-worker JS (name -> abs path) -> Resources/workers/<name>.js. */
53
+ workers?: Record<string, string>;
45
54
  };
46
55
  }
47
56
 
57
+ /** A sidecar binary to bundle: logical name, source path, hardened-runtime entitlements. */
58
+ export interface SidecarBundle {
59
+ name: string;
60
+ src: string;
61
+ entitlements: string[];
62
+ }
63
+
64
+ /** Short entitlement name -> the full `com.apple.security.cs.*` key. */
65
+ const SIDECAR_ENTITLEMENT_KEYS: Record<string, string> = {
66
+ "allow-jit": "com.apple.security.cs.allow-jit",
67
+ "allow-unsigned-executable-memory": "com.apple.security.cs.allow-unsigned-executable-memory",
68
+ "disable-library-validation": "com.apple.security.cs.disable-library-validation",
69
+ };
70
+
71
+ function entitlementsPlist(names: string[]): string {
72
+ const keys = names
73
+ .map((n) => SIDECAR_ENTITLEMENT_KEYS[n])
74
+ .filter(Boolean)
75
+ .map((k) => ` <key>${k}</key><true/>`)
76
+ .join("\n");
77
+ return `<?xml version="1.0" encoding="UTF-8"?>
78
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
79
+ <plist version="1.0">
80
+ <dict>
81
+ ${keys}
82
+ </dict>
83
+ </plist>
84
+ `;
85
+ }
86
+
48
87
  /** The 10 standard iconset renditions (point size + @1x/@2x pixel size). */
49
88
  const ICONSET_RENDITIONS = [
50
89
  { name: "icon_16x16.png", px: 16 },
@@ -89,14 +128,18 @@ async function writeIcon(iconSrc: string, resources: string): Promise<string | u
89
128
  return "icon";
90
129
  }
91
130
 
92
- type PlistValue = string | boolean | Record<string, string>;
131
+ type PlistValue = string | boolean | PlistValue[] | { [k: string]: PlistValue };
132
+
133
+ const xmlEscape = (s: string) =>
134
+ s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
93
135
 
94
136
  function plist(entries: Record<string, PlistValue>): string {
95
137
  const render = (v: PlistValue): string => {
96
138
  if (typeof v === "boolean") return v ? "<true/>" : "<false/>";
97
- if (typeof v === "string") return `<string>${v}</string>`;
139
+ if (typeof v === "string") return `<string>${xmlEscape(v)}</string>`;
140
+ if (Array.isArray(v)) return `<array>${v.map(render).join("")}</array>`;
98
141
  const inner = Object.entries(v)
99
- .map(([k, val]) => `<key>${k}</key><string>${val}</string>`)
142
+ .map(([k, val]) => `<key>${k}</key>${render(val)}`)
100
143
  .join("");
101
144
  return `<dict>${inner}</dict>`;
102
145
  };
@@ -136,6 +179,7 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
136
179
  // Render the icon (if any) into Resources before writing the plist, so we
137
180
  // only set CFBundleIconFile when an icon was actually produced.
138
181
  const iconFile = opts.icon ? await writeIcon(opts.icon, join(contents, "Resources")) : undefined;
182
+ const version = opts.version ?? "0.0.1";
139
183
 
140
184
  const info: Record<string, PlistValue> = {
141
185
  CFBundleDevelopmentRegion: "en",
@@ -145,8 +189,8 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
145
189
  CFBundleInfoDictionaryVersion: "6.0",
146
190
  CFBundleName: appName,
147
191
  CFBundlePackageType: "APPL",
148
- CFBundleShortVersionString: "0.0.1",
149
- CFBundleVersion: "0.0.1",
192
+ CFBundleShortVersionString: version,
193
+ CFBundleVersion: version,
150
194
  LSMinimumSystemVersion: "13.0",
151
195
  NSHighResolutionCapable: true,
152
196
  NSSupportsAutomaticGraphicsSwitching: true,
@@ -154,6 +198,13 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
154
198
  LSEnvironment: { MallocNanoZone: "0" },
155
199
  };
156
200
  if (iconFile) info.CFBundleIconFile = iconFile;
201
+ // Deep-link schemes: register this app as the macOS handler for app://-style
202
+ // URLs (e.g. anko://…). Delivered at runtime via app.on("open-url").
203
+ if (opts.urlSchemes?.length) {
204
+ info.CFBundleURLTypes = [
205
+ { CFBundleURLName: bundleId, CFBundleURLSchemes: opts.urlSchemes },
206
+ ];
207
+ }
157
208
 
158
209
  writeFileSync(join(contents, "Info.plist"), plist(info));
159
210
 
@@ -173,6 +224,33 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
173
224
  if (opts.resources?.manifestJson != null) {
174
225
  writeFileSync(join(resources, "mirin.manifest.json"), opts.resources.manifestJson);
175
226
  }
227
+ // version.json is the running app's self-knowledge for the updater. Written
228
+ // before the codesign loop below so the signature covers it.
229
+ if (opts.resources?.versionJson != null) {
230
+ writeFileSync(join(resources, "version.json"), opts.resources.versionJson);
231
+ }
232
+ // Extra Bun Worker bundles -> Resources/workers/<name>.js (resolveWorker()).
233
+ if (opts.resources?.workers && Object.keys(opts.resources.workers).length) {
234
+ const workersDir = join(resources, "workers");
235
+ mkdirSync(workersDir, { recursive: true });
236
+ for (const [name, src] of Object.entries(opts.resources.workers)) {
237
+ cpSync(src, join(workersDir, `${name}.js`));
238
+ }
239
+ }
240
+ // Sidecar binaries -> Resources/sidecars/<name> (chmod +x; signed below). Paths
241
+ // collected here so the codesign loop can sign them inside-out with the app.
242
+ const sidecarDests: SidecarBundle[] = [];
243
+ if (opts.resources?.sidecars?.length) {
244
+ const sidecarsDir = join(resources, "sidecars");
245
+ mkdirSync(sidecarsDir, { recursive: true });
246
+ for (const sc of opts.resources.sidecars) {
247
+ if (!existsSync(sc.src)) throw new Error(`sidecar "${sc.name}" not found: ${sc.src}`);
248
+ const dest = join(sidecarsDir, sc.name);
249
+ cpSync(sc.src, dest);
250
+ chmodSync(dest, 0o755);
251
+ sidecarDests.push({ name: sc.name, src: dest, entitlements: sc.entitlements });
252
+ }
253
+ }
176
254
 
177
255
  for (const { suffix, id } of HELPER_TYPES) {
178
256
  const name = `${appName} Helper${suffix}`;
@@ -189,8 +267,8 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
189
267
  CFBundleInfoDictionaryVersion: "6.0",
190
268
  CFBundleName: name,
191
269
  CFBundlePackageType: "APPL",
192
- CFBundleShortVersionString: "0.0.1",
193
- CFBundleVersion: "0.0.1",
270
+ CFBundleShortVersionString: version,
271
+ CFBundleVersion: version,
194
272
  LSMinimumSystemVersion: "13.0",
195
273
  LSUIElement: "1",
196
274
  NSHighResolutionCapable: true,
@@ -199,14 +277,81 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
199
277
  );
200
278
  }
201
279
 
202
- // Sign inside-out: framework, helpers, then the outer app. Ad-hoc ("-") by
203
- // default; pass a Developer ID to produce a distributable, notarizable app.
280
+ // Sign inside-out. Ad-hoc ("-") by default for local builds; pass a Developer
281
+ // ID to produce a distributable, notarizable app. Notarization requires the
282
+ // hardened runtime (--options runtime), a secure timestamp (--timestamp), and
283
+ // entitlements that let CEF + Bun JIT and load unsigned executable memory —
284
+ // without all three the Apple notary service returns "Invalid".
204
285
  const identity = opts.signIdentity ?? "-";
205
- await $`codesign --force --sign ${identity} ${join(frameworks, FRAMEWORK)}`.quiet();
206
- for (const { suffix } of HELPER_TYPES) {
207
- await $`codesign --force --sign ${identity} ${join(frameworks, `${appName} Helper${suffix}.app`)}`.quiet();
286
+ const notarizable = identity !== "-";
287
+ const cef = join(frameworks, FRAMEWORK);
288
+
289
+ if (notarizable) {
290
+ // Mirrors the entitlement set Electrobun ships for Bun + CEF under hardened
291
+ // runtime (electrobun-reference/package/src/cli/index.ts).
292
+ const entitlements = join(opts.outDir, "_entitlements.plist");
293
+ writeFileSync(
294
+ entitlements,
295
+ `<?xml version="1.0" encoding="UTF-8"?>
296
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
297
+ <plist version="1.0">
298
+ <dict>
299
+ <key>com.apple.security.cs.allow-jit</key><true/>
300
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
301
+ <key>com.apple.security.cs.disable-library-validation</key><true/>
302
+ </dict>
303
+ </plist>
304
+ `,
305
+ );
306
+ const sign = (path: string, ents = false) =>
307
+ ents
308
+ ? $`codesign --force --timestamp --options runtime --entitlements ${entitlements} --sign ${identity} ${path}`.quiet()
309
+ : $`codesign --force --timestamp --options runtime --sign ${identity} ${path}`.quiet();
310
+
311
+ // 1. CEF's nested libraries, then 2. the framework bundle itself.
312
+ const cefLibs = join(cef, "Libraries");
313
+ if (existsSync(cefLibs)) {
314
+ for (const lib of readdirSync(cefLibs)) {
315
+ if (lib.endsWith(".dylib")) await sign(join(cefLibs, lib));
316
+ }
317
+ }
318
+ await sign(cef);
319
+ // 3. our FFI core dylib.
320
+ await sign(join(macos, "libmirin_core.dylib"));
321
+ // 3b. sidecars: hardened runtime + timestamp; per-binary entitlements only
322
+ // when the spec asks (most CLIs need none — over-entitling is a smell).
323
+ for (const sc of sidecarDests) {
324
+ if (sc.entitlements.length) {
325
+ const ent = join(opts.outDir, `_sidecar_${sc.name}.plist`);
326
+ writeFileSync(ent, entitlementsPlist(sc.entitlements));
327
+ await $`codesign --force --timestamp --options runtime --entitlements ${ent} --sign ${identity} ${sc.src}`.quiet();
328
+ rmSync(ent, { force: true });
329
+ } else {
330
+ await sign(sc.src);
331
+ }
332
+ }
333
+ // 4. each helper: the inner executable, then the .app wrapper (entitlements
334
+ // on both — the renderer/GPU helpers are what actually JIT).
335
+ for (const { suffix } of HELPER_TYPES) {
336
+ const name = `${appName} Helper${suffix}`;
337
+ const helperApp = join(frameworks, `${name}.app`);
338
+ await sign(join(helperApp, "Contents", "MacOS", name), true);
339
+ await sign(helperApp, true);
340
+ }
341
+ // 5. finally the outer app.
342
+ await sign(app, true);
343
+ rmSync(entitlements, { force: true });
344
+ } else {
345
+ // Ad-hoc: enough to launch locally; not distributable or notarizable.
346
+ await $`codesign --force --sign ${identity} ${cef}`.quiet();
347
+ for (const sc of sidecarDests) {
348
+ await $`codesign --force --sign ${identity} ${sc.src}`.quiet();
349
+ }
350
+ for (const { suffix } of HELPER_TYPES) {
351
+ await $`codesign --force --sign ${identity} ${join(frameworks, `${appName} Helper${suffix}.app`)}`.quiet();
352
+ }
353
+ await $`codesign --force --sign ${identity} ${app}`.quiet();
208
354
  }
209
- await $`codesign --force --sign ${identity} ${app}`.quiet();
210
355
 
211
356
  return { app, exe: join(macos, appName) };
212
357
  }