diff --git a/apps/server/src/bin.ts b/apps/server/src/bin.ts index 53a1b9205c9a1..936c79943fd74 100644 --- a/apps/server/src/bin.ts +++ b/apps/server/src/bin.ts @@ -5,6 +5,7 @@ * of evaluation) loads; everything else defers to the real CLI in ./binCli.ts. */ import { isEntrypoint } from "./entrypoint.ts"; +import { enableCliCompileCache } from "./cliCompileCache.ts"; if ( isEntrypoint({ @@ -35,6 +36,7 @@ if ( const { runAcpMcpCliFastPath } = await import("./mcp/AcpMcpStdioBridge.ts"); await runAcpMcpCliFastPath(command, process.argv.slice(3)); } else { + enableCliCompileCache(); // Windows desktop launches without stdin; attach before reading, except on fast paths using pipes. const { attachConsoleStdin } = await import("./cli/consoleStdin.ts"); attachConsoleStdin(); diff --git a/apps/server/src/cliCompileCache.test.ts b/apps/server/src/cliCompileCache.test.ts new file mode 100644 index 0000000000000..0603ce5178624 --- /dev/null +++ b/apps/server/src/cliCompileCache.test.ts @@ -0,0 +1,69 @@ +// @effect-diagnostics nodeBuiltinImport:off -- Resolves the real CLI entrypoint for subprocess tests. +import * as NodeURL from "node:url"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Stream from "effect/Stream"; +import { ChildProcess, ChildProcessSpawner } from "effect/process"; + +it.live("enables CLI caching while respecting overrides and unavailable storage", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-cli-cache-test-" }); + const report = `import { getCompileCacheDir, registerHooks } from 'node:module'; registerHooks({ resolve(specifier, context, nextResolve) { if (specifier === './binCli.ts') console.error('CACHE=' + (getCompileCacheDir() ?? 'disabled')); return nextResolve(specifier, context); } });`; + const run = Effect.fn("cliCompileCache.test.run")(function* (env: Record) { + const child = yield* spawner.spawn( + ChildProcess.make( + process.execPath, + [ + "--experimental-strip-types", + "--import", + `data:text/javascript,${encodeURIComponent(report)}`, + NodeURL.fileURLToPath(new URL("./bin.ts", import.meta.url)), + "--help", + ], + { + env: { + XDG_CACHE_HOME: `${root}/cache`, + T3CODE_HOME: `${root}/home`, + NODE_COMPILE_CACHE: undefined, + NODE_DISABLE_COMPILE_CACHE: undefined, + APPIMAGE: "", + ...env, + }, + }, + ), + ); + return yield* Effect.all( + { + stdout: child.stdout.pipe(Stream.decodeText(), Stream.mkString), + stderr: child.stderr.pipe(Stream.decodeText(), Stream.mkString), + exitCode: child.exitCode.pipe(Effect.map(Number)), + }, + { concurrency: "unbounded" }, + ); + }, Effect.scoped); + const normal = yield* run({}); + expect(normal.exitCode).toBe(0); + expect(normal.stderr.replaceAll("\\", "/")).toContain( + `CACHE=${root}/cache/t3code/compile-cache`.replaceAll("\\", "/"), + ); + const appImage = yield* run({ APPIMAGE: `${root}/T3Code.AppImage` }); + expect(appImage.exitCode).toBe(0); + expect(appImage.stderr).toContain("CACHE=disabled"); + const existing = yield* run({ NODE_COMPILE_CACHE: `${root}/explicit` }); + expect(existing.exitCode).toBe(0); + expect(existing.stderr.replaceAll("\\", "/")).toContain( + `${root}/explicit`.replaceAll("\\", "/"), + ); + const disabled = yield* run({ NODE_DISABLE_COMPILE_CACHE: "1" }); + expect(disabled.exitCode).toBe(0); + expect(disabled.stderr).toContain("CACHE=disabled"); + yield* fs.writeFileString(`${root}/blocked`, "not a directory"); + const unavailable = yield* run({ XDG_CACHE_HOME: `${root}/blocked` }); + expect(unavailable.exitCode).toBe(0); + expect(unavailable.stderr).toContain("CACHE=disabled"); + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), +); diff --git a/apps/server/src/cliCompileCache.ts b/apps/server/src/cliCompileCache.ts new file mode 100644 index 0000000000000..4387de73ba4cf --- /dev/null +++ b/apps/server/src/cliCompileCache.ts @@ -0,0 +1,17 @@ +// @effect-diagnostics nodeBuiltinImport:off -- Runs before the CLI's Effect runtime loads. +import * as NodeModule from "node:module"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; +import * as NodeSea from "node:sea"; + +export function enableCliCompileCache(): void { + try { + // AppImage paths change each launch; SEA has no separate modules to cache. + if (process.env.APPIMAGE || NodeSea.isSea()) return; + const cacheRoot = + process.env.XDG_CACHE_HOME || + // oxlint-disable-next-line t3code/no-global-process-runtime -- Runs before the CLI's Effect runtime loads. + (process.platform === "linux" ? NodePath.join(NodeOS.homedir(), ".cache") : NodeOS.tmpdir()); + NodeModule.enableCompileCache(NodePath.join(cacheRoot, "t3code", "compile-cache")); + } catch {} +}