authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-02-19 01:35:50-08:00
committergravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-02-19 01:35:50-08:00
logc89798ecb12d0cbda6c69edb3b8675636c93f271
tree9f1f269836b14c6446fedf9f59cb2354487ee73b
parenta32701ef8d6565875d126115389839abaea72c21
signature Commit is signed but in an unrecognized format.

fix naming with annoying hacks


6 files changed, 212 insertions(+), 10 deletions(-)

apps/desktop/.gitignore+1
......@@ -1 +1,2 @@
11dist-electron/
2.electron-runtime/
apps/desktop/package.json+1-1
......@@ -9,7 +9,7 @@
99 "dev:bundle": "tsdown --watch",
1010 "dev:electron": "bun run scripts/dev-electron.mjs",
1111 "build": "tsdown",
12 "start": "electron dist-electron/main.js",
12 "start": "bun run scripts/start-electron.mjs",
1313 "typecheck": "tsc --noEmit",
1414 "test": "vitest run --passWithNoTests",
1515 "smoke-test": "node scripts/smoke-test.mjs"
apps/desktop/scripts/dev-electron.mjs+7-9
......@@ -1,7 +1,8 @@
1import { spawn } from "node:child_process";
2
1import electronmon from "electronmon";
32import waitOn from "wait-on";
43
4import { desktopDir, resolveElectronPath } from "./electron-launcher.mjs";
5
56const port = Number(process.env.ELECTRON_RENDERER_PORT ?? 5173);
67const devServerUrl = `http://localhost:${port}`;
78
......@@ -14,18 +15,15 @@ await waitOn({
1415 ],
1516});
1617
17const command = process.platform === "win32" ? "electronmon.cmd" : "electronmon";
1818const childEnv = { ...process.env };
1919delete childEnv.ELECTRON_RUN_AS_NODE;
2020
21const child = spawn(command, ["dist-electron/main.js"], {
22 stdio: "inherit",
21await electronmon({
22 cwd: desktopDir,
23 args: ["dist-electron/main.js"],
2324 env: {
2425 ...childEnv,
2526 VITE_DEV_SERVER_URL: devServerUrl,
2627 },
27});
28
29child.on("exit", (code) => {
30 process.exit(code ?? 0);
28 electronPath: resolveElectronPath(),
3129});
apps/desktop/scripts/electron-launcher.mjs created+172
......@@ -0,0 +1,172 @@
1// This file mostly exists because we want dev mode to say "T3 Code" instead of "electron"
2
3import { spawnSync } from "node:child_process";
4import {
5 copyFileSync,
6 cpSync,
7 existsSync,
8 mkdirSync,
9 readFileSync,
10 readdirSync,
11 rmSync,
12 statSync,
13 writeFileSync,
14} from "node:fs";
15import { createRequire } from "node:module";
16import { dirname, join, resolve } from "node:path";
17import { fileURLToPath } from "node:url";
18
19const APP_DISPLAY_NAME = "T3 Code";
20const APP_BUNDLE_ID = "com.t3tools.t3code";
21const LAUNCHER_VERSION = 1;
22
23const __dirname = dirname(fileURLToPath(import.meta.url));
24export const desktopDir = resolve(__dirname, "..");
25
26function setPlistString(plistPath, key, value) {
27 const replaceResult = spawnSync(
28 "plutil",
29 ["-replace", key, "-string", value, plistPath],
30 {
31 encoding: "utf8",
32 },
33 );
34 if (replaceResult.status === 0) {
35 return;
36 }
37
38 const insertResult = spawnSync(
39 "plutil",
40 ["-insert", key, "-string", value, plistPath],
41 {
42 encoding: "utf8",
43 },
44 );
45 if (insertResult.status === 0) {
46 return;
47 }
48
49 const details = [replaceResult.stderr, insertResult.stderr]
50 .filter(Boolean)
51 .join("\n");
52 throw new Error(
53 `Failed to update plist key "${key}" at ${plistPath}: ${details}`.trim(),
54 );
55}
56
57function patchMainBundleInfoPlist(appBundlePath, iconPath) {
58 const infoPlistPath = join(appBundlePath, "Contents", "Info.plist");
59 setPlistString(infoPlistPath, "CFBundleDisplayName", APP_DISPLAY_NAME);
60 setPlistString(infoPlistPath, "CFBundleName", APP_DISPLAY_NAME);
61 setPlistString(infoPlistPath, "CFBundleIdentifier", APP_BUNDLE_ID);
62 setPlistString(infoPlistPath, "CFBundleIconFile", "icon.icns");
63
64 const resourcesDir = join(appBundlePath, "Contents", "Resources");
65 copyFileSync(iconPath, join(resourcesDir, "icon.icns"));
66 copyFileSync(iconPath, join(resourcesDir, "electron.icns"));
67}
68
69function patchHelperBundleInfoPlists(appBundlePath) {
70 const frameworksDir = join(appBundlePath, "Contents", "Frameworks");
71 if (!existsSync(frameworksDir)) {
72 return;
73 }
74
75 for (const entry of readdirSync(frameworksDir, { withFileTypes: true })) {
76 if (!entry.isDirectory() || !entry.name.endsWith(".app")) {
77 continue;
78 }
79 if (!entry.name.startsWith("Electron Helper")) {
80 continue;
81 }
82
83 const helperPlistPath = join(
84 frameworksDir,
85 entry.name,
86 "Contents",
87 "Info.plist",
88 );
89 if (!existsSync(helperPlistPath)) {
90 continue;
91 }
92
93 const suffix = entry.name
94 .replace("Electron Helper", "")
95 .replace(".app", "")
96 .trim();
97 const helperName = suffix
98 ? `${APP_DISPLAY_NAME} Helper ${suffix}`
99 : `${APP_DISPLAY_NAME} Helper`;
100 const helperIdSuffix = suffix
101 .replace(/[()]/g, "")
102 .trim()
103 .toLowerCase()
104 .replace(/\s+/g, "-");
105 const helperBundleId = helperIdSuffix
106 ? `${APP_BUNDLE_ID}.helper.${helperIdSuffix}`
107 : `${APP_BUNDLE_ID}.helper`;
108
109 setPlistString(helperPlistPath, "CFBundleDisplayName", helperName);
110 setPlistString(helperPlistPath, "CFBundleName", helperName);
111 setPlistString(helperPlistPath, "CFBundleIdentifier", helperBundleId);
112 }
113}
114
115function readJson(path) {
116 try {
117 return JSON.parse(readFileSync(path, "utf8"));
118 } catch {
119 return null;
120 }
121}
122
123function buildMacLauncher(electronBinaryPath) {
124 const sourceAppBundlePath = resolve(electronBinaryPath, "../../..");
125 const runtimeDir = join(desktopDir, ".electron-runtime");
126 const targetAppBundlePath = join(runtimeDir, `${APP_DISPLAY_NAME}.app`);
127 const targetBinaryPath = join(
128 targetAppBundlePath,
129 "Contents",
130 "MacOS",
131 "Electron",
132 );
133 const iconPath = join(desktopDir, "resources", "icon.icns");
134 const metadataPath = join(runtimeDir, "metadata.json");
135
136 mkdirSync(runtimeDir, { recursive: true });
137
138 const expectedMetadata = {
139 launcherVersion: LAUNCHER_VERSION,
140 sourceAppBundlePath,
141 sourceAppMtimeMs: statSync(sourceAppBundlePath).mtimeMs,
142 iconMtimeMs: statSync(iconPath).mtimeMs,
143 };
144
145 const currentMetadata = readJson(metadataPath);
146 if (
147 existsSync(targetBinaryPath) &&
148 currentMetadata &&
149 JSON.stringify(currentMetadata) === JSON.stringify(expectedMetadata)
150 ) {
151 return targetBinaryPath;
152 }
153
154 rmSync(targetAppBundlePath, { recursive: true, force: true });
155 cpSync(sourceAppBundlePath, targetAppBundlePath, { recursive: true });
156 patchMainBundleInfoPlist(targetAppBundlePath, iconPath);
157 patchHelperBundleInfoPlists(targetAppBundlePath);
158 writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);
159
160 return targetBinaryPath;
161}
162
163export function resolveElectronPath() {
164 const require = createRequire(import.meta.url);
165 const electronBinaryPath = require("electron");
166
167 if (process.platform !== "darwin") {
168 return electronBinaryPath;
169 }
170
171 return buildMacLauncher(electronBinaryPath);
172}
apps/desktop/scripts/start-electron.mjs created+20
......@@ -0,0 +1,20 @@
1import { spawn } from "node:child_process";
2
3import { desktopDir, resolveElectronPath } from "./electron-launcher.mjs";
4
5const childEnv = { ...process.env };
6delete childEnv.ELECTRON_RUN_AS_NODE;
7
8const child = spawn(resolveElectronPath(), ["dist-electron/main.js"], {
9 stdio: "inherit",
10 cwd: desktopDir,
11 env: childEnv,
12});
13
14child.on("exit", (code, signal) => {
15 if (signal) {
16 process.kill(process.pid, signal);
17 return;
18 }
19 process.exit(code ?? 0);
20});
apps/desktop/src/main.ts+11
......@@ -55,6 +55,9 @@ function resolveIconPath(ext: "ico" | "icns" | "png"): string | null {
5555
5656function configureAppIdentity(): void {
5757 app.setName(APP_DISPLAY_NAME);
58 app.setAboutPanelOptions({
59 applicationName: APP_DISPLAY_NAME,
60 });
5861
5962 if (process.platform === "win32") {
6063 app.setAppUserModelId(APP_USER_MODEL_ID);
......@@ -269,6 +272,13 @@ function createWindow(): BrowserWindow {
269272 });
270273
271274 window.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
275 window.on("page-title-updated", (event) => {
276 event.preventDefault();
277 window.setTitle(APP_DISPLAY_NAME);
278 });
279 window.webContents.on("did-finish-load", () => {
280 window.setTitle(APP_DISPLAY_NAME);
281 });
272282 window.once("ready-to-show", () => {
273283 window.show();
274284 });
......@@ -311,6 +321,7 @@ app.on("before-quit", () => {
311321});
312322
313323app.whenReady().then(() => {
324 configureAppIdentity();
314325 void bootstrap();
315326
316327 app.on("activate", () => {