import { createRequire } from "node:module"; import { execFileSync } from "node:child_process"; import { existsSync, mkdirSync, unlinkSync, writeFileSync, } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import pngToIco from "png-to-ico"; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const require = createRequire(path.join(rootDir, "package.json")); /** Default icon path from electrobun.config.ts build.win.icon */ export const DEFAULT_WINDOWS_ICON = "assets/icon.ico"; function resolveRceditExe() { const projectRcedit = path.join(rootDir, "node_modules/rcedit/package.json"); const rceditPkgPath = existsSync(projectRcedit) ? projectRcedit : require.resolve("rcedit/package.json"); const rceditDir = path.dirname(rceditPkgPath); const rceditX64 = path.join(rceditDir, "bin", "rcedit-x64.exe"); if (existsSync(rceditX64)) { return rceditX64; } const rceditExe = path.join(rceditDir, "bin", "rcedit.exe"); if (!existsSync(rceditExe)) { throw new Error(`embed-windows-exe-icon: rcedit not found under ${rceditDir}`); } return rceditExe; } /** * Resolve the Windows app icon path per Electrobun docs: * https://framework.blackboard.sh/electrobun/apis/application-icons/ */ export function resolveWindowsIconSource(iconConfigPath = DEFAULT_WINDOWS_ICON) { const candidates = []; if (iconConfigPath) { const configured = iconConfigPath.startsWith("/") || /^[a-zA-Z]:/.test(iconConfigPath) ? iconConfigPath : path.join(rootDir, iconConfigPath); candidates.push(configured); } candidates.push( path.join(rootDir, "assets/icon.ico"), path.join(rootDir, "assets/icon.iconset/icon_256x256.png"), path.join(rootDir, "assets/icon.png"), ); const found = candidates.find((candidate) => existsSync(candidate)); if (!found) { throw new Error( "embed-windows-exe-icon: Windows icon not found. Set build.win.icon to assets/icon.ico or run scripts/prepare-app-icons.mjs", ); } return found; } async function materializeIco(iconSourcePath, workDir) { if (iconSourcePath.toLowerCase().endsWith(".ico")) { return { icoPath: iconSourcePath, temp: false }; } if (!iconSourcePath.toLowerCase().endsWith(".png")) { throw new Error( `embed-windows-exe-icon: unsupported icon format: ${iconSourcePath}`, ); } mkdirSync(workDir, { recursive: true }); const icoPath = path.join(workDir, "app-icon.ico"); const icoBuffer = await pngToIco(iconSourcePath); writeFileSync(icoPath, new Uint8Array(icoBuffer)); console.log(`embed-windows-exe-icon: converted PNG to ICO (${icoPath})`); return { icoPath, temp: true }; } /** * Embed an icon into a Windows executable using rcedit, matching Electrobun's * build.win.icon behavior (launcher, runtime, and installer executables). */ export async function embedWindowsExeIcon(exePath, options = {}) { if (process.platform !== "win32") { console.log(`embed-windows-exe-icon: skipped ${path.basename(exePath)} (not Windows)`); return; } if (!existsSync(exePath)) { throw new Error(`embed-windows-exe-icon: executable not found: ${exePath}`); } const iconSourcePath = resolveWindowsIconSource(options.icon); const workDir = options.workDir || path.join(rootDir, "build", ".windows-icon-work"); const { icoPath, temp } = await materializeIco(iconSourcePath, workDir); const rceditExe = resolveRceditExe(); const resolvedExe = path.resolve(exePath); const resolvedIcon = path.resolve(icoPath); console.log( `embed-windows-exe-icon: embedding icon into ${path.basename(resolvedExe)}`, ); try { execFileSync(rceditExe, [resolvedExe, "--set-icon", resolvedIcon], { stdio: "inherit", windowsHide: true, }); } finally { if (temp && options.cleanupTemp) { unlinkSync(icoPath); } } } /** * Embed icons into the launcher and Bun runtime executables (Electrobun docs). */ export async function embedWindowsAppIcons(exePaths, options = {}) { for (const exePath of exePaths) { await embedWindowsExeIcon(exePath, options); } } async function main() { const args = process.argv.slice(2); const exePath = args.find((arg) => !arg.startsWith("-")); if (!exePath) { console.error("embed-windows-exe-icon: usage: bun scripts/embed-windows-exe-icon.mjs [--icon path]"); process.exit(1); } const iconArgIndex = args.indexOf("--icon"); const icon = iconArgIndex >= 0 && args[iconArgIndex + 1] ? args[iconArgIndex + 1] : DEFAULT_WINDOWS_ICON; await embedWindowsExeIcon(exePath, { icon, cleanupTemp: true }); } const invokedPath = path.resolve(process.argv[1] ?? ""); const modulePath = path.resolve(fileURLToPath(import.meta.url)); if (invokedPath === modulePath) { main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); }); }