Reject export clauses inside a non-declare TypeScript namespace instead of panicking (#31266)
### Repro
Found by transpiler fuzzing (80 bytes, minimized):
```
bun -e 'new Bun.Transpiler({loader:"tsx", target:"node", minifyWhitespace:true, deadCodeElimination:true}).transformSync(atob("bmFtZXNwYWNlIE0gew1leHBvcnQgaW1wb3J0IE1fQSA9IE07fQ0KZXhwb3J0IG5hbWVzcGFjZSBNIHsNZXhwb3J0IHtNX0EgYXMgYX07fQ0="))'
panic: index out of bounds: the len is 0 but the index is 4294967295
Crashed while printing input.tsx
```
Decoded, the input is:
```ts
namespace M {
export import M_A = M;}
export namespace M {
export {M_A as a};}
```
The minify/DCE/target options are irrelevant; any `transformSync` of
this source panics.
### Cause
`export { ... }` / `export * from ...` clauses inside a non-declare
`namespace`/`module` block are invalid TypeScript — tsc reports TS1194
("Export declarations are not permitted in a namespace") and esbuild
rejects them with `Unexpected "{"` / `Unexpected "*"`. Bun accepted them
because the guard in `t_export` (`src/js_parser/parse/parse_stmt.rs`)
has a misplaced negation relative to esbuild:
```rust
// Bun (TAsterisk / TOpenBrace): error iff !module && !namespace && declare ← unreachable, dead code
!opts.is_module_scope && !(opts.is_namespace_scope || !opts.is_typescript_declare)
// esbuild (and Bun's own TDefault arm): error iff !module && (!namespace || !declare)
!opts.is_module_scope && (!opts.is_namespace_scope || !opts.is_typescript_declare)
```
So the clause parses, and `export {M_A as a}` in the second namespace
block resolves `M_A` through the sibling namespace's exported members.
That lazily creates a property-access symbol whose `namespace_alias` has
the default `import_record_index: u32::MAX` (there is no import record —
it aliases the namespace IIFE argument). The printer's `SExportClause`
arm then does
```rust
let import_record = self.import_record(namespace.import_record_index as usize);
```
before checking `was_originally_property_access`, indexing
`import_records` (len 0) with `u32::MAX` → panic. The Zig implementation
had the same pattern but only "worked" because release builds skip the
bounds check and the bogus pointer is never dereferenced when
`was_originally_property_access` is false; it silently emitted invalid
JS instead:
```js
// bun 1.3.13 output for the repro — `export` inside an arrow body, SyntaxError when loaded
((M) => {
export { M_A as a };
})(M ||= {});
```
### Fix
- `src/js_parser/parse/parse_stmt.rs`: use esbuild's condition for the
`export *` and `export {` arms, so these clauses are a syntax error
inside a non-declare namespace (`Unexpected {` / `Unexpected *`).
Ambient contexts (`declare namespace`, `declare module "m"`, nested
namespaces inside a `declare`) still allow them and erase the body as
before, and `export import A = B`, `export
const/function/class/enum/namespace ...` inside namespaces are
untouched.
- `src/js_printer/lib.rs`: in the two `SExportClause` arms, only look up
the import record inside the `was_originally_property_access` branch —
the only place it's used and the only case where the index is guaranteed
valid. This mirrors the bounds-guarded `EImportIdentifier` path so a
namespace-alias symbol without an import record can never panic the
printer.
### Verification
New test `Bun.Transpiler > TypeScript > rejects export clauses inside a
non-declare namespace` in `test/bundler/transpiler/transpiler.test.js`
covers the fuzz input (plus simpler variants), the ambient-context cases
that must keep working, and `export import` inside a namespace.
```
USE_SYSTEM_BUN=1 bun test test/bundler/transpiler/transpiler.test.js -t "rejects export clauses" # panics: index out of bounds (the bug)
bun bd test test/bundler/transpiler/transpiler.test.js # 127 pass, 0 fail
bun bd test test/bundler/esbuild/ts.test.ts # 57 pass, 0 fail
bun bd test test/bundler/transpiler/scope-mismatch-panic.test.ts # 8 pass, 0 fail
bun bd test test/bundler/bundler_edgecase.test.ts -t "Namespace" # 6 pass, 0 fail
```
esbuild parity spot-checks: `namespace M { export {x}; }` → `Unexpected
"{"`, `namespace M { export * from "y"; }` → `Unexpected "*"`, `declare
namespace M { export {x}; }` / `declare module "foo" { export {x}; }` →
accepted and erased. R
robobun committed
5da1ff86de6139ac08942992459e51eb1baec057
Parent: 41aa672
Committed by GitHub <noreply@github.com>
on 5/24/2026, 2:29:30 AM