diff --git a/apps/server/package.json b/apps/server/package.json index de012aada4d..01003d7c176 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -27,7 +27,7 @@ "@effect/platform-node": "catalog:", "@effect/platform-node-shared": "catalog:", "@effect/sql-sqlite-bun": "catalog:", - "@ff-labs/fff-node": "^0.9.4", + "@ff-labs/fff-node": "0.9.4", "@opencode-ai/sdk": "^1.3.15", "@pierre/diffs": "catalog:", "effect": "catalog:", diff --git a/patches/@ff-labs__fff-node@0.9.4.patch b/patches/@ff-labs__fff-node@0.9.4.patch new file mode 100644 index 00000000000..2d0c16133eb --- /dev/null +++ b/patches/@ff-labs__fff-node@0.9.4.patch @@ -0,0 +1,37 @@ +diff --git a/dist/src/binary.js b/dist/src/binary.js +index ee181aef5007e4bf34a49479c089ca30f73a320b..327e2c55c83cc4c50d396a3109190ef10af75ca7 100644 +--- a/dist/src/binary.js ++++ b/dist/src/binary.js +@@ -7,7 +7,7 @@ + */ + import { existsSync, readFileSync } from "node:fs"; + import { createRequire } from "node:module"; +-import { dirname, join } from "node:path"; ++import { dirname, join, sep } from "node:path"; + import { fileURLToPath } from "node:url"; + import { getLibFilename, getNpmPackageName } from "./platform.js"; + /** +@@ -46,6 +46,14 @@ function getPackageDir() { + // Fallback: assume we're one level deep in src/ + return dirname(currentDir); + } ++function resolveUnpackedAsarPath(binaryPath) { ++ const asarSegment = `${sep}app.asar${sep}`; ++ if (!binaryPath.includes(asarSegment)) { ++ return binaryPath; ++ } ++ const unpackedPath = binaryPath.replace(asarSegment, `${sep}app.asar.unpacked${sep}`); ++ return existsSync(unpackedPath) ? unpackedPath : binaryPath; ++} + /** + * Check if the binary exists in any known location + */ +@@ -69,7 +77,7 @@ function resolveFromNpmPackage() { + const packageDir = dirname(packageJsonPath); + const binaryPath = join(packageDir, getLibFilename()); + if (existsSync(binaryPath)) { +- return binaryPath; ++ return resolveUnpackedAsarPath(binaryPath); + } + } + catch { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1bd60b2fc9..1fa241735ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,6 +66,9 @@ patchedDependencies: '@expo/metro-config@56.0.13': hash: 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46 path: patches/@expo%2Fmetro-config@56.0.13.patch + '@ff-labs/fff-node@0.9.4': + hash: 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8 + path: patches/@ff-labs__fff-node@0.9.4.patch '@pierre/diffs@1.3.0-beta.4': hash: f5d41705ce94bbafc731d92e9cb1671db710df046dd135cb894e3e3e9164a75b path: patches/@pierre%2Fdiffs@1.3.0-beta.4.patch @@ -396,8 +399,8 @@ importers: specifier: 4.0.0-beta.78 version: 4.0.0-beta.78(effect@4.0.0-beta.78(patch_hash=883249d8efbb462e928e21fefef96027b66aec50751178cafdce45f08eee3754)) '@ff-labs/fff-node': - specifier: ^0.9.4 - version: 0.9.4 + specifier: 0.9.4 + version: 0.9.4(patch_hash=2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8) '@opencode-ai/sdk': specifier: ^1.3.15 version: 1.15.13 @@ -11854,7 +11857,7 @@ snapshots: '@ff-labs/fff-bin-win32-x64@0.9.4': optional: true - '@ff-labs/fff-node@0.9.4': + '@ff-labs/fff-node@0.9.4(patch_hash=2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8)': dependencies: ffi-rs: 1.3.2 optionalDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 605fe4df4aa..58f4b6e0dfe 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,22 +1,11 @@ packages: - - "apps/*" - - "infra/*" - - "oxlint-plugin-t3code" - - "packages/*" - - "scripts" - -onlyBuiltDependencies: - - electron - - esbuild - - msgpackr-extract - - node-pty - - sharp - -ignoredBuiltDependencies: - - msw + - apps/* + - infra/* + - oxlint-plugin-t3code + - packages/* + - scripts catalog: - effect: 4.0.0-beta.78 "@effect/atom-react": 4.0.0-beta.78 "@effect/openapi-generator": 4.0.0-beta.78 "@effect/platform-bun": 4.0.0-beta.78 @@ -24,23 +13,33 @@ catalog: "@effect/platform-node-shared": 4.0.0-beta.78 "@effect/sql-pg": 4.0.0-beta.78 "@effect/sql-sqlite-bun": 4.0.0-beta.78 - "@effect/vitest": 4.0.0-beta.78 "@effect/tsgo": 0.13.2 + "@effect/vitest": 4.0.0-beta.78 "@noble/curves": 1.9.1 "@noble/hashes": 1.8.0 "@pierre/diffs": 1.3.0-beta.4 - "@vitest/runner": 4.1.8 "@types/node": 24.12.4 "@typescript/native-preview": 7.0.0-dev.20260604.1 + "@vitest/runner": 4.1.8 + effect: 4.0.0-beta.78 jose: 6.2.2 typescript: ~6.0.3 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.24 vite: npm:@voidzero-dev/vite-plus-core@0.1.24 vite-plus: 0.1.24 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.24 yaml: ^2.9.0 + +ignoredBuiltDependencies: + - msw + +onlyBuiltDependencies: + - electron + - esbuild + - msgpackr-extract + - node-pty + - sharp + overrides: - # Clerk publishes wallet auth integrations as required dependencies. T3 Code does - # not support wallet auth, so keep that unused dependency tree out of installs. "@clerk/clerk-js>@base-org/account": "-" "@clerk/clerk-js>@coinbase/wallet-sdk": "-" "@clerk/clerk-js>@solana/wallet-adapter-base": "-" @@ -56,22 +55,13 @@ overrides: "@effect/vitest": "catalog:" "@effect/vitest>@vitest/runner": "-" "@effect/vitest>vitest": "-" - # Pinned to the version the patch in patchedDependencies targets — a fresh - # resolve (e.g. release-smoke regenerating the lockfile) must not drift to a - # newer release and orphan the patch (ERR_PNPM_UNUSED_PATCH). - "@expo/metro-config": "56.0.13" + "@expo/metro-config": 56.0.13 "@types/node": "catalog:" effect: "catalog:" vite: "catalog:" vitest: "catalog:" yaml: "catalog:" -peerDependencyRules: - allowAny: - - vite - - vitest - allowedVersions: - vite: "*" - vitest: "*" + packageExtensions: "@effect/vitest@*": dependencies: @@ -79,14 +69,22 @@ packageExtensions: peerDependenciesMeta: vitest: optional: true - "vite-plus@*": + vite-plus@*: dependencies: vite: "catalog:" + patchedDependencies: - # Keep non-browser consumers off the root entrypoint, which eagerly imports - # DOM-dependent renderers. These utility/type subpaths are safe for mobile. - "@pierre/diffs@1.3.0-beta.4": patches/@pierre%2Fdiffs@1.3.0-beta.4.patch "@effect/vitest@4.0.0-beta.78": patches/@effect__vitest@4.0.0-beta.78.patch "@expo/metro-config@56.0.13": patches/@expo%2Fmetro-config@56.0.13.patch + "@ff-labs/fff-node@0.9.4": patches/@ff-labs__fff-node@0.9.4.patch + "@pierre/diffs@1.3.0-beta.4": patches/@pierre%2Fdiffs@1.3.0-beta.4.patch effect@4.0.0-beta.78: patches/effect@4.0.0-beta.78.patch react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch + +peerDependencyRules: + allowAny: + - vite + - vitest + allowedVersions: + vite: "*" + vitest: "*" diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index 974f3d036f0..8135f7e259d 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -6,8 +6,11 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import { + createStageWorkspaceConfig, createStagePnpmConfig, + DESKTOP_ASAR_UNPACK, resolveDesktopRuntimeDependencies, + resolveFffNativeDependencies, resolveBuildOptions, resolveDesktopBuildIconAssets, resolveDesktopProductName, @@ -15,6 +18,7 @@ import { resolveGitHubPublishConfig, resolveMockUpdateServerPort, resolveMockUpdateServerUrl, + STAGE_INSTALL_ARGS, } from "./build-desktop-artifact.ts"; import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts"; import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; @@ -114,17 +118,20 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { createStagePnpmConfig( { "@expo/metro-config@56.0.13": "patches/@expo%2Fmetro-config@56.0.13.patch", + "@ff-labs/fff-node@0.9.4": "patches/@ff-labs__fff-node@0.9.4.patch", "@pierre/diffs@1.1.20": "patches/@pierre%2Fdiffs@1.1.20.patch", "alchemy@2.0.0-beta.49": "patches/alchemy@2.0.0-beta.49.patch", "effect@4.0.0-beta.73": "patches/effect@4.0.0-beta.73.patch", }, { + "@ff-labs/fff-node": "0.9.4", "@pierre/diffs": "1.1.20", effect: "4.0.0-beta.73", }, ), { patchedDependencies: { + "@ff-labs/fff-node@0.9.4": "patches/@ff-labs__fff-node@0.9.4.patch", "@pierre/diffs@1.1.20": "patches/@pierre%2Fdiffs@1.1.20.patch", "effect@4.0.0-beta.73": "patches/effect@4.0.0-beta.73.patch", }, @@ -142,6 +149,49 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { ); }); + it("installs optional native dependencies for the target desktop architecture", () => { + assert.deepStrictEqual(STAGE_INSTALL_ARGS, ["install", "--prod"]); + assert.deepStrictEqual(createStageWorkspaceConfig("mac", "x64"), { + supportedArchitectures: { + os: ["darwin"], + cpu: ["x64"], + }, + }); + assert.deepStrictEqual(createStageWorkspaceConfig("win", "arm64"), { + supportedArchitectures: { + os: ["win32"], + cpu: ["arm64"], + }, + }); + assert.deepStrictEqual(createStageWorkspaceConfig("mac", "universal"), { + supportedArchitectures: { + os: ["darwin"], + cpu: ["arm64", "x64"], + }, + }); + }); + + it("unpacks the fff shared library for filesystem and FFI access", () => { + assert.deepStrictEqual(DESKTOP_ASAR_UNPACK, ["node_modules/@ff-labs/fff-bin-*/**/*"]); + }); + + it("promotes target fff binaries to direct staged dependencies", () => { + assert.deepStrictEqual(resolveFffNativeDependencies("mac", "arm64", "0.9.4"), { + "@ff-labs/fff-bin-darwin-arm64": "0.9.4", + }); + assert.deepStrictEqual(resolveFffNativeDependencies("mac", "universal", "0.9.4"), { + "@ff-labs/fff-bin-darwin-arm64": "0.9.4", + "@ff-labs/fff-bin-darwin-x64": "0.9.4", + }); + assert.deepStrictEqual(resolveFffNativeDependencies("win", "x64", "0.9.4"), { + "@ff-labs/fff-bin-win32-x64": "0.9.4", + }); + assert.deepStrictEqual(resolveFffNativeDependencies("linux", "arm64", "0.9.4"), { + "@ff-labs/fff-bin-linux-arm64-gnu": "0.9.4", + "@ff-labs/fff-bin-linux-arm64-musl": "0.9.4", + }); + }); + it("falls back to the default mock update port when the configured port is blank", () => { assert.equal(resolveMockUpdateServerUrl(undefined), "http://localhost:3000"); assert.equal(resolveMockUpdateServerUrl(4123), "http://localhost:4123"); diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index f5785f904aa..6b519b1d4e3 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -38,11 +38,19 @@ const WorkspaceConfig = Schema.Struct({ }); type WorkspaceConfig = typeof WorkspaceConfig.Type; +const StageWorkspaceConfig = Schema.Struct({ + supportedArchitectures: Schema.Struct({ + os: Schema.Array(Schema.String), + cpu: Schema.Array(Schema.String), + }), +}); + const RepoRoot = Effect.service(Path.Path).pipe( Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), ); const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString); const decodeWorkspaceConfig = Schema.decodeEffect(fromYaml(WorkspaceConfig)); +const encodeStageWorkspaceConfig = Schema.encodeEffect(fromYaml(StageWorkspaceConfig)); const readWorkspaceConfig = Effect.fn("readWorkspaceConfig")(function* () { const fs = yield* FileSystem.FileSystem; @@ -282,6 +290,47 @@ 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 function resolveFffNativeDependencies( + platform: typeof BuildPlatform.Type, + arch: typeof BuildArch.Type, + version: string, +): Record { + const architectures = arch === "universal" ? (["arm64", "x64"] as const) : [arch]; + + if (platform === "mac") { + return Object.fromEntries( + architectures.map((architecture) => [`@ff-labs/fff-bin-darwin-${architecture}`, version]), + ); + } + + if (platform === "win") { + return Object.fromEntries( + architectures.map((architecture) => [`@ff-labs/fff-bin-win32-${architecture}`, version]), + ); + } + + return Object.fromEntries( + architectures.flatMap((architecture) => + ["gnu", "musl"].map((libc) => [`@ff-labs/fff-bin-linux-${architecture}-${libc}`, version]), + ), + ); +} + +export function createStageWorkspaceConfig( + platform: typeof BuildPlatform.Type, + arch: typeof BuildArch.Type, +): typeof StageWorkspaceConfig.Type { + return { + supportedArchitectures: { + os: [platform === "mac" ? "darwin" : platform === "win" ? "win32" : "linux"], + cpu: arch === "universal" ? ["arm64", "x64"] : [arch], + }, + }; +} + export function createStagePnpmConfig( patchedDependencies: Record, dependencies: Record, @@ -702,6 +751,7 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* ( appId: "com.t3tools.t3code", productName: resolveDesktopProductName(version), artifactName: "T3-Code-${version}-${arch}.${ext}", + asarUnpack: [...DESKTOP_ASAR_UNPACK], directories: { buildResources: "apps/desktop/resources", }, @@ -909,6 +959,11 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( const stageDependencies = { ...resolvedServerDependencies, ...resolvedDesktopRuntimeDependencies, + ...resolveFffNativeDependencies( + options.platform, + options.arch, + serverPackageJson.dependencies["@ff-labs/fff-node"], + ), }; const stagePnpmConfig = createStagePnpmConfig(workspacePatchedDependencies, stageDependencies); const stagePackageJson: StagePackageJson = { @@ -939,19 +994,25 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( const stagePackageJsonString = yield* encodeJsonString(stagePackageJson); yield* fs.writeFileString(path.join(stageAppDir, "package.json"), `${stagePackageJsonString}\n`); + const stageWorkspaceConfig = createStageWorkspaceConfig(options.platform, options.arch); + const stageWorkspaceConfigString = yield* encodeStageWorkspaceConfig(stageWorkspaceConfig); + yield* fs.writeFileString( + path.join(stageAppDir, "pnpm-workspace.yaml"), + stageWorkspaceConfigString, + ); if (Object.keys(workspacePatchedDependencies).length > 0) { yield* fs.copy(path.join(repoRoot, "patches"), path.join(stageAppDir, "patches")); } yield* Effect.log("[desktop-artifact] Installing staged production dependencies..."); - const installCommand = yield* resolveSpawnCommand("vp", ["install", "--prod", "--no-optional"]); + const installCommand = yield* resolveSpawnCommand("vp", [...STAGE_INSTALL_ARGS]); yield* runCommand( ChildProcess.make(installCommand.command, installCommand.args, { cwd: stageAppDir, shell: installCommand.shell, }), - { label: "vp install --prod --no-optional", verbose: options.verbose }, + { label: "vp install --prod", verbose: options.verbose }, ); // electron-builder treats several set-but-empty variables (e.g. CSC_LINK="")