@mirinjs/cli 0.1.0-alpha.25 → 0.1.0-alpha.27

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.25",
3
+ "version": "0.1.0-alpha.27",
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.25",
31
- "mirinjs": "0.1.0-alpha.25"
30
+ "create-mirinjs": "0.1.0-alpha.27",
31
+ "mirinjs": "0.1.0-alpha.27"
32
32
  },
33
33
  "optionalDependencies": {
34
- "@mirinjs/darwin-arm64": "0.1.0-alpha.25",
35
- "@mirinjs/win32-x64": "0.1.0-alpha.25",
36
- "@mirinjs/win32-arm64": "0.1.0-alpha.25",
37
- "@mirinjs/linux-arm64": "0.1.0-alpha.25",
38
- "@mirinjs/linux-x64": "0.1.0-alpha.25"
34
+ "@mirinjs/darwin-arm64": "0.1.0-alpha.27",
35
+ "@mirinjs/win32-x64": "0.1.0-alpha.27",
36
+ "@mirinjs/win32-arm64": "0.1.0-alpha.27",
37
+ "@mirinjs/linux-arm64": "0.1.0-alpha.27",
38
+ "@mirinjs/linux-x64": "0.1.0-alpha.27"
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
  */
@@ -21,7 +23,7 @@ import { buildLinuxBundle } from "./bundle/linux/index.ts";
21
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;
@@ -209,10 +212,12 @@ export async function build(
209
212
 
210
213
  // Host compilation, main Worker bundling, and extra Worker bundling are independent.
211
214
  console.log("[mirin build] compiling host + bundling workers…");
212
- const signIdentity = process.env.MIRIN_SIGN_IDENTITY;
215
+ // An empty value (a CI job that exports it unconditionally) means ad-hoc, not an identity named "".
216
+ const signIdentity = process.env.MIRIN_SIGN_IDENTITY || undefined;
213
217
  const hostExe = join(work, IS_WINDOWS ? "host-release.exe" : "host-release");
214
218
  const workerJs = join(work, "worker.release.js");
215
219
  const sidecars = normalizeSidecars(projectDir, config.sidecars);
220
+ const extraResources = normalizeResources(projectDir, config.resources);
216
221
  const hostBuild = (async (): Promise<void> => {
217
222
  // Bun's Windows arm64 build currently has no bun:ffi/TinyCC runtime. The
218
223
  // win32-arm64 Mirin package therefore carries an x64 compatibility payload,
@@ -256,9 +261,12 @@ export async function build(
256
261
  singleInstance: config.singleInstance,
257
262
  // Lets a diagnostic build opt in via `devtools.production`; otherwise fully off.
258
263
  devtools: config.devtools,
264
+ // Production: names resolve under the bundle resources directory.
265
+ resources: Object.fromEntries(extraResources.map((r) => [r.name, r.name])),
259
266
  }),
260
267
  versionJson,
261
268
  workers: extraWorkers,
269
+ extra: extraResources,
262
270
  };
263
271
  const { app } = IS_WINDOWS
264
272
  ? 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) {
@@ -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
+ }
@@ -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
@@ -29,7 +29,7 @@ import { buildWindowsBundle } from "./bundle/windows/index.ts";
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";
@@ -247,6 +247,10 @@ export async function dev(projectDir = process.cwd(), options?: DevOptions): Pro
247
247
  id: bundleId,
248
248
  windows: config.windows,
249
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
+ ),
250
254
  }),
251
255
  // dev: true → enables inspect-element AND gives this run its own `-dev`
252
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,