diff --git a/src/bundler/LinkerContext.zig b/src/bundler/LinkerContext.zig index 9d6a86cdfa..9fa37e1df7 100644 --- a/src/bundler/LinkerContext.zig +++ b/src/bundler/LinkerContext.zig @@ -2492,6 +2492,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 => { @@ -2510,6 +2511,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 => { diff --git a/src/bundler/linker_context/scanImportsAndExports.zig b/src/bundler/linker_context/scanImportsAndExports.zig index b6bdb6bf0a..4f2e4fcba5 100644 --- a/src/bundler/linker_context/scanImportsAndExports.zig +++ b/src/bundler/linker_context/scanImportsAndExports.zig @@ -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) { diff --git a/src/js_printer.zig b/src/js_printer.zig index 951bf3f200..1c01a0f95e 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -2976,17 +2976,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) { diff --git a/test/regression/issue/20670.test.ts b/test/regression/issue/20670.test.ts new file mode 100644 index 0000000000..fecafaec34 --- /dev/null +++ b/test/regression/issue/20670.test.ts @@ -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); + }); +});