@mirinjs/cli 0.0.1-alpha.0 → 0.0.1-alpha.10

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.0",
3
+ "version": "0.0.1-alpha.10",
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.0"
30
+ "create-mirinjs": "0.0.1-alpha.10"
31
31
  },
32
32
  "optionalDependencies": {
33
- "@mirinjs/darwin-arm64": "0.0.1-alpha.0"
33
+ "@mirinjs/darwin-arm64": "0.0.1-alpha.10"
34
34
  },
35
35
  "publishConfig": {
36
36
  "access": "public"
package/src/artifacts.ts CHANGED
@@ -118,13 +118,17 @@ async function ensureCef(): Promise<string> {
118
118
  const url = `https://github.com/${REPO_SLUG}/releases/download/v${version}/${asset}`;
119
119
  console.log(`[mirin] downloading CEF for ${platformTag()} (one-time, ~hundreds of MB)…`);
120
120
 
121
- const res = await fetch(url);
122
- if (!res.ok) {
123
- throw new Error(`mirin: failed to download CEF from ${url} (HTTP ${res.status}).`);
124
- }
121
+ // Download with curl, not `Bun.write(file, await fetch(url))`: the latter
122
+ // pins a core at 100% CPU and never completes on large gzip responses
123
+ // (the streaming write path). curl is fast and ubiquitous.
125
124
  const archive = join(tmpdir(), asset);
126
- await Bun.write(archive, res);
127
- await $`tar -xzf ${archive} -C ${cacheDir}`;
125
+ try {
126
+ await $`curl -fSL --retry 3 -o ${archive} ${url}`;
127
+ await $`tar -xzf ${archive} -C ${cacheDir}`;
128
+ } catch (err) {
129
+ rmSync(archive, { force: true });
130
+ throw new Error(`mirin: failed to download CEF from ${url} (${err}).`);
131
+ }
128
132
  rmSync(archive, { force: true });
129
133
  return cacheDir;
130
134
  }
package/src/build.ts CHANGED
@@ -20,12 +20,14 @@ import { mkdirSync, rmSync } 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
+ import { sweepBuildTemps } from "./temps.ts";
23
24
 
24
25
  export async function build(projectDir = process.cwd()): Promise<number> {
25
26
  const outDir = join(projectDir, "build");
26
27
  const work = join(projectDir, ".mirin");
27
28
  mkdirSync(outDir, { recursive: true });
28
29
  mkdirSync(work, { recursive: true });
30
+ sweepBuildTemps(projectDir);
29
31
 
30
32
  const config = (await import(join(projectDir, "mirin.config.ts"))).default;
31
33
  const appName: string = config.name ?? "Mirin App";
@@ -59,6 +61,7 @@ export async function build(projectDir = process.cwd()): Promise<number> {
59
61
  coreDylib: artifacts.coreDylib,
60
62
  helperBin: artifacts.helperBin,
61
63
  cefPath: artifacts.cefPath,
64
+ icon: config.icon ? join(projectDir, config.icon) : undefined,
62
65
  signIdentity: process.env.MIRIN_SIGN_IDENTITY,
63
66
  resources: {
64
67
  uiDir: join(projectDir, "dist"),
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 icon source: a .icns, a .iconset dir, or a square .png. Optional. */
37
+ icon?: string;
36
38
  /** Codesign identity; "-" (default) is ad-hoc. Set to a Developer ID to ship. */
37
39
  signIdentity?: string;
38
40
  /** Production-only resources placed under Contents/Resources. */
@@ -43,6 +45,50 @@ export interface BundleOptions {
43
45
  };
44
46
  }
45
47
 
48
+ /** The 10 standard iconset renditions (point size + @1x/@2x pixel size). */
49
+ const ICONSET_RENDITIONS = [
50
+ { name: "icon_16x16.png", px: 16 },
51
+ { name: "icon_16x16@2x.png", px: 32 },
52
+ { name: "icon_32x32.png", px: 32 },
53
+ { name: "icon_32x32@2x.png", px: 64 },
54
+ { name: "icon_128x128.png", px: 128 },
55
+ { name: "icon_128x128@2x.png", px: 256 },
56
+ { name: "icon_256x256.png", px: 256 },
57
+ { name: "icon_256x256@2x.png", px: 512 },
58
+ { name: "icon_512x512.png", px: 512 },
59
+ { name: "icon_512x512@2x.png", px: 1024 },
60
+ ];
61
+
62
+ /**
63
+ * Render the app icon to `Resources/icon.icns` and return the CFBundleIconFile
64
+ * stem ("icon"), or undefined if there's no usable source. Accepts a `.icns`
65
+ * (copied), a `.iconset` directory (iconutil), or a square `.png` (rendered to
66
+ * a full iconset via sips, then iconutil).
67
+ */
68
+ async function writeIcon(iconSrc: string, resources: string): Promise<string | undefined> {
69
+ if (!existsSync(iconSrc)) {
70
+ console.warn(`[mirin] icon not found, skipping: ${iconSrc}`);
71
+ return undefined;
72
+ }
73
+ const icns = join(resources, "icon.icns");
74
+
75
+ if (iconSrc.endsWith(".icns")) {
76
+ cpSync(iconSrc, icns);
77
+ } else if (iconSrc.endsWith(".iconset")) {
78
+ await $`iconutil -c icns ${iconSrc} -o ${icns}`.quiet();
79
+ } else {
80
+ const iconset = join(resources, "icon.iconset");
81
+ rmSync(iconset, { recursive: true, force: true });
82
+ mkdirSync(iconset, { recursive: true });
83
+ for (const { name, px } of ICONSET_RENDITIONS) {
84
+ await $`sips -z ${px} ${px} ${iconSrc} --out ${join(iconset, name)}`.quiet();
85
+ }
86
+ await $`iconutil -c icns ${iconset} -o ${icns}`.quiet();
87
+ rmSync(iconset, { recursive: true, force: true });
88
+ }
89
+ return "icon";
90
+ }
91
+
46
92
  type PlistValue = string | boolean | Record<string, string>;
47
93
 
48
94
  function plist(entries: Record<string, PlistValue>): string {
@@ -87,25 +133,29 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
87
133
  cpSync(opts.hostExe, join(macos, appName));
88
134
  cpSync(opts.coreDylib, join(macos, "libmirin_core.dylib"));
89
135
 
90
- writeFileSync(
91
- join(contents, "Info.plist"),
92
- plist({
93
- CFBundleDevelopmentRegion: "en",
94
- CFBundleDisplayName: appName,
95
- CFBundleExecutable: appName,
96
- CFBundleIdentifier: bundleId,
97
- CFBundleInfoDictionaryVersion: "6.0",
98
- CFBundleName: appName,
99
- CFBundlePackageType: "APPL",
100
- CFBundleShortVersionString: "0.0.1",
101
- CFBundleVersion: "0.0.1",
102
- LSMinimumSystemVersion: "13.0",
103
- NSHighResolutionCapable: true,
104
- NSSupportsAutomaticGraphicsSwitching: true,
105
- LSFileQuarantineEnabled: true,
106
- LSEnvironment: { MallocNanoZone: "0" },
107
- }),
108
- );
136
+ // Render the icon (if any) into Resources before writing the plist, so we
137
+ // only set CFBundleIconFile when an icon was actually produced.
138
+ const iconFile = opts.icon ? await writeIcon(opts.icon, join(contents, "Resources")) : undefined;
139
+
140
+ const info: Record<string, PlistValue> = {
141
+ CFBundleDevelopmentRegion: "en",
142
+ CFBundleDisplayName: appName,
143
+ CFBundleExecutable: appName,
144
+ CFBundleIdentifier: bundleId,
145
+ CFBundleInfoDictionaryVersion: "6.0",
146
+ CFBundleName: appName,
147
+ CFBundlePackageType: "APPL",
148
+ CFBundleShortVersionString: "0.0.1",
149
+ CFBundleVersion: "0.0.1",
150
+ LSMinimumSystemVersion: "13.0",
151
+ NSHighResolutionCapable: true,
152
+ NSSupportsAutomaticGraphicsSwitching: true,
153
+ LSFileQuarantineEnabled: true,
154
+ LSEnvironment: { MallocNanoZone: "0" },
155
+ };
156
+ if (iconFile) info.CFBundleIconFile = iconFile;
157
+
158
+ writeFileSync(join(contents, "Info.plist"), plist(info));
109
159
 
110
160
  cpSync(join(cefPath, FRAMEWORK), join(frameworks, FRAMEWORK), {
111
161
  recursive: true,
package/src/dev.ts CHANGED
@@ -13,12 +13,16 @@ import { mkdirSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { buildAppBundle } from "./bundle.ts";
15
15
  import { resolveArtifacts } from "./artifacts.ts";
16
+ import { sweepBuildTemps } from "./temps.ts";
16
17
 
17
18
  const DEV_URL = "http://localhost:5173";
18
19
 
19
20
  export async function dev(projectDir = process.cwd()): Promise<number> {
20
21
  const work = join(projectDir, ".mirin");
21
22
  mkdirSync(work, { recursive: true });
23
+ // `bun build --compile` drops temp *.bun-build files in the cwd; clear any left
24
+ // behind by a previously interrupted run so they don't pile up in the project.
25
+ sweepBuildTemps(projectDir);
22
26
 
23
27
  // --- load the manifest ---
24
28
  const config = (await import(join(projectDir, "mirin.config.ts"))).default;
@@ -46,6 +50,7 @@ export async function dev(projectDir = process.cwd()): Promise<number> {
46
50
  coreDylib: artifacts.coreDylib,
47
51
  helperBin: artifacts.helperBin,
48
52
  cefPath: artifacts.cefPath,
53
+ icon: config.icon ? join(projectDir, config.icon) : undefined,
49
54
  });
50
55
 
51
56
  // --- start Vite ---
package/src/temps.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Remove `bun build --compile` scratch files (`.*.bun-build`) from a directory.
3
+ *
4
+ * `bun build --compile` writes these temp files into the working directory and
5
+ * normally cleans them up — but a hard-killed build leaves them behind, where
6
+ * they accumulate in the project root. The dev/build commands sweep them first.
7
+ */
8
+
9
+ import { readdirSync, rmSync } from "node:fs";
10
+ import { join } from "node:path";
11
+
12
+ export function sweepBuildTemps(dir: string): void {
13
+ let entries: string[];
14
+ try {
15
+ entries = readdirSync(dir);
16
+ } catch {
17
+ return;
18
+ }
19
+ for (const name of entries) {
20
+ if (name.endsWith(".bun-build")) {
21
+ rmSync(join(dir, name), { force: true });
22
+ }
23
+ }
24
+ }