@mirinjs/cli 0.1.0-alpha.19 → 0.1.0-alpha.20
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 +8 -8
- package/src/build.ts +9 -9
- package/src/bundle/macos/app.ts +48 -17
- package/src/icons/macos/appearance.ts +96 -0
- package/src/icons/macos/index.ts +1 -0
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.20",
|
|
4
4
|
"description": "CLI for mirin apps: dev, build, init.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,15 +27,15 @@
|
|
|
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.20",
|
|
31
|
+
"mirinjs": "0.1.0-alpha.20"
|
|
32
32
|
},
|
|
33
33
|
"optionalDependencies": {
|
|
34
|
-
"@mirinjs/darwin-arm64": "0.1.0-alpha.
|
|
35
|
-
"@mirinjs/win32-x64": "0.1.0-alpha.
|
|
36
|
-
"@mirinjs/win32-arm64": "0.1.0-alpha.
|
|
37
|
-
"@mirinjs/linux-arm64": "0.1.0-alpha.
|
|
38
|
-
"@mirinjs/linux-x64": "0.1.0-alpha.
|
|
34
|
+
"@mirinjs/darwin-arm64": "0.1.0-alpha.20",
|
|
35
|
+
"@mirinjs/win32-x64": "0.1.0-alpha.20",
|
|
36
|
+
"@mirinjs/win32-arm64": "0.1.0-alpha.20",
|
|
37
|
+
"@mirinjs/linux-arm64": "0.1.0-alpha.20",
|
|
38
|
+
"@mirinjs/linux-x64": "0.1.0-alpha.20"
|
|
39
39
|
},
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
package/src/build.ts
CHANGED
|
@@ -66,10 +66,10 @@ function patchPeToGuiSubsystem(exePath: string): void {
|
|
|
66
66
|
async function brandWindowsExe(
|
|
67
67
|
exe: string,
|
|
68
68
|
opts: {
|
|
69
|
-
projectDir: string;
|
|
70
69
|
work: string;
|
|
71
70
|
appName: string;
|
|
72
71
|
version: string;
|
|
72
|
+
/** Absolute path to the icon source. */
|
|
73
73
|
icon?: string;
|
|
74
74
|
publisher: string;
|
|
75
75
|
},
|
|
@@ -83,7 +83,7 @@ async function brandWindowsExe(
|
|
|
83
83
|
}
|
|
84
84
|
const args: string[] = [];
|
|
85
85
|
if (opts.icon) {
|
|
86
|
-
const ico = makeWindowsIcon(
|
|
86
|
+
const ico = makeWindowsIcon(opts.icon, join(opts.work, "exe-icon.ico"), {
|
|
87
87
|
onlyLargest: true,
|
|
88
88
|
});
|
|
89
89
|
if (ico) args.push("--set-icon", ico);
|
|
@@ -194,6 +194,7 @@ export async function build(
|
|
|
194
194
|
const linux: boolean | import("mirinjs").LinuxConfig = config.linux ?? true;
|
|
195
195
|
const publisher: string = config.publisher ?? appName;
|
|
196
196
|
const cefLocales = normalizeCefLocales(config.cef?.locales);
|
|
197
|
+
const appIconPath: string | undefined = config.icon ? join(projectDir, config.icon) : undefined;
|
|
197
198
|
|
|
198
199
|
console.log(`[mirin build] ${appName} ${version}`);
|
|
199
200
|
|
|
@@ -221,11 +222,10 @@ export async function build(
|
|
|
221
222
|
if (IS_WINDOWS) {
|
|
222
223
|
patchPeToGuiSubsystem(hostExe);
|
|
223
224
|
await brandWindowsExe(hostExe, {
|
|
224
|
-
projectDir,
|
|
225
225
|
work,
|
|
226
226
|
appName,
|
|
227
227
|
version,
|
|
228
|
-
icon:
|
|
228
|
+
icon: appIconPath,
|
|
229
229
|
publisher,
|
|
230
230
|
});
|
|
231
231
|
}
|
|
@@ -266,7 +266,7 @@ export async function build(
|
|
|
266
266
|
helperExe: artifacts.helperBin,
|
|
267
267
|
cefPath: artifacts.cefPath,
|
|
268
268
|
cefLocales,
|
|
269
|
-
icon:
|
|
269
|
+
icon: appIconPath,
|
|
270
270
|
resources: { ...resources, sidecars: sidecars.map((s) => ({ name: s.name, src: s.src })) },
|
|
271
271
|
})
|
|
272
272
|
: IS_LINUX
|
|
@@ -278,7 +278,7 @@ export async function build(
|
|
|
278
278
|
helperExe: artifacts.helperBin,
|
|
279
279
|
cefPath: artifacts.cefPath,
|
|
280
280
|
cefLocales,
|
|
281
|
-
icon:
|
|
281
|
+
icon: appIconPath,
|
|
282
282
|
resources: {
|
|
283
283
|
...resources,
|
|
284
284
|
sidecars: sidecars.map((s) => ({ name: s.name, src: s.src })),
|
|
@@ -294,7 +294,7 @@ export async function build(
|
|
|
294
294
|
cefPath: artifacts.cefPath,
|
|
295
295
|
cefLocales,
|
|
296
296
|
version,
|
|
297
|
-
icon:
|
|
297
|
+
icon: appIconPath,
|
|
298
298
|
signIdentity,
|
|
299
299
|
urlSchemes: config.urlSchemes,
|
|
300
300
|
resources: { ...resources, sidecars },
|
|
@@ -316,7 +316,7 @@ export async function build(
|
|
|
316
316
|
publisher,
|
|
317
317
|
outDir,
|
|
318
318
|
projectDir,
|
|
319
|
-
icon:
|
|
319
|
+
icon: appIconPath,
|
|
320
320
|
options: typeof linux === "object" ? linux : {},
|
|
321
321
|
formats,
|
|
322
322
|
});
|
|
@@ -340,7 +340,7 @@ export async function build(
|
|
|
340
340
|
inno,
|
|
341
341
|
linux,
|
|
342
342
|
publisher,
|
|
343
|
-
icon:
|
|
343
|
+
icon: appIconPath,
|
|
344
344
|
signIdentity,
|
|
345
345
|
};
|
|
346
346
|
}
|
package/src/bundle/macos/app.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "node:fs";
|
|
23
23
|
import { join } from "node:path";
|
|
24
24
|
import { $ } from "bun";
|
|
25
|
+
import { isIconComposerDoc, writeAppearanceCatalog } from "../../icons/macos/index.ts";
|
|
25
26
|
import { pruneMacCefLocales } from "../shared/cef-locales.ts";
|
|
26
27
|
|
|
27
28
|
const FRAMEWORK = "Chromium Embedded Framework.framework";
|
|
@@ -46,7 +47,10 @@ export interface BundleOptions {
|
|
|
46
47
|
cefLocales?: string[];
|
|
47
48
|
/** App version → Info.plist CFBundleShortVersionString/CFBundleVersion. */
|
|
48
49
|
version?: string;
|
|
49
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* App icon source: a .icns, a .iconset dir, a square .png, or an Icon Composer
|
|
52
|
+
* .icon document (the only form carrying appearance variants). Optional.
|
|
53
|
+
*/
|
|
50
54
|
icon?: string;
|
|
51
55
|
/** Codesign identity; "-" (default) is ad-hoc. Set to a Developer ID to ship. */
|
|
52
56
|
signIdentity?: string;
|
|
@@ -109,19 +113,8 @@ const ICONSET_RENDITIONS = [
|
|
|
109
113
|
{ name: "icon_512x512@2x.png", px: 1024 },
|
|
110
114
|
];
|
|
111
115
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
* stem ("icon"), or undefined if there's no usable source. Accepts a `.icns`
|
|
115
|
-
* (copied), a `.iconset` directory (iconutil), or a square `.png` (rendered to
|
|
116
|
-
* a full iconset via sips, then iconutil).
|
|
117
|
-
*/
|
|
118
|
-
async function writeIcon(iconSrc: string, resources: string): Promise<string | undefined> {
|
|
119
|
-
if (!existsSync(iconSrc)) {
|
|
120
|
-
console.warn(`[mirin] icon not found, skipping: ${iconSrc}`);
|
|
121
|
-
return undefined;
|
|
122
|
-
}
|
|
123
|
-
const icns = join(resources, "icon.icns");
|
|
124
|
-
|
|
116
|
+
/** Render one icon source to `Resources/icon.icns`. */
|
|
117
|
+
async function writeIcns(iconSrc: string, icns: string, resources: string): Promise<void> {
|
|
125
118
|
if (iconSrc.endsWith(".icns")) {
|
|
126
119
|
cpSync(iconSrc, icns);
|
|
127
120
|
} else if (iconSrc.endsWith(".iconset")) {
|
|
@@ -136,7 +129,38 @@ async function writeIcon(iconSrc: string, resources: string): Promise<string | u
|
|
|
136
129
|
await $`iconutil -c icns ${iconset} -o ${icns}`.quiet();
|
|
137
130
|
rmSync(iconset, { recursive: true, force: true });
|
|
138
131
|
}
|
|
139
|
-
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The Info.plist keys an icon contributes, empty when there's no usable source. */
|
|
135
|
+
interface IconKeys {
|
|
136
|
+
/** CFBundleIconFile — `Resources/icon.icns`, read by macOS 25 and earlier. */
|
|
137
|
+
iconFile?: string;
|
|
138
|
+
/** CFBundleIconName — `Resources/Assets.car`, preferred on macOS 26+. */
|
|
139
|
+
iconName?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Render the app icon into Resources. An `.icon` document compiles to an
|
|
144
|
+
* `Assets.car` (macOS 26+ light/dark/tinted) and contributes the `.icns` actool
|
|
145
|
+
* derives from it; every other source takes the plain iconutil path.
|
|
146
|
+
*/
|
|
147
|
+
async function writeIcon(iconSrc: string, resources: string, work: string): Promise<IconKeys> {
|
|
148
|
+
if (!existsSync(iconSrc)) {
|
|
149
|
+
console.warn(`[mirin] icon not found, skipping: ${iconSrc}`);
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const icns = join(resources, "icon.icns");
|
|
154
|
+
if (isIconComposerDoc(iconSrc)) {
|
|
155
|
+
// An Icon Composer document can't go through iconutil, so the only .icns
|
|
156
|
+
// available for it is the one actool derives.
|
|
157
|
+
const catalog = await writeAppearanceCatalog(iconSrc, resources, work);
|
|
158
|
+
if (catalog?.icns) cpSync(catalog.icns, icns);
|
|
159
|
+
return { iconFile: existsSync(icns) ? "icon" : undefined, iconName: catalog?.name };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await writeIcns(iconSrc, icns, resources);
|
|
163
|
+
return { iconFile: "icon" };
|
|
140
164
|
}
|
|
141
165
|
|
|
142
166
|
type PlistValue = string | boolean | PlistValue[] | { [k: string]: PlistValue };
|
|
@@ -188,8 +212,12 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
|
|
|
188
212
|
cpSync(opts.coreDylib, join(macos, "libmirin_core.dylib"));
|
|
189
213
|
|
|
190
214
|
// Render the icon (if any) into Resources before writing the plist, so we
|
|
191
|
-
// only set
|
|
192
|
-
const
|
|
215
|
+
// only set the icon keys when an icon was actually produced.
|
|
216
|
+
const iconWork = join(opts.outDir, ".mirin-icon");
|
|
217
|
+
const { iconFile, iconName } = opts.icon
|
|
218
|
+
? await writeIcon(opts.icon, join(contents, "Resources"), iconWork)
|
|
219
|
+
: ({} as IconKeys);
|
|
220
|
+
rmSync(iconWork, { recursive: true, force: true });
|
|
193
221
|
const version = opts.version ?? "0.0.1";
|
|
194
222
|
|
|
195
223
|
const info: Record<string, PlistValue> = {
|
|
@@ -209,6 +237,9 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
|
|
|
209
237
|
LSEnvironment: { MallocNanoZone: "0" },
|
|
210
238
|
};
|
|
211
239
|
if (iconFile) info.CFBundleIconFile = iconFile;
|
|
240
|
+
// macOS 26+ resolves the appearance-aware icon out of Assets.car via this key
|
|
241
|
+
// and falls back to CFBundleIconFile when the catalog has no match.
|
|
242
|
+
if (iconName) info.CFBundleIconName = iconName;
|
|
212
243
|
// Deep-link schemes: register this app as the macOS handler for app://-style
|
|
213
244
|
// URLs (e.g. anko://…). Delivered at runtime via app.on("open-url").
|
|
214
245
|
if (opts.urlSchemes?.length) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile macOS appearance-variant app icons (light / dark / tinted, macOS 26+).
|
|
3
|
+
*
|
|
4
|
+
* `.icns` has no notion of appearance — the Dock picks a variant out of a
|
|
5
|
+
* compiled asset catalog keyed by `CFBundleIconName`, and the only input that
|
|
6
|
+
* produces one is an Icon Composer `.icon` document. `actool` compiles it to
|
|
7
|
+
* the same `Assets.car` shape Apple's own bundled apps ship.
|
|
8
|
+
*
|
|
9
|
+
* Best-effort throughout: `actool` only ships with a full Xcode, so a machine
|
|
10
|
+
* with just Command Line Tools warns and keeps the plain `.icns`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
15
|
+
import { $ } from "bun";
|
|
16
|
+
|
|
17
|
+
/** True when the source is an Icon Composer document, which actool reads directly. */
|
|
18
|
+
export const isIconComposerDoc = (path: string): boolean => path.endsWith(".icon");
|
|
19
|
+
|
|
20
|
+
/** Asset-catalog icon name; also the `CFBundleIconName` written to Info.plist. */
|
|
21
|
+
const ICON_NAME = "AppIcon";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Locate `actool`. `xcrun` honours the selected developer dir, which is often
|
|
25
|
+
* Command Line Tools (no actool), so fall back to a full Xcode if one is
|
|
26
|
+
* installed but not selected.
|
|
27
|
+
*/
|
|
28
|
+
async function findActool(): Promise<string | undefined> {
|
|
29
|
+
const xcrun = await $`xcrun --find actool`.nothrow().quiet();
|
|
30
|
+
if (xcrun.exitCode === 0) {
|
|
31
|
+
const path = xcrun.stdout.toString().trim();
|
|
32
|
+
if (path && existsSync(path)) return path;
|
|
33
|
+
}
|
|
34
|
+
for (const app of ["/Applications/Xcode.app", "/Applications/Xcode-beta.app"]) {
|
|
35
|
+
const path = join(app, "Contents/Developer/usr/bin/actool");
|
|
36
|
+
if (existsSync(path)) return path;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AppearanceCatalog {
|
|
42
|
+
/** Goes in Info.plist as `CFBundleIconName`. */
|
|
43
|
+
name: string;
|
|
44
|
+
/** The legacy `.icns` actool derives alongside the catalog, if it emitted one. */
|
|
45
|
+
icns?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compile `icon` into `<Resources>/Assets.car`, or return undefined if the
|
|
50
|
+
* catalog could not be built (no actool, unreadable art) — callers then keep
|
|
51
|
+
* the plain `.icns` path.
|
|
52
|
+
*/
|
|
53
|
+
export async function writeAppearanceCatalog(
|
|
54
|
+
iconDoc: string,
|
|
55
|
+
resourcesDir: string,
|
|
56
|
+
workDir: string,
|
|
57
|
+
): Promise<AppearanceCatalog | undefined> {
|
|
58
|
+
// actool resolves its own arguments, so hand it absolute paths regardless of
|
|
59
|
+
// what the caller passed and of the subprocess working directory.
|
|
60
|
+
const resources = resolve(resourcesDir);
|
|
61
|
+
const work = resolve(workDir);
|
|
62
|
+
const actool = await findActool();
|
|
63
|
+
if (!actool) {
|
|
64
|
+
console.warn(
|
|
65
|
+
"[mirin] actool not found — appearance variants need a full Xcode " +
|
|
66
|
+
"(Command Line Tools alone is not enough); shipping the default icon only.",
|
|
67
|
+
);
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
rmSync(work, { recursive: true, force: true });
|
|
72
|
+
mkdirSync(work, { recursive: true });
|
|
73
|
+
|
|
74
|
+
const doc = join(work, `${ICON_NAME}.icon`);
|
|
75
|
+
cpSync(iconDoc, doc, { recursive: true });
|
|
76
|
+
|
|
77
|
+
const compiled = join(work, "compiled");
|
|
78
|
+
mkdirSync(compiled, { recursive: true });
|
|
79
|
+
const res =
|
|
80
|
+
await $`${actool} --output-format human-readable-text --notices --warnings --app-icon ${ICON_NAME} --output-partial-info-plist ${join(work, "partial.plist")} --enable-on-demand-resources NO --target-device mac --minimum-deployment-target 26.0 --platform macosx --compile ${compiled} ${doc}`
|
|
81
|
+
.nothrow()
|
|
82
|
+
.quiet();
|
|
83
|
+
|
|
84
|
+
const car = join(compiled, "Assets.car");
|
|
85
|
+
if (res.exitCode !== 0 || !existsSync(car)) {
|
|
86
|
+
console.warn(
|
|
87
|
+
`[mirin] actool failed (exit ${res.exitCode}) — shipping the default icon only.\n` +
|
|
88
|
+
res.stdout.toString().trim(),
|
|
89
|
+
);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
cpSync(car, join(resources, "Assets.car"));
|
|
94
|
+
const icns = join(compiled, `${ICON_NAME}.icns`);
|
|
95
|
+
return { name: ICON_NAME, icns: existsSync(icns) ? icns : undefined };
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { isIconComposerDoc, writeAppearanceCatalog } from "./appearance.ts";
|