@mirinjs/cli 0.1.0-alpha.24 → 0.1.0-alpha.26

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.1.0-alpha.24",
3
+ "version": "0.1.0-alpha.26",
4
4
  "description": "CLI for mirin apps: dev, build, init.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,15 +27,15 @@
27
27
  "bun": ">=1.2.0"
28
28
  },
29
29
  "dependencies": {
30
- "create-mirinjs": "0.1.0-alpha.24",
31
- "mirinjs": "0.1.0-alpha.24"
30
+ "create-mirinjs": "0.1.0-alpha.26",
31
+ "mirinjs": "0.1.0-alpha.26"
32
32
  },
33
33
  "optionalDependencies": {
34
- "@mirinjs/darwin-arm64": "0.1.0-alpha.24",
35
- "@mirinjs/win32-x64": "0.1.0-alpha.24",
36
- "@mirinjs/win32-arm64": "0.1.0-alpha.24",
37
- "@mirinjs/linux-arm64": "0.1.0-alpha.24",
38
- "@mirinjs/linux-x64": "0.1.0-alpha.24"
34
+ "@mirinjs/darwin-arm64": "0.1.0-alpha.26",
35
+ "@mirinjs/win32-x64": "0.1.0-alpha.26",
36
+ "@mirinjs/win32-arm64": "0.1.0-alpha.26",
37
+ "@mirinjs/linux-arm64": "0.1.0-alpha.26",
38
+ "@mirinjs/linux-x64": "0.1.0-alpha.26"
39
39
  },
