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
+
+ );
+ }
+}