Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
ac01044276 fix(bundler): default import from 'bun' with --bytecode no longer undefined
When using `--bytecode`, the bundler wraps output in CJS format. The CJS
lowering pass was incorrectly adding `.default` property access to
`globalThis.Bun` for default imports (e.g. `import Bun from 'bun'`).
Since `globalThis.Bun` is already the namespace object and doesn't have
a `default` property, this caused the imported value to be `undefined`.

The fix has three parts:
- Skip CJS interop wrapping (`__toESM`) for `bun` tagged imports in
  scanImportsAndExports, since `globalThis.Bun` doesn't need it
- Pass `import_record_index` through to `namespace_alias` so the printer
  can identify the import source
- In the printer, skip the `.default` property access for `bun` tagged
  import records, while preserving named property accesses like `.serve`

Closes #20670

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 10:30:56 +00:00
4 changed files with 197 additions and 11 deletions

View File

@@ -2480,6 +2480,7 @@ pub const LinkerContext = struct {
c.graph.symbols.get(import_ref).?.namespace_alias = js_ast.G.NamespaceAlias{
.namespace_ref = result.namespace_ref,
.alias = result.alias,
.import_record_index = named_import.import_record_index,
};
},
.normal_and_namespace => {
@@ -2498,6 +2499,7 @@ pub const LinkerContext = struct {
c.graph.symbols.get(import_ref).?.namespace_alias = js_ast.G.NamespaceAlias{
.namespace_ref = result.namespace_ref,
.alias = result.alias,
.import_record_index = named_import.import_record_index,
};
},
.cycle => {

View File

@@ -659,6 +659,10 @@ pub fn scanImportsAndExports(this: *LinkerContext) ScanImportsAndExportsError!vo
if (!record.source_index.isValid() or this.isExternalDynamicImport(record, source_index)) {
if (output_format == .internal_bake_dev) continue;
// The "bun" module is replaced with "globalThis.Bun" by the
// printer. It doesn't need CJS interop wrapping.
if (record.tag == .bun) continue;
// This is an external import. Check if it will be a "require()" call.
if (kind == .require or !output_format.keepES6ImportExportSyntax() or kind == .dynamic) {
if (record.source_index.isValid() and kind == .dynamic and ast_flags[other_id].force_cjs_to_esm) {

View File

@@ -2970,17 +2970,29 @@ fn NewPrinter(
p.printSpaceBeforeIdentifier();
p.addSourceMapping(expr.loc);
p.printSymbol(namespace.namespace_ref);
const alias = namespace.alias;
if (js_lexer.isIdentifier(alias)) {
p.print(".");
// TODO: addSourceMappingForName
p.printIdentifier(alias);
} else {
p.print("[");
// TODO: addSourceMappingForName
// p.addSourceMappingForName(alias);
p.printStringLiteralUTF8(alias, false);
p.print("]");
// The "bun" module is replaced with "globalThis.Bun"
// by the printer, which is already the namespace object
// itself. For default imports, skip the ".default" property
// access since globalThis.Bun IS the namespace and does
// not have a "default" property.
const skip_alias = is_bun_platform and
strings.eqlComptime(namespace.alias, "default") and
namespace.import_record_index < p.import_records.len and
p.importRecord(namespace.import_record_index).tag == .bun;
if (!skip_alias) {
const alias = namespace.alias;
if (js_lexer.isIdentifier(alias)) {
p.print(".");
// TODO: addSourceMappingForName
p.printIdentifier(alias);
} else {
p.print("[");
// TODO: addSourceMappingForName
// p.addSourceMappingForName(alias);
p.printStringLiteralUTF8(alias, false);
p.print("]");
}
}
if (wrap) {

View File

@@ -0,0 +1,168 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
// Regression test for https://github.com/oven-sh/bun/issues/20670
// Default import from 'bun' was undefined when using --bytecode flag.
// The CJS lowering pass incorrectly added ".default" property access
// to globalThis.Bun, which doesn't have a "default" property.
describe("issue #20670: import from 'bun' with --bytecode", () => {
test("default import", async () => {
using dir = tempDir("20670-default", {
"index.js": `
import Bun from 'bun';
console.log(Bun === undefined ? 'FAIL' : 'PASS');
`,
});
await using build = Bun.spawn({
cmd: [
bunExe(),
"build",
"--target=bun",
"--bytecode",
"--outdir",
String(dir) + "/out",
join(String(dir), "index.js"),
],
env: bunEnv,
stderr: "pipe",
});
const [buildStderr, buildExit] = await Promise.all([build.stderr.text(), build.exited]);
expect(buildStderr).toBe("");
expect(buildExit).toBe(0);
await using run = Bun.spawn({
cmd: [bunExe(), join(String(dir), "out", "index.js")],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([run.stdout.text(), run.stderr.text(), run.exited]);
expect(stdout).toContain("PASS");
expect(exitCode).toBe(0);
});
test("default import with alias name", async () => {
using dir = tempDir("20670-alias", {
"index.js": `
import MyBun from 'bun';
console.log(MyBun === undefined ? 'FAIL' : 'PASS');
`,
});
await using build = Bun.spawn({
cmd: [
bunExe(),
"build",
"--target=bun",
"--bytecode",
"--outdir",
String(dir) + "/out",
join(String(dir), "index.js"),
],
env: bunEnv,
stderr: "pipe",
});
const [buildStderr, buildExit] = await Promise.all([build.stderr.text(), build.exited]);
expect(buildStderr).toBe("");
expect(buildExit).toBe(0);
await using run = Bun.spawn({
cmd: [bunExe(), join(String(dir), "out", "index.js")],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([run.stdout.text(), run.stderr.text(), run.exited]);
expect(stdout).toContain("PASS");
expect(exitCode).toBe(0);
});
test("default import combined with named import", async () => {
using dir = tempDir("20670-combined", {
"index.js": `
import Bun, { serve } from 'bun';
console.log(Bun === undefined ? 'FAIL:default' : 'PASS:default');
console.log(typeof serve === 'function' ? 'PASS:named' : 'FAIL:named');
`,
});
await using build = Bun.spawn({
cmd: [
bunExe(),
"build",
"--target=bun",
"--bytecode",
"--outdir",
String(dir) + "/out",
join(String(dir), "index.js"),
],
env: bunEnv,
stderr: "pipe",
});
const [buildStderr, buildExit] = await Promise.all([build.stderr.text(), build.exited]);
expect(buildStderr).toBe("");
expect(buildExit).toBe(0);
await using run = Bun.spawn({
cmd: [bunExe(), join(String(dir), "out", "index.js")],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([run.stdout.text(), run.stderr.text(), run.exited]);
expect(stdout).toContain("PASS:default");
expect(stdout).toContain("PASS:named");
expect(exitCode).toBe(0);
});
test("namespace import", async () => {
using dir = tempDir("20670-star", {
"index.js": `
import * as Bun from 'bun';
console.log(Bun === undefined ? 'FAIL' : 'PASS');
`,
});
await using build = Bun.spawn({
cmd: [
bunExe(),
"build",
"--target=bun",
"--bytecode",
"--outdir",
String(dir) + "/out",
join(String(dir), "index.js"),
],
env: bunEnv,
stderr: "pipe",
});
const [buildStderr, buildExit] = await Promise.all([build.stderr.text(), build.exited]);
expect(buildStderr).toBe("");
expect(buildExit).toBe(0);
await using run = Bun.spawn({
cmd: [bunExe(), join(String(dir), "out", "index.js")],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([run.stdout.text(), run.stderr.text(), run.exited]);
expect(stdout).toContain("PASS");
expect(exitCode).toBe(0);
});
});