diff --git a/src/ast/P.zig b/src/ast/P.zig index 56ca06ca27..3923f2c4c1 100644 --- a/src/ast/P.zig +++ b/src/ast/P.zig @@ -3768,7 +3768,10 @@ pub fn NewParser_( } }, else => { - Output.panic("Unexpected type in export default", .{}); + // Standard decorator lowering can produce non-class + // statements as the export default value; conservatively + // assume they have side effects. + return false; }, } }, diff --git a/src/ast/parseProperty.zig b/src/ast/parseProperty.zig index 24e81306f2..670f3ca889 100644 --- a/src/ast/parseProperty.zig +++ b/src/ast/parseProperty.zig @@ -421,7 +421,7 @@ pub fn ParseProperty( try p.lexer.next(); } else if (p.lexer.token == .t_exclamation and !p.lexer.has_newline_before and - kind == .normal and + (kind == .normal or kind == .auto_accessor) and !opts.is_async and !opts.is_generator) { diff --git a/src/ast/visitStmt.zig b/src/ast/visitStmt.zig index ccd7f9e835..e7d71e9a94 100644 --- a/src/ast/visitStmt.zig +++ b/src/ast/visitStmt.zig @@ -461,17 +461,29 @@ pub fn VisitStmt( } } - // This is to handle TS decorators, mostly. + // Lower the class (handles both TS legacy and standard decorators). + // Standard decorator lowering may produce prefix statements + // (variable declarations) before the class statement. var class_stmts = p.lowerClass(.{ .stmt = s2 }); - bun.assert(class_stmts[0].data == .s_class); - if (class_stmts.len > 1) { - data.value.stmt = class_stmts[0]; - stmts.append(stmt.*) catch {}; - stmts.appendSlice(class_stmts[1..]) catch {}; - } else { - data.value.stmt = class_stmts[0]; - stmts.append(stmt.*) catch {}; + // Find the s_class statement in the returned list + var class_stmt_idx: usize = 0; + for (class_stmts, 0..) |cs, idx| { + if (cs.data == .s_class) { + class_stmt_idx = idx; + break; + } + } + + // Emit any prefix statements before the export default + stmts.appendSlice(class_stmts[0..class_stmt_idx]) catch {}; + + data.value.stmt = class_stmts[class_stmt_idx]; + stmts.append(stmt.*) catch {}; + + // Emit any suffix statements after the export default + if (class_stmt_idx + 1 < class_stmts.len) { + stmts.appendSlice(class_stmts[class_stmt_idx + 1 ..]) catch {}; } if (p.options.features.server_components.wrapsExports()) { diff --git a/test/bundler/transpiler/es-decorators.test.ts b/test/bundler/transpiler/es-decorators.test.ts index a22d911054..6a0563109d 100644 --- a/test/bundler/transpiler/es-decorators.test.ts +++ b/test/bundler/transpiler/es-decorators.test.ts @@ -472,4 +472,147 @@ describe("ES Decorators", () => { expect(exitCode).toBe(0); }); }); + + describe("export default class", () => { + test("export default class with method decorator", async () => { + using dir = tempDir("es-dec-export-default", { + "entry.js": ` + import Cls from "./mod.js"; + const c = new Cls(); + console.log(c.foo()); + `, + "mod.js": ` + function dec(target, ctx) { return target; } + export default class { + @dec foo() { return 42; } + } + `, + }); + + 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("42\n"); + expect(exitCode).toBe(0); + }); + + test("export default class with class decorator", async () => { + using dir = tempDir("es-dec-export-default-cls", { + "entry.js": ` + import Cls from "./mod.js"; + const c = new Cls(); + console.log(c.value); + `, + "mod.js": ` + function addValue(cls, ctx) { + return class extends cls { value = "decorated"; }; + } + @addValue export default class {} + `, + }); + + 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("decorated\n"); + expect(exitCode).toBe(0); + }); + + test("export default named class with decorator", async () => { + using dir = tempDir("es-dec-export-default-named", { + "entry.js": ` + import Cls from "./mod.js"; + const c = new Cls(); + console.log(c.foo()); + `, + "mod.js": ` + function dec(target, ctx) { return target; } + export default class MyClass { + @dec foo() { return "named"; } + } + `, + }); + + 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("named\n"); + expect(exitCode).toBe(0); + }); + }); + + describe("accessor with TypeScript annotations", () => { + test("accessor with definite assignment assertion (!)", async () => { + using dir = tempDir("es-dec-accessor-bang", { + "tsconfig.json": JSON.stringify({ compilerOptions: {} }), + "test.ts": ` + function dec(target: any, ctx: any) { return target; } + class Foo { + @dec accessor child!: string; + } + const f = new Foo(); + f.child = "hello"; + console.log(f.child); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.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("accessor with optional marker (?)", async () => { + using dir = tempDir("es-dec-accessor-optional", { + "tsconfig.json": JSON.stringify({ compilerOptions: {} }), + "test.ts": ` + function dec(target: any, ctx: any) { return target; } + class Foo { + @dec accessor child?: string; + } + const f = new Foo(); + console.log(f.child); + f.child = "world"; + console.log(f.child); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.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("undefined\nworld\n"); + expect(exitCode).toBe(0); + }); + }); });