@mirinjs/cli 0.0.1-alpha.10 → 0.0.1-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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mirinjs/cli",
3
- "version": "0.0.1-alpha.10",
3
+ "version": "0.0.1-alpha.11",
4
4
  "description": "CLI for mirin apps: dev, build, init.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,10 +27,10 @@
27
27
  "bun": ">=1.2.0"
28
28
  },
29
29
  "dependencies": {
30
- "create-mirinjs": "0.0.1-alpha.10"
30
+ "create-mirinjs": "0.0.1-alpha.11"
31
31
  },
32
32
  "optionalDependencies": {
33
- "@mirinjs/darwin-arm64": "0.0.1-alpha.10"
33
+ "@mirinjs/darwin-arm64": "0.0.1-alpha.11"
34
34
  },
35
35
  "publishConfig": {
36
36
  "access": "public"
package/src/build.ts CHANGED
@@ -16,13 +16,37 @@
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
22
  import { resolveArtifacts } from "./artifacts.ts";
23
23
  import { sweepBuildTemps } from "./temps.ts";
24
24
 
25
- export async function build(projectDir = process.cwd()): Promise<number> {
25
+ export interface BuildResult {
26
+ /** Path to the assembled .app. */
27
+ app: string;
28
+ appName: string;
29
+ bundleId: string;
30
+ /** App version (from the project's package.json). */
31
+ version: string;
32
+ /** Update channel (config.release.channel ?? "stable"). */
33
+ channel: string;
34
+ /** Update baseUrl, if `release` is configured. */
35
+ baseUrl?: string;
36
+ }
37
+
38
+ /** Read the project's package.json version (the single source of app version). */
39
+ function appVersion(projectDir: string): string {
40
+ const pkgPath = join(projectDir, "package.json");
41
+ if (!existsSync(pkgPath)) return "0.0.0";
42
+ try {
43
+ return JSON.parse(readFileSync(pkgPath, "utf8")).version ?? "0.0.0";
44
+ } catch {
45
+ return "0.0.0";
46
+ }
47
+ }
48
+
49
+ export async function build(projectDir = process.cwd()): Promise<BuildResult> {
26
50
  const outDir = join(projectDir, "build");
27
51
  const work = join(projectDir, ".mirin");
28
52
  mkdirSync(outDir, { recursive: true });
@@ -33,8 +57,11 @@ export async function build(projectDir = process.cwd()): Promise<number> {
33
57
  const appName: string = config.name ?? "Mirin App";
34
58
  const bundleId: string = config.id ?? "dev.mirin.app";
35
59
  const mainEntry = join(projectDir, config.main ?? "main/main.ts");
60
+ const version = appVersion(projectDir);
61
+ const channel: string = config.release?.channel ?? "stable";
62
+ const baseUrl: string | undefined = config.release?.baseUrl;
36
63
 
37
- console.log(`[mirin build] ${appName}`);
64
+ console.log(`[mirin build] ${appName} ${version}`);
38
65
 
39
66
  // 1. production UI
40
67
  console.log("[mirin build] vite build…");
@@ -53,6 +80,11 @@ export async function build(projectDir = process.cwd()): Promise<number> {
53
80
  // 5. assemble + sign
54
81
  console.log("[mirin build] assembling .app…");
55
82
  rmSync(join(outDir, `${appName}.app`), { recursive: true, force: true });
83
+ // version.json embeds the running app's update identity (read by app.updater).
84
+ // Only when `release` is configured — otherwise the app has no updater.
85
+ const versionJson = baseUrl
86
+ ? JSON.stringify({ version, channel, baseUrl, name: appName, identifier: bundleId })
87
+ : undefined;
56
88
  const { app } = await buildAppBundle({
57
89
  appName,
58
90
  bundleId,
@@ -61,16 +93,18 @@ export async function build(projectDir = process.cwd()): Promise<number> {
61
93
  coreDylib: artifacts.coreDylib,
62
94
  helperBin: artifacts.helperBin,
63
95
  cefPath: artifacts.cefPath,
96
+ version,
64
97
  icon: config.icon ? join(projectDir, config.icon) : undefined,
65
98
  signIdentity: process.env.MIRIN_SIGN_IDENTITY,
66
99
  resources: {
67
100
  uiDir: join(projectDir, "dist"),
68
101
  workerJs,
69
102
  manifestJson: JSON.stringify({ windows: config.windows }),
103
+ versionJson,
70
104
  },
71
105
  });
72
106
 
73
107
  console.log(`\n[mirin build] done → ${app}`);
74
108
  console.log(` open "${app}"`);
75
- return 0;
109
+ return { app, appName, bundleId, version, channel, baseUrl };
76
110
  }
package/src/bundle.ts CHANGED
@@ -33,6 +33,8 @@ 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. */
@@ -42,6 +44,7 @@ export interface BundleOptions {
42
44
  uiDir?: string; // Vite dist/, copied to Resources/ui (served via app://ui)
43
45
  workerJs?: string; // bundled main-process Worker entry -> Resources/worker.js
44
46
  manifestJson?: string; // serialized manifest -> Resources/mirin.manifest.json
47
+ versionJson?: string; // serialized version.json -> Resources/version.json (updater)
45
48
  };
46
49
  }
47
50
 
@@ -136,6 +139,7 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
136
139
  // Render the icon (if any) into Resources before writing the plist, so we
137
140
  // only set CFBundleIconFile when an icon was actually produced.
138
141
  const iconFile = opts.icon ? await writeIcon(opts.icon, join(contents, "Resources")) : undefined;
142
+ const version = opts.version ?? "0.0.1";
139
143
 
140
144
  const info: Record<string, PlistValue> = {
141
145
  CFBundleDevelopmentRegion: "en",
@@ -145,8 +149,8 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
145
149
  CFBundleInfoDictionaryVersion: "6.0",
146
150
  CFBundleName: appName,
147
151
  CFBundlePackageType: "APPL",
148
- CFBundleShortVersionString: "0.0.1",
149
- CFBundleVersion: "0.0.1",
152
+ CFBundleShortVersionString: version,
153
+ CFBundleVersion: version,
150
154
  LSMinimumSystemVersion: "13.0",
151
155
  NSHighResolutionCapable: true,
152
156
  NSSupportsAutomaticGraphicsSwitching: true,
@@ -173,6 +177,11 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
173
177
  if (opts.resources?.manifestJson != null) {
174
178
  writeFileSync(join(resources, "mirin.manifest.json"), opts.resources.manifestJson);
175
179
  }
180
+ // version.json is the running app's self-knowledge for the updater. Written
181
+ // before the codesign loop below so the signature covers it.
182
+ if (opts.resources?.versionJson != null) {
183
+ writeFileSync(join(resources, "version.json"), opts.resources.versionJson);
184
+ }
176
185
 
177
186
  for (const { suffix, id } of HELPER_TYPES) {
178
187
  const name = `${appName} Helper${suffix}`;
@@ -189,8 +198,8 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
189
198
  CFBundleInfoDictionaryVersion: "6.0",
190
199
  CFBundleName: name,
191
200
  CFBundlePackageType: "APPL",
192
- CFBundleShortVersionString: "0.0.1",
193
- CFBundleVersion: "0.0.1",
201
+ CFBundleShortVersionString: version,
202
+ CFBundleVersion: version,
194
203
  LSMinimumSystemVersion: "13.0",
195
204
  LSUIElement: "1",
196
205
  NSHighResolutionCapable: true,
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import { resolve } from "node:path";
7
7
  import { dev } from "./dev.ts";
8
8
  import { build } from "./build.ts";
9
+ import { release } from "./release.ts";
9
10
  import { scaffold } from "create-mirinjs";
10
11
 
11
12
  const [command, arg] = Bun.argv.slice(2);
@@ -15,6 +16,7 @@ const USAGE = `mirin — build desktop apps with Bun + Chromium
15
16
  Usage:
16
17
  mirin dev run the app against the Vite dev server (HMR + typed RPC)
17
18
  mirin build package a standalone, signed .app (output: ./build)
19
+ mirin release build + emit update artifacts (output: ./build/release)
18
20
  mirin init [dir] scaffold a new app
19
21
  `;
20
22
 
@@ -24,7 +26,12 @@ switch (command) {
24
26
  break;
25
27
  }
26
28
  case "build": {
27
- process.exit(await build());
29
+ await build();
30
+ process.exit(0);
31
+ break;
32
+ }
33
+ case "release": {
34
+ process.exit(await release());
28
35
  break;
29
36
  }
30
37
  case "init": {
package/src/release.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `mirin release` — build the app and emit flat-named update artifacts.
3
+ *
4
+ * Produces, under `build/release/`:
5
+ * {channel}-{platform}-{arch}-update.json the manifest the app polls
6
+ * {channel}-{platform}-{arch}-{Name}.app.tar.gz the full bundle
7
+ *
8
+ * These names are flat (no folders) so they upload as-is to GitHub Releases,
9
+ * S3/R2, or any static host — whatever `release.baseUrl` points at. Channels
10
+ * coexist at the same baseUrl because the channel is part of every filename.
11
+ *
12
+ * The app's `app.updater` polls `{baseUrl}/{prefix}-update.json`, compares the
13
+ * advertised version to its own, downloads `url`, verifies `sha256`, then swaps
14
+ * the whole `.app` and relaunches. (A signed/notarized `.app` must be replaced
15
+ * whole — never modified in place — so the artifact is the entire bundle.)
16
+ */
17
+
18
+ import { $ } from "bun";
19
+ import { mkdirSync, rmSync, readFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { build } from "./build.ts";
22
+
23
+ export async function release(projectDir = process.cwd()): Promise<number> {
24
+ const result = await build(projectDir);
25
+ if (!result.baseUrl) {
26
+ console.error(
27
+ "[mirin release] no `release.baseUrl` in mirin.config.ts — nothing to publish.",
28
+ );
29
+ return 1;
30
+ }
31
+
32
+ const platform = "darwin";
33
+ const arch = process.arch; // "arm64" | "x64"
34
+ const prefix = `${result.channel}-${platform}-${arch}`;
35
+ // Sanitize the app name for a URL-safe artifact filename (spaces break hosts).
36
+ const safeName = result.appName.replace(/[^A-Za-z0-9._-]/g, "");
37
+
38
+ const buildDir = join(projectDir, "build");
39
+ const outDir = join(buildDir, "release");
40
+ rmSync(outDir, { recursive: true, force: true });
41
+ mkdirSync(outDir, { recursive: true });
42
+
43
+ // Notarize + staple before packing (so the shipped bundle passes Gatekeeper on
44
+ // end-user machines), when notary credentials are present in the environment.
45
+ const apple = process.env.MIRIN_NOTARY_APPLE_ID;
46
+ const pw = process.env.MIRIN_NOTARY_PASSWORD;
47
+ const team = process.env.MIRIN_NOTARY_TEAM_ID;
48
+ if (apple && pw && team) {
49
+ console.log("[mirin release] notarizing (this can take a few minutes)…");
50
+ const zip = join(buildDir, "_notarize.zip");
51
+ await $`ditto -c -k --keepParent ${result.app} ${zip}`;
52
+ await $`xcrun notarytool submit ${zip} --apple-id ${apple} --password ${pw} --team-id ${team} --wait`;
53
+ await $`xcrun stapler staple ${result.app}`;
54
+ rmSync(zip, { force: true });
55
+ }
56
+
57
+ const tarName = `${prefix}-${safeName}.app.tar.gz`;
58
+ const tarPath = join(outDir, tarName);
59
+
60
+ console.log(`[mirin release] packing ${result.appName}.app → ${tarName}`);
61
+ // BSD tar preserves the CEF framework's symlinks + executable bits.
62
+ await $`tar -czf ${tarPath} -C ${buildDir} ${`${result.appName}.app`}`;
63
+
64
+ const bytes = readFileSync(tarPath);
65
+ const sha256 = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
66
+
67
+ const manifest = {
68
+ version: result.version,
69
+ channel: result.channel,
70
+ platform,
71
+ arch,
72
+ url: tarName,
73
+ sha256,
74
+ size: bytes.byteLength,
75
+ };
76
+ const manifestName = `${prefix}-update.json`;
77
+ await Bun.write(join(outDir, manifestName), `${JSON.stringify(manifest, null, 2)}\n`);
78
+
79
+ const mb = (bytes.byteLength / 1e6).toFixed(1);
80
+ console.log(`\n[mirin release] done → build/release/`);
81
+ console.log(` ${manifestName}`);
82
+ console.log(` ${tarName} (${mb} MB)`);
83
+ console.log(`\nUpload both to: ${result.baseUrl}`);
84
+ return 0;
85
+ }