Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions scripts/build-desktop-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
createStageWorkspaceConfig,
createStagePatchedDependencies,
createBuildConfig,
DESKTOP_ASAR_UNPACK,
DESKTOP_ELECTRON_LANGUAGES,
DESKTOP_FILE_EXCLUSIONS,
InvalidMacPasskeyRpDomainError,
InvalidMacPasskeyPublishableKeyError,
InvalidMockUpdateServerPortError,
Expand All @@ -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";
Expand Down Expand Up @@ -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<string> }> = [];

Expand Down
35 changes: 20 additions & 15 deletions scripts/build-desktop-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Loading