SIGN IN SIGN UP
oven-sh / bun UNCLAIMED

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one

0 0 150 Rust

Keep the TS namespace closure argument from colliding with namespace members (#31261)

### Repro

Found by parser fuzzing (invariant: printed output does not reparse,
loader=tsx):

```ts
bun -e 'new Bun.Transpiler({loader:"tsx", target:"browser"}).transformSync("namespace m2{class m2 { } class _m2 { }}")'
```

produced

```js
var m2;
((_m2) => {
  class m2 {
  }
  class _m2 {
  }
})(m2 ||= {});
```

which is not valid JavaScript — the closure argument `_m2` re-declares
the class `_m2`. The same input as a regular `.ts` file fails at
runtime, so this breaks executing valid TypeScript, not just
`Bun.Transpiler`:

```ts
namespace m2 {
  class m2 {}
  class _m2 {}
  export const x = 42;
}
console.log(m2.x);
// SyntaxError: Cannot declare a class twice: '_m2'.
```

`tsc` compiles this fine (it names the argument `m2_1`), and the bundler
path is unaffected because the linker runs the symbol renamer.

### Cause

In `parse_type_script_namespace`
(`src/js_parser/parse/parse_typescript.rs`), when the namespace name
collides with one of its own members, the generated closure argument
falls back to `"_" + name` without checking whether that prefixed name
is *also* declared in the namespace body. The comment inherited from
esbuild says the renamer will fix collisions, but the non-bundler print
paths (runtime transpiler, `Bun.Transpiler`, `bun build --no-bundle`)
print symbols with `NoOpRenamer`, i.e. by their original names, so the
collision ends up in the output as a duplicate block-scoped declaration.

### Fix

Keep prepending `_` until the generated argument name no longer collides
with any symbol declared in the namespace scope (`_m2` → `__m2` for the
repro). Enum closures don't need the same treatment because their bodies
only contain property assignments, never declarations.

### Verification

- Original repro now prints `((__m2) => { ... })(m2 ||= {})` and the
output reparses; the runtime example prints `42`.
- New tests in `test/bundler/transpiler/transpiler.test.js` ("generated
closures"): exact printed output for the colliding case, a baseline for
the existing single-collision rename, and a spawn test that runs the
colliding namespace and checks its exports. The two collision tests fail
on bun without this change and pass with it:

```
bun bd test test/bundler/transpiler/transpiler.test.js   # 129 pass, 0 fail
bun bd test test/bundler/esbuild/ts.test.ts              # 57 pass, 0 fail
bun bd test test/bundler/transpiler/runtime-transpiler.test.ts  # 13 pass, 0 fail
```
R
robobun committed
b733df44bcef0d9c691e99253c0c67eca681effb
Parent: aad5561
Committed by GitHub <noreply@github.com> on 5/23/2026, 6:30:33 AM