@@ -4,11 +4,53 @@ import ts from 'typescript';
44
55import type * as d from '../../../declarations' ;
66import { createModule , getModule } from '../../transpile/transpiled-module' ;
7+ import { getComponentTagName , isStaticGetter } from '../transform-utils' ;
78import { parseCallExpression } from './call-expression' ;
89import { parseStaticComponentMeta } from './component' ;
910import { parseModuleImport } from './import' ;
1011import { parseStringLiteral } from './string-literal' ;
1112
13+ /**
14+ * Stencil static getter names that indicate a class has Stencil metadata
15+ * and can be extended by other components (mixin/abstract class pattern).
16+ */
17+ const STENCIL_MIXIN_STATIC_MEMBERS = [ 'properties' , 'states' , 'methods' , 'events' , 'listeners' , 'watchers' ] ;
18+
19+ /**
20+ * Gets the name of a class member as a string, safely handling cases where
21+ * getText() might not work (e.g., synthetic nodes without source file context).
22+ */
23+ const getMemberName = ( member : ts . ClassElement ) : string | undefined => {
24+ if ( ! member . name ) return undefined ;
25+ if ( ts . isIdentifier ( member . name ) ) {
26+ return member . name . text ?? member . name . escapedText ?. toString ( ) ;
27+ }
28+ if ( ts . isStringLiteral ( member . name ) ) {
29+ return member . name . text ;
30+ }
31+ return undefined ;
32+ } ;
33+
34+ /**
35+ * Checks if a class declaration is an exportable mixin - i.e., it has Stencil
36+ * static getters (properties, states, etc.) but is NOT a component (no tag name).
37+ * These are abstract/partial classes meant to be extended by actual components.
38+ */
39+ const isExportableMixinClass = ( classNode : ts . ClassDeclaration ) : boolean => {
40+ const staticGetters = classNode . members . filter ( isStaticGetter ) ;
41+ if ( staticGetters . length === 0 ) return false ;
42+
43+ // If it has a tag name, it's a component, not a mixin
44+ const tagName = getComponentTagName ( staticGetters ) ;
45+ if ( tagName ) return false ;
46+
47+ // Check if it has any Stencil mixin static members
48+ return staticGetters . some ( ( getter ) => {
49+ const name = getMemberName ( getter ) ;
50+ return name && STENCIL_MIXIN_STATIC_MEMBERS . includes ( name ) ;
51+ } ) ;
52+ } ;
53+
1254export const updateModule = (
1355 config : d . ValidatedConfig ,
1456 compilerCtx : d . CompilerCtx ,
@@ -46,7 +88,13 @@ export const updateModule = (
4688
4789 const visitNode = ( node : ts . Node ) => {
4890 if ( ts . isClassDeclaration ( node ) ) {
91+ // First try to parse as a component
4992 parseStaticComponentMeta ( compilerCtx , typeChecker , node , moduleFile , buildCtx , undefined ) ;
93+
94+ // Also check if this is an exportable mixin class (has Stencil static members but no tag)
95+ if ( isExportableMixinClass ( node ) ) {
96+ moduleFile . hasExportableMixins = true ;
97+ }
5098 return ;
5199 } else if ( ts . isImportDeclaration ( node ) ) {
52100 parseModuleImport ( config , compilerCtx , buildCtx , moduleFile , srcDirPath , node , true ) ;
@@ -64,8 +112,11 @@ export const updateModule = (
64112 // Handle functions with block body: (Base) => { class MyMixin ... }
65113 if ( ts . isBlock ( funcBody ) ) {
66114 funcBody . statements . forEach ( ( statement ) => {
67- // Look for class declarations in the function body
115+ // Look for class declarations in the function body (mixin factory pattern)
68116 if ( ts . isClassDeclaration ( statement ) ) {
117+ if ( isExportableMixinClass ( statement ) ) {
118+ moduleFile . hasExportableMixins = true ;
119+ }
69120 statement . members . forEach ( ( member ) => {
70121 if ( ts . isPropertyDeclaration ( member ) && member . initializer ) {
71122 // Traverse into the property initializer (e.g., arrow function)
@@ -91,7 +142,9 @@ export const updateModule = (
91142
92143 // TODO: workaround around const enums
93144 // find better way
94- if ( moduleFile . cmps . length > 0 ) {
145+ // Create staticSourceFile for modules with components OR exportable mixins
146+ // (needed for class-extension to process mixin metadata from external collections)
147+ if ( moduleFile . cmps . length > 0 || moduleFile . hasExportableMixins ) {
95148 moduleFile . staticSourceFile = ts . createSourceFile (
96149 sourceFilePath ,
97150 sourceFileText ,
0 commit comments