Skip to content

Commit

Permalink
fix #227: "export declare" inside "namespace" bug
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jul 6, 2020
1 parent eef1992 commit dd9fc67
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 7 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## Unreleased

* Fix `export declare` inside `namespace` in TypeScript ([#227](https://github.com/evanw/esbuild/issues/227))

The TypeScript parser assumed that ambient declarations (the `declare` keyword) just declared types and did not affect the output. This was an incorrect assumption for exported declarations of local variables inside namespaces. The assignment to `foo` in the example below must be rewritten to an assignment to `ns.foo`:

```ts
namespace ns {
export declare let foo: string
foo = 123
}
```

This should now work correctly.

## 0.5.22

* JavaScript build API can now avoid writing to the file system ([#139](https://github.com/evanw/esbuild/issues/139) and [#220](https://github.com/evanw/esbuild/issues/220))
Expand Down
65 changes: 63 additions & 2 deletions internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ func (p *parser) declareBinding(kind ast.SymbolKind, binding ast.Binding, opts p

case *ast.BIdentifier:
name := p.loadNameFromRef(b.Ref)
if !opts.isTypeScriptDeclare {
if !opts.isTypeScriptDeclare || (opts.isNamespaceScope && opts.isExport) {
b.Ref = p.declareSymbol(kind, binding.Loc, name)
if opts.isExport {
p.recordExport(binding.Loc, name, b.Ref)
Expand Down Expand Up @@ -4777,10 +4777,47 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt {
}

// "declare const x: any"
p.parseStmt(opts)
stmt := p.parseStmt(opts)
if opts.tsDecorators != nil {
p.discardScopesUpTo(opts.tsDecorators.scopeIndex)
}

// Unlike almost all uses of "declare", statements that use
// "export declare" with "var/let/const" inside a namespace affect
// code generation. They cause any declared bindings to be
// considered exports of the namespace. Identifier references to
// those names must be converted into property accesses off the
// namespace object:
//
// namespace ns {
// export declare const x
// export function y() { return x }
// }
//
// (ns as any).x = 1
// console.log(ns.y())
//
// In this example, "return x" must be replaced with "return ns.x".
// This is handled by replacing each "export declare" statement
// inside a namespace with an "export var" statement containing all
// of the declared bindings. That "export var" statement will later
// cause identifiers to be transformed into property accesses.
if opts.isNamespaceScope && opts.isExport {
var decls []ast.Decl
if s, ok := stmt.Data.(*ast.SLocal); ok {
for _, decl := range s.Decls {
decls = extractDeclsForBinding(decl.Binding, decls)
}
}
if len(decls) > 0 {
return ast.Stmt{Loc: loc, Data: &ast.SLocal{
Kind: ast.LocalVar,
IsExport: true,
Decls: decls,
}}
}
}

return ast.Stmt{Loc: loc, Data: &ast.STypeScript{}}
}
}
Expand All @@ -4798,6 +4835,30 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt {
}
}

func extractDeclsForBinding(binding ast.Binding, decls []ast.Decl) []ast.Decl {
switch b := binding.Data.(type) {
case *ast.BMissing:

case *ast.BIdentifier:
decls = append(decls, ast.Decl{Binding: binding})

case *ast.BArray:
for _, item := range b.Items {
decls = extractDeclsForBinding(item.Binding, decls)
}

case *ast.BObject:
for _, property := range b.Properties {
decls = extractDeclsForBinding(property.Value, decls)
}

default:
panic("Internal error")
}

return decls
}

func (p *parser) addImportRecord(kind ast.ImportKind, path ast.Path) uint32 {
index := uint32(len(p.importRecords))
p.importRecords = append(p.importRecords, ast.ImportRecord{
Expand Down
44 changes: 39 additions & 5 deletions internal/parser/parser_ts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ func TestTSNamespaceExports(t *testing.T) {
class Class {}
}
}
`, `var A;
`, `var A;
(function(A) {
let B;
(function(B) {
Expand Down Expand Up @@ -531,7 +531,7 @@ func TestTSNamespaceExports(t *testing.T) {
enum Enum {}
}
}
`, `var A;
`, `var A;
(function(A) {
let B;
(function(B) {
Expand Down Expand Up @@ -569,7 +569,7 @@ func TestTSNamespaceExports(t *testing.T) {
foo += foo
}
}
`, `var A;
`, `var A;
(function(A) {
let B;
(function(B) {
Expand Down Expand Up @@ -604,7 +604,7 @@ func TestTSNamespaceExports(t *testing.T) {
foo += foo
}
}
`, `var A;
`, `var A;
(function(A) {
let B;
(function(B) {
Expand Down Expand Up @@ -639,7 +639,7 @@ func TestTSNamespaceExports(t *testing.T) {
foo += foo
}
}
`, `var A;
`, `var A;
(function(A) {
let B;
(function(B) {
Expand All @@ -659,6 +659,40 @@ func TestTSNamespaceExports(t *testing.T) {
})(A || (A = {}));
`)

expectPrintedTS(t, `
namespace ns {
export declare const L1
console.log(L1)
export declare let [[L2 = x, { [y]: L3 }]]
console.log(L2, L3)
export declare function F()
console.log(F)
export declare function F2() { }
console.log(F2)
export declare class C { }
console.log(C)
export declare enum E { }
console.log(E)
export declare namespace N { }
console.log(N)
}
`, `var ns;
(function(ns) {
console.log(ns.L1);
console.log(ns.L2, ns.L3);
console.log(F);
console.log(F2);
console.log(C);
console.log(E);
console.log(N);
})(ns || (ns = {}));
`)
}

func TestTSNamespaceDestructuring(t *testing.T) {
Expand Down

0 comments on commit dd9fc67

Please sign in to comment.