| 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,7 +3,7 @@ |
| 3 | "version": "0.0.42", | 3 | "version": "0.0.42", |
| 4 | "private": true, | 4 | "private": true, |
| 5 | "type": "module", | 5 | "type": "module", |
| 6 | "main": "dist-electron/main.cjs", | 6 | "main": "dist-electron/boot.cjs", |
| 7 | "scripts": { | 7 | "scripts": { |
| 8 | "ensure:electron": "node scripts/ensure-electron-runtime.mjs", | 8 | "ensure:electron": "node scripts/ensure-electron-runtime.mjs", |
| 9 | "start": "node scripts/start-electron.mjs", | 9 | "start": "node scripts/start-electron.mjs", |
apps/desktop/scripts/main-process-bundle.test.mjs+63| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import * as NodeChildProcess from "node:child_process"; | ||
| 1 | import * as NodeFSP from "node:fs/promises"; | 2 | import * as NodeFSP from "node:fs/promises"; |
| 2 | import * as NodeOS from "node:os"; | 3 | import * as NodeOS from "node:os"; |
| 3 | import * as NodePath from "node:path"; | 4 | import * as NodePath from "node:path"; |
| ... | @@ -108,3 +109,65 @@ void import("./linux.ts").then(({ result }) => process.emit("ready", result));`, | ... | @@ -108,3 +109,65 @@ void import("./linux.ts").then(({ result }) => process.emit("ready", result));`, |
| 108 | await NodeFSP.rm(directory, { recursive: true, force: true }); | 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,7 +5,7 @@ import { resolveElectronLaunchCommand } from "./electron-launcher.mjs"; |
| 5 | 5 | ||
| 6 | const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)); | 6 | const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)); |
| 7 | const desktopDir = NodePath.resolve(__dirname, ".."); | 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 | console.log("\nLaunching Electron smoke test..."); | 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,6 +66,8 @@ export class DesktopEnvironment extends Context.Service< |
| 66 | readonly clientAssetsDir: string; | 66 | readonly clientAssetsDir: string; |
| 67 | readonly backendCwd: string; | 67 | readonly backendCwd: string; |
| 68 | readonly preloadPath: string; | 68 | readonly preloadPath: string; |
| 69 | // Preload that turns on the V8 compile cache for the local backend. | ||
| 70 | readonly compileCachePath: string; | ||
| 69 | readonly appUpdateYmlPath: string; | 71 | readonly appUpdateYmlPath: string; |
| 70 | readonly devServerUrl: Option.Option<URL>; | 72 | readonly devServerUrl: Option.Option<URL>; |
| 71 | readonly devRemoteT3ServerEntryPath: Option.Option<string>; | 73 | readonly devRemoteT3ServerEntryPath: Option.Option<string>; |
| ... | @@ -221,6 +223,7 @@ const make = Effect.fn("desktop.environment.make")(function* ( | ... | @@ -221,6 +223,7 @@ const make = Effect.fn("desktop.environment.make")(function* ( |
| 221 | clientAssetsDir: path.join(serverRoot, "apps/server/dist/client"), | 223 | clientAssetsDir: path.join(serverRoot, "apps/server/dist/client"), |
| 222 | backendCwd: input.isPackaged ? homeDirectory : appRoot, | 224 | backendCwd: input.isPackaged ? homeDirectory : appRoot, |
| 223 | preloadPath: path.join(input.dirname, "preload.cjs"), | 225 | preloadPath: path.join(input.dirname, "preload.cjs"), |
| 226 | compileCachePath: path.join(input.dirname, "compileCache.cjs"), | ||
| 224 | appUpdateYmlPath: input.isPackaged | 227 | appUpdateYmlPath: input.isPackaged |
| 225 | ? path.join(resourcesPath, "app-update.yml") | 228 | ? path.join(resourcesPath, "app-update.yml") |
| 226 | : path.join(input.appPath, "dev-app-update.yml"), | 229 | : path.join(input.appPath, "dev-app-update.yml"), |
apps/desktop/src/backend/DesktopBackendConfiguration.test.ts+7| ... | @@ -235,6 +235,11 @@ describe("DesktopBackendConfiguration", () => { | ... | @@ -235,6 +235,11 @@ describe("DesktopBackendConfiguration", () => { |
| 235 | const second = yield* configuration.resolvePrimary; | 235 | const second = yield* configuration.resolvePrimary; |
| 236 | 236 | ||
| 237 | assert.equal(first.executablePath, process.execPath); | 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 | assert.equal(first.entryPath, environment.backendEntryPath); | 243 | assert.equal(first.entryPath, environment.backendEntryPath); |
| 239 | assert.equal(first.cwd, environment.backendCwd); | 244 | assert.equal(first.cwd, environment.backendCwd); |
| 240 | assert.equal(first.captureOutput, true); | 245 | assert.equal(first.captureOutput, true); |
| ... | @@ -927,6 +932,8 @@ describe("DesktopBackendConfiguration", () => { | ... | @@ -927,6 +932,8 @@ describe("DesktopBackendConfiguration", () => { |
| 927 | const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration; | 932 | const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration; |
| 928 | const config = yield* configuration.resolvePrimary; | 933 | const config = yield* configuration.resolvePrimary; |
| 929 | assert.equal(config.captureOutput, true); | 934 | assert.equal(config.captureOutput, true); |
| 935 | // Dev never shares the prod compile cache. | ||
| 936 | assert.notInclude(config.args, "--require"); | ||
| 930 | }).pipe( | 937 | }).pipe( |
| 931 | Effect.provide( | 938 | Effect.provide( |
| 932 | DesktopBackendConfiguration.layer.pipe( | 939 | DesktopBackendConfiguration.layer.pipe( |
apps/desktop/src/backend/DesktopBackendConfiguration.ts+10-1| ... | @@ -550,7 +550,16 @@ const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolv | ... | @@ -550,7 +550,16 @@ const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolv |
| 550 | 550 | ||
| 551 | return { | 551 | return { |
| 552 | executablePath: process.execPath, | 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 | entryPath: environment.backendEntryPath, | 563 | entryPath: environment.backendEntryPath, |
| 555 | cwd: environment.backendCwd, | 564 | cwd: environment.backendCwd, |
| 556 | env: { | 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,6 +126,9 @@ function registerDesktopSchemePrivilegesSync(): void { |
| 126 | supportFetchAPI: true, | 126 | supportFetchAPI: true, |
| 127 | corsEnabled: true, | 127 | corsEnabled: true, |
| 128 | stream: true, | 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,6 +85,19 @@ export default defineConfig({ |
| 85 | onlyBundle: false, | 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 | format: "cjs", | 102 | format: "cjs", |
| 90 | outDir: "dist-electron", | 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,6 +33,7 @@ import * as Schema from "effect/Schema"; |
| 33 | import * as Scope from "effect/Scope"; | 33 | import * as Scope from "effect/Scope"; |
| 34 | 34 | ||
| 35 | import * as ServerConfig from "./config.ts"; | 35 | import * as ServerConfig from "./config.ts"; |
| 36 | import { flushCompileCache } from "./compileCache.ts"; | ||
| 36 | import * as Keybindings from "./keybindings.ts"; | 37 | import * as Keybindings from "./keybindings.ts"; |
| 37 | import * as ExternalLauncher from "./process/externalLauncher.ts"; | 38 | import * as ExternalLauncher from "./process/externalLauncher.ts"; |
| 38 | import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts"; | 39 | import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts"; |
| ... | @@ -1074,6 +1075,7 @@ export const make = (options?: StartupOptions) => | ... | @@ -1074,6 +1075,7 @@ export const make = (options?: StartupOptions) => |
| 1074 | }), | 1075 | }), |
| 1075 | ); | 1076 | ); |
| 1076 | yield* Effect.logDebug("startup phase: complete"); | 1077 | yield* Effect.logDebug("startup phase: complete"); |
| 1078 | yield* flushCompileCache; | ||
| 1077 | }).pipe( | 1079 | }).pipe( |
| 1078 | Effect.annotateSpans({ | 1080 | Effect.annotateSpans({ |
| 1079 | "server.mode": serverConfig.mode, | 1081 | "server.mode": serverConfig.mode, |
knip.jsonc+2| ... | @@ -38,6 +38,8 @@ | ... | @@ -38,6 +38,8 @@ |
| 38 | "src/snapShot/GlobalShiftShortcutWorker.ts!", | 38 | "src/snapShot/GlobalShiftShortcutWorker.ts!", |
| 39 | "src/snapShot/RegionSnapShotWorker.ts!", | 39 | "src/snapShot/RegionSnapShotWorker.ts!", |
| 40 | "src/snapShot/SnapShotAccessibilityWorker.ts!", | 40 | "src/snapShot/SnapShotAccessibilityWorker.ts!", |
| 41 | "src/boot.ts!", | ||
| 42 | "src/compileCache.ts!", | ||
| 41 | "src/main.ts!", | 43 | "src/main.ts!", |
| 42 | "src/preload.ts!", | 44 | "src/preload.ts!", |
| 43 | "src/preview-pick-preload.ts!", | 45 | "src/preview-pick-preload.ts!", |
scripts/build-desktop-artifact.ts+1-1| ... | @@ -3670,7 +3670,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( | ... | @@ -3670,7 +3670,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( |
| 3670 | // Required by the .deb control file. | 3670 | // Required by the .deb control file. |
| 3671 | homepage: "https://t3.codes", | 3671 | homepage: "https://t3.codes", |
| 3672 | author: "T3 Tools", | 3672 | author: "T3 Tools", |
| 3673 | main: "apps/desktop/dist-electron/main.cjs", | 3673 | main: "apps/desktop/dist-electron/boot.cjs", |
| 3674 | build: yield* createBuildConfig( | 3674 | build: yield* createBuildConfig( |
| 3675 | options.platform, | 3675 | options.platform, |
| 3676 | options.target, | 3676 | options.target, |