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 @@...@@ -1 +1,2 @@
1dist-electron/1dist-electron/
2.electron-runtime/
apps/desktop/package.json+1-1
...@@ -9,7 +9,7 @@...@@ -9,7 +9,7 @@
9 "dev:bundle": "tsdown --watch",9 "dev:bundle": "tsdown --watch",
10 "dev:electron": "bun run scripts/dev-electron.mjs",10 "dev:electron": "bun run scripts/dev-electron.mjs",
11 "build": "tsdown",11 "build": "tsdown",
12 "start": "electron dist-electron/main.js",12 "start": "bun run scripts/start-electron.mjs",
13 "typecheck": "tsc --noEmit",13 "typecheck": "tsc --noEmit",
14 "test": "vitest run --passWithNoTests",14 "test": "vitest run --passWithNoTests",
15 "smoke-test": "node scripts/smoke-test.mjs"15 "smoke-test": "node scripts/smoke-test.mjs"
apps/desktop/scripts/dev-electron.mjs+7-9
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1import { spawn } from "node:child_process";1import electronmon from "electronmon";
2
3import waitOn from "wait-on";2import waitOn from "wait-on";
43
4import { desktopDir, resolveElectronPath } from "./electron-launcher.mjs";
5
5const port = Number(process.env.ELECTRON_RENDERER_PORT ?? 5173);6const port = Number(process.env.ELECTRON_RENDERER_PORT ?? 5173);
6const devServerUrl = `http://localhost:${port}`;7const devServerUrl = `http://localhost:${port}`;
78
...@@ -14,18 +15,15 @@ await waitOn({...@@ -14,18 +15,15 @@ await waitOn({
14 ],15 ],
15});16});
1617
17const command = process.platform === "win32" ? "electronmon.cmd" : "electronmon";
18const childEnv = { ...process.env };18const childEnv = { ...process.env };
19delete childEnv.ELECTRON_RUN_AS_NODE;19delete childEnv.ELECTRON_RUN_AS_NODE;
2020
21const child = spawn(command, ["dist-electron/main.js"], {21await electronmon({
22 stdio: "inherit",22 cwd: desktopDir,
23 args: ["dist-electron/main.js"],
23 env: {24 env: {
24 ...childEnv,25 ...childEnv,
25 VITE_DEV_SERVER_URL: devServerUrl,26 VITE_DEV_SERVER_URL: devServerUrl,
26 },27 },
27});28 electronPath: resolveElectronPath(),
28
29child.on("exit", (code) => {
30 process.exit(code ?? 0);
31});29});
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 {...@@ -55,6 +55,9 @@ function resolveIconPath(ext: "ico" | "icns" | "png"): string | null {
5555
56function configureAppIdentity(): void {56function configureAppIdentity(): void {
57 app.setName(APP_DISPLAY_NAME);57 app.setName(APP_DISPLAY_NAME);
58 app.setAboutPanelOptions({
59 applicationName: APP_DISPLAY_NAME,
60 });
5861
59 if (process.platform === "win32") {62 if (process.platform === "win32") {
60 app.setAppUserModelId(APP_USER_MODEL_ID);63 app.setAppUserModelId(APP_USER_MODEL_ID);
...@@ -269,6 +272,13 @@ function createWindow(): BrowserWindow {...@@ -269,6 +272,13 @@ function createWindow(): BrowserWindow {
269 });272 });
270273
271 window.webContents.setWindowOpenHandler(() => ({ action: "deny" }));274 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 });
272 window.once("ready-to-show", () => {282 window.once("ready-to-show", () => {
273 window.show();283 window.show();
274 });284 });
...@@ -311,6 +321,7 @@ app.on("before-quit", () => {...@@ -311,6 +321,7 @@ app.on("before-quit", () => {
311});321});
312322
313app.whenReady().then(() => {323app.whenReady().then(() => {
324 configureAppIdentity();
314 void bootstrap();325 void bootstrap();
315326
316 app.on("activate", () => {327 app.on("activate", () => {