@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 +5 -5
- package/src/build.ts +48 -2
- package/src/dmg.ts +29 -1
- package/src/release.ts +26 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirinjs/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
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.
|
|
31
|
-
"mirinjs": "0.1.0-alpha.
|
|
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.
|
|
35
|
-
"@mirinjs/win32-x64": "0.1.0-alpha.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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);
|