Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
bcf12f361f fix(transpiler): convert import to require for runtime helpers in CJS modules
When transpiling `using` declarations in CommonJS modules, the parser
generates `import { __using, __callDispose } from "bun:wrap"` statements.
These `import` statements are invalid inside the CJS function wrapper,
causing a "Expected CommonJS module to have a function wrapper" error.

Three changes:
- In js_printer.zig: convert `import` statements to `var { ... } = require(...)`
  when the output module type is CJS
- In transpiler.zig: pass `resolve_result.module_type` to ParseOptions so the
  parser correctly identifies .cjs files
- In transpiler.zig: prioritize `ast.exports_kind == .cjs` over the CLI
  output_format when determining printer module_type

Closes #11100

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 10:05:18 +00:00
3 changed files with 165 additions and 3 deletions

View File

@@ -4560,6 +4560,63 @@ fn NewPrinter(
return;
}
// When rewriting ESM to CJS (e.g. for .cjs files or files detected as CommonJS),
// convert `import { x, y } from "path"` to `var { x, y } = require("path")`.
// This is needed because import statements are invalid inside a CJS function wrapper.
if (rewrite_esm_to_cjs or p.options.module_type == .cjs) {
const has_items = s.items.len > 0 or s.default_name != null or record.flags.contains_import_star;
if (has_items) {
p.print("var ");
if (record.flags.contains_import_star and s.items.len == 0 and s.default_name == null) {
// import * as ns from "path" → var ns = require("path")
p.printSymbol(s.namespace_ref);
} else {
// import { a, b } from "path" → var { a, b } = require("path")
// import def, { a } from "path" → var { default: def, a } = require("path")
p.print("{");
p.printSpace();
var cjs_item_count: usize = 0;
if (s.default_name) |default_name| {
p.print("default:");
p.printSpace();
p.printSymbol(default_name.ref.?);
cjs_item_count += 1;
}
for (s.items) |item| {
if (cjs_item_count > 0) {
p.print(",");
p.printSpace();
}
p.printClauseItemAs(item, .@"var");
cjs_item_count += 1;
}
if (record.flags.contains_import_star) {
// This is a rare edge case; just add it as an extra item
if (cjs_item_count > 0) {
p.print(",");
p.printSpace();
}
}
p.printSpace();
p.print("}");
}
p.@"print = "();
}
p.print("require(");
p.printImportRecordPath(record);
p.print(")");
p.printSemicolonAfterStatement();
return;
}
p.print("import");
var item_count: usize = 0;

View File

@@ -640,6 +640,7 @@ pub const Transpiler = struct {
.jsx = resolve_result.jsx,
.emit_decorator_metadata = resolve_result.flags.emit_decorator_metadata,
.experimental_decorators = resolve_result.flags.experimental_decorators,
.module_type = resolve_result.module_type,
},
client_entry_point_,
) orelse {
@@ -838,6 +839,7 @@ pub const Transpiler = struct {
.minify_syntax = transpiler.options.minify_syntax,
.minify_identifiers = transpiler.options.minify_identifiers,
.transform_only = transpiler.options.transform_only,
.module_type = if (ast.exports_kind == .cjs) .cjs else .esm,
.import_meta_ref = ast.import_meta_ref,
.runtime_transpiler_cache = runtime_transpiler_cache,
.print_dce_annotations = transpiler.options.emit_dce_annotations,
@@ -864,12 +866,12 @@ pub const Transpiler = struct {
.minify_syntax = transpiler.options.minify_syntax,
.minify_identifiers = transpiler.options.minify_identifiers,
.transform_only = transpiler.options.transform_only,
.module_type = if (is_bun and transpiler.options.transform_only)
.module_type = if (ast.exports_kind == .cjs)
.cjs
else if (is_bun and transpiler.options.transform_only)
// this is for when using `bun build --no-bundle`
// it should copy what was passed for the cli
transpiler.options.output_format
else if (ast.exports_kind == .cjs)
.cjs
else
.esm,
.inline_require_and_import_errors = false,

View File

@@ -0,0 +1,103 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/11100
// `using` syntax should work in CommonJS modules
test("using works in .cjs file", async () => {
using dir = tempDir("issue-11100", {
"test.cjs": `
using server = { [Symbol.dispose]() { console.log("disposed"); } };
console.log("hello");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.cjs"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toBe("hello\ndisposed\n");
expect(exitCode).toBe(0);
});
test("using works in .js file with require (CJS detection)", async () => {
using dir = tempDir("issue-11100", {
"test.js": `
const path = require("path");
using server = { [Symbol.dispose]() { console.log("disposed"); } };
console.log("hello", path.join("a", "b"));
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toContain("hello");
expect(stdout).toContain("disposed");
expect(exitCode).toBe(0);
});
test("await using works in .cjs file", async () => {
using dir = tempDir("issue-11100", {
"test.cjs": `
async function main() {
await using server = { [Symbol.asyncDispose]() { console.log("async disposed"); return Promise.resolve(); } };
console.log("hello");
}
main();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.cjs"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toBe("hello\nasync disposed\n");
expect(exitCode).toBe(0);
});
test("bun build --no-bundle emits require for bun:wrap in CJS", async () => {
using dir = tempDir("issue-11100", {
"test.cjs": `
using server = { [Symbol.dispose]() { console.log("disposed"); } };
console.log("hello");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--no-bundle", "test.cjs"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Should use require() not import for CJS files
expect(stdout).toContain("require(");
expect(stdout).not.toContain("import ");
expect(exitCode).toBe(0);
});