From cd23ee5e46057503bab9250bcd758f47b53fb7c1 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Sat, 1 Jul 2023 01:45:32 -0400 Subject: [PATCH] fix #3199: keep names + namespace + export class --- CHANGELOG.md | 30 ++++++++++++++ internal/bundler_tests/bundler_ts_test.go | 41 +++++++++++++++++++ .../bundler_tests/snapshots/snapshots_ts.txt | 39 ++++++++++++++++++ internal/js_parser/js_parser.go | 4 +- scripts/end-to-end-tests.js | 14 +++++++ 5 files changed, 127 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4edf5500fb..cbe4c61a267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/internal/bundler_tests/bundler_ts_test.go b/internal/bundler_tests/bundler_ts_test.go index b746e8ccb45..7244ac33de2 100644 --- a/internal/bundler_tests/bundler_ts_test.go +++ b/internal/bundler_tests/bundler_ts_test.go @@ -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{ diff --git a/internal/bundler_tests/snapshots/snapshots_ts.txt b/internal/bundler_tests/snapshots/snapshots_ts.txt index 46139853e22..95baaa0d9ea 100644 --- a/internal/bundler_tests/snapshots/snapshots_ts.txt +++ b/internal/bundler_tests/snapshots/snapshots_ts.txt @@ -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 ---------- diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 74ea043a75e..659c2e6c94c 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -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 } @@ -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}}, diff --git a/scripts/end-to-end-tests.js b/scripts/end-to-end-tests.js index 731ec97c64b..089ab24e047 100644 --- a/scripts/end-to-end-tests.js +++ b/scripts/end-to-end-tests.js @@ -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(