diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index 1f2447a9ac9e..21acec591e1d 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -43,7 +43,10 @@ bun_core::declare_scope!(cache, visible); /// path reinstates the bug for any previously-cached TLA module (#30887). /// Version 23: `jsx.runtime`/`jsx.development` participate in the features hash, /// and tsconfig `"jsx": "react-jsx"` now emits the production runtime (#4227). -const EXPECTED_VERSION: u32 = 23; +/// Version 24: Worker threads no longer inline `process.env.X` dot-reads. +/// Entries written by a pre-fix worker carry the writing process's env values +/// as string literals; a cache hit reinstates the bug (#34210). +const EXPECTED_VERSION: u32 = 24; /// Source files smaller than this are not written to / read from the on-disk /// transpiler cache. Originally 50 KiB, which excluded almost every file in a diff --git a/src/jsc/web_worker.rs b/src/jsc/web_worker.rs index 3124acf81235..bdc037fb7d85 100644 --- a/src/jsc/web_worker.rs +++ b/src/jsc/web_worker.rs @@ -976,6 +976,9 @@ impl WebWorker { unsafe { let b = &mut (*vm).transpiler; b.resolver.env_loader = NonNull::new(b.env); + // Match run_command.rs: no `process.env.X` dot-read inlining at runtime. + b.options.env.behavior = + bun_options_types::schema::api::DotEnvBehavior::LoadAllWithoutInlining; if let Some(graph) = parent.standalone_module_graph { (hooks.apply_standalone_runtime_flags)(b, graph); diff --git a/test/cli/run/transpiler-cache.test.ts b/test/cli/run/transpiler-cache.test.ts index d9a09d5163d1..f77334df8468 100644 --- a/test/cli/run/transpiler-cache.test.ts +++ b/test/cli/run/transpiler-cache.test.ts @@ -223,6 +223,23 @@ describe("transpiler cache", () => { expect(b.stdout == "production 5"); expect(newCacheCount()).toBe(0); }); + test("does not inline process.env in Worker threads", () => { + // https://github.com/oven-sh/bun/issues/34210 + writeFileSync( + join(temp_dir, "big-env.js"), + dummyFile((50 * 1024 * 1.5) | 0, "1", { code: "process.env.TRANSPILER_CACHE_TEST_ID" }), + ); + writeFileSync(join(temp_dir, "worker-entry.js"), `await import("./big-env.js");`); + writeFileSync(join(temp_dir, "worker-main.js"), `new Worker(new URL("./worker-entry.js", import.meta.url));`); + + const a = bunRun(join(temp_dir, "worker-main.js"), { ...env, TRANSPILER_CACHE_TEST_ID: "first" }); + expect(a.stdout).toBe("first"); + expect(newCacheCount()).toBe(1); + + const b = bunRun(join(temp_dir, "worker-main.js"), { ...env, TRANSPILER_CACHE_TEST_ID: "second" }); + expect(b.stdout).toBe("second"); + expect(newCacheCount()).toBe(0); + }); test("--feature flag invalidates cache", () => { // feature() can only appear in an if/ternary, so wrap it const code = `import { feature } from "bun:bundle";\nif (feature("SUPER_SECRET")) console.log("enabled"); else console.log("disabled");`; diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts index 68d6c6f3f103..e494a9eb6152 100644 --- a/test/js/node/worker_threads/worker_threads.test.ts +++ b/test/js/node/worker_threads/worker_threads.test.ts @@ -1419,6 +1419,39 @@ test("*Internal introspection methods are DontEnum on Worker.prototype", () => { expect(enumerable).not.toContain("cpuUsageInternal"); }); +test("env: {} scrubs the launch environment from the worker's process.env", async () => { + // https://github.com/oven-sh/bun/issues/34210 + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `const { Worker } = require("node:worker_threads"); + const src = \`const { parentPort } = require("node:worker_threads"); + parentPort.postMessage({ + keys: Object.keys(process.env), + secret: process.env.LAUNCH_SECRET ?? null, + only: process.env.ONLY ?? null, + inSecret: "LAUNCH_SECRET" in process.env, + node_env: process.env.NODE_ENV ?? null, + });\`; + const w = new Worker(src, { eval: true, env: { ONLY: "1" } }); + w.once("message", m => { console.log(JSON.stringify(m)); w.terminate(); });`, + ], + env: { ...bunEnv, LAUNCH_SECRET: "s3cr3t", NODE_ENV: "production" }, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(JSON.parse(stdout.trim())).toEqual({ + keys: ["ONLY"], + secret: null, + only: "1", + inSecret: false, + node_env: null, + }); + expect(exitCode).toBe(0); +}); + describe("env: SHARE_ENV shares the spawning thread's env, not a process-wide one", () => { async function run(mode: string) { const proc = Bun.spawn({