diff --git a/src/compiler/output-targets/dist-custom-elements/index.ts b/src/compiler/output-targets/dist-custom-elements/index.ts index fcc64d74802..43288dd3c0c 100644 --- a/src/compiler/output-targets/dist-custom-elements/index.ts +++ b/src/compiler/output-targets/dist-custom-elements/index.ts @@ -204,6 +204,7 @@ export const addCustomElementInputs = ( if (cmp.isPlain) { exp.push(`export { ${importName} as ${exportName} } from '${cmp.sourceFilePath}';`); + // TODO(STENCIL-855): Invalid syntax generation, note the unbalanced left curly brace indexExports.push(`export { {${exportName} } from '${coreKey}';`); } else { // the `importName` may collide with the `exportName`, alias it just in case it does with `importAs` diff --git a/src/compiler/transformers/component-lazy/lazy-component.ts b/src/compiler/transformers/component-lazy/lazy-component.ts index 54d46734937..892bfa9f8b1 100644 --- a/src/compiler/transformers/component-lazy/lazy-component.ts +++ b/src/compiler/transformers/component-lazy/lazy-component.ts @@ -17,7 +17,7 @@ export const updateLazyComponentClass = ( cmp: d.ComponentCompilerMeta ) => { const members = updateLazyComponentMembers(transformOpts, styleStatements, classNode, moduleFile, cmp); - return updateComponentClass(transformOpts, classNode, classNode.heritageClauses, members); + return updateComponentClass(transformOpts, classNode, classNode.heritageClauses, members, moduleFile); }; const updateLazyComponentMembers = ( diff --git a/src/compiler/transformers/component-native/native-component.ts b/src/compiler/transformers/component-native/native-component.ts index 13d1db81fd2..4c202178f4d 100644 --- a/src/compiler/transformers/component-native/native-component.ts +++ b/src/compiler/transformers/component-native/native-component.ts @@ -20,7 +20,7 @@ export const updateNativeComponentClass = ( ): ts.ClassDeclaration | ts.VariableStatement => { const heritageClauses = updateNativeHostComponentHeritageClauses(classNode, moduleFile); const members = updateNativeHostComponentMembers(transformOpts, classNode, moduleFile, cmp); - return updateComponentClass(transformOpts, classNode, heritageClauses, members); + return updateComponentClass(transformOpts, classNode, heritageClauses, members, moduleFile); }; /** diff --git a/src/compiler/transformers/component-native/proxy-custom-element-function.ts b/src/compiler/transformers/component-native/proxy-custom-element-function.ts index 26b12de5a89..9b978c90aef 100644 --- a/src/compiler/transformers/component-native/proxy-custom-element-function.ts +++ b/src/compiler/transformers/component-native/proxy-custom-element-function.ts @@ -84,7 +84,7 @@ export const proxyCustomElement = ( // update the variable statement containing the updated declaration list const updatedVariableStatement = ts.factory.updateVariableStatement( stmt, - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + stmt.modifiers, updatedDeclarationList ); diff --git a/src/compiler/transformers/decorators-to-static/convert-decorators.ts b/src/compiler/transformers/decorators-to-static/convert-decorators.ts index 4f5c5c8fe72..5c4f471ec77 100644 --- a/src/compiler/transformers/decorators-to-static/convert-decorators.ts +++ b/src/compiler/transformers/decorators-to-static/convert-decorators.ts @@ -2,8 +2,14 @@ import { augmentDiagnosticWithNode, buildError } from '@utils'; import ts from 'typescript'; import type * as d from '../../../declarations'; -import { retrieveTsDecorators, retrieveTsModifiers } from '../transform-utils'; +import { + convertValueToLiteral, + createStaticGetter, + retrieveTsDecorators, + retrieveTsModifiers, +} from '../transform-utils'; import { componentDecoratorToStatic } from './component-decorator'; +import { hasStaticInitializerInClass } from './convert-static-members'; import { isDecoratorNamed } from './decorator-utils'; import { CLASS_DECORATORS_TO_REMOVE, @@ -72,6 +78,14 @@ const visitClassDeclaration = ( listenDecoratorsToStatic(diagnostics, decoratedMembers, filteredMethodsAndFields); } + // Handle static members that are initialized + const hasStaticMembersWithInit = classMembers.some(hasStaticInitializerInClass); + if (hasStaticMembersWithInit) { + filteredMethodsAndFields.push( + createStaticGetter('stencilHasStaticMembersWithInit', convertValueToLiteral(hasStaticMembersWithInit)) + ); + } + // We call the `handleClassFields` method which handles transforming any // class fields, removing them from the class and adding statements to the // class' constructor which instantiate them there instead. diff --git a/src/compiler/transformers/decorators-to-static/convert-static-members.ts b/src/compiler/transformers/decorators-to-static/convert-static-members.ts new file mode 100644 index 00000000000..ee701a51c5c --- /dev/null +++ b/src/compiler/transformers/decorators-to-static/convert-static-members.ts @@ -0,0 +1,18 @@ +import ts from 'typescript'; + +/** + * 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`) + * @param classElm the class member to test + * @returns true if the class member fits the above criteria, false otherwise + */ +export const hasStaticInitializerInClass = (classElm: ts.ClassElement): boolean => { + return ( + ts.isPropertyDeclaration(classElm) && + classElm.initializer !== undefined && + Array.isArray(classElm.modifiers) && + classElm.modifiers!.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) + ); +}; diff --git a/src/compiler/transformers/remove-static-meta-properties.ts b/src/compiler/transformers/remove-static-meta-properties.ts index bcadd4b90db..64eacdfd34d 100644 --- a/src/compiler/transformers/remove-static-meta-properties.ts +++ b/src/compiler/transformers/remove-static-meta-properties.ts @@ -24,6 +24,7 @@ export const removeStaticMetaProperties = (classNode: ts.ClassDeclaration): ts.C }); }; +// TODO(STENCIL-856): Move these properties to constants for better type safety within the codebase /** * A list of static getter names that are specific to Stencil to exclude from a class's member list */ @@ -37,6 +38,7 @@ const REMOVE_STATIC_GETTERS = new Set([ 'methods', 'states', 'originalStyleUrls', + 'stencilHasStaticMembersWithInit', 'styleMode', 'style', 'styles', diff --git a/src/compiler/transformers/static-to-meta/component.ts b/src/compiler/transformers/static-to-meta/component.ts index 84018298bd8..abe014726b5 100644 --- a/src/compiler/transformers/static-to-meta/component.ts +++ b/src/compiler/transformers/static-to-meta/component.ts @@ -54,7 +54,6 @@ export const parseStaticComponentMeta = ( const docs = serializeSymbol(typeChecker, symbol); const isCollectionDependency = moduleFile.isCollectionDependency; const encapsulation = parseStaticEncapsulation(staticMembers); - const cmp: d.ComponentCompilerMeta = { tagName: tagName, excludeFromCollection: moduleFile.excludeFromCollection, @@ -62,6 +61,7 @@ export const parseStaticComponentMeta = ( componentClassName: cmpNode.name ? cmpNode.name.text : '', elementRef: parseStaticElementRef(staticMembers), encapsulation, + hasStaticInitializedMember: getStaticValue(staticMembers, 'stencilHasStaticMembersWithInit') ?? false, shadowDelegatesFocus: parseStaticShadowDelegatesFocus(encapsulation, staticMembers), properties: parseStaticProps(staticMembers), virtualProperties: parseVirtualProps(docs), diff --git a/src/compiler/transformers/test/convert-static-members.spec.ts b/src/compiler/transformers/test/convert-static-members.spec.ts new file mode 100644 index 00000000000..4f8344ef84b --- /dev/null +++ b/src/compiler/transformers/test/convert-static-members.spec.ts @@ -0,0 +1,158 @@ +import ts from 'typescript'; + +import { hasStaticInitializerInClass } from '../decorators-to-static/convert-static-members'; + +describe('convert-static-members', () => { + describe('hasStaticInitializerInClass', () => { + it('returns true for a 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.createToken(ts.SyntaxKind.StaticKeyword)], + ts.factory.createIdentifier('propertyName'), + undefined, + undefined, + ts.factory.createStringLiteral('initial value') + ), + ] + ); + expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(true); + }); + + it('returns true for a private 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.createToken(ts.SyntaxKind.PrivateKeyword), 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('returns true for a static property with an initializer with multiple members', () => { + const classWithStaticMembers = ts.factory.createClassDeclaration( + [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier('ClassWithStaticAndNonStaticMembers'), + undefined, + undefined, + [ + ts.factory.createPropertyDeclaration( + undefined, + ts.factory.createIdentifier('nonStaticProperty'), + undefined, + undefined, + ts.factory.createStringLiteral('some value') + ), + ts.factory.createPropertyDeclaration( + [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('returns false for a class without any members', () => { + const classWithStaticMembers = ts.factory.createClassDeclaration( + [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier('ClassWithNoMembers'), + undefined, + undefined, + [] // no members for this class + ); + expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(false); + }); + + it('returns false for a static property without an initializer', () => { + const classWithStaticMembers = ts.factory.createClassDeclaration( + [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier('ClassWithUninitializedStaticMember'), + undefined, + undefined, + [ + ts.factory.createPropertyDeclaration( + [ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], + ts.factory.createIdentifier('propertyName'), + undefined, + undefined, + undefined // the initializer is false + ), + ] + ); + expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(false); + }); + + it('returns false for a private static property without an initializer', () => { + const classWithStaticMembers = ts.factory.createClassDeclaration( + [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier('ClassWithUninitializedStaticMember'), + undefined, + undefined, + [ + ts.factory.createPropertyDeclaration( + [ts.factory.createToken(ts.SyntaxKind.PrivateKeyword), ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], + ts.factory.createIdentifier('propertyName'), + undefined, + undefined, + undefined // the initializer is false + ), + ] + ); + expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(false); + }); + + it('returns false for a modified property with an initializer', () => { + const classWithStaticMembers = ts.factory.createClassDeclaration( + [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier('ClassWithNonStaticMember'), + undefined, + undefined, + [ + ts.factory.createPropertyDeclaration( + [ts.factory.createToken(ts.SyntaxKind.PrivateKeyword)], // the property is declared as private + ts.factory.createIdentifier('propertyName'), + undefined, + undefined, + ts.factory.createStringLiteral('initial value') + ), + ] + ); + expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(false); + }); + + it('returns false for an unmodified property with an initializer', () => { + const classWithStaticMembers = ts.factory.createClassDeclaration( + [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier('ClassWithUnmodifiedMembers'), + undefined, + undefined, + [ + ts.factory.createPropertyDeclaration( + undefined, // the property declaration has no modifiers + ts.factory.createIdentifier('propertyName'), + undefined, + undefined, + ts.factory.createStringLiteral('initial value') + ), + ] + ); + expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(false); + }); + }); +}); diff --git a/src/compiler/transformers/test/parse-members.spec.ts b/src/compiler/transformers/test/parse-members.spec.ts new file mode 100644 index 00000000000..af1616b99c0 --- /dev/null +++ b/src/compiler/transformers/test/parse-members.spec.ts @@ -0,0 +1,18 @@ +import { transpileModule } from './transpile'; + +describe('parse static members', () => { + it('places a static getter on the component', () => { + const t = transpileModule(` + @Component({tag: 'cmp-a'}) + export class CmpA { + static myStatic = 'a value'; + + render() { + return
Hello, I have {CmpA.myStatic}
+ } + } + `); + + expect(t.outputText.includes('static get stencilHasStaticMembersWithInit() { return true; }')).toBe(true); + }); +}); diff --git a/src/compiler/transformers/test/proxy-custom-element-function.spec.ts b/src/compiler/transformers/test/proxy-custom-element-function.spec.ts index 667d3d83129..9cca08b93ab 100644 --- a/src/compiler/transformers/test/proxy-custom-element-function.spec.ts +++ b/src/compiler/transformers/test/proxy-custom-element-function.spec.ts @@ -81,6 +81,19 @@ describe('proxy-custom-element-function', () => { const transformer = proxyCustomElement(compilerCtx, transformOpts); const transpiledModule = transpileModule(code, null, compilerCtx, [], [transformer]); + expect(formatCode(transpiledModule.outputText)).toContain( + formatCode( + `const ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true);` + ) + ); + }); + + it('wraps an exported class initializer in a proxyCustomElement call', () => { + const code = `export const ${componentClassName} = class extends HTMLElement {};`; + + const transformer = proxyCustomElement(compilerCtx, transformOpts); + const transpiledModule = transpileModule(code, null, compilerCtx, [], [transformer]); + expect(formatCode(transpiledModule.outputText)).toContain( formatCode( `export const ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true);` @@ -97,7 +110,7 @@ describe('proxy-custom-element-function', () => { expect(formatCode(transpiledModule.outputText)).toContain( formatCode( - `export const foo = 'hello world!', ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true);` + `const foo = 'hello world!', ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true);` ) ); }); @@ -110,7 +123,7 @@ describe('proxy-custom-element-function', () => { expect(formatCode(transpiledModule.outputText)).toContain( formatCode( - `export const ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true), foo = 'hello world!';` + `const ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true), foo = 'hello world!';` ) ); }); @@ -123,7 +136,7 @@ describe('proxy-custom-element-function', () => { expect(formatCode(transpiledModule.outputText)).toContain( formatCode( - `export const foo = 'hello world!', ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true), bar = 'goodbye?';` + `const foo = 'hello world!', ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true), bar = 'goodbye?';` ) ); }); diff --git a/src/compiler/transformers/test/transpile.ts b/src/compiler/transformers/test/transpile.ts index 1a88d3fedfd..695171fd4c9 100644 --- a/src/compiler/transformers/test/transpile.ts +++ b/src/compiler/transformers/test/transpile.ts @@ -175,11 +175,19 @@ export function transpileModule( */ const prettifyTSOutput = (tsOutput: string): string => tsOutput.replace(/\s+/gm, ' '); -export function getStaticGetter(output: string, prop: string) { - const toEvaluate = `return ${output.replace('export', '')}`; +/** + * Helper function for tests that converts stringified JavaScript to a runtime value. + * A value from the generated JavaScript is returned based on the provided property name. + * @param stringifiedJs the stringified JavaScript + * @param propertyName the property name to pull off the generated JavaScript + * @returns the value associated with the provided property name. Returns undefined if an error occurs while converting + * the stringified JS to JavaScript, or if the property does not exist on the generated JavaScript. + */ +export function getStaticGetter(stringifiedJs: string, propertyName: string): string | void { + const toEvaluate = `return ${stringifiedJs.replace('export', '')}`; try { const Obj = new Function(toEvaluate); - return Obj()[prop]; + return Obj()[propertyName]; } catch (e) { console.error(e); console.error(toEvaluate); diff --git a/src/compiler/transformers/update-component-class.ts b/src/compiler/transformers/update-component-class.ts index 1e8eede9dcb..3519fc15669 100644 --- a/src/compiler/transformers/update-component-class.ts +++ b/src/compiler/transformers/update-component-class.ts @@ -3,11 +3,26 @@ import ts from 'typescript'; import type * as d from '../../declarations'; import { retrieveTsDecorators, retrieveTsModifiers } from './transform-utils'; +/** + * Transformation helper for updating how a Stencil component class is declared. + * + * Based on the output module type (CommonJS or ESM), the behavior is slightly different: + * - For CommonJS, the component class is left as is + * - For ESM, the component class is re-written as a variable statement + * + * @param transformOpts the options provided to TypeScript + Rollup for transforming the AST node + * @param classNode the node in the AST pertaining to the Stencil component class to transform + * @param heritageClauses a collection of heritage clauses associated with the provided class node + * @param members a collection of members attached to the provided class node + * @param moduleFile the Stencil intermediate representation associated with the provided class node + * @returns the updated component class declaration + */ export const updateComponentClass = ( transformOpts: d.TransformOptions, classNode: ts.ClassDeclaration, heritageClauses: ts.HeritageClause[] | ts.NodeArray, - members: ts.ClassElement[] + members: ts.ClassElement[], + moduleFile: d.Module ): ts.ClassDeclaration | ts.VariableStatement => { let classModifiers = retrieveTsModifiers(classNode)?.slice() ?? []; @@ -15,7 +30,8 @@ export const updateComponentClass = ( // CommonJS, leave component class as is if (transformOpts.componentExport === 'customelement') { - // remove export from class + // remove export from class - it may already be removed by the TypeScript compiler in certain circumstances if + // this transformation is run after transpilation occurs classModifiers = classModifiers.filter((m) => { return m.kind !== ts.SyntaxKind.ExportKeyword; }); @@ -31,25 +47,48 @@ export const updateComponentClass = ( } // ESM with export - return createConstClass(transformOpts, classNode, heritageClauses, members); + return createConstClass(transformOpts, classNode, heritageClauses, members, moduleFile); }; +/** + * Rewrites a component class as a variable statement. + * + * After running this function, the following: + * ```ts + * class MyComponent {} + * ``` + * is rewritten as + * ```ts + * const MyComponent = class {} + * ``` + * @param transformOpts the options provided to TypeScript + Rollup for transforming the AST node + * @param classNode the node in the AST pertaining to the Stencil component class to transform + * @param heritageClauses a collection of heritage clauses associated with the provided class node + * @param members a collection of members attached to the provided class node + * @param moduleFile the Stencil intermediate representation associated with the provided class node + * @returns the component class, re-written as a variable statement + */ const createConstClass = ( transformOpts: d.TransformOptions, classNode: ts.ClassDeclaration, heritageClauses: ts.HeritageClause[] | ts.NodeArray, - members: ts.ClassElement[] -) => { + members: ts.ClassElement[], + moduleFile: d.Module +): ts.VariableStatement => { const className = classNode.name; const classModifiers = (retrieveTsModifiers(classNode) ?? []).filter((m) => { - // remove the export + // remove the export - it may already be removed by the TypeScript compiler in certain circumstances if this + // transformation is run after transpilation occurs return m.kind !== ts.SyntaxKind.ExportKeyword; }); const constModifiers: ts.Modifier[] = []; - if (transformOpts.componentExport !== 'customelement') { + if ( + transformOpts.componentExport !== 'customelement' && + !moduleFile.cmps.some((cmp) => cmp.hasStaticInitializedMember) + ) { constModifiers.push(ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)); } diff --git a/src/compiler/types/tests/ComponentCompilerMeta.stub.ts b/src/compiler/types/tests/ComponentCompilerMeta.stub.ts index f510e4a8281..e8ebfbb6352 100644 --- a/src/compiler/types/tests/ComponentCompilerMeta.stub.ts +++ b/src/compiler/types/tests/ComponentCompilerMeta.stub.ts @@ -53,6 +53,7 @@ export const stubComponentCompilerMeta = ( hasReflect: false, hasRenderFn: false, hasState: false, + hasStaticInitializedMember: false, hasStyle: false, hasVdomAttribute: false, hasVdomClass: false, diff --git a/src/declarations/stencil-private.ts b/src/declarations/stencil-private.ts index 56fe5dce13c..5c078a24c55 100644 --- a/src/declarations/stencil-private.ts +++ b/src/declarations/stencil-private.ts @@ -762,6 +762,7 @@ export interface ComponentCompilerFeatures { hasReflect: boolean; hasRenderFn: boolean; hasState: boolean; + hasStaticInitializedMember: boolean; hasStyle: boolean; hasVdomAttribute: boolean; hasVdomClass: boolean; diff --git a/test/karma/test-app/components.d.ts b/test/karma/test-app/components.d.ts index 80cb91f780b..d70fbbfacb4 100644 --- a/test/karma/test-app/components.d.ts +++ b/test/karma/test-app/components.d.ts @@ -318,6 +318,12 @@ export namespace Components { } interface SlottedCss { } + interface StaticMembers { + } + interface StaticMembersSeparateExport { + } + interface StaticMembersSeparateInitializer { + } interface StaticStyles { } interface StencilSibling { @@ -1098,6 +1104,24 @@ declare global { prototype: HTMLSlottedCssElement; new (): HTMLSlottedCssElement; }; + interface HTMLStaticMembersElement extends Components.StaticMembers, HTMLStencilElement { + } + var HTMLStaticMembersElement: { + prototype: HTMLStaticMembersElement; + new (): HTMLStaticMembersElement; + }; + interface HTMLStaticMembersSeparateExportElement extends Components.StaticMembersSeparateExport, HTMLStencilElement { + } + var HTMLStaticMembersSeparateExportElement: { + prototype: HTMLStaticMembersSeparateExportElement; + new (): HTMLStaticMembersSeparateExportElement; + }; + interface HTMLStaticMembersSeparateInitializerElement extends Components.StaticMembersSeparateInitializer, HTMLStencilElement { + } + var HTMLStaticMembersSeparateInitializerElement: { + prototype: HTMLStaticMembersSeparateInitializerElement; + new (): HTMLStaticMembersSeparateInitializerElement; + }; interface HTMLStaticStylesElement extends Components.StaticStyles, HTMLStencilElement { } var HTMLStaticStylesElement: { @@ -1258,6 +1282,9 @@ declare global { "slot-replace-wrapper": HTMLSlotReplaceWrapperElement; "slot-replace-wrapper-root": HTMLSlotReplaceWrapperRootElement; "slotted-css": HTMLSlottedCssElement; + "static-members": HTMLStaticMembersElement; + "static-members-separate-export": HTMLStaticMembersSeparateExportElement; + "static-members-separate-initializer": HTMLStaticMembersSeparateInitializerElement; "static-styles": HTMLStaticStylesElement; "stencil-sibling": HTMLStencilSiblingElement; "svg-attr": HTMLSvgAttrElement; @@ -1582,6 +1609,12 @@ declare namespace LocalJSX { } interface SlottedCss { } + interface StaticMembers { + } + interface StaticMembersSeparateExport { + } + interface StaticMembersSeparateInitializer { + } interface StaticStyles { } interface StencilSibling { @@ -1718,6 +1751,9 @@ declare namespace LocalJSX { "slot-replace-wrapper": SlotReplaceWrapper; "slot-replace-wrapper-root": SlotReplaceWrapperRoot; "slotted-css": SlottedCss; + "static-members": StaticMembers; + "static-members-separate-export": StaticMembersSeparateExport; + "static-members-separate-initializer": StaticMembersSeparateInitializer; "static-styles": StaticStyles; "stencil-sibling": StencilSibling; "svg-attr": SvgAttr; @@ -1853,6 +1889,9 @@ 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-members": LocalJSX.StaticMembers & JSXBase.HTMLAttributes; + "static-members-separate-export": LocalJSX.StaticMembersSeparateExport & JSXBase.HTMLAttributes; + "static-members-separate-initializer": LocalJSX.StaticMembersSeparateInitializer & JSXBase.HTMLAttributes; "static-styles": LocalJSX.StaticStyles & JSXBase.HTMLAttributes; "stencil-sibling": LocalJSX.StencilSibling & JSXBase.HTMLAttributes; "svg-attr": LocalJSX.SvgAttr & JSXBase.HTMLAttributes; diff --git a/test/karma/test-app/static-members/README.md b/test/karma/test-app/static-members/README.md new file mode 100644 index 00000000000..7e6a8a03c6d --- /dev/null +++ b/test/karma/test-app/static-members/README.md @@ -0,0 +1,26 @@ +This test suite is focused on ensuring that component's with static class fields compiles correctly. +With TypeScript v5.0, exported classes with static exports would be compiled as: +```ts +class StaticMembers {}; +StaticMembers.property = ''; +StaticMembers.anotherProperty = ''; +export { StaticMembers }; +``` + +However, Stencil would also inject the `export` keyword in front of the class declaration, resulting in two exports: +```ts +export class StaticMembers {}; +StaticMembers.property = ''; +StaticMembers.anotherProperty = ''; +export { StaticMembers }; // 2 exports causes an error! +``` + +Which produces an error when we get to the rollup stage: +```console +[ ERROR ] Rollup: Parse Error: ./test-app/static-members/cmp.tsx:35:9 + Duplicate export 'StaticMembers' (Note that you need plugins to import files that are not JavaScript) at + ... (elided) ... +``` +See: https://github.com/ionic-team/stencil/issues/4424 for more information +See: https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBAYwDYEMDOa4GUYpgSwQFlgBbAI2CkwG8BYAKDjjVwITjCgjCpgE84AXjgAmANyMAvkA +for a TS playground reproduction of the `export` keyword being moved around \ No newline at end of file diff --git a/test/karma/test-app/static-members/index.html b/test/karma/test-app/static-members/index.html new file mode 100644 index 00000000000..1438519ea4d --- /dev/null +++ b/test/karma/test-app/static-members/index.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/karma/test-app/static-members/karma.spec.ts b/test/karma/test-app/static-members/karma.spec.ts new file mode 100644 index 00000000000..8b4273aa4e9 --- /dev/null +++ b/test/karma/test-app/static-members/karma.spec.ts @@ -0,0 +1,29 @@ +import { setupDomTests } from '../util'; + +describe('static-members', function () { + const { setupDom, tearDownDom } = setupDomTests(document); + let app: HTMLElement; + + beforeEach(async () => { + app = await setupDom('/static-members/index.html'); + }); + afterEach(tearDownDom); + + it('renders properly with initialized static members', async () => { + const cmp = app.querySelector('static-members'); + + expect(cmp.textContent.trim()).toBe('This is a component with static public and private members'); + }); + + it('renders properly with a separate export', async () => { + const cmp = app.querySelector('static-members-separate-export'); + + expect(cmp.textContent.trim()).toBe('This is a component with static public and private members'); + }); + + it('renders properly with a static member initialized outside of a class', async () => { + const cmp = app.querySelector('static-members-separate-initializer'); + + expect(cmp.textContent.trim()).toBe('This is a component with static an externally initialized member'); + }); +}); diff --git a/test/karma/test-app/static-members/static-members-separate-export.tsx b/test/karma/test-app/static-members/static-members-separate-export.tsx new file mode 100644 index 00000000000..f0cb644648e --- /dev/null +++ b/test/karma/test-app/static-members/static-members-separate-export.tsx @@ -0,0 +1,22 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'static-members-separate-export', +}) +class StaticMembersWithSeparateExport { + /** + * See the spec file associated with this file for the motivation for this test + */ + static property = 'public'; + private static anotherProperty = 'private'; + + render() { + return ( +
+ This is a component with static {StaticMembersWithSeparateExport.property} and{' '} + {StaticMembersWithSeparateExport.anotherProperty} members +
+ ); + } +} +export { StaticMembersWithSeparateExport }; diff --git a/test/karma/test-app/static-members/static-members-separate-initializer.tsx b/test/karma/test-app/static-members/static-members-separate-initializer.tsx new file mode 100644 index 00000000000..7ab23452d09 --- /dev/null +++ b/test/karma/test-app/static-members/static-members-separate-initializer.tsx @@ -0,0 +1,15 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'static-members-separate-initializer', +}) +export class StaticMembersWithSeparateInitializer { + /** + * See the spec file associated with this file for the motivation for this test + */ + static property: string; + render() { + return
This is a component with static an {StaticMembersWithSeparateInitializer.property} member
; + } +} +StaticMembersWithSeparateInitializer.property = 'externally initialized'; diff --git a/test/karma/test-app/static-members/static-members.tsx b/test/karma/test-app/static-members/static-members.tsx new file mode 100644 index 00000000000..c219b237879 --- /dev/null +++ b/test/karma/test-app/static-members/static-members.tsx @@ -0,0 +1,20 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'static-members', +}) +export class StaticMembers { + /** + * See the spec file associated with this file for the motivation for this test + */ + static property = 'public'; + private static anotherProperty = 'private'; + + render() { + return ( +
+ This is a component with static {StaticMembers.property} and {StaticMembers.anotherProperty} members +
+ ); + } +}