@@ -2,20 +2,11 @@ import { augmentDiagnosticWithNode, buildError } from '@utils';
2
2
import ts from 'typescript' ;
3
3
4
4
import type * as d from '../../../declarations' ;
5
- import {
6
- retrieveTsDecorators ,
7
- retrieveTsModifiers ,
8
- tsPropDeclNameAsString ,
9
- updateConstructor ,
10
- } from '../transform-utils' ;
5
+ import { retrieveTsDecorators , retrieveTsModifiers , updateConstructor } from '../transform-utils' ;
11
6
import { attachInternalsDecoratorsToStatic } from './attach-internals' ;
12
7
import { componentDecoratorToStatic } from './component-decorator' ;
13
8
import { isDecoratorNamed } from './decorator-utils' ;
14
- import {
15
- CLASS_DECORATORS_TO_REMOVE ,
16
- CONSTRUCTOR_DEFINED_MEMBER_DECORATORS ,
17
- MEMBER_DECORATORS_TO_REMOVE ,
18
- } from './decorators-constants' ;
9
+ import { CLASS_DECORATORS_TO_REMOVE , MEMBER_DECORATORS_TO_REMOVE } from './decorators-constants' ;
19
10
import { elementDecoratorsToStatic } from './element-decorator' ;
20
11
import { eventDecoratorsToStatic } from './event-decorator' ;
21
12
import { ImportAliasMap } from './import-alias-map' ;
@@ -163,14 +154,11 @@ const visitClassDeclaration = (
163
154
) ;
164
155
}
165
156
166
- // We call the `handleClassFields` method which handles transforming any
167
- // class fields, removing them from the class and adding statements to the
168
- // class' constructor which instantiate them there instead.
169
- const updatedClassFields = handleClassFields ( classNode , filteredMethodsAndFields , typeChecker , importAliasMap ) ;
170
-
171
157
validateMethods ( diagnostics , classMembers ) ;
172
158
173
159
const currentDecorators = retrieveTsDecorators ( classNode ) ;
160
+ const updatedClassFields : ts . ClassElement [ ] = updateConstructor ( classNode , filteredMethodsAndFields , [ ] ) ;
161
+
174
162
return ts . factory . updateClassDeclaration (
175
163
classNode ,
176
164
[
@@ -232,7 +220,7 @@ const removeStencilMethodDecorators = (
232
220
} else if ( ts . isGetAccessor ( member ) ) {
233
221
return ts . factory . updateGetAccessorDeclaration (
234
222
member ,
235
- ts . canHaveModifiers ( member ) ? ts . getModifiers ( member ) : undefined ,
223
+ [ ... ( newDecorators ?? [ ] ) , ... ( retrieveTsModifiers ( member ) ?? [ ] ) ] ,
236
224
member . name ,
237
225
member . parameters ,
238
226
member . type ,
@@ -243,25 +231,15 @@ const removeStencilMethodDecorators = (
243
231
err . messageText = 'A get accessor should be decorated before a set accessor' ;
244
232
augmentDiagnosticWithNode ( err , member ) ;
245
233
} else if ( ts . isPropertyDeclaration ( member ) ) {
246
- if ( shouldInitializeInConstructor ( member , importAliasMap ) ) {
247
- // if the current class member is decorated with either 'State' or
248
- // 'Prop' we need to modify the property declaration to transform it
249
- // from a class field but we handle this in the `handleClassFields`
250
- // method below, so we just want to return the class member here
251
- // untouched.
252
- return member ;
253
- } else {
254
- // update the property to remove decorators
255
- const modifiers = retrieveTsModifiers ( member ) ;
256
- return ts . factory . updatePropertyDeclaration (
257
- member ,
258
- [ ...( newDecorators ?? [ ] ) , ...( modifiers ?? [ ] ) ] ,
259
- member . name ,
260
- member . questionToken ,
261
- member . type ,
262
- member . initializer ,
263
- ) ;
264
- }
234
+ const modifiers = retrieveTsModifiers ( member ) ;
235
+ return ts . factory . updatePropertyDeclaration (
236
+ member ,
237
+ [ ...( newDecorators ?? [ ] ) , ...( modifiers ?? [ ] ) ] ,
238
+ member . name ,
239
+ member . questionToken ,
240
+ member . type ,
241
+ member . initializer ,
242
+ ) ;
265
243
} else {
266
244
const err = buildError ( diagnostics ) ;
267
245
err . messageText = 'Unknown class member encountered!' ;
@@ -309,168 +287,3 @@ export const filterDecorators = (
309
287
// return the node's original decorators, or undefined
310
288
return decorators ;
311
289
} ;
312
-
313
- /**
314
- * This updates a Stencil component class declaration AST node to handle any
315
- * class fields with Stencil-specific decorators (`@State`, `@Prop`, etc). For
316
- * reasons explained below, we need to remove these fields from the class and
317
- * add code to the class's constructor to instantiate them manually.
318
- *
319
- * When a class field is decorated with a Stencil-defined decorator, we rely on
320
- * defining our own setters and getters (using `Object.defineProperty`) to
321
- * implement the behavior we want. Unfortunately, in ES2022 and newer versions
322
- * of the EcmaScript standard the behavior for class fields like the following
323
- * is incompatible with using manually-defined getters and setters:
324
- *
325
- * ```ts
326
- * class MyClass {
327
- * foo = "bar"
328
- * }
329
- * ```
330
- *
331
- * In ES2022+ if we try to use `Object.defineProperty` on this class's
332
- * prototype in order to define a `set` and `get` function for the
333
- * property `foo` it will not override the default behavior of the
334
- * instance field `foo`, so doing something like the following:
335
- *
336
- * ```ts
337
- * Object.defineProperty(MyClass.prototype, "foo", {
338
- * get() {
339
- * return "Foo is: " + this.foo
340
- * }
341
- * });
342
- * ```
343
- *
344
- * and then calling `myClassInstance.foo` will _not_ return `"Foo is: bar"` but
345
- * just `"bar"`. This is because the standard ECMAScript behavior is now to use
346
- * the internals of `Object.defineProperty` on a class instance to instantiate
347
- * fields, and that call at instantiation-time overrides what's set on the
348
- * prototype. For details, see the accepted ECMAScript proposal for this
349
- * behavior:
350
- *
351
- * https://github.com/tc39/proposal-class-fields#public-fields-created-with-objectdefineproperty
352
- *
353
- * Why is this important? With `target` set to an ECMAScript version prior to
354
- * ES2022 TypeScript by default would emit a class which instantiated the field
355
- * in its constructor, something like this:
356
- *
357
- * ```ts
358
- * class CompiledMyClass {
359
- * constructor() {
360
- * this.foo = "bar"
361
- * }
362
- * }
363
- * ```
364
- *
365
- * This plays nicely with later using `Object.defineProperty` on the prototype
366
- * to define getters and setters, or simply with defining them right on the
367
- * class (see the code in `proxyComponent`, `proxyCustomElement`, and friends).
368
- *
369
- * However, with a `target` of ES2022 or higher (e.g. `ESNext`) default
370
- * behavior for TypeScript is instead to emit code like this:
371
- *
372
- * ```ts
373
- * class CompiledMyClass {
374
- * foo = "bar"
375
- * }
376
- * ```
377
- *
378
- * This output is more correct because the compiled code 1) more closely
379
- * resembles the TypeScript source and 2) is using standard JS syntax instead
380
- * of desugaring it. There is an announcement in the release notes for
381
- * TypeScript v3.7 which explains some helpful background about the change,
382
- * and about the `useDefineForClassFields` TypeScript option which lets you
383
- * opt-in to the old output:
384
- *
385
- * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier
386
- *
387
- * For our use-case, however, the ES2022+ behavior doesn't work, since we need
388
- * to be able to define getters and setters on these fields. We could require
389
- * that the TypeScript configuration used for Stencil have the
390
- * `useDefineForClassFields` setting set to `false`, but that would have the
391
- * undesirable side-effect that class fields which are _not_
392
- * decorated with a Stencil decorator would also be instantiated in the
393
- * constructor.
394
- *
395
- * So instead, we take matters into our own hands. When we encounter a class
396
- * field which is decorated with a Stencil decorator we remove it from the
397
- * class and add a statement to the constructor to instantiate it with the
398
- * correct default value.
399
- *
400
- * **Note**: this function will modify a constructor if one is already present on
401
- * the class or define a new one otherwise.
402
- *
403
- * @param classNode a TypeScript AST node for a Stencil component class
404
- * @param classMembers the class members that we need to update
405
- * @param typeChecker a reference to the {@link ts.TypeChecker}
406
- * @param importAliasMap a map of Stencil decorator names to their import names
407
- * @returns a list of updated class elements which can be inserted into the class
408
- */
409
- function handleClassFields (
410
- classNode : ts . ClassDeclaration ,
411
- classMembers : ts . ClassElement [ ] ,
412
- typeChecker : ts . TypeChecker ,
413
- importAliasMap : ImportAliasMap ,
414
- ) : ts . ClassElement [ ] {
415
- const statements : ts . ExpressionStatement [ ] = [ ] ;
416
- const updatedClassMembers : ts . ClassElement [ ] = [ ] ;
417
-
418
- for ( const member of classMembers ) {
419
- if ( shouldInitializeInConstructor ( member , importAliasMap ) && ts . isPropertyDeclaration ( member ) ) {
420
- const memberName = tsPropDeclNameAsString ( member , typeChecker ) ;
421
-
422
- // this is a class field that we'll need to handle, so lets push a statement for
423
- // initializing the value onto our statements list
424
- statements . push (
425
- ts . factory . createExpressionStatement (
426
- ts . factory . createBinaryExpression (
427
- ts . factory . createPropertyAccessExpression ( ts . factory . createThis ( ) , ts . factory . createIdentifier ( memberName ) ) ,
428
- ts . factory . createToken ( ts . SyntaxKind . EqualsToken ) ,
429
- // if the member has no initializer we should default to setting it to
430
- // just 'undefined'
431
- member . initializer ?? ts . factory . createIdentifier ( 'undefined' ) ,
432
- ) ,
433
- ) ,
434
- ) ;
435
- } else {
436
- // if it's not a class field that is decorated with a Stencil decorator then
437
- // we just push it onto our class member list
438
- updatedClassMembers . push ( member ) ;
439
- }
440
- }
441
-
442
- if ( statements . length === 0 ) {
443
- // we didn't encounter any class fields we need to update, so we can
444
- // just return the list of class members (no need to create an empty
445
- // constructor)
446
- return updatedClassMembers ;
447
- } else {
448
- // create or update a constructor which contains the initializing statements
449
- // we created above
450
- return updateConstructor ( classNode , updatedClassMembers , statements ) ;
451
- }
452
- }
453
-
454
- /**
455
- * Check whether a given class element should be rewritten from a class field
456
- * to a constructor-initialized value. This is basically the case for fields
457
- * decorated with `@Prop` and `@State`. See {@link handleClassFields} for more
458
- * details.
459
- *
460
- * @param member the member to check
461
- * @param importAliasMap a map of Stencil decorator names to their import names
462
- * @returns whether this should be rewritten or not
463
- */
464
- const shouldInitializeInConstructor = ( member : ts . ClassElement , importAliasMap : ImportAliasMap ) : boolean => {
465
- const currentDecorators = retrieveTsDecorators ( member ) ;
466
- if ( currentDecorators === undefined ) {
467
- // decorators have already been removed from this element, indicating that
468
- // we don't need to do anything
469
- return false ;
470
- }
471
- const filteredDecorators = filterDecorators (
472
- currentDecorators ,
473
- CONSTRUCTOR_DEFINED_MEMBER_DECORATORS . map ( ( decorator ) => importAliasMap . get ( decorator ) ) ,
474
- ) ;
475
- return currentDecorators !== filteredDecorators ;
476
- } ;
0 commit comments