From 73ee85abeb12a858bfbcd8d59c55974d5796e55c Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Fri, 16 Jun 2023 00:46:32 -0400 Subject: [PATCH] overhaul esbuild's class transform (#3167) --- CHANGELOG.md | 85 ++++ internal/bundler_tests/bundler_dce_test.go | 6 +- internal/bundler_tests/bundler_lower_test.go | 5 +- .../bundler_tests/snapshots/snapshots_dce.txt | 72 ++-- .../snapshots/snapshots_default.txt | 25 +- .../snapshots/snapshots_lower.txt | 397 +++++++++--------- .../bundler_tests/snapshots/snapshots_ts.txt | 32 +- internal/js_ast/js_ast.go | 3 + internal/js_parser/js_parser.go | 27 +- internal/js_parser/js_parser_lower.go | 306 ++++++++------ internal/js_parser/js_parser_lower_test.go | 8 +- internal/js_parser/ts_parser_test.go | 2 + internal/logger/msg_ids.go | 5 + internal/runtime/runtime.go | 3 + scripts/end-to-end-tests.js | 151 +++++++ scripts/uglify-tests.js | 1 - 16 files changed, 727 insertions(+), 401 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b774bde26f..3f97a7366e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,90 @@ # Changelog +## Unreleased + +* Bundling no longer unnecessarily transforms class syntax ([#1360](https://github.com/evanw/esbuild/issues/1360), [#1328](https://github.com/evanw/esbuild/issues/1328), [#1524](https://github.com/evanw/esbuild/issues/1524), [#2416](https://github.com/evanw/esbuild/issues/2416)) + + When bundling, esbuild automatically converts top-level class statements to class expressions. Previously this conversion had the unfortunate side-effect of also transforming certain other class-related syntax features to avoid correctness issues when the references to the class name within the class body. This conversion has been reworked to avoid doing this: + + ```js + // Original code + export class Foo { + static foo = () => Foo + } + + // Old output (with --bundle) + var _Foo = class { + }; + var Foo = _Foo; + __publicField(Foo, "foo", () => _Foo); + + // New output (with --bundle) + var Foo = class _Foo { + static foo = () => _Foo; + }; + ``` + + This conversion process is very complicated and has many edge cases (including interactions with static fields, static blocks, private class properties, and TypeScript experimental decorators). It should already be pretty robust but a change like this may introduce new unintentional behavior. Please report any issues with this upgrade on the esbuild bug tracker. + + You may be wondering why esbuild needs to do this at all. One reason to do this is that esbuild's bundler sometimes needs to lazily-evaluate a module. For example, a module may end up being both the target of a dynamic `import()` call and a static `import` statement. Lazy module evaluation is done by wrapping the top-level module code in a closure. To avoid a performance hit for static `import` statements, esbuild stores top-level exported symbols outside of the closure and references them directly instead of indirectly. + + Another reason to do this is that multiple JavaScript VMs have had and continue to have performance issues with TDZ (i.e. "temporal dead zone") checks. These checks validate that a let, or const, or class symbol isn't used before it's initialized. Here are two issues with well-known VMs: + + * V8: https://bugs.chromium.org/p/v8/issues/detail?id=13723 (10% slowdown) + * JavaScriptCore: https://bugs.webkit.org/show_bug.cgi?id=199866 (1,000% slowdown!) + + JavaScriptCore had a severe performance issue as their TDZ implementation had time complexity that was quadratic in the number of variables needing TDZ checks in the same scope (with the top-level scope typically being the worst offender). V8 has ongoing issues with TDZ checks being present throughout the code their JIT generates even when they have already been checked earlier in the same function or when the function in question has already been run (so the checks have already happened). + + Due to esbuild's parallel architecture, esbuild both a) needs to convert class statements into class expressions during parsing and b) doesn't yet know whether this module will need to be lazily-evaluated or not in the parser. So esbuild always does this conversion during bundling in case it's needed for correctness (and also to avoid potentially catastrophic performance issues due to bundling creating a large scope with many TDZ variables). + +* Enforce TDZ errors in computed class property keys ([#2045](https://github.com/evanw/esbuild/issues/2045)) + + JavaScript allows class property keys to be generated at run-time using code, like this: + + ```js + class Foo { + static foo = 'foo' + static [Foo.foo + '2'] = 2 + } + ``` + + Previously esbuild treated references to the containing class name within computed property keys as a reference to the partially-initialized class object. That meant code that attempted to reference properties of the class object (such as the code above) would get back `undefined` instead of throwing an error. + + This release rewrites references to the containing class name within computed property keys into code that always throws an error at run-time, which is how this JavaScript code is supposed to work. Code that does this will now also generate a warning. You should never write code like this, but it now should be more obvious when incorrect code like this is written. + +* Fix an issue with experimental decorators and static fields ([#2629](https://github.com/evanw/esbuild/issues/2629)) + + This release also fixes a bug regarding TypeScript experimental decorators and static class fields which reference the enclosing class name in their initializer. This affected top-level classes when bundling was enabled. Previously code that does this could crash because the class name wasn't initialized yet. This case should now be handled correctly: + + ```ts + // Original code + class Foo { + @someDecorator + static foo = 'foo' + static bar = Foo.foo.length + } + + // Old output + const _Foo = class { + static foo = "foo"; + static bar = _Foo.foo.length; + }; + let Foo = _Foo; + __decorateClass([ + someDecorator + ], Foo, "foo", 2); + + // New output + const _Foo = class _Foo { + static foo = "foo"; + static bar = _Foo.foo.length; + }; + __decorateClass([ + someDecorator + ], _Foo, "foo", 2); + let Foo = _Foo; + ``` + ## 0.18.3 * Fix a panic due to empty static class blocks ([#3161](https://github.com/evanw/esbuild/issues/3161)) diff --git a/internal/bundler_tests/bundler_dce_test.go b/internal/bundler_tests/bundler_dce_test.go index 8a9a93ce244..304e9dc8774 100644 --- a/internal/bundler_tests/bundler_dce_test.go +++ b/internal/bundler_tests/bundler_dce_test.go @@ -2238,7 +2238,7 @@ func TestTreeShakingLoweredClassStaticField(t *testing.T) { options: config.Options{ Mode: config.ModeBundle, AbsOutputDir: "/out", - UnsupportedJSFeatures: compat.ClassField, + UnsupportedJSFeatures: compat.ClassStaticField, }, }) } @@ -2272,7 +2272,7 @@ func TestTreeShakingLoweredClassStaticFieldMinified(t *testing.T) { options: config.Options{ Mode: config.ModeBundle, AbsOutputDir: "/out", - UnsupportedJSFeatures: compat.ClassField, + UnsupportedJSFeatures: compat.ClassStaticField, MinifySyntax: true, }, }) @@ -2305,7 +2305,7 @@ func TestTreeShakingLoweredClassStaticFieldAssignment(t *testing.T) { options: config.Options{ Mode: config.ModeBundle, AbsOutputDir: "/out", - UnsupportedJSFeatures: compat.ClassField, + UnsupportedJSFeatures: compat.ClassStaticField, TS: config.TSOptions{Config: config.TSConfig{ UseDefineForClassFields: config.False, }}, diff --git a/internal/bundler_tests/bundler_lower_test.go b/internal/bundler_tests/bundler_lower_test.go index 8744e76e73c..a500defa3c9 100644 --- a/internal/bundler_tests/bundler_lower_test.go +++ b/internal/bundler_tests/bundler_lower_test.go @@ -2218,8 +2218,9 @@ func TestLowerPrivateClassFieldStaticIssue1424(t *testing.T) { }, entryPaths: []string{"/entry.js"}, options: config.Options{ - Mode: config.ModeBundle, - AbsOutputFile: "/out.js", + Mode: config.ModeBundle, + AbsOutputFile: "/out.js", + UnsupportedJSFeatures: compat.ClassPrivateMethod, }, }) } diff --git a/internal/bundler_tests/snapshots/snapshots_dce.txt b/internal/bundler_tests/snapshots/snapshots_dce.txt index 87ba47a732d..ee92d85f5b5 100644 --- a/internal/bundler_tests/snapshots/snapshots_dce.txt +++ b/internal/bundler_tests/snapshots/snapshots_dce.txt @@ -340,55 +340,59 @@ TestDCEClassStaticBlocks ---------- /out.js ---------- // entry.ts var A_keep = class { + static { + foo; + } }; -foo; -var _B_keep = class { +var B_keep = class { + static { + this.foo; + } }; -var B_keep = _B_keep; -_B_keep.foo; var C_keep = class { -}; -(() => { - try { - foo; - } catch { + static { + try { + foo; + } catch { + } } -})(); -var D_keep = class { }; -(() => { - try { - } finally { - foo; +var D_keep = class { + static { + try { + } finally { + foo; + } } -})(); +}; ================================================================================ TestDCEClassStaticBlocksMinifySyntax ---------- /out.js ---------- // entry.ts var A_keep = class { -}; -foo; -var _B_keep = class { -}, B_keep = _B_keep; -_B_keep.foo; -var C_keep = class { -}; -(() => { - try { + static { foo; - } catch { } -})(); -var D_keep = class { -}; -(() => { - try { - } finally { - foo; +}, B_keep = class { + static { + this.foo; } -})(); +}, C_keep = class { + static { + try { + foo; + } catch { + } + } +}, D_keep = class { + static { + try { + } finally { + foo; + } + } +}; ================================================================================ TestDCEOfIIFE diff --git a/internal/bundler_tests/snapshots/snapshots_default.txt b/internal/bundler_tests/snapshots/snapshots_default.txt index e1697babd25..ae8215ec983 100644 --- a/internal/bundler_tests/snapshots/snapshots_default.txt +++ b/internal/bundler_tests/snapshots/snapshots_default.txt @@ -162,10 +162,9 @@ fs.readFile(); TestAvoidTDZ ---------- /out.js ---------- // entry.js -var _Foo = class { +var Foo = class _Foo { + static foo = new _Foo(); }; -var Foo = _Foo; -__publicField(Foo, "foo", new _Foo()); var foo = Foo.foo; console.log(foo); var Bar = class { @@ -2491,12 +2490,17 @@ x = fnStmtKeep; var fnExprKeep = /* @__PURE__ */ __name(function() { }, "keep"); x = fnExprKeep; -var _clsStmtKeep = class { -}, clsStmtKeep = _clsStmtKeep; -__name(_clsStmtKeep, "clsStmtKeep"); +var clsStmtKeep = class { + static { + __name(this, "clsStmtKeep"); + } +}; new clsStmtKeep(); -var _a, clsExprKeep = (_a = class { -}, __name(_a, "keep"), _a); +var clsExprKeep = class { + static { + __name(this, "keep"); + } +}; new clsExprKeep(); ================================================================================ @@ -5313,8 +5317,9 @@ var objFoo = { console.log(this); } }; -var _Foo = class { +var Foo = class { x = this; + static y = this.z; foo(x = this) { console.log(this); } @@ -5322,8 +5327,6 @@ var _Foo = class { console.log(this); } }; -var Foo = _Foo; -__publicField(Foo, "y", _Foo.z); new Foo(foo(objFoo)); if (nested) { let bar = function(x = this) { diff --git a/internal/bundler_tests/snapshots/snapshots_lower.txt b/internal/bundler_tests/snapshots/snapshots_lower.txt index 2250d471d6d..027ae17b804 100644 --- a/internal/bundler_tests/snapshots/snapshots_lower.txt +++ b/internal/bundler_tests/snapshots/snapshots_lower.txt @@ -95,99 +95,99 @@ export default [ TestLowerAsyncArrowSuperES2016 ---------- /out.js ---------- // foo1.js -var foo1_default = class extends x { +var foo1_default = class _foo1_default extends x { foo1() { return () => __async(this, null, function* () { - return __superGet(foo1_default.prototype, this, "foo").call(this, "foo1"); + return __superGet(_foo1_default.prototype, this, "foo").call(this, "foo1"); }); } }; // foo2.js -var foo2_default = class extends x { +var foo2_default = class _foo2_default extends x { foo2() { return () => __async(this, null, function* () { - return () => __superGet(foo2_default.prototype, this, "foo").call(this, "foo2"); + return () => __superGet(_foo2_default.prototype, this, "foo").call(this, "foo2"); }); } }; // foo3.js -var foo3_default = class extends x { +var foo3_default = class _foo3_default extends x { foo3() { return () => () => __async(this, null, function* () { - return __superGet(foo3_default.prototype, this, "foo").call(this, "foo3"); + return __superGet(_foo3_default.prototype, this, "foo").call(this, "foo3"); }); } }; // foo4.js -var foo4_default = class extends x { +var foo4_default = class _foo4_default extends x { foo4() { return () => __async(this, null, function* () { return () => __async(this, null, function* () { - return __superGet(foo4_default.prototype, this, "foo").call(this, "foo4"); + return __superGet(_foo4_default.prototype, this, "foo").call(this, "foo4"); }); }); } }; // bar1.js -var bar1_default = class extends x { +var bar1_default = class _bar1_default extends x { constructor() { super(...arguments); __publicField(this, "bar1", () => __async(this, null, function* () { - return __superGet(bar1_default.prototype, this, "foo").call(this, "bar1"); + return __superGet(_bar1_default.prototype, this, "foo").call(this, "bar1"); })); } }; // bar2.js -var bar2_default = class extends x { +var bar2_default = class _bar2_default extends x { constructor() { super(...arguments); __publicField(this, "bar2", () => __async(this, null, function* () { - return () => __superGet(bar2_default.prototype, this, "foo").call(this, "bar2"); + return () => __superGet(_bar2_default.prototype, this, "foo").call(this, "bar2"); })); } }; // bar3.js -var bar3_default = class extends x { +var bar3_default = class _bar3_default extends x { constructor() { super(...arguments); __publicField(this, "bar3", () => () => __async(this, null, function* () { - return __superGet(bar3_default.prototype, this, "foo").call(this, "bar3"); + return __superGet(_bar3_default.prototype, this, "foo").call(this, "bar3"); })); } }; // bar4.js -var bar4_default = class extends x { +var bar4_default = class _bar4_default extends x { constructor() { super(...arguments); __publicField(this, "bar4", () => __async(this, null, function* () { return () => __async(this, null, function* () { - return __superGet(bar4_default.prototype, this, "foo").call(this, "bar4"); + return __superGet(_bar4_default.prototype, this, "foo").call(this, "bar4"); }); })); } }; // baz1.js -var baz1_default = class extends x { +var baz1_default = class _baz1_default extends x { baz1() { return __async(this, null, function* () { - return () => __superGet(baz1_default.prototype, this, "foo").call(this, "baz1"); + return () => __superGet(_baz1_default.prototype, this, "foo").call(this, "baz1"); }); } }; // baz2.js -var baz2_default = class extends x { +var baz2_default = class _baz2_default extends x { baz2() { return __async(this, null, function* () { - return () => () => __superGet(baz2_default.prototype, this, "foo").call(this, "baz2"); + return () => () => __superGet(_baz2_default.prototype, this, "foo").call(this, "baz2"); }); } }; @@ -223,99 +223,99 @@ export { TestLowerAsyncArrowSuperSetterES2016 ---------- /out.js ---------- // foo1.js -var foo1_default = class extends x { +var foo1_default = class _foo1_default extends x { foo1() { return () => __async(this, null, function* () { - return __superSet(foo1_default.prototype, this, "foo", "foo1"); + return __superSet(_foo1_default.prototype, this, "foo", "foo1"); }); } }; // foo2.js -var foo2_default = class extends x { +var foo2_default = class _foo2_default extends x { foo2() { return () => __async(this, null, function* () { - return () => __superSet(foo2_default.prototype, this, "foo", "foo2"); + return () => __superSet(_foo2_default.prototype, this, "foo", "foo2"); }); } }; // foo3.js -var foo3_default = class extends x { +var foo3_default = class _foo3_default extends x { foo3() { return () => () => __async(this, null, function* () { - return __superSet(foo3_default.prototype, this, "foo", "foo3"); + return __superSet(_foo3_default.prototype, this, "foo", "foo3"); }); } }; // foo4.js -var foo4_default = class extends x { +var foo4_default = class _foo4_default extends x { foo4() { return () => __async(this, null, function* () { return () => __async(this, null, function* () { - return __superSet(foo4_default.prototype, this, "foo", "foo4"); + return __superSet(_foo4_default.prototype, this, "foo", "foo4"); }); }); } }; // bar1.js -var bar1_default = class extends x { +var bar1_default = class _bar1_default extends x { constructor() { super(...arguments); __publicField(this, "bar1", () => __async(this, null, function* () { - return __superSet(bar1_default.prototype, this, "foo", "bar1"); + return __superSet(_bar1_default.prototype, this, "foo", "bar1"); })); } }; // bar2.js -var bar2_default = class extends x { +var bar2_default = class _bar2_default extends x { constructor() { super(...arguments); __publicField(this, "bar2", () => __async(this, null, function* () { - return () => __superSet(bar2_default.prototype, this, "foo", "bar2"); + return () => __superSet(_bar2_default.prototype, this, "foo", "bar2"); })); } }; // bar3.js -var bar3_default = class extends x { +var bar3_default = class _bar3_default extends x { constructor() { super(...arguments); __publicField(this, "bar3", () => () => __async(this, null, function* () { - return __superSet(bar3_default.prototype, this, "foo", "bar3"); + return __superSet(_bar3_default.prototype, this, "foo", "bar3"); })); } }; // bar4.js -var bar4_default = class extends x { +var bar4_default = class _bar4_default extends x { constructor() { super(...arguments); __publicField(this, "bar4", () => __async(this, null, function* () { return () => __async(this, null, function* () { - return __superSet(bar4_default.prototype, this, "foo", "bar4"); + return __superSet(_bar4_default.prototype, this, "foo", "bar4"); }); })); } }; // baz1.js -var baz1_default = class extends x { +var baz1_default = class _baz1_default extends x { baz1() { return __async(this, null, function* () { - return () => __superSet(baz1_default.prototype, this, "foo", "baz1"); + return () => __superSet(_baz1_default.prototype, this, "foo", "baz1"); }); } }; // baz2.js -var baz2_default = class extends x { +var baz2_default = class _baz2_default extends x { baz2() { return __async(this, null, function* () { - return () => () => __superSet(baz2_default.prototype, this, "foo", "baz2"); + return () => () => __superSet(_baz2_default.prototype, this, "foo", "baz2"); }); } }; @@ -1014,75 +1014,75 @@ console.log(new Foo().bar === 123); TestLowerPrivateClassStaticAccessorOrder ---------- /out.js ---------- var _foo, foo_get, _foo2, foo_get2; -const _Foo = class { +const _Foo = class _Foo { // This must be set before "bar" is initialized }; -let Foo = _Foo; _foo = new WeakSet(); foo_get = function() { return 123; }; -__privateAdd(Foo, _foo); -__publicField(Foo, "bar", __privateGet(_Foo, _foo, foo_get)); +__privateAdd(_Foo, _foo); +__publicField(_Foo, "bar", __privateGet(_Foo, _foo, foo_get)); +let Foo = _Foo; console.log(Foo.bar === 123); -const _FooThis = class { +const _FooThis = class _FooThis { // This must be set before "bar" is initialized }; -let FooThis = _FooThis; _foo2 = new WeakSet(); foo_get2 = function() { return 123; }; -__privateAdd(FooThis, _foo2); -__publicField(FooThis, "bar", __privateGet(_FooThis, _foo2, foo_get2)); +__privateAdd(_FooThis, _foo2); +__publicField(_FooThis, "bar", __privateGet(_FooThis, _foo2, foo_get2)); +let FooThis = _FooThis; console.log(FooThis.bar === 123); ================================================================================ TestLowerPrivateClassStaticFieldOrder ---------- /out.js ---------- var _foo, _foo2; -const _Foo = class { +const _Foo = class _Foo { }; -let Foo = _Foo; _foo = new WeakMap(); -__privateAdd(Foo, _foo, 123); +__privateAdd(_Foo, _foo, 123); // This must be set before "bar" is initialized -__publicField(Foo, "bar", __privateGet(_Foo, _foo)); +__publicField(_Foo, "bar", __privateGet(_Foo, _foo)); +let Foo = _Foo; console.log(Foo.bar === 123); -const _FooThis = class { +const _FooThis = class _FooThis { }; -let FooThis = _FooThis; _foo2 = new WeakMap(); -__privateAdd(FooThis, _foo2, 123); +__privateAdd(_FooThis, _foo2, 123); // This must be set before "bar" is initialized -__publicField(FooThis, "bar", __privateGet(_FooThis, _foo2)); +__publicField(_FooThis, "bar", __privateGet(_FooThis, _foo2)); +let FooThis = _FooThis; console.log(FooThis.bar === 123); ================================================================================ TestLowerPrivateClassStaticMethodOrder ---------- /out.js ---------- var _a, _foo, foo_fn, _b, _foo2, foo_fn2; -const _Foo = class { +const _Foo = class _Foo { // This must be set before "bar" is initialized }; -let Foo = _Foo; _foo = new WeakSet(); foo_fn = function() { return 123; }; -__privateAdd(Foo, _foo); -__publicField(Foo, "bar", __privateMethod(_a = _Foo, _foo, foo_fn).call(_a)); +__privateAdd(_Foo, _foo); +__publicField(_Foo, "bar", __privateMethod(_a = _Foo, _foo, foo_fn).call(_a)); +let Foo = _Foo; console.log(Foo.bar === 123); -const _FooThis = class { +const _FooThis = class _FooThis { // This must be set before "bar" is initialized }; -let FooThis = _FooThis; _foo2 = new WeakSet(); foo_fn2 = function() { return 123; }; -__privateAdd(FooThis, _foo2); -__publicField(FooThis, "bar", __privateMethod(_b = _FooThis, _foo2, foo_fn2).call(_b)); +__privateAdd(_FooThis, _foo2); +__publicField(_FooThis, "bar", __privateMethod(_b = _FooThis, _foo2, foo_fn2).call(_b)); +let FooThis = _FooThis; console.log(FooThis.bar === 123); ================================================================================ @@ -1673,53 +1673,53 @@ TestLowerPrivateSuperES2021 ---------- /out.js ---------- // foo1.js var _foo, foo_fn; -var _foo1_default = class extends x { +var _foo1_default = class _foo1_default extends x { constructor() { super(...arguments); __privateAdd(this, _foo); } }; -var foo1_default = _foo1_default; _foo = new WeakSet(); foo_fn = function() { __superGet(_foo1_default.prototype, this, "foo").call(this); }; +var foo1_default = _foo1_default; // foo2.js var _foo2, foo_fn2; -var _foo2_default = class extends x { +var _foo2_default = class _foo2_default extends x { constructor() { super(...arguments); __privateAdd(this, _foo2); } }; -var foo2_default = _foo2_default; _foo2 = new WeakSet(); foo_fn2 = function() { __superWrapper(_foo2_default.prototype, this, "foo")._++; }; +var foo2_default = _foo2_default; // foo3.js var _foo3, foo_fn3; -var _foo3_default = class extends x { +var _foo3_default = class _foo3_default extends x { }; -var foo3_default = _foo3_default; _foo3 = new WeakSet(); foo_fn3 = function() { __superGet(_foo3_default, this, "foo").call(this); }; -__privateAdd(foo3_default, _foo3); +__privateAdd(_foo3_default, _foo3); +var foo3_default = _foo3_default; // foo4.js var _foo4, foo_fn4; -var _foo4_default = class extends x { +var _foo4_default = class _foo4_default extends x { }; -var foo4_default = _foo4_default; _foo4 = new WeakSet(); foo_fn4 = function() { __superWrapper(_foo4_default, this, "foo")._++; }; -__privateAdd(foo4_default, _foo4); +__privateAdd(_foo4_default, _foo4); +var foo4_default = _foo4_default; // foo5.js var _foo5; @@ -1747,23 +1747,23 @@ _foo6 = new WeakMap(); // foo7.js var _foo7; -var _foo7_default = class extends x { +var _foo7_default = class _foo7_default extends x { }; -var foo7_default = _foo7_default; _foo7 = new WeakMap(); -__privateAdd(foo7_default, _foo7, () => { +__privateAdd(_foo7_default, _foo7, () => { __superGet(_foo7_default, _foo7_default, "foo").call(this); }); +var foo7_default = _foo7_default; // foo8.js var _foo8; -var _foo8_default = class extends x { +var _foo8_default = class _foo8_default extends x { }; -var foo8_default = _foo8_default; _foo8 = new WeakMap(); -__privateAdd(foo8_default, _foo8, () => { +__privateAdd(_foo8_default, _foo8, () => { __superWrapper(_foo8_default, _foo8_default, "foo")._++; }); +var foo8_default = _foo8_default; export { foo1_default as foo1, foo2_default as foo2, @@ -1793,26 +1793,18 @@ var foo2_default = class extends x { }; // foo3.js -var _foo, foo_fn; -var _foo3_default = class extends x { -}; -var foo3_default = _foo3_default; -_foo = new WeakSet(); -foo_fn = function() { - __superGet(_foo3_default, this, "foo").call(this); +var foo3_default = class extends x { + static #foo() { + super.foo(); + } }; -__privateAdd(foo3_default, _foo); // foo4.js -var _foo2, foo_fn2; -var _foo4_default = class extends x { -}; -var foo4_default = _foo4_default; -_foo2 = new WeakSet(); -foo_fn2 = function() { - __superWrapper(_foo4_default, this, "foo")._++; +var foo4_default = class extends x { + static #foo() { + super.foo++; + } }; -__privateAdd(foo4_default, _foo2); // foo5.js var foo5_default = class extends x { @@ -1829,24 +1821,18 @@ var foo6_default = class extends x { }; // foo7.js -var _foo3; -var _foo7_default = class extends x { +var foo7_default = class extends x { + static #foo = () => { + super.foo(); + }; }; -var foo7_default = _foo7_default; -_foo3 = new WeakMap(); -__privateAdd(foo7_default, _foo3, () => { - __superGet(_foo7_default, _foo7_default, "foo").call(this); -}); // foo8.js -var _foo4; -var _foo8_default = class extends x { +var foo8_default = class extends x { + static #foo = () => { + super.foo++; + }; }; -var foo8_default = _foo8_default; -_foo4 = new WeakMap(); -__privateAdd(foo8_default, _foo4, () => { - __superWrapper(_foo8_default, _foo8_default, "foo")._++; -}); export { foo1_default as foo1, foo2_default as foo2, @@ -1862,15 +1848,13 @@ export { TestLowerPrivateSuperStaticBundleIssue2158 ---------- /out.js ---------- // entry.js -var _foo; var Foo = class extends Object { + static FOO; constructor() { super(); - __privateAdd(this, _foo, void 0); } + #foo; }; -_foo = new WeakMap(); -__publicField(Foo, "FOO"); export { Foo }; @@ -1890,91 +1874,91 @@ export { TestLowerStaticAsyncArrowSuperES2016 ---------- /out.js ---------- // foo1.js -var foo1_default = class extends x { +var foo1_default = class _foo1_default extends x { static foo1() { return () => __async(this, null, function* () { - return __superGet(foo1_default, this, "foo").call(this, "foo1"); + return __superGet(_foo1_default, this, "foo").call(this, "foo1"); }); } }; // foo2.js -var foo2_default = class extends x { +var foo2_default = class _foo2_default extends x { static foo2() { return () => __async(this, null, function* () { - return () => __superGet(foo2_default, this, "foo").call(this, "foo2"); + return () => __superGet(_foo2_default, this, "foo").call(this, "foo2"); }); } }; // foo3.js -var foo3_default = class extends x { +var foo3_default = class _foo3_default extends x { static foo3() { return () => () => __async(this, null, function* () { - return __superGet(foo3_default, this, "foo").call(this, "foo3"); + return __superGet(_foo3_default, this, "foo").call(this, "foo3"); }); } }; // foo4.js -var foo4_default = class extends x { +var foo4_default = class _foo4_default extends x { static foo4() { return () => __async(this, null, function* () { return () => __async(this, null, function* () { - return __superGet(foo4_default, this, "foo").call(this, "foo4"); + return __superGet(_foo4_default, this, "foo").call(this, "foo4"); }); }); } }; // bar1.js -var _bar1_default = class extends x { +var _bar1_default = class _bar1_default extends x { }; -var bar1_default = _bar1_default; -__publicField(bar1_default, "bar1", () => __async(_bar1_default, null, function* () { +__publicField(_bar1_default, "bar1", () => __async(_bar1_default, null, function* () { return __superGet(_bar1_default, _bar1_default, "foo").call(this, "bar1"); })); +var bar1_default = _bar1_default; // bar2.js -var _bar2_default = class extends x { +var _bar2_default = class _bar2_default extends x { }; -var bar2_default = _bar2_default; -__publicField(bar2_default, "bar2", () => __async(_bar2_default, null, function* () { +__publicField(_bar2_default, "bar2", () => __async(_bar2_default, null, function* () { return () => __superGet(_bar2_default, _bar2_default, "foo").call(this, "bar2"); })); +var bar2_default = _bar2_default; // bar3.js -var _bar3_default = class extends x { +var _bar3_default = class _bar3_default extends x { }; -var bar3_default = _bar3_default; -__publicField(bar3_default, "bar3", () => () => __async(_bar3_default, null, function* () { +__publicField(_bar3_default, "bar3", () => () => __async(_bar3_default, null, function* () { return __superGet(_bar3_default, _bar3_default, "foo").call(this, "bar3"); })); +var bar3_default = _bar3_default; // bar4.js -var _bar4_default = class extends x { +var _bar4_default = class _bar4_default extends x { }; -var bar4_default = _bar4_default; -__publicField(bar4_default, "bar4", () => __async(_bar4_default, null, function* () { +__publicField(_bar4_default, "bar4", () => __async(_bar4_default, null, function* () { return () => __async(_bar4_default, null, function* () { return __superGet(_bar4_default, _bar4_default, "foo").call(this, "bar4"); }); })); +var bar4_default = _bar4_default; // baz1.js -var baz1_default = class extends x { +var baz1_default = class _baz1_default extends x { static baz1() { return __async(this, null, function* () { - return () => __superGet(baz1_default, this, "foo").call(this, "baz1"); + return () => __superGet(_baz1_default, this, "foo").call(this, "baz1"); }); } }; // baz2.js -var baz2_default = class extends x { +var baz2_default = class _baz2_default extends x { static baz2() { return __async(this, null, function* () { - return () => () => __superGet(baz2_default, this, "foo").call(this, "baz2"); + return () => () => __superGet(_baz2_default, this, "foo").call(this, "baz2"); }); } }; @@ -1982,12 +1966,12 @@ var baz2_default = class extends x { // outer.js var outer_default = function() { return __async(this, null, function* () { - const _y = class extends z { + const _y = class _y extends z { }; - let y = _y; - __publicField(y, "foo", () => __async(_y, null, function* () { + __publicField(_y, "foo", () => __async(_y, null, function* () { return __superGet(_y, _y, "foo").call(this); })); + let y = _y; yield y.foo()(); }); }(); @@ -2008,91 +1992,91 @@ export { TestLowerStaticAsyncArrowSuperSetterES2016 ---------- /out.js ---------- // foo1.js -var foo1_default = class extends x { +var foo1_default = class _foo1_default extends x { static foo1() { return () => __async(this, null, function* () { - return __superSet(foo1_default, this, "foo", "foo1"); + return __superSet(_foo1_default, this, "foo", "foo1"); }); } }; // foo2.js -var foo2_default = class extends x { +var foo2_default = class _foo2_default extends x { static foo2() { return () => __async(this, null, function* () { - return () => __superSet(foo2_default, this, "foo", "foo2"); + return () => __superSet(_foo2_default, this, "foo", "foo2"); }); } }; // foo3.js -var foo3_default = class extends x { +var foo3_default = class _foo3_default extends x { static foo3() { return () => () => __async(this, null, function* () { - return __superSet(foo3_default, this, "foo", "foo3"); + return __superSet(_foo3_default, this, "foo", "foo3"); }); } }; // foo4.js -var foo4_default = class extends x { +var foo4_default = class _foo4_default extends x { static foo4() { return () => __async(this, null, function* () { return () => __async(this, null, function* () { - return __superSet(foo4_default, this, "foo", "foo4"); + return __superSet(_foo4_default, this, "foo", "foo4"); }); }); } }; // bar1.js -var _bar1_default = class extends x { +var _bar1_default = class _bar1_default extends x { }; -var bar1_default = _bar1_default; -__publicField(bar1_default, "bar1", () => __async(_bar1_default, null, function* () { +__publicField(_bar1_default, "bar1", () => __async(_bar1_default, null, function* () { return __superSet(_bar1_default, _bar1_default, "foo", "bar1"); })); +var bar1_default = _bar1_default; // bar2.js -var _bar2_default = class extends x { +var _bar2_default = class _bar2_default extends x { }; -var bar2_default = _bar2_default; -__publicField(bar2_default, "bar2", () => __async(_bar2_default, null, function* () { +__publicField(_bar2_default, "bar2", () => __async(_bar2_default, null, function* () { return () => __superSet(_bar2_default, _bar2_default, "foo", "bar2"); })); +var bar2_default = _bar2_default; // bar3.js -var _bar3_default = class extends x { +var _bar3_default = class _bar3_default extends x { }; -var bar3_default = _bar3_default; -__publicField(bar3_default, "bar3", () => () => __async(_bar3_default, null, function* () { +__publicField(_bar3_default, "bar3", () => () => __async(_bar3_default, null, function* () { return __superSet(_bar3_default, _bar3_default, "foo", "bar3"); })); +var bar3_default = _bar3_default; // bar4.js -var _bar4_default = class extends x { +var _bar4_default = class _bar4_default extends x { }; -var bar4_default = _bar4_default; -__publicField(bar4_default, "bar4", () => __async(_bar4_default, null, function* () { +__publicField(_bar4_default, "bar4", () => __async(_bar4_default, null, function* () { return () => __async(_bar4_default, null, function* () { return __superSet(_bar4_default, _bar4_default, "foo", "bar4"); }); })); +var bar4_default = _bar4_default; // baz1.js -var baz1_default = class extends x { +var baz1_default = class _baz1_default extends x { static baz1() { return __async(this, null, function* () { - return () => __superSet(baz1_default, this, "foo", "baz1"); + return () => __superSet(_baz1_default, this, "foo", "baz1"); }); } }; // baz2.js -var baz2_default = class extends x { +var baz2_default = class _baz2_default extends x { static baz2() { return __async(this, null, function* () { - return () => () => __superSet(baz2_default, this, "foo", "baz2"); + return () => () => __superSet(_baz2_default, this, "foo", "baz2"); }); } }; @@ -2100,12 +2084,12 @@ var baz2_default = class extends x { // outer.js var outer_default = function() { return __async(this, null, function* () { - const _y = class extends z { + const _y = class _y extends z { }; - let y = _y; - __publicField(y, "foo", () => __async(_y, null, function* () { + __publicField(_y, "foo", () => __async(_y, null, function* () { return __superSet(_y, _y, "foo", "foo"); })); + let y = _y; yield y.foo()(); }); }(); @@ -2125,10 +2109,9 @@ export { ================================================================================ TestLowerStaticAsyncSuperES2016NoBundle ---------- /out.js ---------- -const _Derived = class extends Base { +const _Derived = class _Derived extends Base { }; -let Derived = _Derived; -__publicField(Derived, "test", (key) => __async(_Derived, null, function* () { +__publicField(_Derived, "test", (key) => __async(_Derived, null, function* () { var _a, _b, _c, _d; return [ yield __superGet(_Derived, _Derived, "foo"), @@ -2159,6 +2142,7 @@ __publicField(Derived, "test", (key) => __async(_Derived, null, function* () { yield __superGet(_Derived, _Derived, key).bind(this)`` ]; })); +let Derived = _Derived; let fn = () => __async(this, null, function* () { var _a; return _a = class extends Base { @@ -2170,7 +2154,7 @@ let fn = () => __async(this, null, function* () { } }, __publicField(_a, "a", __superGet(_a, _a, "a")), __publicField(_a, "b", () => __superGet(_a, _a, "b")), _a; }); -const _Derived2 = class extends Base { +const _Derived2 = class _Derived2 extends Base { static a() { return __async(this, null, function* () { var _a, _b; @@ -2182,8 +2166,7 @@ const _Derived2 = class extends Base { }); } }; -let Derived2 = _Derived2; -__publicField(Derived2, "b", () => __async(_Derived2, null, function* () { +__publicField(_Derived2, "b", () => __async(_Derived2, null, function* () { var _a, _b; return _b = class { constructor() { @@ -2191,14 +2174,14 @@ __publicField(Derived2, "b", () => __async(_Derived2, null, function* () { } }, _a = __superGet(_Derived2, _Derived2, "foo"), _b; })); +let Derived2 = _Derived2; ================================================================================ TestLowerStaticAsyncSuperES2021NoBundle ---------- /out.js ---------- -const _Derived = class extends Base { +const _Derived = class _Derived extends Base { }; -let Derived = _Derived; -__publicField(Derived, "test", async (key) => { +__publicField(_Derived, "test", async (key) => { return [ await __superGet(_Derived, _Derived, "foo"), await __superGet(_Derived, _Derived, key), @@ -2228,6 +2211,7 @@ __publicField(Derived, "test", async (key) => { await __superGet(_Derived, _Derived, key).bind(this)`` ]; }); +let Derived = _Derived; let fn = async () => { var _a; return _a = class extends Base { @@ -2239,7 +2223,7 @@ let fn = async () => { } }, __publicField(_a, "a", __superGet(_a, _a, "a")), __publicField(_a, "b", () => __superGet(_a, _a, "b")), _a; }; -const _Derived2 = class extends Base { +const _Derived2 = class _Derived2 extends Base { static async a() { var _a, _b; return _b = class { @@ -2249,8 +2233,7 @@ const _Derived2 = class extends Base { }, _a = super.foo, _b; } }; -let Derived2 = _Derived2; -__publicField(Derived2, "b", async () => { +__publicField(_Derived2, "b", async () => { var _a, _b; return _b = class { constructor() { @@ -2258,14 +2241,14 @@ __publicField(Derived2, "b", async () => { } }, _a = __superGet(_Derived2, _Derived2, "foo"), _b; }); +let Derived2 = _Derived2; ================================================================================ TestLowerStaticSuperES2016NoBundle ---------- /out.js ---------- -const _Derived = class extends Base { +const _Derived = class _Derived extends Base { }; -let Derived = _Derived; -__publicField(Derived, "test", (key) => { +__publicField(_Derived, "test", (key) => { var _a, _b, _c, _d; return [ __superGet(_Derived, _Derived, "foo"), @@ -2296,14 +2279,14 @@ __publicField(Derived, "test", (key) => { __superGet(_Derived, _Derived, key).bind(this)`` ]; }); +let Derived = _Derived; ================================================================================ TestLowerStaticSuperES2021NoBundle ---------- /out.js ---------- -const _Derived = class extends Base { +const _Derived = class _Derived extends Base { }; -let Derived = _Derived; -__publicField(Derived, "test", (key) => { +__publicField(_Derived, "test", (key) => { return [ __superGet(_Derived, _Derived, "foo"), __superGet(_Derived, _Derived, key), @@ -2333,6 +2316,7 @@ __publicField(Derived, "test", (key) => { __superGet(_Derived, _Derived, key).bind(this)`` ]; }); +let Derived = _Derived; ================================================================================ TestLowerStrictModeSyntax @@ -2370,13 +2354,13 @@ y = () => [ TestStaticClassBlockES2021 ---------- /out.js ---------- // entry.js -var _A = class { +var _A = class _A { }; -var A = _A; _A.thisField++; _A.classField++; __superSet(_A, _A, "superField", __superGet(_A, _A, "superField") + 1); __superWrapper(_A, _A, "superField")._++; +var A = _A; var _a; var B = (_a = class { }, _a.thisField++, __superSet(_a, _a, "superField", __superGet(_a, _a, "superField") + 1), __superWrapper(_a, _a, "superField")._++, _a); @@ -2385,16 +2369,25 @@ var B = (_a = class { TestStaticClassBlockESNext ---------- /out.js ---------- // entry.js -var _A = class { +var A = class _A { + static { + } + static { + this.thisField++; + _A.classField++; + super.superField = super.superField + 1; + super.superField++; + } +}; +var B = class { + static { + } + static { + this.thisField++; + super.superField = super.superField + 1; + super.superField++; + } }; -var A = _A; -_A.thisField++; -_A.classField++; -__superSet(_A, _A, "superField", __superGet(_A, _A, "superField") + 1); -__superWrapper(_A, _A, "superField")._++; -var _a; -var B = (_a = class { -}, _a.thisField++, __superSet(_a, _a, "superField", __superGet(_a, _a, "superField") + 1), __superWrapper(_a, _a, "superField")._++, _a); ================================================================================ TestTSLowerClassField2020NoBundle @@ -2640,7 +2633,7 @@ _x = new WeakMap(); TestTSLowerPrivateStaticMembers2015NoBundle ---------- /out.js ---------- var _x, _y, y_get, y_set, _z, z_fn; -const _Foo = class { +const _Foo = class _Foo { foo() { var _a; __privateSet(_Foo, _x, __privateGet(_Foo, _x) + 1); @@ -2648,7 +2641,6 @@ const _Foo = class { __privateMethod(_a = _Foo, _z, z_fn).call(_a); } }; -let Foo = _Foo; _x = new WeakMap(); _y = new WeakSet(); y_get = function() { @@ -2658,6 +2650,7 @@ y_set = function(x) { _z = new WeakSet(); z_fn = function() { }; -__privateAdd(Foo, _y); -__privateAdd(Foo, _z); -__privateAdd(Foo, _x, void 0); +__privateAdd(_Foo, _y); +__privateAdd(_Foo, _z); +__privateAdd(_Foo, _x, void 0); +let Foo = _Foo; diff --git a/internal/bundler_tests/snapshots/snapshots_ts.txt b/internal/bundler_tests/snapshots/snapshots_ts.txt index 96990283abb..b9b5d0a2f9d 100644 --- a/internal/bundler_tests/snapshots/snapshots_ts.txt +++ b/internal/bundler_tests/snapshots/snapshots_ts.txt @@ -236,16 +236,15 @@ var Foo = class { (() => new Foo())(); // define-true/index.ts -var _a, _b; +var _a; var Bar = class { constructor() { __publicField(this, "a"); __publicField(this, _a); } + static A; + static [(_a = (() => null, c), () => null, C)]; }; -_a = (() => null, c), _b = (() => null, C); -__publicField(Bar, "A"); -__publicField(Bar, _b); (() => new Bar())(); ================================================================================ @@ -908,10 +907,10 @@ var declare_let_default = foo; var bar2 = 123; // keep/interface-merged.ts -var _foo = class { +var _foo = class _foo { }; +_foo.x = new _foo(); var foo2 = _foo; -foo2.x = new _foo(); var interface_merged_default = foo2; var bar3 = 123; @@ -1530,7 +1529,7 @@ var objFoo = { console.log(this); } }; -var _Foo = class { +var _Foo = class _Foo { constructor() { this.x = this; } @@ -1541,8 +1540,8 @@ var _Foo = class { console.log(this); } }; +_Foo.y = _Foo.z; var Foo = _Foo; -Foo.y = _Foo.z; new Foo(foo(objFoo)); if (nested) { let bar = function(x = this) { @@ -1554,7 +1553,7 @@ if (nested) { console.log(this); } }; - const _Bar = class { + const _Bar = class _Bar { constructor() { this.x = this; } @@ -1565,8 +1564,8 @@ if (nested) { console.log(this); } }; + _Bar.y = _Bar.z; let Bar = _Bar; - Bar.y = _Bar.z; new Bar(bar(objBar)); } var bar2; @@ -1582,7 +1581,7 @@ const objFoo = { console.log(this); } }; -const _Foo = class { +const _Foo = class _Foo { constructor() { this.x = this; } @@ -1593,8 +1592,8 @@ const _Foo = class { console.log(this); } }; +_Foo.y = _Foo.z; let Foo = _Foo; -Foo.y = _Foo.z; new Foo(foo(objFoo)); if (nested) { let bar2 = function(x = this) { @@ -1606,7 +1605,7 @@ if (nested) { console.log(this); } }; - const _Bar = class { + const _Bar = class _Bar { constructor() { this.x = this; } @@ -1617,8 +1616,8 @@ if (nested) { console.log(this); } }; + _Bar.y = _Bar.z; let Bar = _Bar; - Bar.y = _Bar.z; new Bar(bar2(objBar)); } @@ -1679,8 +1678,9 @@ var objFoo = { console.log(this); } }; -var _Foo = class { +var Foo = class { x = this; + static y = this.z; foo(x = this) { console.log(this); } @@ -1688,8 +1688,6 @@ var _Foo = class { console.log(this); } }; -var Foo = _Foo; -__publicField(Foo, "y", _Foo.z); new Foo(foo(objFoo)); if (nested) { let bar = function(x = this) { diff --git a/internal/js_ast/js_ast.go b/internal/js_ast/js_ast.go index 8ccacd476a9..72be817cdfc 100644 --- a/internal/js_ast/js_ast.go +++ b/internal/js_ast/js_ast.go @@ -1230,6 +1230,9 @@ const ( // Classes can merge with TypeScript namespaces. SymbolClass + // Class names are not allowed to be referenced by computed property keys + SymbolClassInComputedPropertyKey + // A class-private identifier (i.e. "#foo"). SymbolPrivateField SymbolPrivateMethod diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index e823655ff49..5671c67bd35 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -11028,15 +11028,26 @@ func (p *parser) visitClass(nameScopeLoc logger.Loc, class *js_ast.Class, defaul property.Decorators = p.visitDecorators(property.Decorators, decoratorScope) - // Special-case private identifiers + // Visit the property key if private, ok := property.Key.Data.(*js_ast.EPrivateIdentifier); ok { + // Special-case private identifiers here p.recordDeclaredSymbol(private.Ref) } else { + // It's forbidden to reference the class name in a computed key + if property.Flags.Has(js_ast.PropertyIsComputed) && class.Name != nil { + p.symbols[result.innerClassNameRef.InnerIndex].Kind = js_ast.SymbolClassInComputedPropertyKey + } + key, _ := p.visitExprInOut(property.Key, exprIn{ shouldMangleStringsAsProps: true, }) property.Key = key + // Re-allow using the class name after visiting a computed key + if property.Flags.Has(js_ast.PropertyIsComputed) && class.Name != nil { + p.symbols[result.innerClassNameRef.InnerIndex].Kind = js_ast.SymbolConst + } + if p.options.minifySyntax { if inlined, ok := key.Data.(*js_ast.EInlinedEnum); ok { switch inlined.Value.Data.(type) { @@ -12361,6 +12372,20 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO e.MustKeepDueToWithStmt = result.isInsideWithScope e.Ref = result.ref + // Handle referencing a class name within that class's computed property + // key. This is not allowed, and must fail at run-time: + // + // class Foo { + // static foo = 'bar' + // static [Foo.foo] = 'foo' + // } + // + if p.symbols[result.ref.InnerIndex].Kind == js_ast.SymbolClassInComputedPropertyKey { + p.log.AddID(logger.MsgID_JS_ClassNameWillThrow, logger.Warning, &p.tracker, js_lexer.RangeOfIdentifier(p.source, expr.Loc), + fmt.Sprintf("Accessing class %q before initialization will throw", name)) + return p.callRuntime(expr.Loc, "__earlyAccess", []js_ast.Expr{{Loc: expr.Loc, Data: &js_ast.EString{Value: helpers.StringToUTF16(name)}}}), exprOut{} + } + // Handle assigning to a constant if in.assignTarget != js_ast.AssignTargetNone { switch p.symbols[result.ref.InnerIndex].Kind { diff --git a/internal/js_parser/js_parser_lower.go b/internal/js_parser/js_parser_lower.go index fd90eca035c..95ae015bb30 100644 --- a/internal/js_parser/js_parser_lower.go +++ b/internal/js_parser/js_parser_lower.go @@ -1962,20 +1962,35 @@ func (p *parser) captureKeyForObjectRest(originalKey js_ast.Expr) (finalKey js_a } type classLoweringInfo struct { - avoidTDZ bool lowerAllInstanceFields bool lowerAllStaticFields bool shimSuperCtorCalls bool } func (p *parser) computeClassLoweringInfo(class *js_ast.Class) (result classLoweringInfo) { - // Safari workaround: Automatically avoid TDZ issues when bundling - result.avoidTDZ = p.options.mode == config.ModeBundle && p.currentScope.Parent == nil - // Name keeping for classes is implemented with a static block. So we need to // lower all static fields if static blocks are unsupported so that the name // keeping comes first before other static initializers. - if p.options.keepNames && (result.avoidTDZ || p.options.unsupportedJSFeatures.Has(compat.ClassStaticBlocks)) { + if p.options.keepNames && p.options.unsupportedJSFeatures.Has(compat.ClassStaticBlocks) { + result.lowerAllStaticFields = true + } + + // TypeScript's "experimentalDecorators" feature replaces all references of + // the class name with the decorated class after class decorators have run. + // This cannot be done by only reassigning to the class symbol in JavaScript + // because it's shadowed by the class name within the class body. Instead, + // we need to hoist all code in static contexts out of the class body so + // that it's no longer shadowed: + // + // const decorate = x => ({ x }) + // @decorate + // class Foo { + // static oldFoo = Foo + // static newFoo = () => Foo + // } + // console.log('This must be false:', Foo.x.oldFoo === Foo.x.newFoo()) + // + if p.options.ts.Parse && p.options.ts.Config.ExperimentalDecorators == config.True && len(class.Decorators) > 0 { result.lowerAllStaticFields = true } @@ -2012,40 +2027,6 @@ func (p *parser) computeClassLoweringInfo(class *js_ast.Class) (result classLowe // _foo = new WeakMap(); // for _, prop := range class.Properties { - // Be conservative and always lower static fields when we're doing TDZ- - // avoidance if the class's inner class name symbol is referenced at all - // (i.e. the class name within the class body, which can be referenced by - // name or by "this" in a static initializer). We can't transform this: - // - // class Foo { - // static foo = new Foo(); - // static #bar = new Foo(); - // static { new Foo(); } - // } - // - // into this: - // - // var Foo = class { - // static foo = new Foo(); - // static #bar = new Foo(); - // static { new Foo(); } - // }; - // - // since "new Foo" will crash. We need to lower this static field to avoid - // crashing due to an uninitialized binding. - if result.avoidTDZ { - // Note that due to esbuild's single-pass design where private fields - // are lowered as they are resolved, we must decide whether to lower - // these private fields before we enter the class body. We can't wait - // until we've scanned the class body and know if the inner class name - // symbol is used or not before we decide, because if "#bar" does need - // to be lowered, references to "#bar" inside the class body weren't - // lowered. So we just unconditionally do this instead. - if prop.Kind == js_ast.PropertyClassStaticBlock || prop.Flags.Has(js_ast.PropertyIsStatic) { - result.lowerAllStaticFields = true - } - } - if prop.Kind == js_ast.PropertyClassStaticBlock { if p.options.unsupportedJSFeatures.Has(compat.ClassStaticBlocks) { result.lowerAllStaticFields = true @@ -2290,6 +2271,15 @@ func (p *parser) lowerClass(stmt js_ast.Stmt, expr js_ast.Expr, result visitClas return name } else { + // If anything referenced the inner class name, then we should use that + // name for any automatically-generated initialization code, since it + // will come before the outer class name is initialized. + if result.innerClassNameRef != js_ast.InvalidRef { + p.recordUsage(result.innerClassNameRef) + return js_ast.Expr{Loc: class.Name.Loc, Data: &js_ast.EIdentifier{Ref: result.innerClassNameRef}} + } + + // Otherwise we should just use the outer class name if class.Name == nil { if kind == classKindExportDefaultStmt { class.Name = &defaultName @@ -2411,24 +2401,27 @@ func (p *parser) lowerClass(stmt js_ast.Stmt, expr js_ast.Expr, result visitClas // Make sure the order of computed property keys doesn't change. These // expressions have side effects and must be evaluated in order. keyExprNoSideEffects := prop.Key - if prop.Flags.Has(js_ast.PropertyIsComputed) && (len(prop.Decorators) > 0 || - mustLowerField || computedPropertyCache.Data != nil) { + if prop.Flags.Has(js_ast.PropertyIsComputed) && + (len(prop.Decorators) > 0 || mustLowerField || computedPropertyCache.Data != nil) { needsKey := true if len(prop.Decorators) == 0 && (prop.Flags.Has(js_ast.PropertyIsMethod) || shouldOmitFieldInitializer || !mustLowerField) { needsKey = false } - if !needsKey { - // Just evaluate the key for its side effects - computedPropertyCache = js_ast.JoinWithComma(computedPropertyCache, prop.Key) - } else if _, ok := prop.Key.Data.(*js_ast.EString); !ok { - // Store the key in a temporary so we can assign to it later - ref := p.generateTempRef(tempRefNeedsDeclare, "") - p.recordUsage(ref) - computedPropertyCache = js_ast.JoinWithComma(computedPropertyCache, - js_ast.Assign(js_ast.Expr{Loc: prop.Key.Loc, Data: &js_ast.EIdentifier{Ref: ref}}, prop.Key)) - prop.Key = js_ast.Expr{Loc: prop.Key.Loc, Data: &js_ast.EIdentifier{Ref: ref}} - keyExprNoSideEffects = prop.Key + // Assume all non-string computed keys have important side effects + if _, ok := prop.Key.Data.(*js_ast.EString); !ok { + if !needsKey { + // Just evaluate the key for its side effects + computedPropertyCache = js_ast.JoinWithComma(computedPropertyCache, prop.Key) + } else { + // Store the key in a temporary so we can assign to it later + ref := p.generateTempRef(tempRefNeedsDeclare, "") + p.recordUsage(ref) + computedPropertyCache = js_ast.JoinWithComma(computedPropertyCache, + js_ast.Assign(js_ast.Expr{Loc: prop.Key.Loc, Data: &js_ast.EIdentifier{Ref: ref}}, prop.Key)) + prop.Key = js_ast.Expr{Loc: prop.Key.Loc, Data: &js_ast.EIdentifier{Ref: ref}} + keyExprNoSideEffects = prop.Key + } } // If this is a computed method, the property value will be used @@ -2773,10 +2766,57 @@ func (p *parser) lowerClass(stmt js_ast.Stmt, expr js_ast.Expr, result visitClas return nil, expr } + // When bundling is enabled, we convert top-level class statements to + // expressions: + // + // // Before + // class Foo { + // static foo = () => Foo + // } + // Foo = wrap(Foo) + // + // // After + // var _Foo = class _Foo { + // static foo = () => _Foo; + // }; + // var Foo = _Foo; + // Foo = wrap(Foo); + // + // One reason to do this is that esbuild's bundler sometimes needs to lazily- + // evaluate a module. For example, a module may end up being both the target + // of a dynamic "import()" call and a static "import" statement. Lazy module + // evaluation is done by wrapping the top-level module code in a closure. To + // avoid a performance hit for static "import" statements, esbuild stores + // top-level exported symbols outside of the closure and references them + // directly instead of indirectly. + // + // Another reason to do this is that multiple JavaScript VMs have had and + // continue to have performance issues with TDZ (i.e. "temporal dead zone") + // checks. These checks validate that a let, or const, or class symbol isn't + // used before it's initialized. Here are two issues with well-known VMs: + // + // * V8: https://bugs.chromium.org/p/v8/issues/detail?id=13723 (10% slowdown) + // * JavaScriptCore: https://bugs.webkit.org/show_bug.cgi?id=199866 (1,000% slowdown!) + // + // JavaScriptCore had a severe performance issue as their TDZ implementation + // had time complexity that was quadratic in the number of variables needing + // TDZ checks in the same scope (with the top-level scope typically being the + // worst offender). V8 has ongoing issues with TDZ checks being present + // throughout the code their JIT generates even when they have already been + // checked earlier in the same function or when the function in question has + // already been run (so the checks have already happened). + // + // Due to esbuild's parallel architecture, we both a) need to transform class + // statements to variables during parsing and b) don't yet know whether this + // module will need to be lazily-evaluated or not in the parser. So we always + // do this just in case it's needed. + mustConvertStmtToExpr := p.options.mode == config.ModeBundle && p.currentScope.Parent == nil + // If this is true, we have removed some code from the class body that could // potentially contain an expression that captures the inner class name. - // This could lead to incorrect behavior if the class is later re-assigned, - // since the removed code would no longer be in the class body scope. + // In this case we must explicitly store the class to a separate inner class + // name binding to avoid incorrect behavior if the class is later re-assigned, + // since the removed code will no longer be in the class body scope. hasPotentialInnerClassNameEscape := result.innerClassNameRef != js_ast.InvalidRef && (computedPropertyCache.Data != nil || len(privateMembers) > 0 || @@ -2789,50 +2829,68 @@ func (p *parser) lowerClass(stmt js_ast.Stmt, expr js_ast.Expr, result visitClas // Pack the class back into a statement, with potentially some extra // statements afterwards var stmts []js_ast.Stmt + var outerClassNameDecl js_ast.Stmt var nameForClassDecorators js_ast.LocRef - generatedLocalStmt := false - if len(class.Decorators) > 0 || hasPotentialInnerClassNameEscape || classLoweringInfo.avoidTDZ { - generatedLocalStmt = true - name := nameFunc() - nameRef := name.Data.(*js_ast.EIdentifier).Ref - nameForClassDecorators = js_ast.LocRef{Loc: name.Loc, Ref: nameRef} + didGenerateLocalStmt := false + if len(class.Decorators) > 0 || hasPotentialInnerClassNameEscape || mustConvertStmtToExpr { + didGenerateLocalStmt = true + + // Determine the name to use for decorators + if kind == classKindExpr { + // For expressions, the inner and outer class names are the same + name := nameFunc() + nameForClassDecorators = js_ast.LocRef{Loc: name.Loc, Ref: name.Data.(*js_ast.EIdentifier).Ref} + } else { + // For statements we need to use the outer class name, not the inner one + if class.Name != nil { + nameForClassDecorators = *class.Name + } else if kind == classKindExportDefaultStmt { + nameForClassDecorators = defaultName + } else { + nameForClassDecorators = js_ast.LocRef{Loc: classLoc, Ref: p.generateTempRef(tempRefNoDeclare, "")} + } + p.recordUsage(nameForClassDecorators.Ref) + } + classExpr := js_ast.EClass{Class: *class} class = &classExpr.Class init := js_ast.Expr{Loc: classLoc, Data: &classExpr} - if hasPotentialInnerClassNameEscape && len(class.Decorators) == 0 { - // If something captures the inner class name and escapes the class body, - // make a new constant to store the class and forward that value to a - // mutable alias. That way if the alias is mutated, everything bound to - // the original constant doesn't change. - // - // class Foo { - // static foo() { return this.#foo() } - // static #foo() { return Foo } - // } - // Foo = class Bar {} - // - // becomes: - // - // var _foo, foo_fn; - // const Foo2 = class { - // static foo() { - // return __privateMethod(this, _foo, foo_fn).call(this); - // } - // }; - // let Foo = Foo2; - // _foo = new WeakSet(); - // foo_fn = function() { - // return Foo2; - // }; - // _foo.add(Foo); - // Foo = class Bar { - // }; - // - // Generate a new symbol instead of using the inner class name directly - // because the inner class name isn't a top-level symbol and we are now - // making a top-level symbol. This symbol must be minified along with - // other top-level symbols to avoid name collisions. + // If the inner class name was referenced, then set the name of the class + // that we will end up printing to the inner class name. Otherwise if the + // inner class name was unused, we can just leave it blank. + if result.innerClassNameRef != js_ast.InvalidRef { + // "class Foo { x = Foo }" => "const Foo = class _Foo { x = _Foo }" + class.Name.Ref = result.innerClassNameRef + } else { + // "class Foo {}" => "const Foo = class {}" + class.Name = nil + } + + // Generate the class initialization statement + if len(class.Decorators) > 0 { + // If there are class decorators, then we actually need to mutate the + // immutable "const" binding that shadows everything in the class body. + // The official TypeScript compiler does this by rewriting all class name + // references in the class body to another temporary variable. This is + // basically what we're doing here. + p.recordUsage(nameForClassDecorators.Ref) + stmts = append(stmts, js_ast.Stmt{Loc: classLoc, Data: &js_ast.SLocal{ + Kind: p.selectLocalKind(js_ast.LocalLet), + IsExport: kind == classKindExportStmt, + Decls: []js_ast.Decl{{ + Binding: js_ast.Binding{Loc: nameForClassDecorators.Loc, Data: &js_ast.BIdentifier{Ref: nameForClassDecorators.Ref}}, + ValueOrNil: init, + }}, + }}) + if class.Name != nil { + p.mergeSymbols(class.Name.Ref, nameForClassDecorators.Ref) + class.Name = nil + } + } else if hasPotentialInnerClassNameEscape { + // If the inner class name was used, then we explicitly generate a binding + // for it. That means the mutable outer class name is separate, and is + // initialized after all static member initializers have finished. captureRef := p.newSymbol(js_ast.SymbolOther, p.symbols[result.innerClassNameRef.InnerIndex].OriginalName) p.currentScope.Generated = append(p.currentScope.Generated, captureRef) p.recordDeclaredSymbol(captureRef) @@ -2840,33 +2898,33 @@ func (p *parser) lowerClass(stmt js_ast.Stmt, expr js_ast.Expr, result visitClas stmts = append(stmts, js_ast.Stmt{Loc: classLoc, Data: &js_ast.SLocal{ Kind: p.selectLocalKind(js_ast.LocalConst), Decls: []js_ast.Decl{{ - Binding: js_ast.Binding{Loc: name.Loc, Data: &js_ast.BIdentifier{Ref: captureRef}}, + Binding: js_ast.Binding{Loc: nameForClassDecorators.Loc, Data: &js_ast.BIdentifier{Ref: captureRef}}, ValueOrNil: init, }}, }}) - init = js_ast.Expr{Loc: classLoc, Data: &js_ast.EIdentifier{Ref: captureRef}} + p.recordUsage(nameForClassDecorators.Ref) p.recordUsage(captureRef) + outerClassNameDecl = js_ast.Stmt{Loc: classLoc, Data: &js_ast.SLocal{ + Kind: p.selectLocalKind(js_ast.LocalLet), + IsExport: kind == classKindExportStmt, + Decls: []js_ast.Decl{{ + Binding: js_ast.Binding{Loc: nameForClassDecorators.Loc, Data: &js_ast.BIdentifier{Ref: nameForClassDecorators.Ref}}, + ValueOrNil: js_ast.Expr{Loc: classLoc, Data: &js_ast.EIdentifier{Ref: captureRef}}, + }}, + }} } else { - // If there are class decorators, then we actually need to mutate the - // immutable "const" binding that shadows everything in the class body. - // The official TypeScript compiler does this by rewriting all class name - // references in the class body to another temporary variable. This is - // basically what we're doing here. - if result.innerClassNameRef != js_ast.InvalidRef { - p.mergeSymbols(result.innerClassNameRef, nameRef) - } + // Otherwise, the inner class name isn't needed and we can just + // use a single variable declaration for the outer class name. + p.recordUsage(nameForClassDecorators.Ref) + stmts = append(stmts, js_ast.Stmt{Loc: classLoc, Data: &js_ast.SLocal{ + Kind: p.selectLocalKind(js_ast.LocalLet), + IsExport: kind == classKindExportStmt, + Decls: []js_ast.Decl{{ + Binding: js_ast.Binding{Loc: nameForClassDecorators.Loc, Data: &js_ast.BIdentifier{Ref: nameForClassDecorators.Ref}}, + ValueOrNil: init, + }}, + }}) } - - // Generate the variable statement that will represent the class statement - stmts = append(stmts, js_ast.Stmt{Loc: classLoc, Data: &js_ast.SLocal{ - Kind: p.selectLocalKind(js_ast.LocalLet), - IsExport: kind == classKindExportStmt, - Decls: []js_ast.Decl{{ - Binding: js_ast.Binding{Loc: name.Loc, Data: &js_ast.BIdentifier{Ref: nameRef}}, - ValueOrNil: init, - }}, - }}) - p.recordUsage(nameRef) } else { switch kind { case classKindStmt: @@ -2907,6 +2965,10 @@ func (p *parser) lowerClass(stmt js_ast.Stmt, expr js_ast.Expr, result visitClas for _, expr := range staticDecorators { stmts = append(stmts, js_ast.Stmt{Loc: expr.Loc, Data: &js_ast.SExpr{Value: expr}}) } + if outerClassNameDecl.Data != nil { + // This must come after the class body initializers have finished + stmts = append(stmts, outerClassNameDecl) + } if len(class.Decorators) > 0 { stmts = append(stmts, js_ast.AssignStmt( js_ast.Expr{Loc: nameForClassDecorators.Loc, Data: &js_ast.EIdentifier{Ref: nameForClassDecorators.Ref}}, @@ -2919,19 +2981,11 @@ func (p *parser) lowerClass(stmt js_ast.Stmt, expr js_ast.Expr, result visitClas p.recordUsage(nameForClassDecorators.Ref) class.Decorators = nil } - if generatedLocalStmt { + if didGenerateLocalStmt && kind == classKindExportDefaultStmt { // "export default class x {}" => "class x {} export {x as default}" - if kind == classKindExportDefaultStmt { - stmts = append(stmts, js_ast.Stmt{Loc: classLoc, Data: &js_ast.SExportClause{ - Items: []js_ast.ClauseItem{{Alias: "default", Name: defaultName}}, - }}) - } - - // Calling "nameFunc" will set the class name, but we don't want it to have - // one. If the class name was necessary, we would have already split it off - // into a variable above. Reset it back to empty here now that we know we - // won't call "nameFunc" after this point. - class.Name = nil + stmts = append(stmts, js_ast.Stmt{Loc: classLoc, Data: &js_ast.SExportClause{ + Items: []js_ast.ClauseItem{{Alias: "default", Name: defaultName}}, + }}) } return stmts, js_ast.Expr{} } diff --git a/internal/js_parser/js_parser_lower_test.go b/internal/js_parser/js_parser_lower_test.go index ff5ed1d5d85..22b2b9ebfc8 100644 --- a/internal/js_parser/js_parser_lower_test.go +++ b/internal/js_parser/js_parser_lower_test.go @@ -476,9 +476,9 @@ func TestLowerClassStaticThis(t *testing.T) { expectPrintedTarget(t, 2015, "class Foo { [this.x] }", "var _a;\nclass Foo {\n constructor() {\n __publicField(this, _a);\n }\n}\n_a = this.x;\n") expectPrintedTarget(t, 2015, "class Foo { static x = this }", - "const _Foo = class {\n};\nlet Foo = _Foo;\n__publicField(Foo, \"x\", _Foo);\n") + "const _Foo = class _Foo {\n};\n__publicField(_Foo, \"x\", _Foo);\nlet Foo = _Foo;\n") expectPrintedTarget(t, 2015, "class Foo { static x = () => this }", - "const _Foo = class {\n};\nlet Foo = _Foo;\n__publicField(Foo, \"x\", () => _Foo);\n") + "const _Foo = class _Foo {\n};\n__publicField(_Foo, \"x\", () => _Foo);\nlet Foo = _Foo;\n") expectPrintedTarget(t, 2015, "class Foo { static x = function() { return this } }", "class Foo {\n}\n__publicField(Foo, \"x\", function() {\n return this;\n});\n") expectPrintedTarget(t, 2015, "class Foo { static [this.x] }", @@ -486,9 +486,9 @@ func TestLowerClassStaticThis(t *testing.T) { expectPrintedTarget(t, 2015, "class Foo { static x = class { y = this } }", "class Foo {\n}\n__publicField(Foo, \"x\", class {\n constructor() {\n __publicField(this, \"y\", this);\n }\n});\n") expectPrintedTarget(t, 2015, "class Foo { static x = class { [this.y] } }", - "var _a, _b;\nconst _Foo = class {\n};\nlet Foo = _Foo;\n__publicField(Foo, \"x\", (_b = class {\n constructor() {\n __publicField(this, _a);\n }\n}, _a = _Foo.y, _b));\n") + "var _a, _b;\nconst _Foo = class _Foo {\n};\n__publicField(_Foo, \"x\", (_b = class {\n constructor() {\n __publicField(this, _a);\n }\n}, _a = _Foo.y, _b));\nlet Foo = _Foo;\n") expectPrintedTarget(t, 2015, "class Foo { static x = class extends this {} }", - "const _Foo = class {\n};\nlet Foo = _Foo;\n__publicField(Foo, \"x\", class extends _Foo {\n});\n") + "const _Foo = class _Foo {\n};\n__publicField(_Foo, \"x\", class extends _Foo {\n});\nlet Foo = _Foo;\n") expectPrintedTarget(t, 2015, "x = class Foo { x = this }", "x = class Foo {\n constructor() {\n __publicField(this, \"x\", this);\n }\n};\n") diff --git a/internal/js_parser/ts_parser_test.go b/internal/js_parser/ts_parser_test.go index d7e443b754e..bfc7e10c009 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -678,6 +678,8 @@ func TestTSClass(t *testing.T) { expectPrintedAssignSemanticsTS(t, "class Foo { foo: number }", "class Foo {\n}\n") expectPrintedAssignSemanticsTS(t, "class Foo { foo: number = 0 }", "class Foo {\n constructor() {\n this.foo = 0;\n }\n}\n") + expectPrintedAssignSemanticsTS(t, "class Foo { ['foo']: number }", "class Foo {\n}\n") + expectPrintedAssignSemanticsTS(t, "class Foo { ['foo']: number = 0 }", "class Foo {\n constructor() {\n this[\"foo\"] = 0;\n }\n}\n") expectPrintedAssignSemanticsTS(t, "class Foo { foo(): void {} }", "class Foo {\n foo() {\n }\n}\n") expectPrintedAssignSemanticsTS(t, "class Foo { foo(): void; foo(): void {} }", "class Foo {\n foo() {\n }\n}\n") expectParseErrorTS(t, "class Foo { foo(): void foo(): void {} }", ": ERROR: Expected \";\" but found \"foo\"\n") diff --git a/internal/logger/msg_ids.go b/internal/logger/msg_ids.go index 16cb008c7d8..7b1b1000380 100644 --- a/internal/logger/msg_ids.go +++ b/internal/logger/msg_ids.go @@ -17,6 +17,7 @@ const ( MsgID_JS_AssignToDefine MsgID_JS_AssignToImport MsgID_JS_CallImportNamespace + MsgID_JS_ClassNameWillThrow MsgID_JS_CommonJSVariableInESM MsgID_JS_DeleteSuperProperty MsgID_JS_DirectEval @@ -100,6 +101,8 @@ func StringToMsgIDs(str string, logLevel LogLevel, overrides map[MsgID]LogLevel) overrides[MsgID_JS_AssignToImport] = logLevel case "call-import-namespace": overrides[MsgID_JS_CallImportNamespace] = logLevel + case "class-name-will-throw": + overrides[MsgID_JS_ClassNameWillThrow] = logLevel case "commonjs-variable-in-esm": overrides[MsgID_JS_CommonJSVariableInESM] = logLevel case "delete-super-property": @@ -218,6 +221,8 @@ func MsgIDToString(id MsgID) string { return "assign-to-import" case MsgID_JS_CallImportNamespace: return "call-import-namespace" + case MsgID_JS_ClassNameWillThrow: + return "class-name-will-throw" case MsgID_JS_CommonJSVariableInESM: return "commonjs-variable-in-esm" case MsgID_JS_DeleteSuperProperty: diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index eef2124f2a2..25fedec7494 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -276,6 +276,9 @@ func Source(unsupportedJSFeatures compat.JSFeature) logger.Source { setter ? setter.call(obj, value) : member.set(obj, value) return value } + export var __earlyAccess = (name) => { + throw ReferenceError('Cannot access "' + name + '" before initialization') + } ` if !unsupportedJSFeatures.Has(compat.ObjectAccessors) { diff --git a/scripts/end-to-end-tests.js b/scripts/end-to-end-tests.js index c191a0175c4..caa1e97a536 100644 --- a/scripts/end-to-end-tests.js +++ b/scripts/end-to-end-tests.js @@ -4555,6 +4555,46 @@ for (let flags of [[], ['--target=es6'], ['--bundle'], ['--bundle', '--target=es `, }), + // Check for the specific reference behavior of TypeScript's implementation + // of "experimentalDecorators" with class decorators, which mutate the class + // binding itself. This test passes on TypeScript's implementation. + test(['in.ts', '--outfile=node.js'].concat(flags), { + 'in.ts': ` + let oldFoo: any + let e: any + let decorate = (foo: any): any => { + oldFoo = foo + return { foo } + } + @decorate + class newFoo { + a(): any { return [newFoo, () => newFoo] } + b: any = [newFoo, () => newFoo] + static c(): any { return [newFoo, () => newFoo] } + static d: any = [newFoo, () => newFoo] + static { e = [newFoo, () => newFoo] } + } + const fail: string[] = [] + if ((newFoo as any).foo !== oldFoo) fail.push('decorate') + if (new oldFoo().a()[0] !== newFoo) fail.push('a[0]') + if (new oldFoo().a()[1]() !== newFoo) fail.push('a[1]') + if (new oldFoo().b[0] !== newFoo) fail.push('b[0]') + if (new oldFoo().b[1]() !== newFoo) fail.push('b[1]') + if (oldFoo.c()[0] !== newFoo) fail.push('c[0]') + if (oldFoo.c()[1]() !== newFoo) fail.push('c[1]') + if (oldFoo.d[0] !== oldFoo) fail.push('d[0]') + if (oldFoo.d[1]() !== newFoo) fail.push('d[1]') + if (e[0] !== oldFoo) fail.push('e[0]') + if (e[1]() !== newFoo) fail.push('e[1]') + if (fail.length) throw 'fail: ' + fail + `, + 'tsconfig.json': `{ + "compilerOptions": { + "experimentalDecorators": true, + }, + }`, + }), + // https://github.com/evanw/esbuild/issues/2800 test(['in.js', '--outfile=node.js'].concat(flags), { 'in.js': ` @@ -4631,6 +4671,117 @@ for (let flags of [[], ['--target=es6'], ['--bundle'], ['--bundle', '--target=es innerScope() `, }), + + // https://github.com/evanw/esbuild/issues/2629 + test(['in.ts', '--outfile=node.js'].concat(flags), { + 'in.ts': ` + // Stub out the decorator so TSC doesn't complain. + const someDecorator = (): PropertyDecorator => () => {}; + + class Foo { + static message = 'Hello world!'; + static msgLength = Foo.message.length; + + @someDecorator() + foo() {} + } + + if (Foo.message !== 'Hello world!' || Foo.msgLength !== 12) throw 'fail' + `, + 'tsconfig.json': `{ + "compilerOptions": { + "experimentalDecorators": true, + }, + }`, + }), + + // https://github.com/evanw/esbuild/issues/2045 + test(['in.js', '--bundle', '--outfile=node.js', '--log-override:class-name-will-throw=silent'].concat(flags), { + 'in.js': ` + let A = {a: 'a'} // This should not be used + + let field + try { class A { static a = 1; [A.a] = 2 } } catch (err) { field = err } + if (!field) throw 'fail: field' + + let staticField + try { class A { static a = 1; static [A.a] = 2 } } catch (err) { staticField = err } + if (!staticField) throw 'fail: staticField' + + let method + try { class A { static a = 1; [A.a]() { return 2 } } } catch (err) { method = err } + if (!method) throw 'fail: method' + + let staticMethod + try { class A { static a = 1; static [A.a]() { return 2 } } } catch (err) { staticMethod = err } + if (!staticMethod) throw 'fail: staticMethod' + `, + }), + test(['in.js', '--bundle', '--outfile=node.js', '--log-override:class-name-will-throw=silent'].concat(flags), { + 'in.js': ` + let A = {a: 'a'} // This should not be used + + let field + try { class A { capture = () => A; static a = 1; [A.a] = 2 } } catch (err) { field = err } + if (!field) throw 'fail: field' + + let staticField + try { class A { capture = () => A; static a = 1; static [A.a] = 2 } } catch (err) { staticField = err } + if (!staticField) throw 'fail: staticField' + + let method + try { class A { capture = () => A; static a = 1; [A.a]() { return 2 } } } catch (err) { method = err } + if (!method) throw 'fail: method' + + let staticMethod + try { class A { capture = () => A; static a = 1; static [A.a]() { return 2 } } } catch (err) { staticMethod = err } + if (!staticMethod) throw 'fail: staticMethod' + `, + }), + test(['in.js', '--bundle', '--outfile=node.js', '--log-override:class-name-will-throw=silent'].concat(flags), { + 'in.js': ` + let A = {a: 'a'} // This should not be used + let temp + + let field + try { temp = (class A { static a = 1; [A.a] = 2 }) } catch (err) { field = err } + if (!field) throw 'fail: field' + + let staticField + try { temp = (class A { static a = 1; static [A.a] = 2 }) } catch (err) { staticField = err } + if (!staticField) throw 'fail: staticField' + + let method + try { temp = (class A { static a = 1; [A.a]() { return 2 } }) } catch (err) { method = err } + if (!method) throw 'fail: method' + + let staticMethod + try { temp = (class A { static a = 1; static [A.a]() { return 2 } }) } catch (err) { staticMethod = err } + if (!staticMethod) throw 'fail: staticMethod' + `, + }), + test(['in.js', '--bundle', '--outfile=node.js', '--log-override:class-name-will-throw=silent'].concat(flags), { + 'in.js': ` + let A = {a: 'a'} // This should not be used + let temp + + let field + try { temp = (class A { capture = () => A; static a = 1; [A.a] = 2 }) } catch (err) { field = err } + if (!field) throw 'fail: field' + + let staticField + try { temp = (class A { capture = () => A; static a = 1; static [A.a] = 2 }) } catch (err) { staticField = err } + if (!staticField) throw 'fail: staticField' + + let method + try { temp = (class A { capture = () => A; static a = 1; [A.a]() { return 2 } }) } catch (err) { method = err } + if (!method) throw 'fail: method' + + let staticMethod + try { temp = (class A { capture = () => A; static a = 1; static [A.a]() { return 2 } }) } catch (err) { staticMethod = err } + if (!staticMethod) throw 'fail: staticMethod' + `, + }), ) } diff --git a/scripts/uglify-tests.js b/scripts/uglify-tests.js index fbd43afaf38..b565d7cb41c 100644 --- a/scripts/uglify-tests.js +++ b/scripts/uglify-tests.js @@ -244,7 +244,6 @@ async function test_case(esbuild, test, basename) { // Ignore the known failures in CI, but not otherwise const isExpectingFailure = !process.env.CI ? false : [ // Stdout difference - 'classes.js: issue_5015_2', 'const.js: issue_4225', 'const.js: issue_4229', 'const.js: issue_4245',