| author | |
| committer | |
| log | d10bd1360ace3ab1dbccc1ee706c9169982d98ae |
| tree | 43b5a0a4fe7ab4c1fbda63b75ba3ea7f7d55d522 |
| parent | c13f7d93fff0f578886985a3f673b4b21b7f6ac8 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>15 files changed, 200 insertions(+), 4 deletions(-)
apps/desktop/package.json+1-1| ... | ... | @@ -3,7 +3,7 @@ |
| 3 | 3 | "version": "0.0.42", |
| 4 | 4 | "private": true, |
| 5 | 5 | "type": "module", |
| 6 | "main": "dist-electron/main.cjs", | |
| 6 | "main": "dist-electron/boot.cjs", | |
| 7 | 7 | "scripts": { |
| 8 | 8 | "ensure:electron": "node scripts/ensure-electron-runtime.mjs", |
| 9 | 9 | "start": "node scripts/start-electron.mjs", |
apps/desktop/scripts/main-process-bundle.test.mjs+63| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import * as NodeChildProcess from "node:child_process"; | |
| 1 | 2 | import * as NodeFSP from "node:fs/promises"; |
| 2 | 3 | import * as NodeOS from "node:os"; |
| 3 | 4 | import * as NodePath from "node:path"; |
| ... | ... | @@ -108,3 +109,65 @@ void import("./linux.ts").then(({ result }) => process.emit("ready", result));`, |
| 108 | 109 | await NodeFSP.rm(directory, { recursive: true, force: true }); |
| 109 | 110 | } |
| 110 | 111 | }); |
| 112 | ||
| 113 | it("loads the emitted packaged boot entry and backend cache preload", async () => { | |
| 114 | const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-desktop-boot-")); | |
| 115 | try { | |
| 116 | const entries = ["src/boot.ts", "src/compileCache.ts"]; | |
| 117 | await NodeFSP.mkdir(NodePath.join(directory, "src")); | |
| 118 | await Promise.all( | |
| 119 | entries.map((entry) => | |
| 120 | NodeFSP.copyFile(new URL(`../${entry}`, import.meta.url), NodePath.join(directory, entry)), | |
| 121 | ), | |
| 122 | ); | |
| 123 | assert.ok(Array.isArray(desktopConfig.pack)); | |
| 124 | for (const packConfig of desktopConfig.pack) { | |
| 125 | if (!Array.isArray(packConfig.entry)) continue; | |
| 126 | if (!packConfig.entry.some((entry) => entries.includes(entry))) continue; | |
| 127 | await build({ | |
| 128 | ...packConfig, | |
| 129 | config: false, | |
| 130 | cwd: directory, | |
| 131 | tsconfig: false, | |
| 132 | sourcemap: false, | |
| 133 | onSuccess: undefined, | |
| 134 | logLevel: "silent", | |
| 135 | }); | |
| 136 | } | |
| 137 | const outputDirectory = NodePath.join(directory, "dist-electron"); | |
| 138 | const fixture = `console.log(require('node:module').getCompileCacheDir() ? 'cached' : 'uncached');`; | |
| 139 | await NodeFSP.writeFile(NodePath.join(outputDirectory, "main.cjs"), fixture); | |
| 140 | await NodeFSP.writeFile( | |
| 141 | NodePath.join(outputDirectory, "backend.mjs"), | |
| 142 | `import { getCompileCacheDir } from 'node:module'; console.log(getCompileCacheDir() ? 'cached' : 'uncached');`, | |
| 143 | ); | |
| 144 | for (const disabled of [false, true]) { | |
| 145 | for (const args of [ | |
| 146 | [NodePath.join(outputDirectory, "boot.cjs")], | |
| 147 | [ | |
| 148 | "--require", | |
| 149 | NodePath.join(outputDirectory, "compileCache.cjs"), | |
| 150 | NodePath.join(outputDirectory, "backend.mjs"), | |
| 151 | ], | |
| 152 | ]) { | |
| 153 | const child = NodeChildProcess.spawnSync(process.execPath, args, { | |
| 154 | encoding: "utf8", | |
| 155 | env: { | |
| 156 | ...process.env, | |
| 157 | APPIMAGE: "", | |
| 158 | NODE_COMPILE_CACHE: undefined, | |
| 159 | NODE_DISABLE_COMPILE_CACHE: disabled ? "1" : undefined, | |
| 160 | XDG_CACHE_HOME: directory, | |
| 161 | TMPDIR: directory, | |
| 162 | TEMP: directory, | |
| 163 | TMP: directory, | |
| 164 | }, | |
| 165 | }); | |
| 166 | assert.equal(child.status, 0, child.stderr); | |
| 167 | assert.equal(child.stdout.trim(), disabled ? "uncached" : "cached"); | |
| 168 | } | |
| 169 | } | |
| 170 | } finally { | |
| 171 | await NodeFSP.rm(directory, { recursive: true, force: true }); | |
| 172 | } | |
| 173 | }); |
apps/desktop/scripts/smoke-test.mjs+1-1| ... | ... | @@ -5,7 +5,7 @@ import { resolveElectronLaunchCommand } from "./electron-launcher.mjs"; |
| 5 | 5 | |
| 6 | 6 | const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)); |
| 7 | 7 | const desktopDir = NodePath.resolve(__dirname, ".."); |
| 8 | const mainJs = NodePath.resolve(desktopDir, "dist-electron/main.cjs"); | |
| 8 | const mainJs = NodePath.resolve(desktopDir, "dist-electron/boot.cjs"); | |
| 9 | 9 | |
| 10 | 10 | console.log("\nLaunching Electron smoke test..."); |
| 11 | 11 |
apps/desktop/src/app/DesktopEnvironment.ts+3| ... | ... | @@ -66,6 +66,8 @@ export class DesktopEnvironment extends Context.Service< |
| 66 | 66 | readonly clientAssetsDir: string; |
| 67 | 67 | readonly backendCwd: string; |
| 68 | 68 | readonly preloadPath: string; |
| 69 | // Preload that turns on the V8 compile cache for the local backend. | |
| 70 | readonly compileCachePath: string; | |
| 69 | 71 | readonly appUpdateYmlPath: string; |
| 70 | 72 | readonly devServerUrl: Option.Option<URL>; |
| 71 | 73 | readonly devRemoteT3ServerEntryPath: Option.Option<string>; |
| ... | ... | @@ -221,6 +223,7 @@ const make = Effect.fn("desktop.environment.make")(function* ( |
| 221 | 223 | clientAssetsDir: path.join(serverRoot, "apps/server/dist/client"), |
| 222 | 224 | backendCwd: input.isPackaged ? homeDirectory : appRoot, |
| 223 | 225 | preloadPath: path.join(input.dirname, "preload.cjs"), |
| 226 | compileCachePath: path.join(input.dirname, "compileCache.cjs"), | |
| 224 | 227 | appUpdateYmlPath: input.isPackaged |
| 225 | 228 | ? path.join(resourcesPath, "app-update.yml") |
| 226 | 229 | : path.join(input.appPath, "dev-app-update.yml"), |
apps/desktop/src/backend/DesktopBackendConfiguration.test.ts+7| ... | ... | @@ -235,6 +235,11 @@ describe("DesktopBackendConfiguration", () => { |
| 235 | 235 | const second = yield* configuration.resolvePrimary; |
| 236 | 236 | |
| 237 | 237 | assert.equal(first.executablePath, process.execPath); |
| 238 | assert.deepEqual(first.args.slice(0, 3), [ | |
| 239 | "--require", | |
| 240 | environment.compileCachePath, | |
| 241 | environment.backendEntryPath, | |
| 242 | ]); | |
| 238 | 243 | assert.equal(first.entryPath, environment.backendEntryPath); |
| 239 | 244 | assert.equal(first.cwd, environment.backendCwd); |
| 240 | 245 | assert.equal(first.captureOutput, true); |
| ... | ... | @@ -927,6 +932,8 @@ describe("DesktopBackendConfiguration", () => { |
| 927 | 932 | const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration; |
| 928 | 933 | const config = yield* configuration.resolvePrimary; |
| 929 | 934 | assert.equal(config.captureOutput, true); |
| 935 | // Dev never shares the prod compile cache. | |
| 936 | assert.notInclude(config.args, "--require"); | |
| 930 | 937 | }).pipe( |
| 931 | 938 | Effect.provide( |
| 932 | 939 | DesktopBackendConfiguration.layer.pipe( |
apps/desktop/src/backend/DesktopBackendConfiguration.ts+10-1| ... | ... | @@ -550,7 +550,16 @@ const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolv |
| 550 | 550 | |
| 551 | 551 | return { |
| 552 | 552 | executablePath: process.execPath, |
| 553 | args: [environment.backendEntryPath, "--bootstrap-fd", "3"], | |
| 553 | // Packaged builds only, so a dev instance never shares the cache with the | |
| 554 | // prod app it is often run from. `--require` rather than NODE_COMPILE_CACHE, | |
| 555 | // so the setting does not leak into the provider and terminal processes | |
| 556 | // the backend starts. | |
| 557 | args: [ | |
| 558 | ...(environment.isPackaged ? ["--require", environment.compileCachePath] : []), | |
| 559 | environment.backendEntryPath, | |
| 560 | "--bootstrap-fd", | |
| 561 | "3", | |
| 562 | ], | |
| 554 | 563 | entryPath: environment.backendEntryPath, |
| 555 | 564 | cwd: environment.backendCwd, |
| 556 | 565 | env: { |
apps/desktop/src/boot.ts created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | // Packaged app entry. Enables the compile cache before the main bundle loads, | |
| 2 | // so the cache also covers main.cjs itself. | |
| 3 | require("./compileCache.cjs"); | |
| 4 | require("./main.cjs"); |
apps/desktop/src/compileCache.ts created+28| ... | ... | @@ -0,0 +1,28 @@ |
| 1 | // @effect-diagnostics nodeBuiltinImport:off | |
| 2 | // Runs before any Effect runtime exists, so it stays on Node built-ins. | |
| 3 | import * as NodeModule from "node:module"; | |
| 4 | import * as NodeOS from "node:os"; | |
| 5 | import * as NodePath from "node:path"; | |
| 6 | ||
| 7 | // Turns on Node's on-disk V8 code cache for every module loaded after this one, | |
| 8 | // so later launches skip recompiling the large main and server bundles. | |
| 9 | // Packaged builds only: boot.ts loads it for the main process, and the local | |
| 10 | // backend gets it with `--require`. Dev launches main.cjs directly and skips it. | |
| 11 | // Linux uses the user's cache dir because /tmp is shared between users; the | |
| 12 | // macOS and Windows temp dirs are already per user. | |
| 13 | // | |
| 14 | // Skipped under AppImage: it mounts the app at a new /tmp/.mount_* path each | |
| 15 | // launch, and Node keys entries by path, so every launch would miss and leave | |
| 16 | // another copy behind. The backend inherits APPIMAGE, so this covers it too. | |
| 17 | try { | |
| 18 | if (!process.env.APPIMAGE) { | |
| 19 | const cacheRoot = | |
| 20 | // oxlint-disable-next-line t3code/no-global-process-runtime -- Loads before any Effect runtime. | |
| 21 | process.platform === "linux" | |
| 22 | ? process.env.XDG_CACHE_HOME || NodePath.join(NodeOS.homedir(), ".cache") | |
| 23 | : NodeOS.tmpdir(); | |
| 24 | NodeModule.enableCompileCache(NodePath.join(cacheRoot, "t3code", "compile-cache")); | |
| 25 | } | |
| 26 | } catch { | |
| 27 | // The cache is only a speedup. Never let it stop the app from starting. | |
| 28 | } |
apps/desktop/src/electron/ElectronProtocol.ts+3| ... | ... | @@ -126,6 +126,9 @@ function registerDesktopSchemePrivilegesSync(): void { |
| 126 | 126 | supportFetchAPI: true, |
| 127 | 127 | corsEnabled: true, |
| 128 | 128 | stream: true, |
| 129 | // Custom schemes skip Chromium's V8 code cache unless they opt in. | |
| 130 | // Dev stays off: Vite serves changing code at stable URLs. | |
| 131 | codeCache: true, | |
| 129 | 132 | }, |
| 130 | 133 | }, |
| 131 | 134 | { |
apps/desktop/vite.config.ts+13| ... | ... | @@ -85,6 +85,19 @@ export default defineConfig({ |
| 85 | 85 | onlyBundle: false, |
| 86 | 86 | }, |
| 87 | 87 | }, |
| 88 | { | |
| 89 | // boot.cjs requires the other two at runtime, so all three stay separate files. | |
| 90 | format: "cjs", | |
| 91 | outDir: "dist-electron", | |
| 92 | dts: false, | |
| 93 | sourcemap: true, | |
| 94 | outExtensions: () => ({ js: ".cjs" }), | |
| 95 | entry: ["src/boot.ts", "src/compileCache.ts"], | |
| 96 | clean: false, | |
| 97 | deps: { | |
| 98 | neverBundle: (id) => id === "./main.cjs" || id === "./compileCache.cjs", | |
| 99 | }, | |
| 100 | }, | |
| 88 | 101 | { |
| 89 | 102 | format: "cjs", |
| 90 | 103 | outDir: "dist-electron", |
apps/server/src/compileCache.test.ts created+53| ... | ... | @@ -0,0 +1,53 @@ |
| 1 | // @effect-diagnostics nodeBuiltinImport:off - the test kills a real Node process to prove the cache is on disk. | |
| 2 | import * as NodeChildProcess from "node:child_process"; | |
| 3 | import * as NodeFSP from "node:fs/promises"; | |
| 4 | import * as NodeOS from "node:os"; | |
| 5 | import * as NodePath from "node:path"; | |
| 6 | import { assert, it } from "@effect/vitest"; | |
| 7 | ||
| 8 | it.each([false, true])( | |
| 9 | "persists an enabled cache before forced exit (disabled: %s)", | |
| 10 | async (disabled) => { | |
| 11 | const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-compile-cache-")); | |
| 12 | try { | |
| 13 | const cacheDirectory = NodePath.join(directory, "cache"); | |
| 14 | const child = NodeChildProcess.spawnSync( | |
| 15 | process.execPath, | |
| 16 | [ | |
| 17 | "--input-type=module", | |
| 18 | "--eval", | |
| 19 | `import * as Effect from ${JSON.stringify(import.meta.resolve("effect/Effect"))}; | |
| 20 | const { flushCompileCache } = await import(${JSON.stringify(new URL("./compileCache.ts", import.meta.url).href)}); | |
| 21 | await Effect.runPromise(flushCompileCache); | |
| 22 | process.kill(process.pid, "SIGKILL");`, | |
| 23 | ], | |
| 24 | { | |
| 25 | encoding: "utf8", | |
| 26 | env: { | |
| 27 | ...process.env, | |
| 28 | NODE_COMPILE_CACHE: cacheDirectory, | |
| 29 | NODE_DISABLE_COMPILE_CACHE: disabled ? "1" : undefined, | |
| 30 | }, | |
| 31 | }, | |
| 32 | ); | |
| 33 | assert.equal(child.error, undefined); | |
| 34 | assert.equal(child.stderr, ""); | |
| 35 | assert.notEqual(child.status, 0); | |
| 36 | const entries = await NodeFSP.readdir(cacheDirectory, { recursive: true }).catch( | |
| 37 | (error: NodeJS.ErrnoException) => { | |
| 38 | if (error.code === "ENOENT") return []; | |
| 39 | throw error; | |
| 40 | }, | |
| 41 | ); | |
| 42 | const files = await Promise.all( | |
| 43 | entries.map((entry) => NodeFSP.stat(NodePath.join(cacheDirectory, entry))), | |
| 44 | ); | |
| 45 | assert.equal( | |
| 46 | files.some((entry) => entry.isFile()), | |
| 47 | !disabled, | |
| 48 | ); | |
| 49 | } finally { | |
| 50 | await NodeFSP.rm(directory, { recursive: true, force: true }); | |
| 51 | } | |
| 52 | }, | |
| 53 | ); |
apps/server/src/compileCache.ts created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | import * as NodeModule from "node:module"; | |
| 2 | import * as Effect from "effect/Effect"; | |
| 3 | ||
| 4 | // Desktop enables this cache before loading the backend. Windows force-kills | |
| 5 | // the backend on quit, so persist it after startup instead of waiting for exit. | |
| 6 | // This is a no-op when caching is disabled, including normal dev launches. | |
| 7 | export const flushCompileCache = Effect.try(() => NodeModule.flushCompileCache()).pipe( | |
| 8 | Effect.ignore, | |
| 9 | ); |
apps/server/src/serverRuntimeStartup.ts+2| ... | ... | @@ -33,6 +33,7 @@ import * as Schema from "effect/Schema"; |
| 33 | 33 | import * as Scope from "effect/Scope"; |
| 34 | 34 | |
| 35 | 35 | import * as ServerConfig from "./config.ts"; |
| 36 | import { flushCompileCache } from "./compileCache.ts"; | |
| 36 | 37 | import * as Keybindings from "./keybindings.ts"; |
| 37 | 38 | import * as ExternalLauncher from "./process/externalLauncher.ts"; |
| 38 | 39 | import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts"; |
| ... | ... | @@ -1074,6 +1075,7 @@ export const make = (options?: StartupOptions) => |
| 1074 | 1075 | }), |
| 1075 | 1076 | ); |
| 1076 | 1077 | yield* Effect.logDebug("startup phase: complete"); |
| 1078 | yield* flushCompileCache; | |
| 1077 | 1079 | }).pipe( |
| 1078 | 1080 | Effect.annotateSpans({ |
| 1079 | 1081 | "server.mode": serverConfig.mode, |
knip.jsonc+2| ... | ... | @@ -38,6 +38,8 @@ |
| 38 | 38 | "src/snapShot/GlobalShiftShortcutWorker.ts!", |
| 39 | 39 | "src/snapShot/RegionSnapShotWorker.ts!", |
| 40 | 40 | "src/snapShot/SnapShotAccessibilityWorker.ts!", |
| 41 | "src/boot.ts!", | |
| 42 | "src/compileCache.ts!", | |
| 41 | 43 | "src/main.ts!", |
| 42 | 44 | "src/preload.ts!", |
| 43 | 45 | "src/preview-pick-preload.ts!", |
scripts/build-desktop-artifact.ts+1-1| ... | ... | @@ -3670,7 +3670,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( |
| 3670 | 3670 | // Required by the .deb control file. |
| 3671 | 3671 | homepage: "https://t3.codes", |
| 3672 | 3672 | author: "T3 Tools", |
| 3673 | main: "apps/desktop/dist-electron/main.cjs", | |
| 3673 | main: "apps/desktop/dist-electron/boot.cjs", | |
| 3674 | 3674 | build: yield* createBuildConfig( |
| 3675 | 3675 | options.platform, |
| 3676 | 3676 | options.target, |