// This file mostly exists because we want dev mode to say "T3 Code (Dev)" instead of "electron" import { spawnSync } from "node:child_process";import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync,} from "node:fs";import { createRequire } from "node:module";import { dirname, join, resolve } from "node:path";import { fileURLToPath } from "node:url";import { ensureElectronRuntime } from "./ensure-electron-runtime.mjs"; const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";const APP_BUNDLE_ID = isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code";const LAUNCHER_VERSION = 2; const __dirname = dirname(fileURLToPath(import.meta.url));export const desktopDir = resolve(__dirname, "..");const repoRoot = resolve(desktopDir, "..", "..");const defaultIconPath = join(desktopDir, "resources", "icon.icns");const developmentMacIconPngPath = join(repoRoot, "assets", "dev", "blueprint-macos-1024.png"); function setPlistString(plistPath, key, value) { const replaceResult = spawnSync("plutil", ["-replace", key, "-string", value, plistPath], { encoding: "utf8", }); if (replaceResult.status === 0) { return; } const insertResult = spawnSync("plutil", ["-insert", key, "-string", value, plistPath], { encoding: "utf8", }); if (insertResult.status === 0) { return; } const details = [replaceResult.stderr, insertResult.stderr].filter(Boolean).join("\n"); throw new Error(`Failed to update plist key "${key}" at ${plistPath}: ${details}`.trim());} function runChecked(command, args) { const result = spawnSync(command, args, { encoding: "utf8" }); if (result.status === 0) { return; } const details = [result.stdout, result.stderr].filter(Boolean).join("\n"); throw new Error(`Failed to run ${command} ${args.join(" ")}: ${details}`.trim());} function ensureDevelopmentIconIcns(runtimeDir) { const generatedIconPath = join(runtimeDir, "icon-dev.icns"); mkdirSync(runtimeDir, { recursive: true }); if (!existsSync(developmentMacIconPngPath)) { return defaultIconPath; } const sourceMtimeMs = statSync(developmentMacIconPngPath).mtimeMs; if (existsSync(generatedIconPath) && statSync(generatedIconPath).mtimeMs >= sourceMtimeMs) { return generatedIconPath; } const iconsetRoot = mkdtempSync(join(runtimeDir, "dev-iconset-")); const iconsetDir = join(iconsetRoot, "icon.iconset"); mkdirSync(iconsetDir, { recursive: true }); try { for (const size of [16, 32, 128, 256, 512]) { runChecked("sips", [ "-z", String(size), String(size), developmentMacIconPngPath, "--out", join(iconsetDir, `icon_${size}x${size}.png`), ]); const retinaSize = size * 2; runChecked("sips", [ "-z", String(retinaSize), String(retinaSize), developmentMacIconPngPath, "--out", join(iconsetDir, `icon_${size}x${size}@2x.png`), ]); } runChecked("iconutil", ["-c", "icns", iconsetDir, "-o", generatedIconPath]); return generatedIconPath; } catch (error) { console.warn( "[desktop-launcher] Failed to generate dev macOS icon, falling back to default icon.", error, ); return defaultIconPath; } finally { rmSync(iconsetRoot, { recursive: true, force: true }); }} function patchMainBundleInfoPlist(appBundlePath, iconPath) { const infoPlistPath = join(appBundlePath, "Contents", "Info.plist"); setPlistString(infoPlistPath, "CFBundleDisplayName", APP_DISPLAY_NAME); setPlistString(infoPlistPath, "CFBundleName", APP_DISPLAY_NAME); setPlistString(infoPlistPath, "CFBundleIdentifier", APP_BUNDLE_ID); setPlistString(infoPlistPath, "CFBundleIconFile", "icon.icns"); const resourcesDir = join(appBundlePath, "Contents", "Resources"); copyFileSync(iconPath, join(resourcesDir, "icon.icns")); copyFileSync(iconPath, join(resourcesDir, "electron.icns"));} function readJson(path) { try { return JSON.parse(readFileSync(path, "utf8")); } catch { return null; }} function buildMacLauncher(electronBinaryPath) { const sourceAppBundlePath = resolve(dirname(electronBinaryPath), "../.."); const runtimeDir = join(desktopDir, ".electron-runtime"); const targetAppBundlePath = join(runtimeDir, `${APP_DISPLAY_NAME}.app`); const targetBinaryPath = join(targetAppBundlePath, "Contents", "MacOS", "Electron"); const iconPath = isDevelopment ? ensureDevelopmentIconIcns(runtimeDir) : defaultIconPath; const metadataPath = join(runtimeDir, "metadata.json"); mkdirSync(runtimeDir, { recursive: true }); const expectedMetadata = { launcherVersion: LAUNCHER_VERSION, sourceAppBundlePath, sourceAppMtimeMs: statSync(sourceAppBundlePath).mtimeMs, iconMtimeMs: statSync(iconPath).mtimeMs, }; const currentMetadata = readJson(metadataPath); if ( existsSync(targetBinaryPath) && currentMetadata && JSON.stringify(currentMetadata) === JSON.stringify(expectedMetadata) ) { return targetBinaryPath; } rmSync(targetAppBundlePath, { recursive: true, force: true }); cpSync(sourceAppBundlePath, targetAppBundlePath, { recursive: true }); patchMainBundleInfoPlist(targetAppBundlePath, iconPath); writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`); return targetBinaryPath;} export function resolveElectronPath() { ensureElectronRuntime(); const require = createRequire(import.meta.url); const electronBinaryPath = require("electron"); if (process.platform !== "darwin") { return electronBinaryPath; } // Dev launches do not need a renamed app bundle badly enough to risk breaking // Electron helper resource lookup on macOS. if (isDevelopment) { return electronBinaryPath; } return buildMacLauncher(electronBinaryPath);}
// This file mostly exists because we want dev mode to say "T3 Code (Dev)" instead of "electron"
import { spawnSync } from "node:child_process";
import {
copyFileSync,
cpSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { ensureElectronRuntime } from "./ensure-electron-runtime.mjs";
const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";
const APP_BUNDLE_ID = isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code";
const LAUNCHER_VERSION = 2;
const __dirname = dirname(fileURLToPath(import.meta.url));
export const desktopDir = resolve(__dirname, "..");
const repoRoot = resolve(desktopDir, "..", "..");
const defaultIconPath = join(desktopDir, "resources", "icon.icns");
const developmentMacIconPngPath = join(repoRoot, "assets", "dev", "blueprint-macos-1024.png");
function setPlistString(plistPath, key, value) {
const replaceResult = spawnSync("plutil", ["-replace", key, "-string", value, plistPath], {
encoding: "utf8",
});
if (replaceResult.status === 0) {
return;
}
const insertResult = spawnSync("plutil", ["-insert", key, "-string", value, plistPath], {
encoding: "utf8",
});
if (insertResult.status === 0) {
return;
}
const details = [replaceResult.stderr, insertResult.stderr].filter(Boolean).join("\n");
throw new Error(`Failed to update plist key "${key}" at ${plistPath}: ${details}`.trim());
}
function runChecked(command, args) {
const result = spawnSync(command, args, { encoding: "utf8" });
if (result.status === 0) {
return;
}
const details = [result.stdout, result.stderr].filter(Boolean).join("\n");
throw new Error(`Failed to run ${command} ${args.join(" ")}: ${details}`.trim());
}
function ensureDevelopmentIconIcns(runtimeDir) {
const generatedIconPath = join(runtimeDir, "icon-dev.icns");
mkdirSync(runtimeDir, { recursive: true });
if (!existsSync(developmentMacIconPngPath)) {
return defaultIconPath;
}
const sourceMtimeMs = statSync(developmentMacIconPngPath).mtimeMs;
if (existsSync(generatedIconPath) && statSync(generatedIconPath).mtimeMs >= sourceMtimeMs) {
return generatedIconPath;
}
const iconsetRoot = mkdtempSync(join(runtimeDir, "dev-iconset-"));
const iconsetDir = join(iconsetRoot, "icon.iconset");
mkdirSync(iconsetDir, { recursive: true });
try {
for (const size of [16, 32, 128, 256, 512]) {
runChecked("sips", [
"-z",
String(size),
String(size),
developmentMacIconPngPath,
"--out",
join(iconsetDir, `icon_${size}x${size}.png`),
]);
const retinaSize = size * 2;
runChecked("sips", [
"-z",
String(retinaSize),
String(retinaSize),
developmentMacIconPngPath,
"--out",
join(iconsetDir, `icon_${size}x${size}@2x.png`),
]);
}
runChecked("iconutil", ["-c", "icns", iconsetDir, "-o", generatedIconPath]);
return generatedIconPath;
} catch (error) {
console.warn(
"[desktop-launcher] Failed to generate dev macOS icon, falling back to default icon.",
error,
);
return defaultIconPath;
} finally {
rmSync(iconsetRoot, { recursive: true, force: true });
}
}
function patchMainBundleInfoPlist(appBundlePath, iconPath) {
const infoPlistPath = join(appBundlePath, "Contents", "Info.plist");
setPlistString(infoPlistPath, "CFBundleDisplayName", APP_DISPLAY_NAME);
setPlistString(infoPlistPath, "CFBundleName", APP_DISPLAY_NAME);
setPlistString(infoPlistPath, "CFBundleIdentifier", APP_BUNDLE_ID);
setPlistString(infoPlistPath, "CFBundleIconFile", "icon.icns");
const resourcesDir = join(appBundlePath, "Contents", "Resources");
copyFileSync(iconPath, join(resourcesDir, "icon.icns"));
copyFileSync(iconPath, join(resourcesDir, "electron.icns"));
}
function readJson(path) {
try {
return JSON.parse(readFileSync(path, "utf8"));
} catch {
return null;
}
}
function buildMacLauncher(electronBinaryPath) {
const sourceAppBundlePath = resolve(dirname(electronBinaryPath), "../..");
const runtimeDir = join(desktopDir, ".electron-runtime");
const targetAppBundlePath = join(runtimeDir, `${APP_DISPLAY_NAME}.app`);
const targetBinaryPath = join(targetAppBundlePath, "Contents", "MacOS", "Electron");
const iconPath = isDevelopment ? ensureDevelopmentIconIcns(runtimeDir) : defaultIconPath;
const metadataPath = join(runtimeDir, "metadata.json");
mkdirSync(runtimeDir, { recursive: true });
const expectedMetadata = {
launcherVersion: LAUNCHER_VERSION,
sourceAppBundlePath,
sourceAppMtimeMs: statSync(sourceAppBundlePath).mtimeMs,
iconMtimeMs: statSync(iconPath).mtimeMs,
};
const currentMetadata = readJson(metadataPath);
if (
existsSync(targetBinaryPath) &&
currentMetadata &&
JSON.stringify(currentMetadata) === JSON.stringify(expectedMetadata)
) {
return targetBinaryPath;
}
rmSync(targetAppBundlePath, { recursive: true, force: true });
cpSync(sourceAppBundlePath, targetAppBundlePath, { recursive: true });
patchMainBundleInfoPlist(targetAppBundlePath, iconPath);
writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);
return targetBinaryPath;
}
export function resolveElectronPath() {
ensureElectronRuntime();
const require = createRequire(import.meta.url);
const electronBinaryPath = require("electron");
if (process.platform !== "darwin") {
return electronBinaryPath;
}
// Dev launches do not need a renamed app bundle badly enough to risk breaking
// Electron helper resource lookup on macOS.
if (isDevelopment) {
return electronBinaryPath;
}
return buildMacLauncher(electronBinaryPath);
}