Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
c710e113ba Fix bundler generating invalid syntax for void import() expressions
When bundling code with `void import("./module")` where the result is
unused, the bundler was generating invalid JavaScript syntax:
`Promise.resolve().then(() => )`

The issue occurred when:
1. A dynamic import was detected (record.kind == .dynamic)
2. The import result was unused (expr_result_is_unused flag set)
3. exports_ref was set to None due to unused result
4. No wrapper_ref was available
5. The .then() callback was printed but with no body

This fix checks upfront whether there's anything to import before
emitting the Promise wrapper. If there's nothing to import (no
wrapper_ref, no exports_ref, no dev server files), it emits `void 0`
instead of creating an unnecessary Promise. Source mapping is added
to map the generated code back to the original import location.

When there IS something to import, it still uses the proper
`Promise.resolve().then(() => ...)` wrapper to preserve side effects.

Fixes #24709

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 11:19:52 +00:00
2 changed files with 119 additions and 3 deletions

View File

@@ -1651,13 +1651,28 @@ fn NewPrinter(
}
// Internal "require()" or "import()"
if (record.kind == .dynamic) {
// For dynamic imports, check if there's actually something to import
const needs_dynamic_wrapper = record.kind == .dynamic;
const wrap_with_to_esm = record.wrap_with_to_esm;
const will_print_something = meta.wrapper_ref.isValid() or
meta.exports_ref.isValid() or
p.options.input_files_for_dev_server != null;
if (needs_dynamic_wrapper) {
if (!will_print_something) {
// Nothing to import - emit void 0 and return early
// Add source mapping so debuggers map to the original import location
p.addSourceMapping(record.range.loc);
p.printSpaceBeforeIdentifier();
p.print("void 0");
return;
}
p.printSpaceBeforeIdentifier();
p.print("Promise.resolve()");
level = p.printDotThenPrefix();
}
defer if (record.kind == .dynamic) p.printDotThenSuffix();
defer if (needs_dynamic_wrapper) p.printDotThenSuffix();
// Make sure the comma operator is properly wrapped
const wrap_comma_operator = meta.exports_ref.isValid() and
@@ -1667,7 +1682,6 @@ fn NewPrinter(
defer if (wrap_comma_operator) p.print(")");
// Wrap this with a call to "__toESM()" if this is a CommonJS file
const wrap_with_to_esm = record.wrap_with_to_esm;
if (wrap_with_to_esm) {
p.printSpaceBeforeIdentifier();
p.printSymbol(p.options.to_esm_ref);

View File

@@ -0,0 +1,102 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("bundler should generate valid syntax for void import() expressions - issue #24709", async () => {
using dir = tempDir("issue-24709", {
"bug.ts": `export function main() {
void import("./bug.ts");
}`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "bug.ts", "--format=esm"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// The output should contain valid JavaScript
// It should NOT contain invalid syntax like `() => )`
expect(stdout).not.toContain("() => )");
// When there's nothing to import (self-import with void), it should emit void 0
expect(stdout).toContain("void 0");
// The bundled output should be syntactically valid
// Try to parse it by running it through Bun
await using parseProc = Bun.spawn({
cmd: [bunExe(), "-e", stdout],
env: bunEnv,
stderr: "pipe",
});
const parseExitCode = await parseProc.exited;
expect(parseExitCode).toBe(0);
expect(exitCode).toBe(0);
});
test("bundler should preserve import for side effects in void import()", async () => {
using dir = tempDir("issue-24709-external", {
"entry.ts": `export function loadModule() {
void import("some-external-module");
}`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "entry.ts", "--format=esm", "--external", "some-external-module"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
// External imports should be preserved as actual import() calls
expect(stdout).toContain('import("some-external-module")');
expect(exitCode).toBe(0);
});
test("bundler should execute side effects when bundling void import() with multiple files", async () => {
using dir = tempDir("issue-24709-side-effects", {
"entry.ts": `export function loadOther() {
void import("./other.ts");
}`,
"other.ts": `console.log("Side effect executed!");
export const value = 42;`,
"test.ts": `import { loadOther } from "./bundle.js";
loadOther();
await Bun.sleep(0);`,
});
// First, bundle entry.ts
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "entry.ts", "--format=esm", "--outfile=bundle.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
await buildProc.exited;
// Then run the test to ensure side effects execute
await using runProc = Bun.spawn({
cmd: [bunExe(), "test.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, exitCode] = await Promise.all([runProc.stdout.text(), runProc.exited]);
// The side effect should have executed
expect(stdout).toContain("Side effect executed!");
expect(exitCode).toBe(0);
});