Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2541,6 +2541,15 @@ fn transpile_source_code_inner(
return Err(bun_core::err!("ParseError"));
}

// `has_loaded` gates every later module request (concurrent transpile
// dispatch, `require.extensions`, unknown-extension loaders), so set it on
// every path producing the entry's source, not only the printed one.
// Print-only fetches are excluded; they never load a module.
Comment thread
robobun marked this conversation as resolved.
Outdated
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.
Expand Down Expand Up @@ -2955,11 +2964,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
Expand Down
29 changes: 28 additions & 1 deletion test/cli/run/run-cjs.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -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 });
});
});
47 changes: 47 additions & 0 deletions test/cli/run/transpiler-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,53 @@ 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", () => {
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";`);

const a = bunRun(join(temp_dir, "entry.js"), env);
expect(a.stdout).toBe("custom-loader");
expect(newCacheCount()).toBe(1);

const b = bunRun(join(temp_dir, "entry.js"), env);
expect(b.stdout).toBe("custom-loader");
expect(newCacheCount()).toBe(0);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
robobun marked this conversation as resolved.
Outdated
});

test("unknown extensions still use the file loader", () => {
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`);

const a = bunRun(join(temp_dir, "entry.mjs"), env);
expect(a.stdout).toBe("file-loader");
expect(newCacheCount()).toBe(1);

const b = bunRun(join(temp_dir, "entry.mjs"), env);
expect(b.stdout).toBe("file-loader");
expect(newCacheCount()).toBe(0);
});
});
});

test("rejects cached module records containing out-of-range string indices", () => {
Expand Down
Loading