Skip to content

Commit

Permalink
fix #3199: keep names + namespace + export class
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jul 1, 2023
1 parent 52110fd commit cd23ee5
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 1 deletion.
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Changelog

## Unreleased

* Fix a TypeScript code generation edge case ([#3199](https://github.com/evanw/esbuild/issues/3199))

This release fixes a regression in version 0.18.4 where using a TypeScript `namespace` that exports a `class` declaration combined with `--keep-names` and a `--target` of `es2021` or earlier could cause esbuild to export the class from the namespace using an incorrect name (notice the assignment to `X2._Y` vs. `X2.Y`):

```ts
// Original code

// Old output (with --keep-names --target=es2021)
var X;
((X2) => {
const _Y = class _Y {
};
__name(_Y, "Y");
let Y = _Y;
X2._Y = _Y;
})(X || (X = {}));

// New output (with --keep-names --target=es2021)
var X;
((X2) => {
const _Y = class _Y {
};
__name(_Y, "Y");
let Y = _Y;
X2.Y = _Y;
})(X || (X = {}));
```

## 0.18.10

* Fix a tree-shaking bug that removed side effects ([#3195](https://github.com/evanw/esbuild/issues/3195))
Expand Down
41 changes: 41 additions & 0 deletions internal/bundler_tests/bundler_ts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,47 @@ func TestTSExportNamespace(t *testing.T) {
})
}

func TestTSNamespaceKeepNames(t *testing.T) {
ts_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.ts": `
namespace ns {
export let foo = () => {}
export function bar() {}
export class Baz {}
}
`,
},
entryPaths: []string{"/entry.ts"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.js",
KeepNames: true,
},
})
}

func TestTSNamespaceKeepNamesTargetES2015(t *testing.T) {
ts_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.ts": `
namespace ns {
export let foo = () => {}
export function bar() {}
export class Baz {}
}
`,
},
entryPaths: []string{"/entry.ts"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.js",
KeepNames: true,
UnsupportedJSFeatures: es(2015),
},
})
}

func TestTSMinifyEnum(t *testing.T) {
ts_suite.expectBundled(t, bundled{
files: map[string]string{
Expand Down
39 changes: 39 additions & 0 deletions internal/bundler_tests/snapshots/snapshots_ts.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1610,6 +1610,45 @@ function foo(){let u;return(n=>(n[n.A=0]="A",n[n.B=1]="B",n[n.C=n]="C"))(u||(u={
---------- /b.js ----------
export function foo(){let e;return(n=>(n[n.X=0]="X",n[n.Y=1]="Y",n[n.Z=n]="Z"))(e||(e={})),e}

================================================================================
TestTSNamespaceKeepNames
---------- /out.js ----------
// entry.ts
var ns;
((ns2) => {
ns2.foo = /* @__PURE__ */ __name(() => {
}, "foo");
function bar() {
}
ns2.bar = bar;
__name(bar, "bar");
class Baz {
static {
__name(this, "Baz");
}
}
ns2.Baz = Baz;
})(ns || (ns = {}));

================================================================================
TestTSNamespaceKeepNamesTargetES2015
---------- /out.js ----------
// entry.ts
var ns;
((ns2) => {
ns2.foo = /* @__PURE__ */ __name(() => {
}, "foo");
function bar() {
}
ns2.bar = bar;
__name(bar, "bar");
const _Baz = class _Baz {
};
__name(_Baz, "Baz");
let Baz = _Baz;
ns2.Baz = _Baz;
})(ns || (ns = {}));

================================================================================
TestTSPreferJSOverTSInsideNodeModules
---------- /out/main.js ----------
Expand Down
4 changes: 3 additions & 1 deletion internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -10617,8 +10617,10 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_
result := p.visitClass(stmt.Loc, &s.Class, js_ast.InvalidRef, "")

// Remove the export flag inside a namespace
var nameToExport string
wasExportInsideNamespace := s.IsExport && p.enclosingNamespaceArgRef != nil
if wasExportInsideNamespace {
nameToExport = p.symbols[s.Class.Name.Ref.InnerIndex].OriginalName
s.IsExport = false
}

Expand All @@ -10641,7 +10643,7 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_
stmts = append(stmts, js_ast.AssignStmt(
js_ast.Expr{Loc: stmt.Loc, Data: p.dotOrMangledPropVisit(
js_ast.Expr{Loc: stmt.Loc, Data: &js_ast.EIdentifier{Ref: *p.enclosingNamespaceArgRef}},
p.symbols[s.Class.Name.Ref.InnerIndex].OriginalName,
nameToExport,
s.Class.Name.Loc,
)},
js_ast.Expr{Loc: s.Class.Name.Loc, Data: &js_ast.EIdentifier{Ref: s.Class.Name.Ref}},
Expand Down
14 changes: 14 additions & 0 deletions scripts/end-to-end-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -3628,6 +3628,20 @@ for (let flags of [[], ['--minify', '--keep-names']]) {
test(['in.js', '--outfile=node.js', '--bundle'].concat(flags), {
'in.js': `(() => { let Foo = class { static foo() {} }; if (Foo.foo.name !== 'foo') throw 'fail: ' + Foo.foo.name })()`,
}),

// See: https://github.com/evanw/esbuild/issues/3199
test(['in.ts', '--outfile=node.js', '--target=es6'].concat(flags), {
'in.ts': `
namespace foo { export class Foo {} }
if (foo.Foo.name !== 'Foo') throw 'fail: ' + foo.Foo.name
`,
}),
test(['in.ts', '--outfile=node.js', '--target=esnext'].concat(flags), {
'in.ts': `
namespace foo { export class Foo {} }
if (foo.Foo.name !== 'Foo') throw 'fail: ' + foo.Foo.name
`,
}),
)
}
tests.push(
Expand Down

0 comments on commit cd23ee5

Please sign in to comment.