diff --git a/internal/bundler_tests/bundler_ts_test.go b/internal/bundler_tests/bundler_ts_test.go index 8588b22cd27..ee22c40540d 100644 --- a/internal/bundler_tests/bundler_ts_test.go +++ b/internal/bundler_tests/bundler_ts_test.go @@ -619,6 +619,79 @@ func TestTSMinifyDerivedClass(t *testing.T) { }) } +func TestTSMinifyEnumPropertyNames(t *testing.T) { + ts_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/entry.ts": ` + import { CrossFileGood, CrossFileBad } from './cross-file' + const enum SameFileGood { + STR = 'str 1', + NUM = 123, + } + const enum SameFileBad { + PROTO = '__proto__', + CONSTRUCTOR = 'constructor', + PROTOTYPE = 'prototype', + } + class Foo { + [100] = 100; + '200' = 200; + ['300'] = 300; + [SameFileGood.STR] = SameFileGood.STR; + [SameFileGood.NUM] = SameFileGood.NUM; + [CrossFileGood.STR] = CrossFileGood.STR; + [CrossFileGood.NUM] = CrossFileGood.NUM; + } + shouldNotBeComputed( + class { + [100] = 100; + '200' = 200; + ['300'] = 300; + [SameFileGood.STR] = SameFileGood.STR; + [SameFileGood.NUM] = SameFileGood.NUM; + [CrossFileGood.STR] = CrossFileGood.STR; + [CrossFileGood.NUM] = CrossFileGood.NUM; + }, + { + [100]: 100, + '200': 200, + ['300']: 300, + [SameFileGood.STR]: SameFileGood.STR, + [SameFileGood.NUM]: SameFileGood.NUM, + [CrossFileGood.STR]: CrossFileGood.STR, + [CrossFileGood.NUM]: CrossFileGood.NUM, + }, + ) + mustBeComputed( + { [SameFileBad.PROTO]: null }, + { [CrossFileBad.PROTO]: null }, + class { [SameFileBad.CONSTRUCTOR]() {} }, + class { [CrossFileBad.CONSTRUCTOR]() {} }, + class { static [SameFileBad.PROTOTYPE]() {} }, + class { static [CrossFileBad.PROTOTYPE]() {} }, + ) + `, + "/cross-file.ts": ` + export const enum CrossFileGood { + STR = 'str 2', + NUM = 321, + } + export const enum CrossFileBad { + PROTO = '__proto__', + CONSTRUCTOR = 'constructor', + PROTOTYPE = 'prototype', + } + `, + }, + entryPaths: []string{"/entry.ts"}, + options: config.Options{ + Mode: config.ModeBundle, + MinifySyntax: true, + AbsOutputFile: "/out.js", + }, + }) +} + func TestTSImportVsLocalCollisionAllTypes(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 5e6cd3f70e5..d9c48ecfeb3 100644 --- a/internal/bundler_tests/snapshots/snapshots_ts.txt +++ b/internal/bundler_tests/snapshots/snapshots_ts.txt @@ -1180,6 +1180,60 @@ 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",e))(Foo||{}); +================================================================================ +TestTSMinifyEnumPropertyNames +---------- /out.js ---------- +// entry.ts +var Foo = class { + 100 = 100; + 200 = 200; + 300 = 300; + "str 1" = "str 1" /* STR */; + 123 = 123 /* NUM */; + "str 2" = "str 2" /* STR */; + 321 = 321 /* NUM */; +}; +shouldNotBeComputed( + class { + 100 = 100; + 200 = 200; + 300 = 300; + "str 1" = "str 1" /* STR */; + 123 = 123 /* NUM */; + "str 2" = "str 2" /* STR */; + 321 = 321 /* NUM */; + }, + { + 100: 100, + 200: 200, + 300: 300, + "str 1": "str 1" /* STR */, + 123: 123 /* NUM */, + "str 2": "str 2" /* STR */, + 321: 321 /* NUM */ + } +); +mustBeComputed( + { ["__proto__"]: null }, + { ["__proto__"]: null }, + class { + ["constructor"]() { + } + }, + class { + ["constructor"]() { + } + }, + class { + static ["prototype"]() { + } + }, + class { + static ["prototype"]() { + } + } +); + ================================================================================ TestTSMinifyNamespace ---------- /a.js ---------- diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 5af404d8c52..7d34f1ca7bc 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -10893,15 +10893,26 @@ func (p *parser) visitClass(nameScopeLoc logger.Loc, class *js_ast.Class, defaul property.Key = key if p.options.minifySyntax { - if str, ok := key.Data.(*js_ast.EString); ok { - if numberValue, ok := js_ast.StringToEquivalentNumberValue(str.Value); ok && numberValue >= 0 { + if inlined, ok := key.Data.(*js_ast.EInlinedEnum); ok { + switch inlined.Value.Data.(type) { + case *js_ast.EString, *js_ast.ENumber: + key.Data = inlined.Value.Data + property.Key.Data = key.Data + } + } + switch k := key.Data.(type) { + case *js_ast.ENumber: + // "class { [123] }" => "class { 123 }" + property.Flags &= ^js_ast.PropertyIsComputed + case *js_ast.EString: + if numberValue, ok := js_ast.StringToEquivalentNumberValue(k.Value); ok && numberValue >= 0 { // "class { '123' }" => "class { 123 }" property.Key.Data = &js_ast.ENumber{Value: numberValue} property.Flags &= ^js_ast.PropertyIsComputed } else if property.Flags.Has(js_ast.PropertyIsComputed) { // "class {['x'] = y}" => "class {'x' = y}" isInvalidConstructor := false - if helpers.UTF16EqualsString(str.Value, "constructor") { + if helpers.UTF16EqualsString(k.Value, "constructor") { if !property.Flags.Has(js_ast.PropertyIsMethod) { // "constructor" is an invalid name for both instance and static fields isInvalidConstructor = true @@ -10912,7 +10923,7 @@ func (p *parser) visitClass(nameScopeLoc logger.Loc, class *js_ast.Class, defaul } // A static property must not be called "prototype" - isInvalidPrototype := property.Flags.Has(js_ast.PropertyIsStatic) && helpers.UTF16EqualsString(str.Value, "prototype") + isInvalidPrototype := property.Flags.Has(js_ast.PropertyIsStatic) && helpers.UTF16EqualsString(k.Value, "prototype") if !isInvalidConstructor && !isInvalidPrototype { property.Flags &= ^js_ast.PropertyIsComputed @@ -13276,8 +13287,20 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO // "{['x']: y}" => "{x: y}" if p.options.minifySyntax && property.Flags.Has(js_ast.PropertyIsComputed) { - if str, ok := key.Data.(*js_ast.EString); ok && js_ast.IsIdentifierUTF16(str.Value) && !helpers.UTF16EqualsString(str.Value, "__proto__") { + if inlined, ok := key.Data.(*js_ast.EInlinedEnum); ok { + switch inlined.Value.Data.(type) { + case *js_ast.EString, *js_ast.ENumber: + key.Data = inlined.Value.Data + property.Key.Data = key.Data + } + } + switch k := key.Data.(type) { + case *js_ast.ENumber: property.Flags &= ^js_ast.PropertyIsComputed + case *js_ast.EString: + if !helpers.UTF16EqualsString(k.Value, "__proto__") { + property.Flags &= ^js_ast.PropertyIsComputed + } } } } else { diff --git a/internal/js_printer/js_printer.go b/internal/js_printer/js_printer.go index 3bb03e84ca9..a042fe96990 100644 --- a/internal/js_printer/js_printer.go +++ b/internal/js_printer/js_printer.go @@ -982,6 +982,34 @@ func (p *printer) printProperty(property js_ast.Property) { return } + // Handle key syntax compression for cross-module constant inlining of enums + if p.options.MinifySyntax && property.Flags.Has(js_ast.PropertyIsComputed) { + if dot, ok := property.Key.Data.(*js_ast.EDot); ok { + if id, ok := dot.Target.Data.(*js_ast.EImportIdentifier); ok { + ref := js_ast.FollowSymbols(p.symbols, id.Ref) + if symbol := p.symbols.Get(ref); symbol.Kind == js_ast.SymbolTSEnum { + if enum, ok := p.options.TSEnums[ref]; ok { + if value, ok := enum[dot.Name]; ok { + if value.String != nil { + property.Key.Data = &js_ast.EString{Value: value.String} + + // Problematic key names must stay computed for correctness + if !helpers.UTF16EqualsString(value.String, "__proto__") && + !helpers.UTF16EqualsString(value.String, "constructor") && + !helpers.UTF16EqualsString(value.String, "prototype") { + property.Flags &= ^js_ast.PropertyIsComputed + } + } else { + property.Key.Data = &js_ast.ENumber{Value: value.Number} + property.Flags &= ^js_ast.PropertyIsComputed + } + } + } + } + } + } + } + if property.Flags.Has(js_ast.PropertyIsStatic) { p.addSourceMapping(property.Loc) p.print("static")