40
40
  "publishConfig": {
41
41
  "access": "public"
package/src/build.ts CHANGED
@@ -9,6 +9,8 @@
9
9
  * from inside the bundle, and webviews load their `app://` URLs served from
10
10
  * Contents/Resources by the native scheme handler.
11
11
  *
12
+ * Release channel / feed: MIRIN_RELEASE_CHANNEL and MIRIN_RELEASE_BASE_URL override
13
+ * config.release so one config can produce stable and nightly builds.
12
14
  * Codesign identity: ad-hoc by default; set MIRIN_SIGN_IDENTITY to a Developer
13
15
  * ID to produce a distributable, notarizable app.
14
16
  */
@@ -18,10 +20,10 @@ import { join } from "node:path";
18
20
  import { $ } from "bun";
19
21
  import { resolveArtifacts } from "./artifacts.ts";
20
22
  import { buildLinuxBundle } from "./bundle/linux/index.ts";
21
- import { buildAppBundle } from "./bundle/macos/index.ts";
23
+ import { buildAppBundle, stampMacSdk } from "./bundle/macos/index.ts";
22
24
  import { normalizeCefLocales } from "./bundle/shared/cef-locales.ts";
23
25
  import { buildWindowsBundle } from "./bundle/windows/index.ts";
24
- import { compileWorkers, normalizeSidecars } from "./extras.ts";
26
+ import { compileWorkers, normalizeResources, normalizeSidecars } from "./extras.ts";
25
27
  import { resolveIconFallback, resolveIconSource } from "./icons/sources.ts";
26
28
  import { makeWindowsIcon } from "./icons/windows/index.ts";
27
29
  import { buildLinuxPackages, resolveLinuxFormats } from "./package/linux/index.ts";
@@ -186,8 +188,9 @@ export async function build(
186
188
  const bundleId: string = config.id ?? "dev.mirin.app";
187
189
  const mainEntry = join(projectDir, config.main ?? "main/main.ts");
188
190
  const version = opts.version ?? appVersion(projectDir);
189
- const channel: string = config.release?.channel ?? "stable";
190
- const baseUrl: string | undefined = config.release?.baseUrl;
191
+ // A release pipeline builds several channels from one config: the env wins.
192
+ const channel: string = process.env.MIRIN_RELEASE_CHANNEL || config.release?.channel || "stable";
193
+ const baseUrl: string | undefined = process.env.MIRIN_RELEASE_BASE_URL || config.release?.baseUrl;
191
194
  const releaseNotes: string | undefined = config.release?.notes;
192
195
  const dmg: boolean | import("mirinjs").DmgConfig = config.dmg ?? true;
193
196
  const nsis: boolean | import("mirinjs").NsisConfig = config.nsis ?? true;
@@ -213,6 +216,7 @@ export async function build(
213
216
  const hostExe = join(work, IS_WINDOWS ? "host-release.exe" : "host-release");
214
217
  const workerJs = join(work, "worker.release.js");
215
218
  const sidecars = normalizeSidecars(projectDir, config.sidecars);
219
+ const extraResources = normalizeResources(projectDir, config.resources);
216
220
  const hostBuild = (async (): Promise<void> => {
217
221
  // Bun's Windows arm64 build currently has no bun:ffi/TinyCC runtime. The
218
222
  // win32-arm64 Mirin package therefore carries an x64 compatibility payload,
@@ -221,6 +225,7 @@ export async function build(
221
225
  await $`bun build --compile --minify ${target} ${artifacts.hostEntry} --outfile ${hostExe}`.cwd(
222
226
  projectDir,
223
227
  );
228
+ await stampMacSdk(hostExe);
224
229
  if (IS_WINDOWS) {
225
230
  patchPeToGuiSubsystem(hostExe);
226
231
  await brandWindowsExe(hostExe, {
@@ -255,9 +260,12 @@ export async function build(
255
260
  singleInstance: config.singleInstance,
256
261
  // Lets a diagnostic build opt in via `devtools.production`; otherwise fully off.
257
262
  devtools: config.devtools,
263
+ // Production: names resolve under the bundle resources directory.
264
+ resources: Object.fromEntries(extraResources.map((r) => [r.name, r.name])),
258
265
  }),
259
266
  versionJson,
260
267
  workers: extraWorkers,
268
+ extra: extraResources,
261
269
  };
262
270
  const { app } = IS_WINDOWS
263
271
  ? await buildWindowsBundle({
@@ -61,6 +61,8 @@ export interface LinuxBundleOptions {
61
61
  versionJson?: string; // serialized version.json → resources/version.json (updater)
62
62
  sidecars?: { name: string; src: string }[]; // → resources/sidecars/<name>
63
63
  workers?: Record<string, string>; // name → abs path → resources/workers/<name>.js
64
+ /** Extra resource directories (name -> abs dir) copied verbatim -> resources/<name>. */
65
+ extra?: { name: string; src: string }[];
64
66
  };
65
67
  }
66
68
 
@@ -234,6 +236,9 @@ export async function buildLinuxBundle(
234
236
  cpSync(src, join(workersDir, `${name}.js`));
235
237
  }
236
238
  }
239
+ for (const extra of opts.resources?.extra ?? []) {
240
+ cpSync(extra.src, join(res, extra.name), { recursive: true });
241
+ }
237
242
  if (opts.resources.sidecars?.length) {
238
243
  const sidecarsDir = join(res, "sidecars");
239
244
  mkdirSync(sidecarsDir, { recursive: true });
@@ -24,6 +24,7 @@ import { join } from "node:path";
24
24
  import { $ } from "bun";
25
25
  import { isIconComposerDoc, writeAppearanceCatalog } from "../../icons/macos/index.ts";
26
26
  import { pruneMacCefLocales } from "../shared/cef-locales.ts";
27
+ import { findMachOFiles } from "./macho.ts";
27
28
 
28
29
  const FRAMEWORK = "Chromium Embedded Framework.framework";
29
30
 
@@ -68,6 +69,8 @@ export interface BundleOptions {
68
69
  sidecars?: SidecarBundle[];
69
70
  /** Compiled extra-worker JS (name -> abs path) -> Resources/workers/<name>.js. */
70
71
  workers?: Record<string, string>;
72
+ /** Extra resource directories (name -> abs dir) copied verbatim -> Resources/<name>. */
73
+ extra?: { name: string; src: string }[];
71
74
  };
72
75
  }
73
76
 
@@ -292,6 +295,15 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
292
295
  cpSync(src, join(workersDir, `${name}.js`));
293
296
  }
294
297
  }
298
+ // Extra resource dirs -> Resources/<name>. Any Mach-O inside (a bundled server's
299
+ // native addons, helper executables) is signed below: notarization rejects
300
+ // unsigned code anywhere in the bundle.
301
+ const extraDirs: string[] = [];
302
+ for (const extra of opts.resources?.extra ?? []) {
303
+ const dest = join(resources, extra.name);
304
+ cpSync(extra.src, dest, { recursive: true });
305
+ extraDirs.push(dest);
306
+ }
295
307
  // Sidecar binaries -> Resources/sidecars/<name> (chmod +x; signed below). Paths
296
308
  // collected here so the codesign loop can sign them inside-out with the app.
297
309
  const sidecarDests: SidecarBundle[] = [];
@@ -385,6 +397,10 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
385
397
  await sign(sc.src);
386
398
  }
387
399
  }
400
+ // 3c. Mach-O files shipped inside resource dirs (.node addons, dylibs, CLIs).
401
+ for (const dir of extraDirs) {
402
+ for (const binary of findMachOFiles(dir)) await sign(binary);
403
+ }
388
404
  // 4. each helper: the inner executable, then the .app wrapper (entitlements
389
405
  // on both — the renderer/GPU helpers are what actually JIT).
390
406
  for (const { suffix } of HELPER_TYPES) {
@@ -1 +1,2 @@
1
1
  export * from "./app.ts";
2
+ export { stampMacSdk } from "./sdk-stamp.ts";
@@ -0,0 +1,42 @@
1
+ import { closeSync, lstatSync, openSync, readdirSync, readSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ // Thin (LE/BE, 32/64-bit) and fat Mach-O magics. A fat header shares 0xCAFEBABE
5
+ // with Java class files; `nfat_arch` separates them (a class file has a version there).
6
+ const THIN_MAGICS = new Set([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe]);
7
+ const FAT_MAGICS = new Set([0xcafebabe, 0xcafebabf]);
8
+ const MAX_FAT_ARCHS = 32;
9
+
10
+ /** True when the file starts with a Mach-O header (thin or fat), regardless of extension. */
11
+ export function isMachO(path: string): boolean {
12
+ const fd = openSync(path, "r");
13
+ try {
14
+ const header = Buffer.alloc(8);
15
+ if (readSync(fd, header, 0, 8, 0) < 8) return false;
16
+ const magic = header.readUInt32BE(0);
17
+ if (THIN_MAGICS.has(magic)) return true;
18
+ if (!FAT_MAGICS.has(magic)) return false;
19
+ const archCount = header.readUInt32BE(4);
20
+ return archCount > 0 && archCount <= MAX_FAT_ARCHS;
21
+ } finally {
22
+ closeSync(fd);
23
+ }
24
+ }
25
+
26
+ /** Every regular Mach-O file under `root` (symlinks skipped), sorted for stable signing. */
27
+ export function findMachOFiles(root: string): string[] {
28
+ const found: string[] = [];
29
+ const walk = (dir: string) => {
30
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
31
+ const path = join(dir, entry.name);
32
+ if (entry.isSymbolicLink()) continue;
33
+ if (entry.isDirectory()) {
34
+ walk(path);
35
+ continue;
36
+ }
37
+ if (entry.isFile() && lstatSync(path).size >= 8 && isMachO(path)) found.push(path);
38
+ }
39
+ };
40
+ walk(root);
41
+ return found.sort();
42
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Stamp the compiled host with the macOS 26 SDK version.
3
+ *
4
+ * `bun build --compile` clones the Bun runtime binary, so the host inherits
5
+ * Bun's LC_BUILD_VERSION (SDK 15.x). AppKit gates the macOS 26 window design
6
+ * ("linked-on-or-after") on the MAIN executable's SDK, which leaves mirin apps
7
+ * with the pre-Tahoe compatibility chrome — flat traffic lights, old window
8
+ * metrics. Rewriting the load command opts the app into the current design.
9
+ * Must run before codesigning; vtool invalidates the signature.
10
+ */
11
+
12
+ import { existsSync, renameSync } from "node:fs";
13
+ import { $ } from "bun";
14
+
15
+ /** Matches the LSMinimumSystemVersion mirin writes into Info.plist. */
16
+ const MIN_OS = "13.0";
17
+ /** The design-gate threshold; the number is declarative, no SDK needs to exist locally. */
18
+ const SDK = "26.0";
19
+
20
+ /** Best-effort: a missing vtool (bare machines) keeps Bun's original header. */
21
+ export async function stampMacSdk(hostExe: string): Promise<void> {
22
+ if (process.platform !== "darwin") return;
23
+ if (!Bun.which("vtool")) {
24
+ console.warn("[mirin] vtool not found — the app keeps the compatibility window design.");
25
+ return;
26
+ }
27
+ const out = `${hostExe}.sdkstamp`;
28
+ const res =
29
+ await $`vtool -set-build-version macos ${MIN_OS} ${SDK} -replace -output ${out} ${hostExe}`
30
+ .nothrow()
31
+ .quiet();
32
+ if (res.exitCode !== 0 || !existsSync(out)) {
33
+ console.warn(`[mirin] vtool failed (exit ${res.exitCode}) — keeping Bun's SDK header.`);
34
+ return;
35
+ }
36
+ renameSync(out, hostExe);
37
+ await $`chmod +x ${hostExe}`.quiet();
38
+ }
@@ -48,6 +48,8 @@ export interface WinBundleOptions {
48
48
  versionJson?: string; // serialized version.json → resources/version.json (updater)
49
49
  sidecars?: { name: string; src: string }[]; // → resources/sidecars/<name>
50
50
  workers?: Record<string, string>; // name → abs path → resources/workers/<name>.js
51
+ /** Extra resource directories (name -> abs dir) copied verbatim -> resources/<name>. */
52
+ extra?: { name: string; src: string }[];
51
53
  };
52
54
  }
53
55
 
@@ -94,6 +96,9 @@ export async function buildWindowsBundle(
94
96
  cpSync(src, join(workersDir, `${name}.js`));
95
97
  }
96
98
  }
99
+ for (const extra of opts.resources?.extra ?? []) {
100
+ cpSync(extra.src, join(res, extra.name), { recursive: true });
101
+ }
97
102
  if (opts.resources.sidecars?.length) {
98
103
  const sidecarsDir = join(res, "sidecars");
99
104
  mkdirSync(sidecarsDir, { recursive: true });
package/src/dev.ts CHANGED
@@ -23,13 +23,13 @@ import {
23
23
  resolveLinuxDesktopIconPng,
24
24
  resolveLinuxIconPng,
25
25
  } from "./bundle/linux/index.ts";
26
- import { buildAppBundle } from "./bundle/macos/index.ts";
26
+ import { buildAppBundle, stampMacSdk } from "./bundle/macos/index.ts";
27
27
  import { buildWindowsBundle } from "./bundle/windows/index.ts";
28
28
 
29
29
  const IS_WINDOWS = process.platform === "win32";
30
30
  const IS_LINUX = process.platform === "linux";
31
31
 
32
- import { compileWorkers, normalizeSidecars } from "./extras.ts";
32
+ import { compileWorkers, normalizeResources, normalizeSidecars } from "./extras.ts";
33
33
  import { resolveIconFallback, resolveIconSource } from "./icons/sources.ts";
34
34
  import { createReporter } from "./shared/report.ts";
35
35
  import { DevSession } from "./shared/session.ts";
@@ -104,6 +104,7 @@ export async function dev(projectDir = process.cwd(), options?: DevOptions): Pro
104
104
  projectDir,
105
105
  ),
106
106
  );
107
+ await stampMacSdk(hostExe);
107
108
  await reporter.build(
108
109
  $`bun build ${mainEntry} --target=bun --outfile ${workerJs}`.cwd(projectDir),
109
110
  );
@@ -246,6 +247,10 @@ export async function dev(projectDir = process.cwd(), options?: DevOptions): Pro
246
247
  id: bundleId,
247
248
  windows: config.windows,
248
249
  devtools: config.devtools,
250
+ // Dev: resources resolve straight to their source directories.
251
+ resources: Object.fromEntries(
252
+ normalizeResources(projectDir, config.resources).map((r) => [r.name, r.src]),
253
+ ),
249
254
  }),
