diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index bdd8ac7f48f8..cad7564a5787 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -2845,6 +2845,13 @@ fn transpile_source_code_inner( return Err(crate::Error::ParseError); } + // Set before the early returns below (cache hit, `// @bun`, async queue): + // every later module load is gated on this flag. + if is_main && !disable_transpilying { + // SAFETY: per fn contract — `jsc_vm` is the live per-thread VM. + unsafe { (*jsc_vm).has_loaded = true }; + } + let source = &parse_result.source; // Raw JSON: hand the source bytes straight to JSC. @@ -3269,11 +3276,6 @@ fn transpile_source_code_inner( print_result?; } - if is_main { - // SAFETY: per fn contract — `jsc_vm` is the live per-thread VM. - unsafe { (*jsc_vm).has_loaded = true }; - } - // `module_info.asDeserialized()`: finalize the // printer-filled record into the FFI shape consumed by C++ // (freed by C++ `~SourceProvider` via diff --git a/test/cli/run/run-cjs.test.ts b/test/cli/run/run-cjs.test.ts index ebf49dba3f5a..6c214dd6111b 100644 --- a/test/cli/run/run-cjs.test.ts +++ b/test/cli/run/run-cjs.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { mkdirSync } from "fs"; -import { bunEnv, bunExe, tmpdirSync } from "harness"; +import { bunEnv, bunExe, tempDir, tmpdirSync } from "harness"; import { join } from "path"; describe.concurrent("run-cjs", () => { @@ -17,4 +17,31 @@ describe.concurrent("run-cjs", () => { const stdout = await proc.stdout.text(); expect(stdout).toEqual("hello world\n"); }); + + test("a pre-bundled entry point still consults require.extensions", async () => { + // `bun build --target=bun --format=cjs` emits this header, and the `// @bun` + // pragma makes the parser hand the source straight to JSC without printing + // it. That must not change how the modules this entry loads are resolved. + using dir = tempDir("run-cjs-bundled-entry", { + "entry.cjs": `// @bun @bun-cjs +(function(exports, require, module, __filename, __dirname) { + require.extensions[".data"] = (module, filename) => { + module.exports = "custom-loader"; + }; + console.log(require("./asset.data")); +})`, + // If the custom loader is skipped, this is transpiled as JS/TS instead. + "asset.data": `module.exports = "default-loader";`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "entry.cjs"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toMatchObject({ stdout: "custom-loader\n", exitCode: 0 }); + }); }); diff --git a/test/cli/run/transpiler-cache.test.ts b/test/cli/run/transpiler-cache.test.ts index 2b6561b37bb2..01d569bb15ab 100644 --- a/test/cli/run/transpiler-cache.test.ts +++ b/test/cli/run/transpiler-cache.test.ts @@ -249,6 +249,49 @@ describe("transpiler cache", () => { expect(run(["--feature=OTHER", "--feature=SUPER_SECRET"])).toBe("enabled"); expect(newCacheCount()).toBe(0); // cache hit, order doesn't matter }); + + // Serving the entry point from the cache must not change how the modules it + // loads are resolved. Both of these are gated on the `has_loaded` flag, which + // used to be set only on the path that runs the printer. + describe("a cached entry point does not change how later modules load", () => { + // Padding so the entry point clears MINIMUM_CACHE_SIZE (4 KiB) and is + // eligible for the cache at all. + const filler = "\n//" + Buffer.alloc(5 * 1024, "f").toString(); + + test("require.extensions is still consulted", async () => { + writeFileSync( + join(temp_dir, "entry.js"), + `require.extensions[".data"] = (module, filename) => { + module.exports = "custom-loader"; + }; + console.log(require("./asset.data"));${filler}`, + ); + // If the custom loader is skipped, this is transpiled as JS/TS instead. + writeFileSync(join(temp_dir, "asset.data"), `module.exports = "default-loader";`); + + expect(await bunRun(join(temp_dir, "entry.js"), env)).toSpawn("custom-loader"); + expect(newCacheCount()).toBe(1); + + expect(await bunRun(join(temp_dir, "entry.js"), env)).toSpawn("custom-loader"); + expect(newCacheCount()).toBe(0); + }); + + test("unknown extensions still use the file loader", async () => { + writeFileSync( + join(temp_dir, "entry.mjs"), + `import asset from "./asset.someext"; + console.log(typeof asset === "string" ? "file-loader" : "???");${filler}`, + ); + // Not valid JS/TS, so a non-file loader fails the run outright. + writeFileSync(join(temp_dir, "asset.someext"), `hello world contents\n`); + + expect(await bunRun(join(temp_dir, "entry.mjs"), env)).toSpawn("file-loader"); + expect(newCacheCount()).toBe(1); + + expect(await bunRun(join(temp_dir, "entry.mjs"), env)).toSpawn("file-loader"); + expect(newCacheCount()).toBe(0); + }); + }); }); test("rejects cached module records containing out-of-range string indices", () => {