diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index 98e9aa0a506b..495eecc88590 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -635,7 +635,10 @@ impl RuntimeTranspilerCache { // that `absBufZ` used. let top = FileSystem::instance().top_level_dir; - if let Some(dir) = env_var::XDG_CACHE_HOME.get() { + if let Some(dir) = env_var::XDG_CACHE_HOME + .get() + .filter(|d| paths::is_absolute(d)) + { let parts: &[&[u8]] = &[dir, b"bun", b"@t@"]; return path_handler::join_abs_string_buf_z::( top, @@ -649,7 +652,7 @@ impl RuntimeTranspilerCache { { // On a mac, default to ~/Library/Caches/bun/* // This is different than ~/.bun/install/cache, and not configurable by the user. - if let Some(home) = env_var::HOME.get() { + if let Some(home) = env_var::HOME.get().filter(|d| paths::is_absolute(d)) { let parts: &[&[u8]] = &[home, b"Library/", b"Caches/", b"bun", b"@t@"]; return path_handler::join_abs_string_buf_z::( top, @@ -660,7 +663,7 @@ impl RuntimeTranspilerCache { } } - if let Some(dir) = env_var::HOME.get() { + if let Some(dir) = env_var::HOME.get().filter(|d| paths::is_absolute(d)) { let parts: &[&[u8]] = &[dir, b".bun", b"install", b"cache", b"@t@"]; return path_handler::join_abs_string_buf_z::( top, diff --git a/test/cli/run/transpiler-cache.test.ts b/test/cli/run/transpiler-cache.test.ts index 606582c1f2d3..415e697c115d 100644 --- a/test/cli/run/transpiler-cache.test.ts +++ b/test/cli/run/transpiler-cache.test.ts @@ -12,7 +12,7 @@ import { unlinkSync, writeFileSync, } from "fs"; -import { bunEnv, bunExe, bunRun, isWindows, tmpdirSync } from "harness"; +import { bunEnv, bunExe, bunRun, isMacOS, isWindows, tmpdirSync } from "harness"; import { mkfifo } from "mkfifo"; import { join } from "path"; @@ -210,6 +210,56 @@ describe("transpiler cache", () => { expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("no-tmpdir-cache"); expect(newCacheCount()).toBe(1); }); + test.each([ + ["an empty", ""], + ["a relative", "relxdg"], + ])("ignores %s XDG_CACHE_HOME instead of caching next to the project", async (_label, xdg_cache_home) => { + writeFileSync(join(temp_dir, "a.js"), dummyFile((50 * 1024 * 1.5) | 0, "1", "xdg-not-absolute")); + + // The XDG base directory specification requires an absolute path, so the + // next candidate in the chain is HOME. + const home = join(temp_dir, "home"); + mkdirSync(home, { recursive: true }); + const home_cache = isMacOS + ? join(home, "Library", "Caches", "bun", "@t@") + : join(home, ".bun", "install", "cache", "@t@"); + + expect( + await bunRun(join(temp_dir, "a.js"), { + ...env, + BUN_RUNTIME_TRANSPILER_CACHE_PATH: undefined, + XDG_CACHE_HOME: xdg_cache_home, + HOME: home, + USERPROFILE: home, + }), + ).toSpawn("xdg-not-absolute"); + + expect(readdirSync(home_cache)).toHaveLength(1); + // The cwd is not a cache location: neither `bun/@t@` (empty value) nor + // `relxdg/bun/@t@` (relative value) may appear in the project. + expect(existsSync(join(temp_dir, "bun"))).toBeFalse(); + expect(existsSync(join(temp_dir, "relxdg"))).toBeFalse(); + }); + test.each([ + ["an empty", ""], + ["a relative", "relhome"], + ])("disables the cache for %s HOME instead of caching next to the project", async (_label, home) => { + writeFileSync(join(temp_dir, "a.js"), dummyFile((50 * 1024 * 1.5) | 0, "1", "home-not-absolute")); + + // HOME is the last candidate, so a value that is not absolute leaves no + // cache location and the cache is disabled. + expect( + await bunRun(join(temp_dir, "a.js"), { + ...env, + BUN_RUNTIME_TRANSPILER_CACHE_PATH: undefined, + XDG_CACHE_HOME: undefined, + HOME: home, + USERPROFILE: home, + }), + ).toSpawn("home-not-absolute"); + + expect(readdirSync(temp_dir)).toEqual(["a.js"]); + }); test("works if the cache is not user-readable", async () => { mkdirSync(cache_dir, { recursive: true }); writeFileSync(join(temp_dir, "a.js"), dummyFile((50 * 1024 * 1.5) | 0, "1", "b"));