Skip to content

Commit 6e049b8

Browse files
committed
fix #3913: useDefineForClassFields and decorators
1 parent 9c26f98 commit 6e049b8

File tree

5 files changed

+42
-4
lines changed

5 files changed

+42
-4
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
./esbuild --version
2020
```
2121
22+
* Fix class field decorators in TypeScript if `useDefineForClassFields` is `false` ([#3913](https://github.com/evanw/esbuild/issues/3913))
23+
24+
Setting the `useDefineForClassFields` flag to `false` in `tsconfig.json` means class fields use the legacy TypeScript behavior instead of the standard JavaScript behavior. Specifically they use assign semantics instead of define semantics (e.g. setters are triggered) and fields without an initializer are not initialized at all. However, when this legacy behavior is combined with standard JavaScript decorators, TypeScript switches to always initializing all fields, even those without initializers. Previously esbuild incorrectly continued to omit field initializers for this edge case. These field initializers in this case should now be emitted starting with this release.
25+
2226
* Avoid incorrect cycle warning with `tsconfig.json` multiple inheritance ([#3898](https://github.com/evanw/esbuild/issues/3898))
2327
2428
TypeScript 5.0 introduced multiple inheritance for `tsconfig.json` files where `extends` can be an array of file paths. Previously esbuild would incorrectly treat files encountered more than once when processing separate subtrees of the multiple inheritance hierarchy as an inheritance cycle. With this release, `tsconfig.json` files containing this edge case should work correctly without generating a warning.

internal/bundler_tests/snapshots/snapshots_tsconfig.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ _foo_dec2 = [dec], _bar_dec = [dec];
132132
var ClassField = class {
133133
constructor() {
134134
this.foo = __runInitializers(_init3, 8, this, 123), __runInitializers(_init3, 11, this);
135+
this.bar = __runInitializers(_init3, 12, this), __runInitializers(_init3, 15, this);
135136
}
136137
};
137138
_init3 = __decoratorStart(null);

internal/js_parser/js_parser.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6287,6 +6287,7 @@ func (p *parser) parseClass(classKeyword logger.Range, name *ast.LocRef, classOp
62876287
bodyLoc := p.lexer.Loc()
62886288
p.lexer.Expect(js_lexer.TOpenBrace)
62896289
properties := []js_ast.Property{}
6290+
hasPropertyDecorator := false
62906291

62916292
// Allow "in" and private fields inside class bodies
62926293
oldAllowIn := p.allowIn
@@ -6316,6 +6317,9 @@ func (p *parser) parseClass(classKeyword logger.Range, name *ast.LocRef, classOp
63166317
firstDecoratorLoc := p.lexer.Loc()
63176318
scopeIndex := len(p.scopesInOrder)
63186319
opts.decorators = p.parseDecorators(p.currentScope, classKeyword, opts.decoratorContext)
6320+
if len(opts.decorators) > 0 {
6321+
hasPropertyDecorator = true
6322+
}
63196323

63206324
// This property may turn out to be a type in TypeScript, which should be ignored
63216325
if property, ok := p.parseProperty(p.saveExprCommentsHere(), js_ast.PropertyField, opts, nil); ok {
@@ -6393,9 +6397,10 @@ func (p *parser) parseClass(classKeyword logger.Range, name *ast.LocRef, classOp
63936397
// "useDefineForClassFields" setting is false even if the configured target
63946398
// environment supports decorators. This setting changes the behavior of
63956399
// class fields, and so we must lower decorators so they behave correctly.
6396-
ShouldLowerStandardDecorators: (!p.options.ts.Parse && p.options.unsupportedJSFeatures.Has(compat.Decorators)) ||
6397-
(p.options.ts.Parse && p.options.ts.Config.ExperimentalDecorators != config.True &&
6398-
(p.options.unsupportedJSFeatures.Has(compat.Decorators) || !useDefineForClassFields)),
6400+
ShouldLowerStandardDecorators: (len(classOpts.decorators) > 0 || hasPropertyDecorator) &&
6401+
((!p.options.ts.Parse && p.options.unsupportedJSFeatures.Has(compat.Decorators)) ||
6402+
(p.options.ts.Parse && p.options.ts.Config.ExperimentalDecorators != config.True &&
6403+
(p.options.unsupportedJSFeatures.Has(compat.Decorators) || !useDefineForClassFields))),
63996404

64006405
UseDefineForClassFields: useDefineForClassFields,
64016406
}

internal/js_parser/js_parser_lower_class.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1107,7 +1107,7 @@ func (ctx *lowerClassContext) analyzeProperty(p *parser, prop js_ast.Property, c
11071107
analysis.private, _ = prop.Key.Data.(*js_ast.EPrivateIdentifier)
11081108
mustLowerPrivate := analysis.private != nil && p.privateSymbolNeedsToBeLowered(analysis.private)
11091109
analysis.shouldOmitFieldInitializer = p.options.ts.Parse && !prop.Kind.IsMethodDefinition() && prop.InitializerOrNil.Data == nil &&
1110-
!ctx.class.UseDefineForClassFields && !mustLowerPrivate
1110+
!ctx.class.UseDefineForClassFields && !mustLowerPrivate && !ctx.class.ShouldLowerStandardDecorators
11111111

11121112
// Class fields must be lowered if the environment doesn't support them
11131113
if !prop.Kind.IsMethodDefinition() {

scripts/end-to-end-tests.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5779,6 +5779,34 @@ for (let flags of [['--target=es2022'], ['--target=es6'], ['--bundle', '--target
57795779
}`,
57805780
}),
57815781

5782+
// https://github.com/evanw/esbuild/issues/3913
5783+
test(['in.ts', '--outfile=node.js'].concat(flags), {
5784+
'in.ts': `
5785+
function testDecorator(_value: unknown, context: DecoratorContext) {
5786+
if (context.kind === "field") {
5787+
return () => "dec-ok";
5788+
}
5789+
}
5790+
5791+
class DecClass {
5792+
@testDecorator
5793+
decInit = "init";
5794+
5795+
@testDecorator
5796+
decNoInit: any;
5797+
}
5798+
5799+
const foo = new DecClass
5800+
if (foo.decInit !== 'dec-ok') throw 'fail: decInit'
5801+
if (foo.decNoInit !== 'dec-ok') throw 'fail: decNoInit'
5802+
`,
5803+
'tsconfig.json': `{
5804+
"compilerOptions": {
5805+
"useDefineForClassFields": false,
5806+
},
5807+
}`,
5808+
}),
5809+
57825810
// Check various combinations of flags
57835811
test(['in.ts', '--outfile=node.js', '--supported:class-field=false'].concat(flags), {
57845812
'in.ts': `

0 commit comments

Comments
 (0)