mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix(bundler): throw error when Bun.build is called from macro during bundling (#26361)
## Summary - Fixes #26360 - Detects when `Bun.build` is called from within macro mode during bundling and throws a clear error instead of hanging indefinitely ## Problem When `Bun.build` API is called to bundle a file that imports from a macro which itself uses `Bun.build`, the process would hang indefinitely due to a deadlock: 1. The bundler uses a singleton thread for processing `Bun.build` calls 2. During parsing, when a macro is encountered, it's evaluated on that thread 3. If the macro calls `Bun.build`, it tries to enqueue to the same singleton thread 4. The singleton is blocked waiting for macro completion → deadlock ## Solution Added a check in `Bun.build` that detects when it's called from macro mode (`vm.macro_mode`) and throws a clear error with guidance: ``` Bun.build cannot be called from within a macro during bundling. This would cause a deadlock because the bundler is waiting for the macro to complete, but the macro's Bun.build call is waiting for the bundler. To bundle code at compile time in a macro, use Bun.spawnSync to invoke the CLI: const result = Bun.spawnSync(["bun", "build", entrypoint, "--format=esm"]); ``` ## Test plan - [x] Added regression test in `test/regression/issue/26360.test.ts` - [x] Verified test hangs/fails with system Bun (the bug exists) - [x] Verified test passes with the fix applied - [x] Verified regular `Bun.build` (not in macro context) still works 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
139
test/regression/issue/26360.test.ts
Normal file
139
test/regression/issue/26360.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/26360
|
||||
// Bug: Bun.build API hangs indefinitely when called from within a macro that is
|
||||
// evaluated during another Bun.build call. The CLI `bun build` works correctly.
|
||||
//
|
||||
// Root cause: The bundler uses a singleton thread for processing Bun.build calls.
|
||||
// When a macro is evaluated during bundling and that macro calls Bun.build:
|
||||
// 1. The singleton bundler thread is processing the outer Bun.build
|
||||
// 2. The macro runs on the bundler thread and calls Bun.build
|
||||
// 3. The inner Bun.build tries to enqueue to the same singleton thread
|
||||
// 4. The singleton thread is blocked waiting for the macro to complete -> deadlock
|
||||
//
|
||||
// Fix: Detect when Bun.build is called from within macro mode and throw a clear error.
|
||||
|
||||
test("Bun.build from macro during bundling throws instead of hanging", async () => {
|
||||
using dir = tempDir("issue-26360", {
|
||||
// A simple file that will be bundled by the macro
|
||||
"browser.ts": `console.log("browser code");
|
||||
export default "";
|
||||
`,
|
||||
|
||||
// A macro that calls Bun.build and catches the error
|
||||
// The error should indicate that Bun.build cannot be called from macro context
|
||||
"macro.ts": `import browserCode from "./browser" with { type: "file" };
|
||||
|
||||
let errorMessage = "no error";
|
||||
try {
|
||||
const built = await Bun.build({
|
||||
entrypoints: [browserCode],
|
||||
format: "esm",
|
||||
});
|
||||
} catch (e) {
|
||||
errorMessage = "CAUGHT: " + e.message;
|
||||
}
|
||||
export const getErrorMessage = (): string => errorMessage;
|
||||
`,
|
||||
|
||||
// File that imports from the macro
|
||||
"index.ts": `import { getErrorMessage } from "./macro" with { type: "macro" };
|
||||
console.log("ERROR_MSG:", getErrorMessage());
|
||||
`,
|
||||
|
||||
// Build script that uses Bun.build API (this would hang before the fix)
|
||||
"build_script.ts": `const result = await Bun.build({
|
||||
entrypoints: ["./index.ts"],
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.log("BUILD_ERROR");
|
||||
for (const log of result.logs) {
|
||||
console.log(log.message);
|
||||
}
|
||||
} else {
|
||||
console.log("BUILD_SUCCESS");
|
||||
// Print the output to verify the macro caught the error
|
||||
const text = await result.outputs[0].text();
|
||||
console.log(text);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
// Run the build script - should complete (not hang) and the macro should have caught the error
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "build_script.ts"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// The build should succeed (the macro catches the error)
|
||||
expect(stdout).toContain("BUILD_SUCCESS");
|
||||
// The macro should have received the error message about Bun.build not being allowed
|
||||
expect(stdout).toContain("Bun.build cannot be called from within a macro");
|
||||
});
|
||||
|
||||
test("CLI bun build with macro that calls Bun.build also throws", async () => {
|
||||
using dir = tempDir("issue-26360-cli", {
|
||||
"browser.ts": `console.log("browser code");
|
||||
export default "";
|
||||
`,
|
||||
|
||||
// A macro that calls Bun.build and catches the error
|
||||
"macro.ts": `import browserCode from "./browser" with { type: "file" };
|
||||
|
||||
let errorMessage = "";
|
||||
try {
|
||||
const built = await Bun.build({
|
||||
entrypoints: [browserCode],
|
||||
format: "esm",
|
||||
});
|
||||
} catch (e) {
|
||||
errorMessage = e.message;
|
||||
}
|
||||
export const getErrorMessage = (): string => errorMessage;
|
||||
`,
|
||||
|
||||
"index.ts": `import { getErrorMessage } from "./macro" with { type: "macro" };
|
||||
console.log("ERROR_MSG:", getErrorMessage());
|
||||
`,
|
||||
});
|
||||
|
||||
// Run via CLI
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "build", "index.ts", "--target=node"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// The CLI build should also work and show the error message was caught
|
||||
expect(stdout).toContain("Bun.build cannot be called from within a macro");
|
||||
});
|
||||
|
||||
test("regular Bun.build (not in macro) still works", async () => {
|
||||
using dir = tempDir("issue-26360-normal", {
|
||||
"entry.ts": `
|
||||
console.log("hello world");
|
||||
export default "";
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [`${dir}/entry.ts`],
|
||||
format: "esm",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs.length).toBeGreaterThan(0);
|
||||
const text = await result.outputs[0].text();
|
||||
expect(text).toContain("hello world");
|
||||
});
|
||||
Reference in New Issue
Block a user