diff --git a/CHANGELOG.md b/CHANGELOG.md index 514ffd07d86..0f1ee7c99e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,55 @@ # Changelog +## Unreleased + +* Change the context of TypeScript parameter decorators + + While TypeScript parameter decorators are expressions, they are not evaluated where they exist in the code. They are moved to after the class declaration and evaluated there instead. Specifically this TypeScript code: + + ```ts + class Foo { + foo(@bar() baz) {} + } + ``` + + becomes this JavaScript code: + + ```js + class Foo { + foo(baz) {} + } + __decorate([ + __param(0, bar()) + ], Foo.prototype, "foo", null); + ``` + + One consequence of this is that whether `await` is allowed or not depends on whether the class declaration itself is inside an `async` function or not. The TypeScript compiler allows code that does this: + + ```ts + async function fn(foo) { + class Foo { + foo(@bar(await foo) baz) {} + } + return Foo + } + ``` + + because that becomes the following valid JavaScript: + + ```js + async function fn(foo) { + class Foo { + foo(baz) {} + } + __decorate([ + __param(0, bar(await foo)) + ], Foo.prototype, "foo", null); + return Foo; + } + ``` + + Previously using `await` like this wasn't allowed. With this release, esbuild now handles `await` correctly in TypeScript parameter decorators. Note that the TypeScript compiler currently has some bugs regarding this behavior: https://github.com/microsoft/TypeScript/issues/48509. Also while TypeScript currently allows `await` to be used like this in `async` functions, it doesn't currently allow `yield` to be used like this in generator functions. It's not yet clear whether this behavior with `yield` is a bug or by design, so I haven't made any changes to esbuild's handling of `yield` inside decorator expressions. + ## 0.14.29 * Fix a minification bug with a double-nested `if` inside a label followed by `else` ([#2139](https://github.com/evanw/esbuild/issues/2139)) diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 67ec1580ae8..4a1ca4d13c1 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -5146,7 +5146,60 @@ func (p *parser) parseFn(name *js_ast.LocRef, classKeyword logger.Range, data fn var tsDecorators []js_ast.Expr if data.allowTSDecorators { + oldAwait := p.fnOrArrowDataParse.await + oldNeedsAsyncLoc := p.fnOrArrowDataParse.needsAsyncLoc + + // While TypeScript parameter decorators are expressions, they are not + // evaluated where they exist in the code. They are moved to after the + // class declaration and evaluated there instead. Specifically this + // TypeScript code: + // + // class Foo { + // foo(@bar() baz) {} + // } + // + // becomes this JavaScript code: + // + // class Foo { + // foo(baz) {} + // } + // __decorate([ + // __param(0, bar()) + // ], Foo.prototype, "foo", null); + // + // One consequence of this is that whether "await" is allowed or not + // depends on whether the class declaration itself is inside an "async" + // function or not. The TypeScript compiler allows code that does this: + // + // async function fn(foo) { + // class Foo { + // foo(@bar(await foo) baz) {} + // } + // return Foo + // } + // + // because that becomes the following valid JavaScript: + // + // async function fn(foo) { + // class Foo { + // foo(baz) {} + // } + // __decorate([ + // __param(0, bar(await foo)) + // ], Foo.prototype, "foo", null); + // return Foo; + // } + // + if oldFnOrArrowData.await == allowExpr { + p.fnOrArrowDataParse.await = allowExpr + } else { + p.fnOrArrowDataParse.needsAsyncLoc = oldFnOrArrowData.needsAsyncLoc + } + tsDecorators = p.parseTypeScriptDecorators() + + p.fnOrArrowDataParse.await = oldAwait + p.fnOrArrowDataParse.needsAsyncLoc = oldNeedsAsyncLoc } else if classKeyword.Len > 0 { p.logInvalidDecoratorError(classKeyword) } diff --git a/internal/js_parser/ts_parser_test.go b/internal/js_parser/ts_parser_test.go index c19dce1b0f2..5fbf3b990ce 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -1457,6 +1457,71 @@ func TestTSDecorator(t *testing.T) { // Decorators aren't allowed on class constructors expectParseErrorTS(t, "class Foo { @dec constructor() {} }", ": ERROR: TypeScript does not allow decorators on class constructors\n") expectParseErrorTS(t, "class Foo { @dec public constructor() {} }", ": ERROR: TypeScript does not allow decorators on class constructors\n") + + // Check use of "await" + friendlyAwaitErrorWithNote := ": ERROR: \"await\" can only be used inside an \"async\" function\n" + + ": NOTE: Consider adding the \"async\" keyword here:\n" + expectPrintedTS(t, "async function foo() { @dec(await x) class Foo {} }", + "async function foo() {\n let Foo = class {\n };\n Foo = __decorateClass([\n dec(await x)\n ], Foo);\n}\n") + expectPrintedTS(t, "async function foo() { class Foo { @dec(await x) foo() {} } }", + "async function foo() {\n class Foo {\n foo() {\n }\n }\n __decorateClass([\n dec(await x)\n ], Foo.prototype, \"foo\", 1);\n}\n") + expectPrintedTS(t, "async function foo() { class Foo { foo(@dec(await x) y) {} } }", + "async function foo() {\n class Foo {\n foo(y) {\n }\n }\n __decorateClass([\n __decorateParam(0, dec(await x))\n ], Foo.prototype, \"foo\", 1);\n}\n") + expectParseErrorTS(t, "function foo() { @dec(await x) class Foo {} }", friendlyAwaitErrorWithNote) + expectParseErrorTS(t, "function foo() { class Foo { @dec(await x) foo() {} } }", friendlyAwaitErrorWithNote) + expectParseErrorTS(t, "function foo() { class Foo { foo(@dec(await x) y) {} } }", friendlyAwaitErrorWithNote) + expectParseErrorTS(t, "function foo() { class Foo { @dec(await x) async foo() {} } }", friendlyAwaitErrorWithNote) + expectParseErrorTS(t, "function foo() { class Foo { async foo(@dec(await x) y) {} } }", + ": ERROR: The keyword \"await\" cannot be used here:\n: ERROR: Expected \")\" but found \"x\"\n") + + // Check lowered use of "await" + expectPrintedTargetTS(t, 2015, "async function foo() { @dec(await x) class Foo {} }", + `function foo() { + return __async(this, null, function* () { + let Foo = class { + }; + Foo = __decorateClass([ + dec(yield x) + ], Foo); + }); +} +`) + expectPrintedTargetTS(t, 2015, "async function foo() { class Foo { @dec(await x) foo() {} } }", + `function foo() { + return __async(this, null, function* () { + class Foo { + foo() { + } + } + __decorateClass([ + dec(yield x) + ], Foo.prototype, "foo", 1); + }); +} +`) + expectPrintedTargetTS(t, 2015, "async function foo() { class Foo { foo(@dec(await x) y) {} } }", + `function foo() { + return __async(this, null, function* () { + class Foo { + foo(y) { + } + } + __decorateClass([ + __decorateParam(0, dec(yield x)) + ], Foo.prototype, "foo", 1); + }); +} +`) + + // Check use of "yield" + expectPrintedTS(t, "function *foo() { @dec(yield x) class Foo {} }", // We currently allow this but TypeScript doesn't + "function* foo() {\n let Foo = class {\n };\n Foo = __decorateClass([\n dec(yield x)\n ], Foo);\n}\n") + expectPrintedTS(t, "function *foo() { class Foo { @dec(yield x) foo() {} } }", // We currently allow this but TypeScript doesn't + "function* foo() {\n class Foo {\n foo() {\n }\n }\n __decorateClass([\n dec(yield x)\n ], Foo.prototype, \"foo\", 1);\n}\n") + expectParseErrorTS(t, "function *foo() { class Foo { foo(@dec(yield x) y) {} } }", // TypeScript doesn't allow this (although it could because it would work fine) + ": ERROR: Cannot use \"yield\" outside a generator function\n") + expectParseErrorTS(t, "function foo() { @dec(yield x) class Foo {} }", ": ERROR: Cannot use \"yield\" outside a generator function\n") + expectParseErrorTS(t, "function foo() { class Foo { @dec(yield x) foo() {} } }", ": ERROR: Cannot use \"yield\" outside a generator function\n") } func TestTSTry(t *testing.T) {