250
255
  // dev: true → enables inspect-element AND gives this run its own `-dev`
251
256
  // CEF cache dir, so `mirin dev` can run alongside the installed app.
package/src/extras.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * place so the two CLI paths stay in sync.
6
6
  */
7
7
 
8
- import { mkdirSync } from "node:fs";
8
+ import { mkdirSync, statSync } from "node:fs";
9
9
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
10
10
  import { $ } from "bun";
11
11
  import { createReporter, type Reporter } from "./shared/report.ts";
@@ -13,6 +13,13 @@ import { createReporter, type Reporter } from "./shared/report.ts";
13
13
  /** Config shapes (mirrors packages/mirin/src/config/index.ts; CLI can't import the runtime). */
14
14
  export type SidecarConfig = Record<string, string | { bin: string; entitlements?: string[] }>;
15
15
  export type WorkersConfig = Record<string, string>;
16
+ export type ResourcesConfig = Record<string, string>;
17
+
18
+ /** A resource directory resolved to an absolute source path. */
19
+ export interface NormalizedResource {
20
+ name: string;
21
+ src: string;
22
+ }
16
23
 
17
24
  /** A sidecar resolved to an absolute source path + its requested entitlements. */
18
25
  export interface NormalizedSidecar {
@@ -53,6 +60,25 @@ function projectRelativePath(projectDir: string, file: string, label: string): s
53
60
  return full;
54
61
  }
55
62
 
63
+ /** Resolve `config.resources` to absolute source directories. */
64
+ export function normalizeResources(
65
+ projectDir: string,
66
+ resources: ResourcesConfig | undefined,
67
+ ): NormalizedResource[] {
68
+ return Object.entries(resources ?? {}).map(([name, dir]) => {
69
+ const safeName = safeExtraAssetName(name, "resource name");
70
+ const src = projectRelativePath(projectDir, dir, `resource "${safeName}" path`);
71
+ let isDirectory = false;
72
+ try {
73
+ isDirectory = statSync(src).isDirectory();
74
+ } catch {
75
+ isDirectory = false;
76
+ }
77
+ if (!isDirectory) throw new Error(`[mirin] resource "${safeName}" is not a directory: ${dir}`);
78
+ return { name: safeName, src };
79
+ });
80
+ }
81
+
56
82
  /** Resolve `config.sidecars` to absolute source paths. */
57
83
  export function normalizeSidecars(
58
84
  projectDir: string,