From 21f59ad7737568bff560ff7d0230d455bda49509 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 24 May 2026 09:18:20 +0000 Subject: [PATCH 1/2] Fix panic on anonymous export default class with an auto-accessor field Standard-decorator lowering requires the class to have a name ref, but s_export_default only injected the generated default name when the class had decorators. A class with only `accessor` fields (no decorators) still takes the standard-decorator lowering path, so the missing name hit an unwrap on None in lower_decorators. Inject the default name whenever the class will go through standard decorator lowering, matching what the expression path already does. --- src/js_parser/visit/visit_stmt.rs | 9 +- test/bundler/transpiler/es-decorators.test.ts | 92 +++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/js_parser/visit/visit_stmt.rs b/src/js_parser/visit/visit_stmt.rs index d2f2e24086a1..ff476e076585 100644 --- a/src/js_parser/visit/visit_stmt.rs +++ b/src/js_parser/visit/visit_stmt.rs @@ -826,8 +826,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O p.create_default_name(stmt.loc).expect("unreachable"); } - // We only inject a name into classes when there is a decorator - if class.class.has_decorators { + // We only inject a name into classes when decorator lowering + // needs one: legacy TS decorators (`has_decorators`) or + // standard decorator lowering, which also covers classes with + // only auto-accessor fields and no decorators. + if class.class.has_decorators + || class.class.should_lower_standard_decorators + { if class.class.class_name.is_none() || class.class.class_name.unwrap().ref_.is_none() { diff --git a/test/bundler/transpiler/es-decorators.test.ts b/test/bundler/transpiler/es-decorators.test.ts index 8244165c3e42..552260cf7e93 100644 --- a/test/bundler/transpiler/es-decorators.test.ts +++ b/test/bundler/transpiler/es-decorators.test.ts @@ -637,6 +637,98 @@ describe("ES Decorators", () => { expect(stdout).toBe("decorated foo\n42\n"); expect(exitCode).toBe(0); }); + + test("export default anonymous class with auto-accessor and no decorators", async () => { + using dir = tempDir("es-dec-export-default-anon-accessor", { + "entry.js": ` + import Cls from "./mod.js"; + const c = new Cls(); + console.log(c.op); + c.op = 42; + console.log(c.op); + const desc = Object.getOwnPropertyDescriptor(Cls.prototype, "op"); + console.log(typeof desc.get, typeof desc.set); + `, + "mod.js": ` + export default class { + accessor op; + } + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "entry.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(filterStderr(rawStderr)).toBe(""); + expect(stdout).toBe("undefined\n42\nfunction function\n"); + expect(exitCode).toBe(0); + }); + + test("export default anonymous TypeScript class with auto-accessor and no decorators", async () => { + using dir = tempDir("es-dec-export-default-anon-accessor-ts", { + "tsconfig.json": JSON.stringify({ compilerOptions: {} }), + "entry.ts": ` + import Cls from "./mod.ts"; + const c = new Cls(); + c.op = "hello"; + console.log(c.op); + `, + "mod.ts": ` + export default class { + accessor op: string | undefined; + } + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "entry.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(filterStderr(rawStderr)).toBe(""); + expect(stdout).toBe("hello\n"); + expect(exitCode).toBe(0); + }); + + test("Bun.build bundles export default anonymous class with auto-accessor", async () => { + using dir = tempDir("es-dec-build-anon-accessor", { + "build.js": ` + const result = await Bun.build({ + entrypoints: ["./mod.ts"], + target: "bun", + minify: true, + sourcemap: "external", + throw: false, + }); + console.log(result.success); + `, + "mod.ts": ` + export default class { + accessor op; + } + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "build.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(filterStderr(rawStderr)).toBe(""); + expect(stdout).toBe("true\n"); + expect(exitCode).toBe(0); + }); }); describe("anonymous class expressions with reserved-word inferred names", () => { From cc7efb5a39553027293711c8cb27d7ad6c2d9918 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 24 May 2026 11:12:32 +0000 Subject: [PATCH 2/2] ci: retrigger