mirror of
https://github.com/oven-sh/bun
synced 2026-03-02 21:41:03 +01:00
fix(bundler): default import from 'bun' with --bytecode no longer undefined (#27175)
## Summary - Fixes `import Bun from 'bun'` being `undefined` when bundled with `--bytecode` flag - The CJS lowering pass was incorrectly adding `.default` property access to `globalThis.Bun`, which doesn't have a `default` property - Also skips unnecessary `__toESM` wrapper inclusion for built-in `bun` module imports ## Root Cause When `--bytecode` is used, the bundler wraps output in CJS format. For external modules, the linker sets up a `namespace_alias` with `alias: "default"` so the printer generates property access like `require_module().default`. The printer correctly replaces the `bun` import source with `globalThis.Bun`, but was still appending `.default` — producing `import_bun.default` instead of just `import_bun`. ## Fix Three targeted changes: 1. **`scanImportsAndExports.zig`**: Skip CJS interop wrapping (`__toESM`) for `bun` tagged imports since `globalThis.Bun` doesn't need it 2. **`LinkerContext.zig`**: Pass `import_record_index` through to `namespace_alias` so the printer can identify the import source 3. **`js_printer.zig`**: Skip the `.default` property access for `bun` tagged import records, while preserving named property accesses (e.g. `.serve`) ## Test plan - [x] `bun bd test test/regression/issue/20670.test.ts` — 4 pass (default, aliased, combined, namespace imports) - [x] Tests fail with `USE_SYSTEM_BUN=1` confirming they test the actual fix - [x] `bun bd test test/bundler/bundler_bun.test.ts` — 10 pass - [x] `bun bd test test/bundler/bundler_cjs.test.ts` — 23 pass - [x] `bun bd test test/bundler/bundler_edgecase.test.ts` — 99 pass Closes #20670 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
@@ -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 => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
168
test/regression/issue/20670.test.ts
Normal file
168
test/regression/issue/20670.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user