From f424416a0766f441c508afd77c1ae76ccb4ad86c Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 29 Jul 2026 14:24:01 +0600 Subject: [PATCH] build(desktop): reduce installed app size by ~300MB - drop extra Electron language packs (~48MB) - drop the Claude Agent SDK's bundled platform executables (~255MB). T3 Code always runs the user's own Claude install - pack dependencies into ASAR on macOS/Linux, retaining Windows-only unpacking for WSL macOS arm64: 737MB -> 424MB installed, 227MB -> 149MB DMG. --- scripts/build-desktop-artifact.test.ts | 51 ++++++++++++++++++++++++-- scripts/build-desktop-artifact.ts | 35 ++++++++++-------- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index da45376954f..53c5dc4fa07 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -13,7 +13,8 @@ import { createStageWorkspaceConfig, createStagePatchedDependencies, createBuildConfig, - DESKTOP_ASAR_UNPACK, + DESKTOP_ELECTRON_LANGUAGES, + DESKTOP_FILE_EXCLUSIONS, InvalidMacPasskeyRpDomainError, InvalidMacPasskeyPublishableKeyError, InvalidMockUpdateServerPortError, @@ -37,6 +38,7 @@ import { resolvePackageManagerUserAgent, stageLinuxIconSize, STAGE_INSTALL_ARGS, + WINDOWS_ASAR_UNPACK, } from "./build-desktop-artifact.ts"; import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts"; import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; @@ -303,10 +305,53 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { ); }); - it("unpacks the fff shared library for filesystem and FFI access", () => { - assert.deepStrictEqual(DESKTOP_ASAR_UNPACK, ["node_modules/@ff-labs/fff-bin-*/**/*"]); + it("limits Electron locales and excludes the unused Claude SDK executable", () => { + assert.deepStrictEqual(DESKTOP_ELECTRON_LANGUAGES, ["en-US"]); + assert.deepStrictEqual(DESKTOP_FILE_EXCLUSIONS, [ + "!**/node_modules/@anthropic-ai/claude-agent-sdk-*/**/*", + ]); }); + it.effect("applies platform-specific packaging to the build config", () => + Effect.gen(function* () { + const mac = yield* createBuildConfig( + "mac", + "dmg", + "1.2.3", + false, + false, + undefined, + undefined, + ); + const linux = yield* createBuildConfig( + "linux", + "AppImage", + "1.2.3", + false, + false, + undefined, + undefined, + ); + const win = yield* createBuildConfig( + "win", + "nsis", + "1.2.3", + false, + false, + undefined, + undefined, + ); + + assert.notProperty(mac, "asarUnpack"); + assert.notProperty(linux, "asarUnpack"); + assert.deepStrictEqual(win.asarUnpack, WINDOWS_ASAR_UNPACK); + for (const config of [mac, linux, win]) { + assert.deepStrictEqual(config.electronLanguages, DESKTOP_ELECTRON_LANGUAGES); + assert.deepStrictEqual(config.files, DESKTOP_FILE_EXCLUSIONS); + } + }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))), + ); + it.effect("preserves both Linux icon resize failures with structural context", () => { const commands: Array<{ readonly command: string; readonly args: ReadonlyArray }> = []; diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 8a1099a9ed0..0a0287507cc 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -579,7 +579,20 @@ interface StagePackageJson { } export const STAGE_INSTALL_ARGS = ["install", "--prod"] as const; -export const DESKTOP_ASAR_UNPACK = ["node_modules/@ff-labs/fff-bin-*/**/*"] as const; +export const DESKTOP_ELECTRON_LANGUAGES = ["en-US"] as const; +export const DESKTOP_FILE_EXCLUSIONS = [ + // T3 Code always passes the user's installed Claude executable to the SDK, + // so the SDK's optional platform packages (each a ~200MB bundled executable) + // are dead weight. The trailing dash keeps the SDK's own JS package. + "!**/node_modules/@anthropic-ai/claude-agent-sdk-*/**/*", +] as const; +// The WSL backend launches the server with plain `wsl.exe -- node`, which +// cannot read inside an asar archive — and the server bundle externalizes its +// runtime deps, so the whole node_modules tree must be unpacked, not just the +// bundle (otherwise ERR_MODULE_NOT_FOUND: "Cannot find package 'effect'"). +// The Windows primary backend reads the same files through the asar redirect, +// so nothing is duplicated. +export const WINDOWS_ASAR_UNPACK = ["apps/server/dist/**", "**/node_modules/**"] as const; export interface MacPasskeySigningConfiguration { readonly appId: string; @@ -1390,23 +1403,15 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( appId: DESKTOP_APP_ID, productName: resolveDesktopProductName(version), artifactName: "T3-Code-${version}-${arch}.${ext}", + electronLanguages: [...DESKTOP_ELECTRON_LANGUAGES], + files: [...DESKTOP_FILE_EXCLUSIONS], directories: { buildResources: "apps/desktop/resources", }, - // The Windows primary backend runs the server bundle through - // ELECTRON_RUN_AS_NODE (asar-aware), so it reads bin.mjs straight out of - // app.asar. The WSL backend instead launches plain `wsl.exe -- node`, which - // cannot read inside an asar archive, so everything it loads must be on the - // real filesystem. The server bundle externalizes its runtime dependencies - // (effect, @effect/*, node-pty, ...) to node_modules rather than inlining - // them, so unpacking just the bundle + node-pty isn't enough — the Linux Node - // fails with ERR_MODULE_NOT_FOUND (e.g. "Cannot find package 'effect'") before - // it even reaches node-pty. Unpack the server bundle AND the whole - // node_modules tree so every import resolves (this also covers the fff native - // binaries in DESKTOP_ASAR_UNPACK). The Windows primary keeps reading the same - // files through the asar (transparently redirected to the unpacked copy), so - // there's no duplication. - asarUnpack: [...DESKTOP_ASAR_UNPACK, "apps/server/dist/**", "**/node_modules/**"], + // Only the Windows WSL backend needs files outside the asar (see + // WINDOWS_ASAR_UNPACK); macOS and Linux stay packed — smart unpack + // extracts native libraries, which fff-node finds in app.asar.unpacked. + ...(platform === "win" ? { asarUnpack: [...WINDOWS_ASAR_UNPACK] } : {}), }; const updateChannel = resolveDesktopUpdateChannel(version); const publishConfig = yield* resolveGitHubPublishConfig(updateChannel);