diff --git a/src/compiler/transformers/decorators-to-static/convert-static-members.ts b/src/compiler/transformers/decorators-to-static/convert-static-members.ts index ee701a51c5c2..11d3f644560f 100644 --- a/src/compiler/transformers/decorators-to-static/convert-static-members.ts +++ b/src/compiler/transformers/decorators-to-static/convert-static-members.ts @@ -1,10 +1,13 @@ import ts from 'typescript'; +import { CONSTRUCTOR_DEFINED_MEMBER_DECORATORS } from './decorators-constants'; + /** * Helper function to detect if a class element fits the following criteria: * - It is a property declaration (e.g. `foo`) * - It has an initializer (e.g. `foo *=1*`) * - The property declaration has the `static` modifier on it (e.g. `*static* foo =1`) + * - The property declaration does not include a Stencil @Prop or @State decorator * @param classElm the class member to test * @returns true if the class member fits the above criteria, false otherwise */ @@ -13,6 +16,25 @@ export const hasStaticInitializerInClass = (classElm: ts.ClassElement): boolean ts.isPropertyDeclaration(classElm) && classElm.initializer !== undefined && Array.isArray(classElm.modifiers) && - classElm.modifiers!.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) + classElm.modifiers!.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) && + !classElm.modifiers!.some(isStencilStateOrPropDecorator) ); }; + +/** + * Determines is a Modifier-like node is a Stencil `@Prop()` or `@State()` decorator + * @param modifier the AST node to evaluate + * @returns true if the node is a decorator with the name 'Prop' or 'State', false otherwise + */ +const isStencilStateOrPropDecorator = (modifier: ts.ModifierLike): boolean => { + if (ts.isDecorator(modifier)) { + const decoratorName = + ts.isCallExpression(modifier.expression) && + ts.isIdentifier(modifier.expression.expression) && + modifier.expression.expression.text; + return ( + typeof decoratorName !== 'boolean' && CONSTRUCTOR_DEFINED_MEMBER_DECORATORS.some((val) => val === decoratorName) + ); + } + return false; +}; diff --git a/src/compiler/transformers/test/convert-static-members.spec.ts b/src/compiler/transformers/test/convert-static-members.spec.ts index 4f8344ef84b4..6158d6d27f68 100644 --- a/src/compiler/transformers/test/convert-static-members.spec.ts +++ b/src/compiler/transformers/test/convert-static-members.spec.ts @@ -42,6 +42,62 @@ describe('convert-static-members', () => { expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(true); }); + it('returns true for a decorated (non-Stencil) static property with an initializer', () => { + const classWithStaticMembers = ts.factory.createClassDeclaration( + [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier('ClassWithStaticMember'), + undefined, + undefined, + [ + ts.factory.createPropertyDeclaration( + [ + ts.factory.createDecorator( + ts.factory.createCallExpression( + ts.factory.createIdentifier('SomeDecorator'), // Imaginary decorator + undefined, + [] + ) + ), + ts.factory.createToken(ts.SyntaxKind.StaticKeyword), + ], + ts.factory.createIdentifier('propertyName'), + undefined, + undefined, + ts.factory.createStringLiteral('initial value') + ), + ] + ); + expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(true); + }); + + it.each(['Prop', 'State'])('returns false for a static property decorated with @%s with an initializer', (decoratorName) => { + const classWithStaticMembers = ts.factory.createClassDeclaration( + [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier('ClassWithStaticMember'), + undefined, + undefined, + [ + ts.factory.createPropertyDeclaration( + [ + ts.factory.createDecorator( + ts.factory.createCallExpression( + ts.factory.createIdentifier(decoratorName), // Stencil decorator + undefined, + [] + ) + ), + ts.factory.createToken(ts.SyntaxKind.StaticKeyword), + ], + ts.factory.createIdentifier('propertyName'), + undefined, + undefined, + ts.factory.createStringLiteral('initial value') + ), + ] + ); + expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(false); + }); + it('returns true for a static property with an initializer with multiple members', () => { const classWithStaticMembers = ts.factory.createClassDeclaration( [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], diff --git a/test/karma/test-app/components.d.ts b/test/karma/test-app/components.d.ts index d70fbbfacb49..7732f5200b4d 100644 --- a/test/karma/test-app/components.d.ts +++ b/test/karma/test-app/components.d.ts @@ -318,6 +318,8 @@ export namespace Components { } interface SlottedCss { } + interface StaticDecoratedMembers { + } interface StaticMembers { } interface StaticMembersSeparateExport { @@ -1104,6 +1106,12 @@ declare global { prototype: HTMLSlottedCssElement; new (): HTMLSlottedCssElement; }; + interface HTMLStaticDecoratedMembersElement extends Components.StaticDecoratedMembers, HTMLStencilElement { + } + var HTMLStaticDecoratedMembersElement: { + prototype: HTMLStaticDecoratedMembersElement; + new (): HTMLStaticDecoratedMembersElement; + }; interface HTMLStaticMembersElement extends Components.StaticMembers, HTMLStencilElement { } var HTMLStaticMembersElement: { @@ -1282,6 +1290,7 @@ declare global { "slot-replace-wrapper": HTMLSlotReplaceWrapperElement; "slot-replace-wrapper-root": HTMLSlotReplaceWrapperRootElement; "slotted-css": HTMLSlottedCssElement; + "static-decorated-members": HTMLStaticDecoratedMembersElement; "static-members": HTMLStaticMembersElement; "static-members-separate-export": HTMLStaticMembersSeparateExportElement; "static-members-separate-initializer": HTMLStaticMembersSeparateInitializerElement; @@ -1609,6 +1618,8 @@ declare namespace LocalJSX { } interface SlottedCss { } + interface StaticDecoratedMembers { + } interface StaticMembers { } interface StaticMembersSeparateExport { @@ -1751,6 +1762,7 @@ declare namespace LocalJSX { "slot-replace-wrapper": SlotReplaceWrapper; "slot-replace-wrapper-root": SlotReplaceWrapperRoot; "slotted-css": SlottedCss; + "static-decorated-members": StaticDecoratedMembers; "static-members": StaticMembers; "static-members-separate-export": StaticMembersSeparateExport; "static-members-separate-initializer": StaticMembersSeparateInitializer; @@ -1889,6 +1901,7 @@ declare module "@stencil/core" { "slot-replace-wrapper": LocalJSX.SlotReplaceWrapper & JSXBase.HTMLAttributes; "slot-replace-wrapper-root": LocalJSX.SlotReplaceWrapperRoot & JSXBase.HTMLAttributes; "slotted-css": LocalJSX.SlottedCss & JSXBase.HTMLAttributes; + "static-decorated-members": LocalJSX.StaticDecoratedMembers & JSXBase.HTMLAttributes; "static-members": LocalJSX.StaticMembers & JSXBase.HTMLAttributes; "static-members-separate-export": LocalJSX.StaticMembersSeparateExport & JSXBase.HTMLAttributes; "static-members-separate-initializer": LocalJSX.StaticMembersSeparateInitializer & JSXBase.HTMLAttributes; diff --git a/test/karma/test-app/static-members/index.html b/test/karma/test-app/static-members/index.html index 1438519ea4d6..9b7837ac9566 100644 --- a/test/karma/test-app/static-members/index.html +++ b/test/karma/test-app/static-members/index.html @@ -4,5 +4,6 @@ + diff --git a/test/karma/test-app/static-members/karma.spec.ts b/test/karma/test-app/static-members/karma.spec.ts index 8b4273aa4e9d..2fe7bca5c762 100644 --- a/test/karma/test-app/static-members/karma.spec.ts +++ b/test/karma/test-app/static-members/karma.spec.ts @@ -15,6 +15,12 @@ describe('static-members', function () { expect(cmp.textContent.trim()).toBe('This is a component with static public and private members'); }); + it('renders properly with initialized, decorated static members', async () => { + const cmp = app.querySelector('static-decorated-members'); + + expect(cmp.textContent.trim()).toBe('This is a component with a static @State-ful member'); + }); + it('renders properly with a separate export', async () => { const cmp = app.querySelector('static-members-separate-export'); diff --git a/test/karma/test-app/static-members/static-decorated-members.tsx b/test/karma/test-app/static-members/static-decorated-members.tsx new file mode 100644 index 000000000000..4cb3ee87be9e --- /dev/null +++ b/test/karma/test-app/static-members/static-decorated-members.tsx @@ -0,0 +1,15 @@ +import { Component, h, State } from '@stencil/core'; + +@Component({ + tag: 'static-decorated-members', +}) +export class StaticDecoratedMembers { + /** + * See the spec file associated with this file for the motivation for this test + */ + @State() static property = '@State-ful'; + + render() { + return
This is a component with static a {StaticDecoratedMembers.property} member
; + } +}