@mirinjs/cli 0.1.0-alpha.1 → 0.1.0-alpha.3

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.1",
3
+ "version": "0.1.0-alpha.3",
4
4
  "description": "CLI for mirin apps: dev, build, init.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,12 +27,12 @@
27
27
  "bun": ">=1.2.0"
28
28
  },
29
29
  "dependencies": {
30
- "create-mirinjs": "0.1.0-alpha.1",
31
- "mirinjs": "0.1.0-alpha.1"
30
+ "create-mirinjs": "0.1.0-alpha.3",
31
+ "mirinjs": "0.1.0-alpha.3"
32
32
  },
33
33
  "optionalDependencies": {
34
- "@mirinjs/darwin-arm64": "0.1.0-alpha.1",
35
- "@mirinjs/win32-x64": "0.1.0-alpha.1"
34
+ "@mirinjs/darwin-arm64": "0.1.0-alpha.3",
35
+ "@mirinjs/win32-x64": "0.1.0-alpha.3"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"
package/src/build.ts CHANGED
@@ -16,16 +16,42 @@
16
16
  */
17
17
 
18
18
  import { $ } from "bun";
19
- import { mkdirSync, rmSync, readFileSync, existsSync } from "node:fs";
19
+ import { mkdirSync, rmSync, readFileSync, writeFileSync, existsSync } from "node:fs";
20
20
  import { join } from "node:path";
21
21
  import { buildAppBundle } from "./bundle.ts";
22
22
  import { buildWindowsBundle } from "./bundle-win.ts";
23
+ import { makeWindowsIcon } from "./icon-win.ts";
23
24
  import { resolveArtifacts } from "./artifacts.ts";
24
25
  import { sweepBuildTemps } from "./temps.ts";
25
26
  import { normalizeSidecars, compileWorkers } from "./extras.ts";
26
27
 
27
28
  const IS_WINDOWS = process.platform === "win32";
28
29
 
30
+ /** A PE FILEVERSION (`x.y.z.w`) from a semver string (pre-release suffix dropped). */
31
+ function winFileVersion(v: string): string {
32
+ const m = v.match(/^(\d+)\.(\d+)\.(\d+)/);
33
+ return m ? `${m[1]}.${m[2]}.${m[3]}.0` : "0.0.0.0";
34
+ }
35
+
36
+ /**
37
+ * Flip a PE exe from the CONSOLE subsystem (3) to GUI (2) so Windows allocates
38
+ * **no console window** when it's launched from a shortcut. Bun's
39
+ * `--windows-hide-console` only hides the console at runtime (a brief flash);
40
+ * GUI subsystem prevents it entirely — the equivalent of Electrobun's
41
+ * `exe.subsystem = .Windows`. The optional-header Subsystem field is at
42
+ * `e_lfanew + 24 (PE sig + COFF) + 68`.
43
+ */
44
+ function patchPeToGuiSubsystem(exePath: string): void {
45
+ const buf = readFileSync(exePath);
46
+ const peOff = buf.readUInt32LE(0x3c);
47
+ if (buf.readUInt32LE(peOff) !== 0x0000_4550) return; // not "PE\0\0"
48
+ const subsystemOff = peOff + 92;
49
+ if (buf.readUInt16LE(subsystemOff) === 3 /* CONSOLE */) {
50
+ buf.writeUInt16LE(2 /* GUI */, subsystemOff);
51
+ writeFileSync(exePath, buf);
52
+ }
53
+ }
54
+
29
55
  export interface BuildResult {
30
56
  /** Path to the assembled .app. */
31
57
  app: string;
@@ -91,7 +117,27 @@ export async function build(projectDir = process.cwd()): Promise<BuildResult> {
91
117
  const signIdentity = process.env.MIRIN_SIGN_IDENTITY;
92
118
  const hostExe = join(work, IS_WINDOWS ? "host-release.exe" : "host-release");
93
119
  const workerJs = join(work, "worker.release.js");
94
- await $`bun build --compile --minify ${artifacts.hostEntry} --outfile ${hostExe}`.cwd(projectDir);
120
+ const hostCmd = ["build", "--compile", "--minify", artifacts.hostEntry, "--outfile", hostExe];
121
+ if (IS_WINDOWS) {
122
+ // A GUI-subsystem exe (no console window on launch) with an embedded icon +
123
+ // version metadata — so the Start Menu / Desktop shortcut + taskbar show the
124
+ // app's icon, not a generic one (Bun ≥1.2 PE flags; mirrors Electrobun's
125
+ // GUI-subsystem + embedded-icon launcher).
126
+ const nsisCfg = typeof nsis === "object" ? nsis : {};
127
+ const winIcon = config.icon
128
+ ? makeWindowsIcon(join(projectDir, config.icon), join(work, "host-icon.ico"))
129
+ : undefined;
130
+ hostCmd.push(
131
+ "--windows-hide-console",
132
+ `--windows-title=${appName}`,
133
+ `--windows-version=${winFileVersion(version)}`,
134
+ `--windows-publisher=${nsisCfg.publisher ?? appName}`,
135
+ `--windows-description=${appName}`,
136
+ );
137
+ if (winIcon) hostCmd.push(`--windows-icon=${winIcon}`);
138
+ }
139
+ await $`bun ${hostCmd}`.cwd(projectDir);
140
+ if (IS_WINDOWS) patchPeToGuiSubsystem(hostExe);
95
141
  await $`bun build ${mainEntry} --target=bun --minify --outfile ${workerJs}`.cwd(projectDir);
96
142
 
97
143
  // Extra assets: resolve sidecar binaries + compile any extra worker entries.
package/src/dmg.ts CHANGED
@@ -93,6 +93,34 @@ export async function notarizeAndStaple(target: string): Promise<boolean> {
93
93
  return true;
94
94
  }
95
95
 
96
+ /**
97
+ * `hdiutil create` with retries. "Resource busy" is a transient CI flake — a
98
+ * Spotlight/antivirus/`fseventsd` process touches the staging folder (or a prior
99
+ * image hasn't fully detached) while hdiutil tries to read it. A short backoff
100
+ * clears it; only a non-busy error fails immediately.
101
+ */
102
+ async function hdiutilCreate(
103
+ vol: string,
104
+ staging: string,
105
+ format: string,
106
+ dmgPath: string,
107
+ ): Promise<void> {
108
+ let stderr = "";
109
+ for (let attempt = 1; attempt <= 4; attempt++) {
110
+ const res =
111
+ await $`hdiutil create -volname ${vol} -srcfolder ${staging} -ov -format ${format} ${dmgPath}`
112
+ .quiet()
113
+ .nothrow();
114
+ if (res.exitCode === 0) return;
115
+ stderr = res.stderr.toString().trim();
116
+ if (!/busy/i.test(stderr)) break; // not the transient flake — fail now
117
+ console.warn(`[mirin release] hdiutil create busy (attempt ${attempt}/4); retrying…`);
118
+ rmSync(dmgPath, { force: true });
119
+ await Bun.sleep(3000 * attempt);
120
+ }
121
+ throw new Error(`hdiutil create failed: ${stderr}`);
122
+ }
123
+
96
124
  /** Does this config ask for a styled Finder window (vs. a plain DMG)? */
97
125
  function wantsLayout(o: DmgOptions): boolean {
98
126
  return !!(o.background || o.appPosition || o.applicationsPosition || o.windowSize || o.iconSize);
@@ -131,7 +159,7 @@ export async function buildDmg(input: BuildDmgInput): Promise<string> {
131
159
  }
132
160
  }
133
161
  if (!built) {
134
- await $`hdiutil create -volname ${vol} -srcfolder ${staging} -ov -format ${format} ${dmgPath}`.quiet();
162
+ await hdiutilCreate(vol, staging, format, dmgPath);
135
163
  }
136
164
  } finally {
137
165
  rmSync(staging, { recursive: true, force: true });
package/src/release.ts CHANGED
@@ -164,20 +164,32 @@ export async function release(projectDir = process.cwd()): Promise<number> {
164
164
  installerSize = readFileSync(zipPath).byteLength;
165
165
  }
166
166
  } else if (result.dmg !== false) {
167
- installerName = `${prefix}-${safeName}.dmg`;
168
- console.log(`[mirin release] building installer → ${installerName}`);
169
- const options: DmgOptions = typeof result.dmg === "object" ? result.dmg : {};
170
- const dmgPath = await buildDmg({
171
- app: result.app,
172
- appName: result.appName,
173
- outDir,
174
- fileName: installerName,
175
- options,
176
- projectDir: result.projectDir,
177
- signIdentity: result.signIdentity,
178
- });
179
- await notarizeAndStaple(dmgPath); // no-op without notary credentials
180
- installerSize = readFileSync(dmgPath).byteLength;
167
+ // The DMG is the first-install convenience; the updater bundle + manifest (the
168
+ // critical artifacts) are already built. Never fail the whole release over a
169
+ // flaky installer step — warn loudly and ship the rest.
170
+ const dmgName = `${prefix}-${safeName}.dmg`;
171
+ console.log(`[mirin release] building installer → ${dmgName}`);
172
+ try {
173
+ const options: DmgOptions = typeof result.dmg === "object" ? result.dmg : {};
174
+ const dmgPath = await buildDmg({
175
+ app: result.app,
176
+ appName: result.appName,
177
+ outDir,
178
+ fileName: dmgName,
179
+ options,
180
+ projectDir: result.projectDir,
181
+ signIdentity: result.signIdentity,
182
+ });
183
+ await notarizeAndStaple(dmgPath); // no-op without notary credentials
184
+ installerName = dmgName;
185
+ installerSize = readFileSync(dmgPath).byteLength;
186
+ } catch (e) {
187
+ console.warn(
188
+ `\n[mirin release] ⚠️ DMG build failed: ${e instanceof Error ? e.message : e}\n` +
189
+ `[mirin release] shipping the updater bundle + manifest WITHOUT a .dmg installer.\n`,
190
+ );
191
+ rmSync(join(outDir, dmgName), { force: true });
192
+ }
181
193
  }
182
194
 
183
195
  const mb = (n: number) => (n / 1e6).toFixed(1);