diff --git a/apps/desktop/scripts/electron-launcher.mjs b/apps/desktop/scripts/electron-launcher.mjs index 1453cbe666e..4402bf15b07 100644 --- a/apps/desktop/scripts/electron-launcher.mjs +++ b/apps/desktop/scripts/electron-launcher.mjs @@ -8,6 +8,7 @@ import { mkdirSync, mkdtempSync, readFileSync, + readdirSync, rmSync, statSync, writeFileSync, @@ -19,7 +20,7 @@ import { fileURLToPath } from "node:url"; 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 LAUNCHER_VERSION = 3; const __dirname = dirname(fileURLToPath(import.meta.url)); export const desktopDir = resolve(__dirname, ".."); @@ -120,6 +121,41 @@ function patchMainBundleInfoPlist(appBundlePath, iconPath) { copyFileSync(iconPath, join(resourcesDir, "electron.icns")); } +function patchHelperBundleInfoPlists(appBundlePath) { + const frameworksDir = join(appBundlePath, "Contents", "Frameworks"); + if (!existsSync(frameworksDir)) { + return; + } + + for (const entry of readdirSync(frameworksDir, { withFileTypes: true })) { + if ( + !entry.isDirectory() || + !entry.name.startsWith("Electron Helper") || + !entry.name.endsWith(".app") + ) { + continue; + } + + const helperPlistPath = join(frameworksDir, entry.name, "Contents", "Info.plist"); + if (!existsSync(helperPlistPath)) { + continue; + } + + const suffix = entry.name.replace("Electron Helper", "").replace(".app", "").trim(); + const helperName = suffix + ? `${APP_DISPLAY_NAME} Helper ${suffix}` + : `${APP_DISPLAY_NAME} Helper`; + const helperIdSuffix = suffix.replace(/[()]/g, "").trim().toLowerCase().replace(/\s+/g, "-"); + const helperBundleId = helperIdSuffix + ? `${APP_BUNDLE_ID}.helper.${helperIdSuffix}` + : `${APP_BUNDLE_ID}.helper`; + + setPlistString(helperPlistPath, "CFBundleDisplayName", helperName); + setPlistString(helperPlistPath, "CFBundleName", helperName); + setPlistString(helperPlistPath, "CFBundleIdentifier", helperBundleId); + } +} + function readJson(path) { try { return JSON.parse(readFileSync(path, "utf8")); @@ -157,6 +193,7 @@ function buildMacLauncher(electronBinaryPath) { rmSync(targetAppBundlePath, { recursive: true, force: true }); cpSync(sourceAppBundlePath, targetAppBundlePath, { recursive: true }); patchMainBundleInfoPlist(targetAppBundlePath, iconPath); + patchHelperBundleInfoPlists(targetAppBundlePath); writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`); return targetBinaryPath; diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index 86452693d09..bf2f79b889c 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -3,6 +3,8 @@ import { assert, it } from "@effect/vitest"; import { ConfigProvider, Effect, Option } from "effect"; import { + createBuildConfig, + MAC_HELPER_BUNDLE_IDS, resolveBuildOptions, resolveDesktopBuildIconAssets, resolveDesktopProductName, @@ -37,6 +39,19 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { }); }); + it.effect("pins macOS Electron helper bundle IDs to the T3 Code app identity", () => + Effect.gen(function* () { + const buildConfig = yield* createBuildConfig("mac", "dmg", "0.0.21", false, false, undefined); + + assert.deepStrictEqual(buildConfig.mac, { + target: ["dmg", "zip"], + icon: "icon.icns", + category: "public.app-category.developer-tools", + ...MAC_HELPER_BUNDLE_IDS, + }); + }), + ); + it("falls back to the default mock update port when the configured port is blank", () => { assert.equal(resolveMockUpdateServerUrl(undefined), "http://localhost:3000"); assert.equal(resolveMockUpdateServerUrl(4123), "http://localhost:4123"); diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 74e8bed0cb8..230eadb9efa 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -63,6 +63,17 @@ const PLATFORM_CONFIG: Record = { }, }; +const MAC_APP_BUNDLE_ID = "com.t3tools.t3code"; + +export const MAC_HELPER_BUNDLE_IDS = { + helperBundleId: `${MAC_APP_BUNDLE_ID}.helper`, + helperEHBundleId: `${MAC_APP_BUNDLE_ID}.helper.eh`, + helperGPUBundleId: `${MAC_APP_BUNDLE_ID}.helper.gpu`, + helperNPBundleId: `${MAC_APP_BUNDLE_ID}.helper.np`, + helperPluginBundleId: `${MAC_APP_BUNDLE_ID}.helper.plugin`, + helperRendererBundleId: `${MAC_APP_BUNDLE_ID}.helper.renderer`, +} as const; + interface BuildCliInput { readonly platform: Option.Option; readonly target: Option.Option; @@ -558,7 +569,7 @@ export function resolveDesktopProductName(version: string): string { : (desktopPackageJson.productName ?? "T3 Code"); } -const createBuildConfig = Effect.fn("createBuildConfig")(function* ( +export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( platform: typeof BuildPlatform.Type, target: string, version: string, @@ -567,7 +578,7 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* ( mockUpdateServerPort: number | undefined, ) { const buildConfig: Record = { - appId: "com.t3tools.t3code", + appId: MAC_APP_BUNDLE_ID, productName: resolveDesktopProductName(version), artifactName: "T3-Code-${version}-${arch}.${ext}", directories: { @@ -592,6 +603,7 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* ( target: target === "dmg" ? [target, "zip"] : [target], icon: "icon.icns", category: "public.app-category.developer-tools", + ...MAC_HELPER_BUNDLE_IDS, }; }