From 731f559940b1ad37037407d22118f2ec56bd7133 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Fri, 26 Nov 2021 10:15:32 -0600 Subject: [PATCH] change output for top-level TypeScript enums --- CHANGELOG.md | 45 ++++++ internal/bundler/bundler_ts_test.go | 58 ++++++++ .../bundler/snapshots/snapshots_default.txt | 26 ++-- internal/bundler/snapshots/snapshots_ts.txt | 136 ++++++++++++++---- internal/js_parser/js_parser.go | 55 ++++--- internal/js_parser/ts_parser.go | 123 ++++++++++++++++ internal/js_parser/ts_parser_test.go | 112 ++++++++------- scripts/end-to-end-tests.js | 31 +++- scripts/js-api-tests.js | 2 +- 9 files changed, 465 insertions(+), 123 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5033f4da7d..ac4711269ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -125,6 +125,51 @@ In addition to the breaking changes above, the following changes are also includ Note that this behavior does **not** work across files. Each file is still compiled independently so the namespaces in each file are still resolved independently per-file. Implicit namespace cross-references still do not work across files. Getting this to work is counter to esbuild's parallel architecture and does not fit in with esbuild's design. It also doesn't make sense with esbuild's bundling model where input files are either in ESM or CommonJS format and therefore each have their own scope. +* Change output for top-level TypeScript enums + + The output format for top-level TypeScript enums has been changed to reduce code size and improve tree shaking, which means that esbuild's enum output is now somewhat different than TypeScript's enum output. The behavior of both output formats should still be equivalent though. Here's an example that shows the difference: + + ```ts + // Original code + enum x { + y = 1, + z = 2 + } + + // Old output + var x; + (function(x2) { + x2[x2["y"] = 1] = "y"; + x2[x2["z"] = 2] = "z"; + })(x || (x = {})); + + // New output + var x = /* @__PURE__ */ ((x2) => { + x2[x2["y"] = 1] = "y"; + x2[x2["z"] = 2] = "z"; + return x2; + })(x || {}); + ``` + + The function expression has been changed to an arrow expression to reduce code size and the enum initializer has been moved into the variable declaration to make it possible to be marked as `/* @__PURE__ */` to improve tree shaking. The `/* @__PURE__ */` annotation is now automatically added when all of the enum values are side-effect free, which means the entire enum definition can be removed as dead code if it's never referenced. Direct enum value references within the same file that have been inlined do not count as references to the enum definition so this should eliminate enums from the output in many cases: + + ```ts + // Original code + enum Foo { FOO = 1 } + enum Bar { BAR = 2 } + console.log(Foo, Bar.BAR) + + // Old output (with --bundle --minify) + var n;(function(e){e[e.FOO=1]="FOO"})(n||(n={}));var l;(function(e){e[e.BAR=2]="BAR"})(l||(l={}));console.log(n,2); + + // New output (with --bundle --minify) + var n=(e=>(e[e.FOO=1]="FOO",e))(n||{});console.log(n,2); + ``` + + Notice how the new output is much shorter because the entire definition for `Bar` has been completely removed as dead code by esbuild's tree shaking. + + The output may seem strange since it would be simpler to just have a plain object literal as an initializer. However, TypeScript's enum feature behaves similarly to TypeScript's namespace feature which means enums can merge with existing enums and/or existing namespaces (and in some cases also existing objects) if the existing definition has the same name. This new output format keeps its similarity to the original output format so that it still handles all of the various edge cases that TypeScript's enum feature supports. Initializing the enum using a plain object literal would not merge with existing definitions and would break TypeScript's enum semantics. + * Fix legal comment parsing in CSS ([#1796](https://github.com/evanw/esbuild/issues/1796)) Legal comments in CSS either start with `/*!` or contain `@preserve` or `@license` and are preserved by esbuild in the generated CSS output. This release fixes a bug where non-top-level legal comments inside a CSS file caused esbuild to skip any following legal comments even if those following comments are top-level: diff --git a/internal/bundler/bundler_ts_test.go b/internal/bundler/bundler_ts_test.go index 5aecbc6e373..f17bbdfbaa6 100644 --- a/internal/bundler/bundler_ts_test.go +++ b/internal/bundler/bundler_ts_test.go @@ -1457,6 +1457,64 @@ func TestTSSiblingEnum(t *testing.T) { }) } +func TestTSEnumTreeShaking(t *testing.T) { + ts_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/simple-member.ts": ` + enum x { y = 123 } + console.log(x.y) + `, + "/simple-enum.ts": ` + enum x { y = 123 } + console.log(x) + `, + "/sibling-member.ts": ` + enum x { y = 123 } + enum x { z = y * 2 } + console.log(x.y, x.z) + `, + "/sibling-enum-before.ts": ` + console.log(x) + enum x { y = 123 } + enum x { z = y * 2 } + `, + "/sibling-enum-middle.ts": ` + enum x { y = 123 } + console.log(x) + enum x { z = y * 2 } + `, + "/sibling-enum-after.ts": ` + enum x { y = 123 } + enum x { z = y * 2 } + console.log(x) + `, + "/namespace-before.ts": ` + namespace x { console.log(x,y) } + enum x { y = 123 } + `, + "/namespace-after.ts": ` + enum x { y = 123 } + namespace x { console.log(x,y) } + `, + }, + entryPaths: []string{ + "/simple-member.ts", + "/simple-enum.ts", + "/sibling-member.ts", + "/sibling-enum-before.ts", + "/sibling-enum-middle.ts", + "/sibling-enum-after.ts", + "/namespace-before.ts", + "/namespace-after.ts", + }, + options: config.Options{ + Mode: config.ModeBundle, + AbsOutputDir: "/out", + OutputFormat: config.FormatESModule, + }, + }) +} + func TestTSEnumJSX(t *testing.T) { ts_suite.expectBundled(t, bundled{ files: map[string]string{ diff --git a/internal/bundler/snapshots/snapshots_default.txt b/internal/bundler/snapshots/snapshots_default.txt index 37181ae90df..9f5b39d03de 100644 --- a/internal/bundler/snapshots/snapshots_default.txt +++ b/internal/bundler/snapshots/snapshots_default.txt @@ -3323,9 +3323,9 @@ var require_es6_ns_export_enum = __commonJS({ "es6-ns-export-enum.ts"(exports) { var ns; ((ns2) => { - let Foo3; - ((Foo4) => { - })(Foo3 = ns2.Foo || (ns2.Foo = {})); + let Foo; + ((Foo2) => { + })(Foo = ns2.Foo || (ns2.Foo = {})); })(ns || (ns = {})); console.log(exports); } @@ -3336,9 +3336,9 @@ var require_es6_ns_export_const_enum = __commonJS({ "es6-ns-export-const-enum.ts"(exports) { var ns; ((ns2) => { - let Foo3; - ((Foo4) => { - })(Foo3 = ns2.Foo || (ns2.Foo = {})); + let Foo; + ((Foo2) => { + })(Foo = ns2.Foo || (ns2.Foo = {})); })(ns || (ns = {})); console.log(exports); } @@ -3363,9 +3363,9 @@ var require_es6_ns_export_class = __commonJS({ "es6-ns-export-class.ts"(exports) { var ns; ((ns2) => { - class Foo3 { + class Foo { } - ns2.Foo = Foo3; + ns2.Foo = Foo; })(ns || (ns = {})); console.log(exports); } @@ -3376,9 +3376,9 @@ var require_es6_ns_export_abstract_class = __commonJS({ "es6-ns-export-abstract-class.ts"(exports) { var ns; ((ns2) => { - class Foo3 { + class Foo { } - ns2.Foo = Foo3; + ns2.Foo = Foo; })(ns || (ns = {})); console.log(exports); } @@ -3414,15 +3414,9 @@ console.log(void 0); console.log(void 0); // es6-export-enum.ts -var Foo; -((Foo3) => { -})(Foo || (Foo = {})); console.log(void 0); // es6-export-const-enum.ts -var Foo2; -((Foo3) => { -})(Foo2 || (Foo2 = {})); console.log(void 0); // es6-export-module.ts diff --git a/internal/bundler/snapshots/snapshots_ts.txt b/internal/bundler/snapshots/snapshots_ts.txt index d306fe1ba7d..896e4a6bacc 100644 --- a/internal/bundler/snapshots/snapshots_ts.txt +++ b/internal/bundler/snapshots/snapshots_ts.txt @@ -104,26 +104,26 @@ var foo = bar(); ================================================================================ TestTSEnumDefine ---------- /out/entry.js ---------- -var a; -((a2) => { +var a = /* @__PURE__ */ ((a2) => { a2[a2["b"] = 123] = "b"; a2[a2["c"] = 123] = "c"; -})(a || (a = {})); + return a2; +})(a || {}); ================================================================================ TestTSEnumJSX ---------- /out/element.js ---------- -export var Foo; -((Foo2) => { +export var Foo = /* @__PURE__ */ ((Foo2) => { Foo2["Div"] = "div"; -})(Foo || (Foo = {})); + return Foo2; +})(Foo || {}); console.log(/* @__PURE__ */ React.createElement("div", null)); ---------- /out/fragment.js ---------- -export var React; -((React2) => { +export var React = /* @__PURE__ */ ((React2) => { React2["Fragment"] = "div"; -})(React || (React = {})); + return React2; +})(React || {}); console.log(/* @__PURE__ */ React.createElement("div", null, "test")); ---------- /out/nested-element.js ---------- @@ -162,6 +162,81 @@ var x; })(y = x2.y || (x2.y = {})); })(x || (x = {})); +================================================================================ +TestTSEnumTreeShaking +---------- /out/simple-member.js ---------- +// simple-member.ts +console.log(123); + +---------- /out/simple-enum.js ---------- +// simple-enum.ts +var x = /* @__PURE__ */ ((x2) => { + x2[x2["y"] = 123] = "y"; + return x2; +})(x || {}); +console.log(x); + +---------- /out/sibling-member.js ---------- +// sibling-member.ts +console.log(123, 246); + +---------- /out/sibling-enum-before.js ---------- +// sibling-enum-before.ts +console.log(x); +var x = /* @__PURE__ */ ((x2) => { + x2[x2["y"] = 123] = "y"; + return x2; +})(x || {}); +var x = /* @__PURE__ */ ((x2) => { + x2[x2["z"] = 246] = "z"; + return x2; +})(x || {}); + +---------- /out/sibling-enum-middle.js ---------- +// sibling-enum-middle.ts +var x = /* @__PURE__ */ ((x2) => { + x2[x2["y"] = 123] = "y"; + return x2; +})(x || {}); +console.log(x); +var x = /* @__PURE__ */ ((x2) => { + x2[x2["z"] = 246] = "z"; + return x2; +})(x || {}); + +---------- /out/sibling-enum-after.js ---------- +// sibling-enum-after.ts +var x = /* @__PURE__ */ ((x2) => { + x2[x2["y"] = 123] = "y"; + return x2; +})(x || {}); +var x = /* @__PURE__ */ ((x2) => { + x2[x2["z"] = 246] = "z"; + return x2; +})(x || {}); +console.log(x); + +---------- /out/namespace-before.js ---------- +// namespace-before.ts +var x; +((x2) => { + console.log(x2, x2.y); +})(x || (x = {})); +var x = /* @__PURE__ */ ((x2) => { + x2[x2["y"] = 123] = "y"; + return x2; +})(x || {}); + +---------- /out/namespace-after.js ---------- +// namespace-after.ts +var x = /* @__PURE__ */ ((x2) => { + x2[x2["y"] = 123] = "y"; + return x2; +})(x || {}); +((x2) => { + console.log(x2, 123); +})(x || (x = {})); + ================================================================================ TestTSExportDefaultTypeIssue316 ---------- /out.js ---------- @@ -401,10 +476,10 @@ class Foo extends Bar { ================================================================================ TestTSMinifyEnum ---------- /a.js ---------- -var Foo;(e=>{e[e.A=0]="A",e[e.B=1]="B",e[e.C=e]="C"})(Foo||(Foo={})); +var Foo=(e=>(e[e.A=0]="A",e[e.B=1]="B",e[e.C=e]="C",e))(Foo||{}); ---------- /b.js ---------- -export var Foo;(e=>{e[e.X=0]="X",e[e.Y=1]="Y",e[e.Z=e]="Z"})(Foo||(Foo={})); +export var Foo=(e=>(e[e.X=0]="X",e[e.Y=1]="Y",e[e.Z=e]="Z",e))(Foo||{}); ================================================================================ TestTSMinifyNamespace @@ -417,48 +492,51 @@ export var Foo;(e=>{let a;(p=>{foo(e,p)})(a=e.Bar||(e.Bar={}))})(Foo||(Foo={})); ================================================================================ TestTSSiblingEnum ---------- /out/number.js ---------- -export var x; -((x2) => { +export var x = /* @__PURE__ */ ((x2) => { x2[x2["y"] = 0] = "y"; x2[x2["yy"] = 0] = "yy"; -})(x || (x = {})); -((x2) => { + return x2; +})(x || {}); +var x = /* @__PURE__ */ ((x2) => { x2[x2["z"] = 1] = "z"; -})(x || (x = {})); + return x2; +})(x || {}); ((x2) => { console.log(0, 1); })(x || (x = {})); console.log(0, 1); ---------- /out/string.js ---------- -export var x; -((x2) => { +export var x = /* @__PURE__ */ ((x2) => { x2["y"] = "a"; x2["yy"] = "a"; -})(x || (x = {})); -((x2) => { + return x2; +})(x || {}); +var x = /* @__PURE__ */ ((x2) => { x2["z"] = "a"; -})(x || (x = {})); + return x2; +})(x || {}); ((x2) => { console.log("a", "a"); })(x || (x = {})); console.log("a", "a"); ---------- /out/propagation.js ---------- -export var a; -((a2) => { +export var a = /* @__PURE__ */ ((a2) => { a2[a2["b"] = 100] = "b"; -})(a || (a = {})); -export var x; -((x2) => { + return a2; +})(a || {}); +export var x = /* @__PURE__ */ ((x2) => { x2[x2["c"] = 100] = "c"; x2[x2["d"] = 200] = "d"; x2[x2["e"] = 4e4] = "e"; x2[x2["f"] = 1e4] = "f"; -})(x || (x = {})); -((x2) => { + return x2; +})(x || {}); +var x = /* @__PURE__ */ ((x2) => { x2[x2["g"] = 625] = "g"; -})(x || (x = {})); + return x2; +})(x || {}); console.log(100, 100, 625, 625); ---------- /out/nested-number.js ---------- diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index c8efddcf559..a4fa566acf8 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -1358,8 +1358,12 @@ func (p *parser) canMergeSymbols(scope *js_ast.Scope, existing js_ast.SymbolKind } // "enum Foo {} enum Foo {}" + if new == js_ast.SymbolTSEnum && existing == js_ast.SymbolTSEnum { + return mergeKeepExisting + } + // "namespace Foo { ... } enum Foo {}" - if new == js_ast.SymbolTSEnum && (existing == js_ast.SymbolTSEnum || existing == js_ast.SymbolTSNamespace) { + if new == js_ast.SymbolTSEnum && existing == js_ast.SymbolTSNamespace { return mergeReplaceWithNew } @@ -1638,6 +1642,27 @@ func (p *parser) ignoreUsage(ref js_ast.Ref) { // the value is ignored because that's what the TypeScript compiler does. } +func (p *parser) ignoreUsageOfIdentifierInDotChain(expr js_ast.Expr) { + for { + switch e := expr.Data.(type) { + case *js_ast.EIdentifier: + p.ignoreUsage(e.Ref) + + case *js_ast.EDot: + expr = e.Target + continue + + case *js_ast.EIndex: + if _, ok := e.Index.Data.(*js_ast.EString); ok { + expr = e.Target + continue + } + } + + return + } +} + func (p *parser) importFromRuntime(loc logger.Loc, name string) js_ast.Expr { ref, ok := p.runtimeImports[name] if !ok { @@ -9362,6 +9387,7 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_ nextNumericValue := float64(0) hasNumericValue := true valueExprs := []js_ast.Expr{} + allValuesArePure := true // Update the exported members of this enum as we constant fold each one exportedMembers := p.currentScope.TSNamespace.ExportedMembers @@ -9397,6 +9423,11 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_ exportedMembers[name] = member p.refToTSNamespaceMemberData[value.Ref] = member.Data hasStringValue = true + + default: + if !p.exprCanBeRemovedIfUnused(value.ValueOrNil) { + allValuesArePure = false + } } } else if hasNumericValue { member := exportedMembers[name] @@ -9443,30 +9474,16 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_ }}, js_ast.Expr{Loc: value.Loc, Data: &js_ast.EString{Value: value.Name}}, )) + p.recordUsage(s.Arg) } - p.recordUsage(s.Arg) } p.popScope() p.shouldFoldNumericConstants = oldShouldFoldNumericConstants - // Generate statements from expressions - valueStmts := []js_ast.Stmt{} - if len(valueExprs) > 0 { - if p.options.mangleSyntax { - // "a; b; c;" => "a, b, c;" - joined := js_ast.JoinAllWithComma(valueExprs) - valueStmts = append(valueStmts, js_ast.Stmt{Loc: joined.Loc, Data: &js_ast.SExpr{Value: joined}}) - } else { - for _, expr := range valueExprs { - valueStmts = append(valueStmts, js_ast.Stmt{Loc: expr.Loc, Data: &js_ast.SExpr{Value: expr}}) - } - } - } - // Wrap this enum definition in a closure - stmts = p.generateClosureForTypeScriptNamespaceOrEnum( - stmts, stmt.Loc, s.IsExport, s.Name.Loc, s.Name.Ref, s.Arg, valueStmts) + stmts = p.generateClosureForTypeScriptEnum( + stmts, stmt.Loc, s.IsExport, s.Name.Loc, s.Name.Ref, s.Arg, valueExprs, allValuesArePure) return stmts case *js_ast.SNamespace: @@ -10456,9 +10473,11 @@ func (p *parser) maybeRewritePropertyAccess( if member, ok := ns.ExportedMembers[name]; ok { switch m := member.Data.(type) { case *js_ast.TSNamespaceMemberEnumNumber: + p.ignoreUsageOfIdentifierInDotChain(target) return js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: m.Value}}, true case *js_ast.TSNamespaceMemberEnumString: + p.ignoreUsageOfIdentifierInDotChain(target) return js_ast.Expr{Loc: loc, Data: &js_ast.EString{Value: m.Value}}, true case *js_ast.TSNamespaceMemberNamespace: diff --git a/internal/js_parser/ts_parser.go b/internal/js_parser/ts_parser.go index 7b4ffdbbb63..f1c1b07b085 100644 --- a/internal/js_parser/ts_parser.go +++ b/internal/js_parser/ts_parser.go @@ -1436,3 +1436,126 @@ func (p *parser) generateClosureForTypeScriptNamespaceOrEnum( return stmts } + +func (p *parser) generateClosureForTypeScriptEnum( + stmts []js_ast.Stmt, stmtLoc logger.Loc, isExport bool, nameLoc logger.Loc, + nameRef js_ast.Ref, argRef js_ast.Ref, exprsInsideClosure []js_ast.Expr, + allValuesArePure bool, +) []js_ast.Stmt { + // Bail back to the namespace code for enums that aren't at the top level. + // Doing this for nested enums is problematic for two reasons. First of all + // enums inside of namespaces must be property accesses off the namespace + // object instead of variable declarations. Also we'd need to use "let" + // instead of "var" which doesn't allow sibling declarations to be merged. + if p.currentScope != p.moduleScope { + stmtsInsideClosure := []js_ast.Stmt{} + if len(exprsInsideClosure) > 0 { + if p.options.mangleSyntax { + // "a; b; c;" => "a, b, c;" + joined := js_ast.JoinAllWithComma(exprsInsideClosure) + stmtsInsideClosure = append(stmtsInsideClosure, js_ast.Stmt{Loc: joined.Loc, Data: &js_ast.SExpr{Value: joined}}) + } else { + for _, expr := range exprsInsideClosure { + stmtsInsideClosure = append(stmtsInsideClosure, js_ast.Stmt{Loc: expr.Loc, Data: &js_ast.SExpr{Value: expr}}) + } + } + } + return p.generateClosureForTypeScriptNamespaceOrEnum( + stmts, stmtLoc, isExport, nameLoc, nameRef, argRef, stmtsInsideClosure) + } + + // This uses an output format for enums that's different but equivalent to + // what TypeScript uses. Here is TypeScript's output: + // + // var x; + // (function (x) { + // x[x["y"] = 1] = "y"; + // })(x || (x = {})); + // + // And here's our output: + // + // var x = /* @__PURE__ */ ((x) => { + // x[x["y"] = 1] = "y"; + // return x; + // })(x || {}); + // + // One benefit is that the minified output is smaller: + // + // // Old output minified + // var x;(function(n){n[n.y=1]="y"})(x||(x={})); + // + // // New output minified + // var x=(r=>(r[r.y=1]="y",r))(x||{}); + // + // Another benefit is that the @__PURE__ annotation means it automatically + // works with tree-shaking, even with more advanced features such as sibling + // enum declarations and enum/namespace merges. Ideally all uses of the enum + // are just direct references to enum members (and are therefore inlined as + // long as the enum value is a constant) and the enum definition itself is + // unused and can be removed as dead code. + + // Follow the link chain in case symbols were merged + symbol := p.symbols[nameRef.InnerIndex] + for symbol.Link != js_ast.InvalidRef { + nameRef = symbol.Link + symbol = p.symbols[nameRef.InnerIndex] + } + + // Generate the body of the closure, including a return statement at the end + stmtsInsideClosure := []js_ast.Stmt{} + if len(exprsInsideClosure) > 0 { + argExpr := js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: argRef}} + if p.options.mangleSyntax { + // "a; b; return c;" => "return a, b, c;" + joined := js_ast.JoinAllWithComma(exprsInsideClosure) + joined = js_ast.JoinWithComma(joined, argExpr) + stmtsInsideClosure = append(stmtsInsideClosure, js_ast.Stmt{Loc: joined.Loc, Data: &js_ast.SReturn{ValueOrNil: joined}}) + } else { + for _, expr := range exprsInsideClosure { + stmtsInsideClosure = append(stmtsInsideClosure, js_ast.Stmt{Loc: expr.Loc, Data: &js_ast.SExpr{Value: expr}}) + } + stmtsInsideClosure = append(stmtsInsideClosure, js_ast.Stmt{Loc: argExpr.Loc, Data: &js_ast.SReturn{ValueOrNil: argExpr}}) + } + } + + // Try to use an arrow function if possible for compactness + var targetExpr js_ast.Expr + args := []js_ast.Arg{{Binding: js_ast.Binding{Loc: nameLoc, Data: &js_ast.BIdentifier{Ref: argRef}}}} + if p.options.unsupportedJSFeatures.Has(compat.Arrow) { + targetExpr = js_ast.Expr{Loc: stmtLoc, Data: &js_ast.EFunction{Fn: js_ast.Fn{ + Args: args, + Body: js_ast.FnBody{Loc: stmtLoc, Stmts: stmtsInsideClosure}, + }}} + } else { + targetExpr = js_ast.Expr{Loc: stmtLoc, Data: &js_ast.EArrow{ + Args: args, + Body: js_ast.FnBody{Loc: stmtLoc, Stmts: stmtsInsideClosure}, + PreferExpr: p.options.mangleSyntax, + }} + } + + // Call the closure with the name object and store it to the variable + decls := []js_ast.Decl{{ + Binding: js_ast.Binding{Loc: nameLoc, Data: &js_ast.BIdentifier{Ref: nameRef}}, + ValueOrNil: js_ast.Expr{Loc: stmtLoc, Data: &js_ast.ECall{ + Target: targetExpr, + Args: []js_ast.Expr{{Loc: nameLoc, Data: &js_ast.EBinary{ + Op: js_ast.BinOpLogicalOr, + Left: js_ast.Expr{Loc: nameLoc, Data: &js_ast.EIdentifier{Ref: nameRef}}, + Right: js_ast.Expr{Loc: nameLoc, Data: &js_ast.EObject{}}, + }}}, + CanBeUnwrappedIfUnused: allValuesArePure, + }}, + }} + p.recordUsage(nameRef) + + // Use a "var" statement since this is a top-level enum, but only use "export" once + stmts = append(stmts, js_ast.Stmt{Loc: stmtLoc, Data: &js_ast.SLocal{ + Kind: js_ast.LocalVar, + Decls: decls, + IsExport: isExport && !p.emittedNamespaceVars[nameRef], + }}) + p.emittedNamespaceVars[nameRef] = true + + return stmts +} diff --git a/internal/js_parser/ts_parser_test.go b/internal/js_parser/ts_parser_test.go index 39abd939c3a..0fdc0578f48 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -585,10 +585,10 @@ export let x; 0; })(foo || (foo = {})); `) - expectPrintedTS(t, "enum foo { a } namespace foo { 0 }", `var foo; -((foo) => { + expectPrintedTS(t, "enum foo { a } namespace foo { 0 }", `var foo = /* @__PURE__ */ ((foo) => { foo[foo["a"] = 0] = "a"; -})(foo || (foo = {})); + return foo; +})(foo || {}); ((foo) => { 0; })(foo || (foo = {})); @@ -606,9 +606,10 @@ export let x; ((foo) => { 0; })(foo || (foo = {})); -((foo) => { +var foo = /* @__PURE__ */ ((foo) => { foo[foo["a"] = 0] = "a"; -})(foo || (foo = {})); + return foo; +})(foo || {}); `) expectPrintedTS(t, "namespace foo { 0 } namespace foo {}", `var foo; ((foo) => { @@ -1021,37 +1022,36 @@ func TestTSNamespaceDestructuring(t *testing.T) { func TestTSEnum(t *testing.T) { // Check ES5 emit - expectPrintedTargetTS(t, 5, "enum x { y = 1 }", "var x;\n(function(x) {\n x[x[\"y\"] = 1] = \"y\";\n})(x || (x = {}));\n") - expectPrintedTargetTS(t, 2015, "enum x { y = 1 }", "var x;\n((x) => {\n x[x[\"y\"] = 1] = \"y\";\n})(x || (x = {}));\n") + expectPrintedTargetTS(t, 5, "enum x { y = 1 }", "var x = /* @__PURE__ */ function(x) {\n x[x[\"y\"] = 1] = \"y\";\n return x;\n}(x || {});\n") + expectPrintedTargetTS(t, 2015, "enum x { y = 1 }", "var x = /* @__PURE__ */ ((x) => {\n x[x[\"y\"] = 1] = \"y\";\n return x;\n})(x || {});\n") // Certain syntax isn't allowed inside an enum block expectParseErrorTS(t, "enum x { y = this }", ": error: Cannot use \"this\" here\n") expectParseErrorTS(t, "enum x { y = () => this }", ": error: Cannot use \"this\" here\n") expectParseErrorTS(t, "enum x { y = function() { this } }", "") - expectPrintedTS(t, "enum Foo { A, B }", `var Foo; -((Foo) => { + expectPrintedTS(t, "enum Foo { A, B }", `var Foo = /* @__PURE__ */ ((Foo) => { Foo[Foo["A"] = 0] = "A"; Foo[Foo["B"] = 1] = "B"; -})(Foo || (Foo = {})); + return Foo; +})(Foo || {}); `) - expectPrintedTS(t, "export enum Foo { A; B }", `export var Foo; -((Foo) => { + expectPrintedTS(t, "export enum Foo { A; B }", `export var Foo = /* @__PURE__ */ ((Foo) => { Foo[Foo["A"] = 0] = "A"; Foo[Foo["B"] = 1] = "B"; -})(Foo || (Foo = {})); + return Foo; +})(Foo || {}); `) - expectPrintedTS(t, "enum Foo { A, B, C = 3.3, D, E }", `var Foo; -((Foo) => { + expectPrintedTS(t, "enum Foo { A, B, C = 3.3, D, E }", `var Foo = /* @__PURE__ */ ((Foo) => { Foo[Foo["A"] = 0] = "A"; Foo[Foo["B"] = 1] = "B"; Foo[Foo["C"] = 3.3] = "C"; Foo[Foo["D"] = 4.3] = "D"; Foo[Foo["E"] = 5.3] = "E"; -})(Foo || (Foo = {})); + return Foo; +})(Foo || {}); `) - expectPrintedTS(t, "enum Foo { A, B, C = 'x', D, E, F = `y`, G = `${z}`, H = tag`` }", `var Foo; -((Foo) => { + expectPrintedTS(t, "enum Foo { A, B, C = 'x', D, E, F = `y`, G = `${z}`, H = tag`` }", `var Foo = ((Foo) => { Foo[Foo["A"] = 0] = "A"; Foo[Foo["B"] = 1] = "B"; Foo["C"] = "x"; @@ -1060,17 +1060,19 @@ func TestTSEnum(t *testing.T) { Foo["F"] = `+"`y`"+`; Foo[Foo["G"] = `+"`${z}`"+`] = "G"; Foo[Foo["H"] = tag`+"``"+`] = "H"; -})(Foo || (Foo = {})); + return Foo; +})(Foo || {}); `) // TypeScript allows splitting an enum into multiple blocks - expectPrintedTS(t, "enum Foo { A = 1 } enum Foo { B = 2 }", `var Foo; -((Foo) => { + expectPrintedTS(t, "enum Foo { A = 1 } enum Foo { B = 2 }", `var Foo = /* @__PURE__ */ ((Foo) => { Foo[Foo["A"] = 1] = "A"; -})(Foo || (Foo = {})); -((Foo) => { + return Foo; +})(Foo || {}); +var Foo = /* @__PURE__ */ ((Foo) => { Foo[Foo["B"] = 2] = "B"; -})(Foo || (Foo = {})); + return Foo; +})(Foo || {}); `) expectPrintedTS(t, ` @@ -1085,67 +1087,67 @@ func TestTSEnum(t *testing.T) { enum Bar { a = Foo.a } - `, `var Foo; -((Foo) => { + `, `var Foo = ((Foo) => { Foo[Foo["a"] = 10.01] = "a"; Foo[Foo["a b"] = 100] = "a b"; Foo[Foo["c"] = 120.02] = "c"; Foo[Foo["d"] = 121.02] = "d"; Foo[Foo["e"] = 120.02 + Math.random()] = "e"; Foo[Foo["f"] = void 0] = "f"; -})(Foo || (Foo = {})); -var Bar; -((Bar) => { + return Foo; +})(Foo || {}); +var Bar = /* @__PURE__ */ ((Bar) => { Bar[Bar["a"] = 10.01] = "a"; -})(Bar || (Bar = {})); + return Bar; +})(Bar || {}); `) expectPrintedTS(t, ` enum Foo { A } x = [Foo.A, Foo?.A, Foo?.A()] y = [Foo['A'], Foo?.['A'], Foo?.['A']()] - `, `var Foo; -((Foo) => { + `, `var Foo = /* @__PURE__ */ ((Foo) => { Foo[Foo["A"] = 0] = "A"; -})(Foo || (Foo = {})); + return Foo; +})(Foo || {}); x = [0, Foo?.A, Foo?.A()]; y = [0, Foo?.["A"], Foo?.["A"]()]; `) // Check shadowing - expectPrintedTS(t, "enum Foo { Foo }", `var Foo; -((_Foo) => { + expectPrintedTS(t, "enum Foo { Foo }", `var Foo = /* @__PURE__ */ ((_Foo) => { _Foo[_Foo["Foo"] = 0] = "Foo"; -})(Foo || (Foo = {})); + return _Foo; +})(Foo || {}); `) - expectPrintedTS(t, "enum Foo { Bar = Foo }", `var Foo; -((Foo) => { + expectPrintedTS(t, "enum Foo { Bar = Foo }", `var Foo = /* @__PURE__ */ ((Foo) => { Foo[Foo["Bar"] = Foo] = "Bar"; -})(Foo || (Foo = {})); + return Foo; +})(Foo || {}); `) - expectPrintedTS(t, "enum Foo { Foo = 1, Bar = Foo }", `var Foo; -((_Foo) => { + expectPrintedTS(t, "enum Foo { Foo = 1, Bar = Foo }", `var Foo = /* @__PURE__ */ ((_Foo) => { _Foo[_Foo["Foo"] = 1] = "Foo"; _Foo[_Foo["Bar"] = 1] = "Bar"; -})(Foo || (Foo = {})); + return _Foo; +})(Foo || {}); `) // Check top-level "var" and nested "let" - expectPrintedTS(t, "enum a { b = 1 }", "var a;\n((a) => {\n a[a[\"b\"] = 1] = \"b\";\n})(a || (a = {}));\n") + expectPrintedTS(t, "enum a { b = 1 }", "var a = /* @__PURE__ */ ((a) => {\n a[a[\"b\"] = 1] = \"b\";\n return a;\n})(a || {});\n") expectPrintedTS(t, "{ enum a { b = 1 } }", "{\n let a;\n ((a) => {\n a[a[\"b\"] = 1] = \"b\";\n })(a || (a = {}));\n}\n") // Check "await" and "yield" - expectPrintedTS(t, "enum x { await = 1, y = await }", `var x; -((x) => { + expectPrintedTS(t, "enum x { await = 1, y = await }", `var x = /* @__PURE__ */ ((x) => { x[x["await"] = 1] = "await"; x[x["y"] = 1] = "y"; -})(x || (x = {})); + return x; +})(x || {}); `) - expectPrintedTS(t, "enum x { yield = 1, y = yield }", `var x; -((x) => { + expectPrintedTS(t, "enum x { yield = 1, y = yield }", `var x = /* @__PURE__ */ ((x) => { x[x["yield"] = 1] = "yield"; x[x["y"] = 1] = "y"; -})(x || (x = {})); + return x; +})(x || {}); `) expectParseErrorTS(t, "enum x { y = await 1 }", ": error: \"await\" can only be used inside an \"async\" function\n") expectParseErrorTS(t, "function *f() { enum x { y = yield 1 } }", ": error: Cannot use \"yield\" outside a generator function\n") @@ -1187,8 +1189,7 @@ func TestTSEnumConstantFolding(t *testing.T) { pow2 = (-2.25) ** 3, pow3 = (-2.25) ** -3, } - `, `var Foo; -((Foo) => { + `, `var Foo = /* @__PURE__ */ ((Foo) => { Foo[Foo["add"] = 3] = "add"; Foo[Foo["sub"] = -3] = "sub"; Foo[Foo["mul"] = 200] = "mul"; @@ -1212,7 +1213,8 @@ func TestTSEnumConstantFolding(t *testing.T) { Foo[Foo["pow1"] = 0.0877914951989026] = "pow1"; Foo[Foo["pow2"] = -11.390625] = "pow2"; Foo[Foo["pow3"] = -0.0877914951989026] = "pow3"; -})(Foo || (Foo = {})); + return Foo; +})(Foo || {}); `) expectPrintedTS(t, ` @@ -1233,8 +1235,7 @@ func TestTSEnumConstantFolding(t *testing.T) { bitor = 0xDEADF00D | 0xBADCAFE, bitxor = 0xDEADF00D ^ 0xBADCAFE, } - `, `var Foo; -((Foo) => { + `, `var Foo = /* @__PURE__ */ ((Foo) => { Foo[Foo["shl0"] = -344350012] = "shl0"; Foo[Foo["shl1"] = -2147483648] = "shl1"; Foo[Foo["shl2"] = -344350012] = "shl2"; @@ -1247,7 +1248,8 @@ func TestTSEnumConstantFolding(t *testing.T) { Foo[Foo["bitand"] = 179159052] = "bitand"; Foo[Foo["bitor"] = -542246145] = "bitor"; Foo[Foo["bitxor"] = -721405197] = "bitxor"; -})(Foo || (Foo = {})); + return Foo; +})(Foo || {}); `) } diff --git a/scripts/end-to-end-tests.js b/scripts/end-to-end-tests.js index e2bd9d27d83..07c8f107326 100644 --- a/scripts/end-to-end-tests.js +++ b/scripts/end-to-end-tests.js @@ -83,24 +83,47 @@ // Test TypeScript enum scope merging tests.push( - test(['entry.ts', '--bundle', '--outfile=node.js'], { + test(['entry.ts', '--bundle', '--minify', '--outfile=node.js'], { 'entry.ts': ` + const id = x => x enum a { b = 1 } enum a { c = 2 } - if (a.c !== 2 || a[2] !== 'c' || a.b !== 1 || a[1] !== 'b') throw 'fail' + if (id(a).c !== 2 || id(a)[2] !== 'c' || id(a).b !== 1 || id(a)[1] !== 'b') throw 'fail' `, }), - test(['entry.ts', '--bundle', '--outfile=node.js'], { + test(['entry.ts', '--bundle', '--minify', '--outfile=node.js'], { 'entry.ts': ` + const id = x => x { enum a { b = 1 } } { enum a { c = 2 } - if (a.c !== 2 || a[2] !== 'c' || a.b !== void 0 || a[1] !== void 0) throw 'fail' + if (id(a).c !== 2 || id(a)[2] !== 'c' || id(a).b !== void 0 || id(a)[1] !== void 0) throw 'fail' } `, }), + test(['entry.ts', '--bundle', '--minify', '--outfile=node.js'], { + 'entry.ts': ` + const id = x => x + enum a { b = 1 } + namespace a { + if (id(a).b !== 1 || id(a)[1] !== 'b') throw 'fail' + } + `, + }), + test(['entry.ts', '--bundle', '--minify', '--outfile=node.js'], { + 'entry.ts': ` + const id = x => x + namespace a { + export function foo() { + if (id(a).b !== 1 || id(a)[1] !== 'b') throw 'fail' + } + } + enum a { b = 1 } + a.foo() + `, + }), ) // Test coverage for a special JSX error message diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index baee0ff6354..936543fc675 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -3676,7 +3676,7 @@ let transformTests = { async ts({ esbuild }) { const { code } = await esbuild.transform(`enum Foo { FOO }`, { loader: 'ts' }) - assert.strictEqual(code, `var Foo;\n((Foo2) => {\n Foo2[Foo2["FOO"] = 0] = "FOO";\n})(Foo || (Foo = {}));\n`) + assert.strictEqual(code, `var Foo = /* @__PURE__ */ ((Foo2) => {\n Foo2[Foo2["FOO"] = 0] = "FOO";\n return Foo2;\n})(Foo || {});\n`) }, async tsx({ esbuild }) {