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
13 changes: 12 additions & 1 deletion apps/desktop/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import "vite-plus/test/config";
import { defineConfig } from "vite-plus";

import { isDesktopRuntimeExternalDependency } from "../../scripts/lib/desktop-external-packages.ts";
import { loadRepoEnv } from "../../scripts/lib/public-config.ts";

const repoEnv = loadRepoEnv();

// The main process is bundled the same way the server CLI is: every JS
// dependency is inlined and only packages Node must load from disk stay
// external. The packaged app then installs just those externals, instead of a
// full production install of apps/desktop's dependency tree next to a server
// bundle that already carries its own copy of the same libraries.
const isMainProcessExternal = (id: string) =>
id === "electron" || id.startsWith("electron/") || isDesktopRuntimeExternalDependency(id);
const shouldLaunchElectronAfterPack = process.env.T3CODE_DESKTOP_DEV === "1";
const publicConfigDefine = {
__T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: JSON.stringify(
Expand Down Expand Up @@ -55,7 +64,9 @@ export default defineConfig({
],
clean: true,
deps: {
alwaysBundle: (id) => id.startsWith("@t3tools/"),
alwaysBundle: (id) => !id.startsWith("node:") && !isMainProcessExternal(id),
neverBundle: isMainProcessExternal,
onlyBundle: false,
},
...(shouldLaunchElectronAfterPack ? { onSuccess: "node scripts/dev-electron.mjs" } : {}),
},
Expand Down
65 changes: 51 additions & 14 deletions scripts/build-desktop-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
DESKTOP_EXTRA_RESOURCES,
LINUX_CAPTURE_EXTRA_RESOURCES,
LINUX_BROWSER_SECRET_EXTRA_RESOURCES,
LINUX_FILE_EXCLUSIONS,
MAC_FILE_EXCLUSIONS,
InvalidMacPasskeyRpDomainError,
InvalidMacPasskeyPublishableKeyError,
Expand All @@ -47,7 +48,7 @@ import {
resolveClerkPasskeyNativeArtifacts,
resolveMacPasskeySigningConfiguration,
resolveDesktopRuntimeDependencies,
resolveMacStageDependencies,
resolveMergedStageDependencies,
resolveFffNativeDependencies,
resolveBuildOptions,
resolveDesktopBuildIconAssets,
Expand Down Expand Up @@ -367,26 +368,37 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
),
);

it("omits bundled workspace packages from staged desktop dependencies", () => {
it("stages only the desktop main-process externals", () => {
assert.deepStrictEqual(
resolveDesktopRuntimeDependencies(
{
"@clerk/electron": "catalog:",
"@clerk/electron-passkeys": "catalog:",
"@crowecawcaw/xa11y": "0.13.0",
"@effect/platform-node": "catalog:",
"@napi-rs/keyring": "^1.3.0",
"@t3tools/contracts": "workspace:*",
"@t3tools/shared": "workspace:*",
"@t3tools/ssh": "workspace:*",
"@t3tools/tailscale": "workspace:*",
"dbus-next": "0.10.2",
effect: "catalog:",
electron: "41.5.0",
"electron-updater": "^6.6.2",
"ffi-rs": "1.3.2",
"playwright-core": "1.60.0",
},
{
"@clerk/electron": "0.0.37",
"@clerk/electron-passkeys": "0.0.3",
"@effect/platform-node": "4.0.0-beta.59",
effect: "4.0.0-beta.59",
},
),
{
"@effect/platform-node": "4.0.0-beta.59",
effect: "4.0.0-beta.59",
"@clerk/electron-passkeys": "0.0.3",
"@crowecawcaw/xa11y": "0.13.0",
"@napi-rs/keyring": "^1.3.0",
"ffi-rs": "1.3.2",
"playwright-core": "1.60.0",
},
);
});
Expand Down Expand Up @@ -559,6 +571,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {

assert.deepStrictEqual(DESKTOP_FILE_EXCLUSIONS, [
"!**/node_modules/@anthropic-ai/claude-agent-sdk-*/**/*",
"!**/*.map",
"!**/*.d.cts",
"!apps/desktop/resources/browser-secret",
"!apps/desktop/resources/browser-secret/**/*",
"!apps/desktop/prod-resources/browser-secret",
Expand Down Expand Up @@ -661,6 +675,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
"**/node_modules/@anthropic-ai/claude-agent-sdk-*/**",
"**/node_modules/.bin",
"**/node_modules/.bin/**",
"**/*.map",
]);
assert.deepStrictEqual(mac.dmg, {
title: "T3 Code (Alpha) 1.2.3 Installer",
Expand All @@ -679,7 +694,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
{ name: "T3 Code", schemes: ["t3code", "t3code-dev"] },
]);
assert.deepStrictEqual(mac.files, [...DESKTOP_FILE_EXCLUSIONS, ...MAC_FILE_EXCLUSIONS]);
assert.deepStrictEqual(linux.files, DESKTOP_FILE_EXCLUSIONS);
assert.deepStrictEqual(linux.files, [...DESKTOP_FILE_EXCLUSIONS, ...LINUX_FILE_EXCLUSIONS]);
assert.deepStrictEqual(win.files, DESKTOP_FILE_EXCLUSIONS);
assert.deepStrictEqual(winWithoutWslPrebuild.files, win.files);
assert.notProperty(mac.mac as Record<string, unknown>, "sign");
Expand All @@ -690,11 +705,15 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
}).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
);

it("excludes Windows terminal binaries only from macOS packages", () => {
it("excludes foreign node-pty prebuilds from macOS and Linux packages", () => {
assert.deepStrictEqual(MAC_FILE_EXCLUSIONS, [
"!**/node_modules/node-pty/prebuilds/win32-*/**/*",
"!**/node_modules/node-pty/third_party/conpty/**/*",
]);
assert.deepStrictEqual(LINUX_FILE_EXCLUSIONS, [
...MAC_FILE_EXCLUSIONS,
"!**/node_modules/node-pty/prebuilds/darwin-*/**/*",
]);
});

it("unpacks native binaries while keeping their JavaScript and metadata archived", () => {
Expand Down Expand Up @@ -725,9 +744,10 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
}
});

it("stages only server runtime externals in macOS packages", () => {
it("stages only the externals of both bundles in merged packages", () => {
assert.deepStrictEqual(
resolveMacStageDependencies({
resolveMergedStageDependencies({
platform: "mac",
serverDependencies: {
"@anthropic-ai/claude-agent-sdk": "^0.3.170",
"@ff-labs/fff-node": "0.9.4",
Expand All @@ -737,8 +757,8 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
"node-pty": "1.1.0",
},
desktopDependencies: {
"@clerk/electron": "0.0.34",
effect: "4.0.0-beta.103",
"@napi-rs/keyring": "1.3.0",
"playwright-core": "1.60.0",
},
arch: "arm64",
fffNodeVersion: "0.9.4",
Expand All @@ -747,11 +767,28 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
"@ff-labs/fff-node": "0.9.4",
"msgpackr-extract": "3.0.4",
"node-pty": "1.1.0",
"@clerk/electron": "0.0.34",
effect: "4.0.0-beta.103",
"@napi-rs/keyring": "1.3.0",
"playwright-core": "1.60.0",
"@ff-labs/fff-bin-darwin-arm64": "0.9.4",
},
);

assert.deepStrictEqual(
resolveMergedStageDependencies({
platform: "linux",
serverDependencies: { "@ff-labs/fff-node": "0.9.4", "node-pty": "1.1.0", effect: "4.0.0" },
desktopDependencies: { "@crowecawcaw/xa11y": "0.13.0" },
arch: "x64",
fffNodeVersion: "0.9.4",
}),
{
"@ff-labs/fff-node": "0.9.4",
"node-pty": "1.1.0",
"@crowecawcaw/xa11y": "0.13.0",
"@ff-labs/fff-bin-linux-x64-gnu": "0.9.4",
"@ff-labs/fff-bin-linux-x64-musl": "0.9.4",
},
);
});

it("excludes node-pty binaries for the other Windows architecture", () => {
Expand Down
71 changes: 41 additions & 30 deletions scripts/build-desktop-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
selectCliRuntimeExternalDependencies,
} from "./lib/cli-external-packages.ts";
import { loadRepoEnv } from "./lib/public-config.ts";
import { selectDesktopRuntimeExternalDependencies } from "./lib/desktop-external-packages.ts";
import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";

import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
Expand Down Expand Up @@ -959,6 +960,10 @@ export const DESKTOP_FILE_EXCLUSIONS = [
// 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-*/**/*",
// Nothing in the packaged app enables source maps or serves them: the web
// client's maps alone were 50 MB of app.asar that no request ever read.
"!**/*.map",
"!**/*.d.cts",
"!apps/desktop/resources/browser-secret",
"!apps/desktop/resources/browser-secret/**/*",
"!apps/desktop/prod-resources/browser-secret",
Expand All @@ -978,6 +983,12 @@ export const MAC_FILE_EXCLUSIONS = [
"!**/node_modules/node-pty/prebuilds/win32-*/**/*",
"!**/node_modules/node-pty/third_party/conpty/**/*",
] as const;
// Linux builds node-pty from source, so every prebuild in the package is for
// another platform (58 MB of it Windows debug symbols).
export const LINUX_FILE_EXCLUSIONS = [
...MAC_FILE_EXCLUSIONS,
"!**/node_modules/node-pty/prebuilds/darwin-*/**/*",
] as const;

// node-pty publishes both Darwin prebuilds in one package. Single-architecture
// apps only need the native target; universal apps need both. An omitted arch
Expand Down Expand Up @@ -1013,6 +1024,7 @@ export const WINDOWS_SERVER_ASAR_IGNORE_GLOBS = [
"**/node_modules/@anthropic-ai/claude-agent-sdk-*/**",
"**/node_modules/.bin",
"**/node_modules/.bin/**",
"**/*.map",
Comment thread
juliusmarminge marked this conversation as resolved.
] as const;

export function resolveWindowsServerAsarIgnoreGlobs(arch: typeof BuildArch.Type) {
Expand Down Expand Up @@ -1357,7 +1369,10 @@ export function resolveFffNativeDependencies(
);
}

export function resolveMacStageDependencies(input: {
// macOS and Linux run both processes from one app.asar, so the stage installs
// the union of what each bundle leaves external and nothing else.
export function resolveMergedStageDependencies(input: {
readonly platform: "mac" | "linux";
readonly serverDependencies: Record<string, string>;
readonly desktopDependencies: Record<string, string>;
readonly arch: typeof BuildArch.Type;
Expand All @@ -1366,7 +1381,7 @@ export function resolveMacStageDependencies(input: {
return {
...selectCliRuntimeExternalDependencies(input.serverDependencies),
...input.desktopDependencies,
...resolveFffNativeDependencies("mac", input.arch, input.fffNodeVersion),
...resolveFffNativeDependencies(input.platform, input.arch, input.fffNodeVersion),
};
}

Expand Down Expand Up @@ -2551,6 +2566,11 @@ function validateBundledClientAssets(clientDir: string) {
});
}

// The main-process bundle inlines every JS dependency (see
// apps/desktop/vite.config.ts), so the packaged app only installs the packages
// that bundle leaves external: native addons and playwright-core. Everything
// else already lives inside dist-electron and would only duplicate what the
// server bundle carries too.
export function resolveDesktopRuntimeDependencies(
dependencies: Record<string, string> | undefined,
catalog: Record<string, string>,
Expand All @@ -2559,14 +2579,11 @@ export function resolveDesktopRuntimeDependencies(
return {};
}

const runtimeDependencies = Object.fromEntries(
Object.entries(dependencies).filter(
([dependencyName, dependencySpec]) =>
dependencyName !== "electron" && !dependencySpec.startsWith("workspace:"),
),
return resolveCatalogDependencies(
selectDesktopRuntimeExternalDependencies(dependencies),
catalog,
"apps/desktop",
);

return resolveCatalogDependencies(runtimeDependencies, catalog, "apps/desktop");
}

export const resolveGitHubPublishConfig = Effect.fn("resolveGitHubPublishConfig")(function* (
Expand Down Expand Up @@ -2672,7 +2689,11 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
electronLanguages: [...DESKTOP_ELECTRON_LANGUAGES],
files: [
...DESKTOP_FILE_EXCLUSIONS,
...(platform === "mac" ? resolveMacFileExclusions(arch) : []),
...(platform === "mac"
? resolveMacFileExclusions(arch)
: platform === "linux"
? LINUX_FILE_EXCLUSIONS
: []),
],
directories: {
buildResources: "apps/desktop/resources",
Expand Down Expand Up @@ -3735,29 +3756,19 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
}

// Windows splits dependencies per process: app.asar carries only the
// desktop main-process runtime deps, while the server bundle's deps live in
// the server.asar sidecar (see stageWindowsServerSidecar). macOS adds only
// server packages that remain external to its merged app.asar. Linux retains
// its existing full dependency tree.
// desktop main-process externals, while the server bundle's externals live
// in the server.asar sidecar (see stageWindowsServerSidecar). macOS and
// Linux merge both sets into one app.asar.
const stageDependencies =
options.platform === "win"
? { ...resolvedDesktopRuntimeDependencies }
: options.platform === "mac"
? resolveMacStageDependencies({
serverDependencies: resolvedServerDependencies,
desktopDependencies: resolvedDesktopRuntimeDependencies,
arch: options.arch,
fffNodeVersion: serverPackageJson.dependencies["@ff-labs/fff-node"],
})
: {
...resolvedServerDependencies,
...resolvedDesktopRuntimeDependencies,
...resolveFffNativeDependencies(
options.platform,
options.arch,
serverPackageJson.dependencies["@ff-labs/fff-node"],
),
};
: resolveMergedStageDependencies({
platform: options.platform,
serverDependencies: resolvedServerDependencies,
desktopDependencies: resolvedDesktopRuntimeDependencies,
arch: options.arch,
fffNodeVersion: serverPackageJson.dependencies["@ff-labs/fff-node"],
});
const stagePatchedDependencies = createStagePatchedDependencies(
workspacePatchedDependencies,
stageDependencies,
Expand Down
38 changes: 38 additions & 0 deletions scripts/lib/desktop-external-packages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Packages the desktop main-process bundle must NOT inline.
*
* The desktop bundle follows the same policy as the server CLI bundle (see
* cli-external-packages.ts): everything is inlined except what Node has to
* load from the real filesystem. Both `apps/desktop/vite.config.ts` and the
* artifact stage in scripts/build-desktop-artifact.ts derive from this list,
* so a package that is external is also the only kind of package the staged
* production install carries. Anything not listed here ships inside
* `dist-electron/*.cjs` and has no `node_modules` presence at all.
*
* Entries are matched as prefixes so platform-specific siblings are covered.
*/
export const DESKTOP_RUNTIME_EXTERNAL_PREFIXES = [
// Native addons and the wrappers that dlopen them by real path.
"@napi-rs/keyring",
"@crowecawcaw/xa11y",
"@clerk/electron-passkeys",
"ffi-rs",
"@yuuang/",
// Reads its own bundle from disk by resolving `playwright-core/package.json`
// at runtime and ships the browser driver alongside; there is nothing to
// gain from inlining a 10 MB file the code re-reads as text.
"playwright-core",
] as const;

export function isDesktopRuntimeExternalDependency(id: string): boolean {
return DESKTOP_RUNTIME_EXTERNAL_PREFIXES.some((prefix) => id.startsWith(prefix));
}

/** Select the desktop dependency roots whose runtime closure the stage must install. */
export function selectDesktopRuntimeExternalDependencies(
dependencies: Readonly<Record<string, string>>,
): Record<string, string> {
return Object.fromEntries(
Object.entries(dependencies).filter(([name]) => isDesktopRuntimeExternalDependency(name)),
);
}
Loading