diff --git a/src/ast/parseStmt.zig b/src/ast/parseStmt.zig index 8f946e079b7ad5..45c0a0b4277a79 100644 --- a/src/ast/parseStmt.zig +++ b/src/ast/parseStmt.zig @@ -1185,6 +1185,17 @@ pub fn ParseStmt( switch (expr.data) { .e_identifier => |ident| { if (p.lexer.token == .t_colon and !opts.hasDecorators()) { + // In TypeScript declare contexts, "identifier: Type" is a type annotation, not a label + if (comptime is_typescript_enabled) { + if (opts.is_typescript_declare) { + // Skip the colon and type annotation + try p.lexer.next(); + try p.skipTypeScriptType(.lowest); + try p.lexer.expectOrInsertSemicolon(); + return p.s(S.TypeScript{}, loc); + } + } + _ = try p.pushScopeForParsePass(.label, loc); defer p.popScope(); diff --git a/test/js/bun/transpiler/declare-global.test.ts b/test/js/bun/transpiler/declare-global.test.ts new file mode 100644 index 00000000000000..caa35d1bf03ffd --- /dev/null +++ b/test/js/bun/transpiler/declare-global.test.ts @@ -0,0 +1,71 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("declare global with type annotation should not crash", async () => { + using dir = tempDir("declare-global-test", { + "test.ts": ` +declare global { + A: 'a'; +} + +() => {}; + +console.log("SUCCESS"); +`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).not.toContain("panic"); + expect(stderr).not.toContain("Scope mismatch"); + expect(stdout).toContain("SUCCESS"); + expect(exitCode).toBe(0); +}); + +test("declare global with multiple type annotations and nested arrow functions", async () => { + using dir = tempDir("declare-global-multi", { + "test.ts": ` +declare global { + TIMER: NodeJS.Timeout; + FOO: string; + BAR: number; + BAZ: () => void; +} + +// Test nested arrow functions to ensure scope handling is correct +const fn = () => { + const nested = () => { + const deeplyNested = () => "nested"; + return deeplyNested(); + }; + return nested(); +}; + +console.log(fn()); +console.log("SUCCESS"); +`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).not.toContain("panic"); + expect(stderr).not.toContain("Scope mismatch"); + expect(stdout).toContain("SUCCESS"); + expect(exitCode).toBe(0); +});