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:
robobun
2026-01-23 20:24:12 -08:00
committed by GitHub
parent 232e0df956
commit f88f60af5a
2 changed files with 162 additions and 1 deletions

View 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");
});