@mirinjs/cli 0.0.1-alpha.10 → 0.0.1-alpha.12
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 +4 -3
- package/src/build.ts +40 -4
- package/src/bundle.ts +13 -4
- package/src/index.ts +8 -1
- package/src/release.ts +131 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirinjs/cli",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.12",
|
|
4
4
|
"description": "CLI for mirin apps: dev, build, init.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,10 +27,11 @@
|
|
|
27
27
|
"bun": ">=1.2.0"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"create-mirinjs": "0.0.1-alpha.
|
|
30
|
+
"create-mirinjs": "0.0.1-alpha.12",
|
|
31
|
+
"mirinjs": "0.0.1-alpha.12"
|
|
31
32
|
},
|
|
32
33
|
"optionalDependencies": {
|
|
33
|
-
"@mirinjs/darwin-arm64": "0.0.1-alpha.
|
|
34
|
+
"@mirinjs/darwin-arm64": "0.0.1-alpha.12"
|
|
34
35
|
},
|
|
35
36
|
"publishConfig": {
|
|
36
37
|
"access": "public"
|
package/src/build.ts
CHANGED
|
@@ -16,13 +16,39 @@
|
|
|
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
|
|
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
|
+
/** libmirin_core path (for the updater codec at release time). */
|
|
37
|
+
coreDylib: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Read the project's package.json version (the single source of app version). */
|
|
41
|
+
function appVersion(projectDir: string): string {
|
|
42
|
+
const pkgPath = join(projectDir, "package.json");
|
|
43
|
+
if (!existsSync(pkgPath)) return "0.0.0";
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(readFileSync(pkgPath, "utf8")).version ?? "0.0.0";
|
|
46
|
+
} catch {
|
|
47
|
+
return "0.0.0";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function build(projectDir = process.cwd()): Promise<BuildResult> {
|
|
26
52
|
const outDir = join(projectDir, "build");
|
|
27
53
|
const work = join(projectDir, ".mirin");
|
|
28
54
|
mkdirSync(outDir, { recursive: true });
|
|
@@ -33,8 +59,11 @@ export async function build(projectDir = process.cwd()): Promise<number> {
|
|
|
33
59
|
const appName: string = config.name ?? "Mirin App";
|
|
34
60
|
const bundleId: string = config.id ?? "dev.mirin.app";
|
|
35
61
|
const mainEntry = join(projectDir, config.main ?? "main/main.ts");
|
|
62
|
+
const version = appVersion(projectDir);
|
|
63
|
+
const channel: string = config.release?.channel ?? "stable";
|
|
64
|
+
const baseUrl: string | undefined = config.release?.baseUrl;
|
|
36
65
|
|
|
37
|
-
console.log(`[mirin build] ${appName}`);
|
|
66
|
+
console.log(`[mirin build] ${appName} ${version}`);
|
|
38
67
|
|
|
39
68
|
// 1. production UI
|
|
40
69
|
console.log("[mirin build] vite build…");
|
|
@@ -53,6 +82,11 @@ export async function build(projectDir = process.cwd()): Promise<number> {
|
|
|
53
82
|
// 5. assemble + sign
|
|
54
83
|
console.log("[mirin build] assembling .app…");
|
|
55
84
|
rmSync(join(outDir, `${appName}.app`), { recursive: true, force: true });
|
|
85
|
+
// version.json embeds the running app's update identity (read by app.updater).
|
|
86
|
+
// Only when `release` is configured — otherwise the app has no updater.
|
|
87
|
+
const versionJson = baseUrl
|
|
88
|
+
? JSON.stringify({ version, channel, baseUrl, name: appName, identifier: bundleId })
|
|
89
|
+
: undefined;
|
|
56
90
|
const { app } = await buildAppBundle({
|
|
57
91
|
appName,
|
|
58
92
|
bundleId,
|
|
@@ -61,16 +95,18 @@ export async function build(projectDir = process.cwd()): Promise<number> {
|
|
|
61
95
|
coreDylib: artifacts.coreDylib,
|
|
62
96
|
helperBin: artifacts.helperBin,
|
|
63
97
|
cefPath: artifacts.cefPath,
|
|
98
|
+
version,
|
|
64
99
|
icon: config.icon ? join(projectDir, config.icon) : undefined,
|
|
65
100
|
signIdentity: process.env.MIRIN_SIGN_IDENTITY,
|
|
66
101
|
resources: {
|
|
67
102
|
uiDir: join(projectDir, "dist"),
|
|
68
103
|
workerJs,
|
|
69
104
|
manifestJson: JSON.stringify({ windows: config.windows }),
|
|
105
|
+
versionJson,
|
|
70
106
|
},
|
|
71
107
|
});
|
|
72
108
|
|
|
73
109
|
console.log(`\n[mirin build] done → ${app}`);
|
|
74
110
|
console.log(` open "${app}"`);
|
|
75
|
-
return
|
|
111
|
+
return { app, appName, bundleId, version, channel, baseUrl, coreDylib: artifacts.coreDylib };
|
|
76
112
|
}
|
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:
|
|
149
|
-
CFBundleVersion:
|
|
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:
|
|
193
|
-
CFBundleVersion:
|
|
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
|
-
|
|
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,131 @@
|
|
|
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.zst the full bundle (fallback)
|
|
7
|
+
* {channel}-{platform}-{arch}-{prevVersion}.patch delta from the previous release
|
|
8
|
+
*
|
|
9
|
+
* The bundle is a zstd-compressed tar of the whole signed `.app`. Identity is the
|
|
10
|
+
* SHA-256 of the *uncompressed* tar (`tarHash`), so a delta patch can reconstruct
|
|
11
|
+
* it exactly. When the previous release is reachable at `baseUrl`, a bsdiff patch
|
|
12
|
+
* (prev → this) is generated; the app applies it instead of re-downloading the
|
|
13
|
+
* whole bundle, falling back to the full bundle whenever a patch isn't usable.
|
|
14
|
+
*
|
|
15
|
+
* Names are flat (no folders) so they upload as-is to GitHub Releases, S3/R2, or
|
|
16
|
+
* any static host. Channels coexist because the channel is part of every name.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { $ } from "bun";
|
|
20
|
+
import { mkdirSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { build } from "./build.ts";
|
|
24
|
+
import { loadCodec } from "mirinjs/codec";
|
|
25
|
+
|
|
26
|
+
const sha256File = (path: string) =>
|
|
27
|
+
new Bun.CryptoHasher("sha256").update(readFileSync(path)).digest("hex");
|
|
28
|
+
|
|
29
|
+
export async function release(projectDir = process.cwd()): Promise<number> {
|
|
30
|
+
const result = await build(projectDir);
|
|
31
|
+
if (!result.baseUrl) {
|
|
32
|
+
console.error("[mirin release] no `release.baseUrl` in mirin.config.ts — nothing to publish.");
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const platform = "darwin";
|
|
37
|
+
const arch = process.arch; // "arm64" | "x64"
|
|
38
|
+
const prefix = `${result.channel}-${platform}-${arch}`;
|
|
39
|
+
const safeName = result.appName.replace(/[^A-Za-z0-9._-]/g, "");
|
|
40
|
+
const base = result.baseUrl.replace(/\/$/, "");
|
|
41
|
+
|
|
42
|
+
const buildDir = join(projectDir, "build");
|
|
43
|
+
const outDir = join(buildDir, "release");
|
|
44
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
45
|
+
mkdirSync(outDir, { recursive: true });
|
|
46
|
+
|
|
47
|
+
// Optional notarize + staple (Developer ID) before packing, when configured.
|
|
48
|
+
const apple = process.env.MIRIN_NOTARY_APPLE_ID;
|
|
49
|
+
const pw = process.env.MIRIN_NOTARY_PASSWORD;
|
|
50
|
+
const team = process.env.MIRIN_NOTARY_TEAM_ID;
|
|
51
|
+
if (apple && pw && team) {
|
|
52
|
+
console.log("[mirin release] notarizing (this can take a few minutes)…");
|
|
53
|
+
const zip = join(buildDir, "_notarize.zip");
|
|
54
|
+
await $`ditto -c -k --keepParent ${result.app} ${zip}`;
|
|
55
|
+
await $`xcrun notarytool submit ${zip} --apple-id ${apple} --password ${pw} --team-id ${team} --wait`;
|
|
56
|
+
await $`xcrun stapler staple ${result.app}`;
|
|
57
|
+
rmSync(zip, { force: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const codec = loadCodec(result.coreDylib);
|
|
61
|
+
|
|
62
|
+
// Uncompressed tar — the identity + diff/patch basis (BSD tar keeps symlinks).
|
|
63
|
+
const newTar = join(outDir, "_new.tar");
|
|
64
|
+
await $`tar -cf ${newTar} -C ${buildDir} ${`${result.appName}.app`}`;
|
|
65
|
+
const tarHash = sha256File(newTar);
|
|
66
|
+
|
|
67
|
+
// Full bundle: zstd(newTar).
|
|
68
|
+
const bundleName = `${prefix}-${safeName}.app.tar.zst`;
|
|
69
|
+
const bundlePath = join(outDir, bundleName);
|
|
70
|
+
console.log(`[mirin release] compressing → ${bundleName}`);
|
|
71
|
+
codec.compress(newTar, bundlePath, 19);
|
|
72
|
+
const bundleSha = sha256File(bundlePath);
|
|
73
|
+
const bundleSize = readFileSync(bundlePath).byteLength;
|
|
74
|
+
|
|
75
|
+
// Delta patch vs the previous release (if reachable). Best-effort.
|
|
76
|
+
const patches: Array<{ fromVersion: string; url: string; sha256: string; size: number }> = [];
|
|
77
|
+
try {
|
|
78
|
+
const prevRes = await fetch(`${base}/${prefix}-update.json?t=${Date.now()}`, { redirect: "follow" });
|
|
79
|
+
if (prevRes.ok) {
|
|
80
|
+
const prev = (await prevRes.json()) as { version: string; bundle?: { url: string } };
|
|
81
|
+
if (prev.version && prev.version !== result.version && prev.bundle?.url) {
|
|
82
|
+
console.log(`[mirin release] generating delta ${prev.version} → ${result.version}…`);
|
|
83
|
+
const tmp = join(tmpdir(), `mirin-release-${Date.now()}`);
|
|
84
|
+
mkdirSync(tmp, { recursive: true });
|
|
85
|
+
const prevZst = join(tmp, "prev.tar.zst");
|
|
86
|
+
const dl = await fetch(`${base}/${prev.bundle.url}`, { redirect: "follow" });
|
|
87
|
+
if (!dl.ok || !dl.body) throw new Error(`prev bundle ${dl.status}`);
|
|
88
|
+
await Bun.write(prevZst, await dl.arrayBuffer());
|
|
89
|
+
const prevTar = join(tmp, "prev.tar");
|
|
90
|
+
codec.decompress(prevZst, prevTar);
|
|
91
|
+
const rawPatch = join(tmp, "patch.bin");
|
|
92
|
+
codec.diff(prevTar, newTar, rawPatch); // bsdiff
|
|
93
|
+
const patchName = `${prefix}-${prev.version}.patch`;
|
|
94
|
+
const patchPath = join(outDir, patchName);
|
|
95
|
+
codec.compress(rawPatch, patchPath, 19);
|
|
96
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
97
|
+
patches.push({
|
|
98
|
+
fromVersion: prev.version,
|
|
99
|
+
url: patchName,
|
|
100
|
+
sha256: sha256File(patchPath),
|
|
101
|
+
size: readFileSync(patchPath).byteLength,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.warn(`[mirin release] skipping delta patch: ${e instanceof Error ? e.message : e}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
rmSync(newTar, { force: true });
|
|
110
|
+
|
|
111
|
+
const manifest = {
|
|
112
|
+
version: result.version,
|
|
113
|
+
channel: result.channel,
|
|
114
|
+
platform,
|
|
115
|
+
arch,
|
|
116
|
+
tarHash,
|
|
117
|
+
bundle: { url: bundleName, sha256: bundleSha, size: bundleSize },
|
|
118
|
+
patches,
|
|
119
|
+
};
|
|
120
|
+
const manifestName = `${prefix}-update.json`;
|
|
121
|
+
await Bun.write(join(outDir, manifestName), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
122
|
+
|
|
123
|
+
const mb = (n: number) => (n / 1e6).toFixed(1);
|
|
124
|
+
console.log(`\n[mirin release] done → build/release/`);
|
|
125
|
+
console.log(` ${manifestName}`);
|
|
126
|
+
console.log(` ${bundleName} (${mb(bundleSize)} MB)`);
|
|
127
|
+
for (const p of patches) console.log(` ${p.url} (${mb(p.size)} MB delta from ${p.fromVersion})`);
|
|
128
|
+
console.log(`\nUpload all of build/release/ to: ${result.baseUrl}`);
|
|
129
|
+
if (existsSync(join(outDir, "_new.tar"))) rmSync(join(outDir, "_new.tar"), { force: true });
|
|
130
|
+
return 0;
|
|
131
|
+
}
|