authorgravatar for me@wukko.mewukko <me@wukko.me> 2026-07-29 16:39:23+06:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-29 12:39:23+02:00
log72b960fa3985ec61af6a459431861a5dcb360009
treeb14c631e3f2facdc481d562d4526d50cd657266e
parent08e4932f0465327968368b22be343b58a07d476a
signature Signed by PGP key B5690EEEBB952194

build(desktop): reduce installed app size by ~300MB (#4824)


2 files changed, 68 insertions(+), 18 deletions(-)

scripts/build-desktop-artifact.test.ts+48-3
...@@ -13,7 +13,8 @@ import {...@@ -13,7 +13,8 @@ import {
13 createStageWorkspaceConfig,13 createStageWorkspaceConfig,
14 createStagePatchedDependencies,14 createStagePatchedDependencies,
15 createBuildConfig,15 createBuildConfig,
16 DESKTOP_ASAR_UNPACK,16 DESKTOP_ELECTRON_LANGUAGES,
17 DESKTOP_FILE_EXCLUSIONS,
17 InvalidMacPasskeyRpDomainError,18 InvalidMacPasskeyRpDomainError,
18 InvalidMacPasskeyPublishableKeyError,19 InvalidMacPasskeyPublishableKeyError,
19 InvalidMockUpdateServerPortError,20 InvalidMockUpdateServerPortError,
...@@ -37,6 +38,7 @@ import {...@@ -37,6 +38,7 @@ import {
37 resolvePackageManagerUserAgent,38 resolvePackageManagerUserAgent,
38 stageLinuxIconSize,39 stageLinuxIconSize,
39 STAGE_INSTALL_ARGS,40 STAGE_INSTALL_ARGS,
41 WINDOWS_ASAR_UNPACK,
40} from "./build-desktop-artifact.ts";42} from "./build-desktop-artifact.ts";
41import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";43import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
42import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";44import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
...@@ -303,10 +305,53 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -303,10 +305,53 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
303 );305 );
304 });306 });
305307
306 it("unpacks the fff shared library for filesystem and FFI access", () => {308 it("limits Electron locales and excludes the unused Claude SDK executable", () => {
307 assert.deepStrictEqual(DESKTOP_ASAR_UNPACK, ["node_modules/@ff-labs/fff-bin-*/**/*"]);309 assert.deepStrictEqual(DESKTOP_ELECTRON_LANGUAGES, ["en-US"]);
310 assert.deepStrictEqual(DESKTOP_FILE_EXCLUSIONS, [
311 "!**/node_modules/@anthropic-ai/claude-agent-sdk-*/**/*",
312 ]);
308 });313 });
309314
315 it.effect("applies platform-specific packaging to the build config", () =>
316 Effect.gen(function* () {
317 const mac = yield* createBuildConfig(
318 "mac",
319 "dmg",
320 "1.2.3",
321 false,
322 false,
323 undefined,
324 undefined,
325 );
326 const linux = yield* createBuildConfig(
327 "linux",
328 "AppImage",
329 "1.2.3",
330 false,
331 false,
332 undefined,
333 undefined,
334 );
335 const win = yield* createBuildConfig(
336 "win",
337 "nsis",
338 "1.2.3",
339 false,
340 false,
341 undefined,
342 undefined,
343 );
344
345 assert.notProperty(mac, "asarUnpack");
346 assert.notProperty(linux, "asarUnpack");
347 assert.deepStrictEqual(win.asarUnpack, WINDOWS_ASAR_UNPACK);
348 for (const config of [mac, linux, win]) {
349 assert.deepStrictEqual(config.electronLanguages, DESKTOP_ELECTRON_LANGUAGES);
350 assert.deepStrictEqual(config.files, DESKTOP_FILE_EXCLUSIONS);
351 }
352 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
353 );
354
310 it.effect("preserves both Linux icon resize failures with structural context", () => {355 it.effect("preserves both Linux icon resize failures with structural context", () => {
311 const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> = [];356 const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> = [];
312357
scripts/build-desktop-artifact.ts+20-15
...@@ -579,7 +579,20 @@ interface StagePackageJson {...@@ -579,7 +579,20 @@ interface StagePackageJson {
579}579}
580580
581export const STAGE_INSTALL_ARGS = ["install", "--prod"] as const;581export const STAGE_INSTALL_ARGS = ["install", "--prod"] as const;
582export const DESKTOP_ASAR_UNPACK = ["node_modules/@ff-labs/fff-bin-*/**/*"] as const;582export const DESKTOP_ELECTRON_LANGUAGES = ["en-US"] as const;
583export const DESKTOP_FILE_EXCLUSIONS = [
584 // T3 Code always passes the user's installed Claude executable to the SDK,
585 // so the SDK's optional platform packages (each a ~200MB bundled executable)
586 // are dead weight. The trailing dash keeps the SDK's own JS package.
587 "!**/node_modules/@anthropic-ai/claude-agent-sdk-*/**/*",
588] as const;
589// The WSL backend launches the server with plain `wsl.exe -- node`, which
590// cannot read inside an asar archive — and the server bundle externalizes its
591// runtime deps, so the whole node_modules tree must be unpacked, not just the
592// bundle (otherwise ERR_MODULE_NOT_FOUND: "Cannot find package 'effect'").
593// The Windows primary backend reads the same files through the asar redirect,
594// so nothing is duplicated.
595export const WINDOWS_ASAR_UNPACK = ["apps/server/dist/**", "**/node_modules/**"] as const;
583596
584export interface MacPasskeySigningConfiguration {597export interface MacPasskeySigningConfiguration {
585 readonly appId: string;598 readonly appId: string;
...@@ -1390,23 +1403,15 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (...@@ -1390,23 +1403,15 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
1390 appId: DESKTOP_APP_ID,1403 appId: DESKTOP_APP_ID,
1391 productName: resolveDesktopProductName(version),1404 productName: resolveDesktopProductName(version),
1392 artifactName: "T3-Code-${version}-${arch}.${ext}",1405 artifactName: "T3-Code-${version}-${arch}.${ext}",
1406 electronLanguages: [...DESKTOP_ELECTRON_LANGUAGES],
1407 files: [...DESKTOP_FILE_EXCLUSIONS],
1393 directories: {1408 directories: {
1394 buildResources: "apps/desktop/resources",1409 buildResources: "apps/desktop/resources",
1395 },1410 },
1396 // The Windows primary backend runs the server bundle through1411 // Only the Windows WSL backend needs files outside the asar (see
1397 // ELECTRON_RUN_AS_NODE (asar-aware), so it reads bin.mjs straight out of1412 // WINDOWS_ASAR_UNPACK); macOS and Linux stay packed — smart unpack
1398 // app.asar. The WSL backend instead launches plain `wsl.exe -- node`, which1413 // extracts native libraries, which fff-node finds in app.asar.unpacked.
1399 // cannot read inside an asar archive, so everything it loads must be on the1414 ...(platform === "win" ? { asarUnpack: [...WINDOWS_ASAR_UNPACK] } : {}),
1400 // real filesystem. The server bundle externalizes its runtime dependencies
1401 // (effect, @effect/*, node-pty, ...) to node_modules rather than inlining
1402 // them, so unpacking just the bundle + node-pty isn't enough — the Linux Node
1403 // fails with ERR_MODULE_NOT_FOUND (e.g. "Cannot find package 'effect'") before
1404 // it even reaches node-pty. Unpack the server bundle AND the whole
1405 // node_modules tree so every import resolves (this also covers the fff native
1406 // binaries in DESKTOP_ASAR_UNPACK). The Windows primary keeps reading the same
1407 // files through the asar (transparently redirected to the unpacked copy), so
1408 // there's no duplication.
1409 asarUnpack: [...DESKTOP_ASAR_UNPACK, "apps/server/dist/**", "**/node_modules/**"],
1410 };1415 };
1411 const updateChannel = resolveDesktopUpdateChannel(version);1416 const updateChannel = resolveDesktopUpdateChannel(version);
1412 const publishConfig = yield* resolveGitHubPublishConfig(updateChannel);1417 const publishConfig = yield* resolveGitHubPublishConfig(updateChannel);