Files
edgeever/scripts/desktop-icns.mjs
T
tianma-if 2d784297ff fix(desktop): ship complete macOS ICNS so Dock never blanks
Prebuild a full iconutil ICNS (including 1024) instead of converting a lone
512 PWA PNG, validate Dock-critical types at package verify time, and set the
Dock icon at runtime as a Launch Services cache fallback.
2026-08-05 19:22:00 +08:00

117 lines
3.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* macOS ICNS inspection helpers.
*
* Dock and Launch Services need a complete multi-resolution icon pack. A single
* 512 PNG converted by electron-builder often omits the 1024 (ic10) slot and
* can leave Dock tiles stuck on the generic "empty" placeholder after installs.
*/
/** @typedef {{ type: string; length: number; offset: number }} IcnsEntry */
/**
* Types that a modern EdgeEver macOS app icon must include so Finder and Dock
* can pick an appropriate bitmap at every scale.
*
* Note: current `iconutil` emits `ic04`/`ic05` for the smallest slots, while
* older electron-builder packs used `icp4`/`icp5`/`icp6`. Either family is OK
* for the small sizes; the high-resolution set below is mandatory.
*/
export const REQUIRED_MAC_ICNS_TYPES = Object.freeze([
"ic07", // 128
"ic08", // 256
"ic09", // 512
"ic10", // 1024 — required for modern Dock / retina large tiles
"ic11", // 16@2x
"ic12", // 32@2x
"ic13", // 128@2x
"ic14", // 256@2x
]);
/** At least one type from each group must be present. */
export const REQUIRED_MAC_ICNS_ANY_OF = Object.freeze([
Object.freeze(["icp4", "ic04"]), // ~16px family
Object.freeze(["icp5", "ic05", "icp6"]), // ~32–64px family
]);
/**
* Parse the top-level type table of an ICNS container.
* @param {Buffer | Uint8Array} data
* @returns {IcnsEntry[]}
*/
export const parseIcnsEntries = (data) => {
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
if (buffer.length < 8 || buffer.subarray(0, 4).toString("ascii") !== "icns") {
throw new Error("Not a valid ICNS file (missing icns magic).");
}
const declaredLength = buffer.readUInt32BE(4);
if (declaredLength !== buffer.length) {
throw new Error(
`ICNS length mismatch: header claims ${declaredLength} bytes, file has ${buffer.length}.`,
);
}
/** @type {IcnsEntry[]} */
const entries = [];
let offset = 8;
while (offset + 8 <= buffer.length) {
const type = buffer.subarray(offset, offset + 4).toString("ascii");
const length = buffer.readUInt32BE(offset + 4);
if (length < 8 || offset + length > buffer.length) {
throw new Error(`Invalid ICNS entry at offset ${offset} (type=${JSON.stringify(type)}, length=${length}).`);
}
entries.push({ type, length, offset });
offset += length;
}
if (offset !== buffer.length) {
throw new Error(`ICNS trailing bytes: parsed ${offset}, file ${buffer.length}.`);
}
return entries;
};
/**
* @param {Buffer | Uint8Array} data
* @returns {{ types: string[]; missing: string[]; missingGroups: string[][]; ok: boolean }}
*/
export const inspectMacIcns = (data) => {
const entries = parseIcnsEntries(data);
const types = entries.map((entry) => entry.type);
const present = new Set(types);
const missing = REQUIRED_MAC_ICNS_TYPES.filter((type) => !present.has(type));
const missingGroups = REQUIRED_MAC_ICNS_ANY_OF.filter(
(group) => !group.some((type) => present.has(type)),
);
return {
types,
missing,
missingGroups,
ok: missing.length === 0 && missingGroups.length === 0,
};
};
/**
* @param {Buffer | Uint8Array} data
* @param {string} [label]
*/
export const assertMacIcnsComplete = (data, label = "icon.icns") => {
const result = inspectMacIcns(data);
if (!result.ok) {
const parts = [];
if (result.missing.length > 0) {
parts.push(`types ${result.missing.join(", ")}`);
}
if (result.missingGroups.length > 0) {
parts.push(
`small-size groups ${result.missingGroups.map((group) => group.join("|")).join("; ")}`,
);
}
throw new Error(
`${label} is missing Dock-critical ICNS ${parts.join(" and ")}. ` +
`Regenerate with: bun run prepare:desktop:icons`,
);
}
return result;
};