From 5899f95e52d04fdc65c4435ed931af2704d16407 Mon Sep 17 00:00:00 2001 From: John Jenkins Date: Mon, 6 Oct 2025 12:46:26 +0100 Subject: [PATCH] fix(build): add extended class source in collection dependencies --- .../typescript/typescript-resolve-module.ts | 10 +++++ .../transformers/detect-modern-prop-decls.ts | 6 +-- .../static-to-meta/class-extension.ts | 39 +++++++++++++------ 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/compiler/sys/typescript/typescript-resolve-module.ts b/src/compiler/sys/typescript/typescript-resolve-module.ts index a062c62f99b..473716425a3 100644 --- a/src/compiler/sys/typescript/typescript-resolve-module.ts +++ b/src/compiler/sys/typescript/typescript-resolve-module.ts @@ -24,6 +24,16 @@ export const tsResolveModuleName = ( return null; }; +export const tsGetSourceFile = (config: d.ValidatedConfig, module: ts.ResolvedModuleWithFailedLookupLocations) => { + if (!module || !module.resolvedModule) { + return null; + } + const compilerOptions: ts.CompilerOptions = { ...config.tsCompilerOptions }; + const host = ts.createCompilerHost(compilerOptions); + const program = ts.createProgram([module.resolvedModule.resolvedFileName], compilerOptions, host); + return program.getSourceFile(module.resolvedModule.resolvedFileName); +}; + export const tsResolveModuleNamePackageJsonPath = ( config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, diff --git a/src/compiler/transformers/detect-modern-prop-decls.ts b/src/compiler/transformers/detect-modern-prop-decls.ts index 4a089a1c3d6..e9c0b37d059 100644 --- a/src/compiler/transformers/detect-modern-prop-decls.ts +++ b/src/compiler/transformers/detect-modern-prop-decls.ts @@ -29,7 +29,7 @@ import { getStaticValue } from './transform-utils'; * @param cmp metadata about the stencil component of interest * @returns true if the class has modern property declarations, false otherwise */ -export const detectModernPropDeclarations = (classNode: ts.ClassDeclaration) => { +export const detectModernPropDeclarations = (classNode: ts.ClassDeclaration, sourceFile?: ts.SourceFile) => { const parsedProps: { [key: string]: d.ComponentCompilerProperty } = getStaticValue(classNode.members, 'properties'); const parsedStates: { [key: string]: d.ComponentCompilerProperty } = getStaticValue(classNode.members, 'states'); @@ -48,8 +48,8 @@ export const detectModernPropDeclarations = (classNode: ts.ClassDeclaration) => const prop = classNode.members.find((m) => { return ( ts.isPropertyDeclaration(m) && - ((ts.isComputedPropertyName(m.name) && m.name.expression.getText() === dynamicPropName) || - m.name.getText() === propName) + ((ts.isComputedPropertyName(m.name) && m.name.expression.getText(sourceFile) === dynamicPropName) || + m.name.getText(sourceFile) === propName) ); }) as any as ts.PropertyDeclaration; diff --git a/src/compiler/transformers/static-to-meta/class-extension.ts b/src/compiler/transformers/static-to-meta/class-extension.ts index f01308b1ccd..5f318a3f9fe 100644 --- a/src/compiler/transformers/static-to-meta/class-extension.ts +++ b/src/compiler/transformers/static-to-meta/class-extension.ts @@ -1,6 +1,6 @@ import ts from 'typescript'; import { augmentDiagnosticWithNode, buildWarn } from '@utils'; -import { tsResolveModuleName } from '../../sys/typescript/typescript-resolve-module'; +import { tsResolveModuleName, tsGetSourceFile } from '../../sys/typescript/typescript-resolve-module'; import { isStaticGetter } from '../transform-utils'; import { parseStaticEvents } from './events'; import { parseStaticListeners } from './listeners'; @@ -109,7 +109,7 @@ function matchesNamedDeclaration(name: string) { function buildExtendsTree( compilerCtx: d.CompilerCtx, classDeclaration: ts.ClassDeclaration, - dependentClasses: { classNode: ts.ClassDeclaration; fileName: string }[], + dependentClasses: { classNode: ts.ClassDeclaration; sourceFile: ts.SourceFile; fileName: string }[], typeChecker: ts.TypeChecker, buildCtx: d.BuildCtx, ogModule: d.Module, @@ -164,7 +164,7 @@ function buildExtendsTree( const sourceClass = findClassWalk(source, foundClassDeclaration.name?.getText()); if (sourceClass) { - dependentClasses.push({ classNode: sourceClass, fileName: source.fileName }); + dependentClasses.push({ classNode: sourceClass, sourceFile: source, fileName: source.fileName }); if (keepLooking) { buildExtendsTree(compilerCtx, foundClassDeclaration, dependentClasses, typeChecker, buildCtx, ogModule); } @@ -199,7 +199,11 @@ function buildExtendsTree( if (foundClassDeclaration && !dependentClasses.some((dc) => dc.classNode === foundClassDeclaration)) { // we found the class declaration in the current module - dependentClasses.push({ classNode: foundClassDeclaration, fileName: currentSource.fileName }); + dependentClasses.push({ + classNode: foundClassDeclaration, + sourceFile: currentSource, + fileName: currentSource.fileName, + }); if (keepLooking) { buildExtendsTree(compilerCtx, foundClassDeclaration, dependentClasses, typeChecker, buildCtx, ogModule); } @@ -226,12 +230,17 @@ function buildExtendsTree( if (foundFile?.resolvedModule && className) { // 4) resolve the module name to a file - const foundModule = compilerCtx.moduleMap.get(foundFile.resolvedModule.resolvedFileName); + let foundSource: ts.SourceFile = compilerCtx.moduleMap.get( + foundFile.resolvedModule.resolvedFileName, + )?.staticSourceFile; + if (!foundSource) { + // Stencil only loads module entries from node_modules collections, + // so if we didn't find the source file in the module map, + // let's create a temporary program and get the source file from there + foundSource = tsGetSourceFile(buildCtx.config, foundFile); + } - // 5) look for the corresponding resolved statement - const matchedStatement = (foundModule?.staticSourceFile as ts.SourceFile).statements.find( - matchesNamedDeclaration(className), - ); + const matchedStatement = foundSource.statements.find(matchesNamedDeclaration(className)); foundClassDeclaration = matchedStatement ? ts.isClassDeclaration(matchedStatement) ? matchedStatement @@ -247,7 +256,12 @@ function buildExtendsTree( if (foundClassDeclaration && !dependentClasses.some((dc) => dc.classNode === foundClassDeclaration)) { // 6) if we found the class declaration, push it and check if it itself extends from another class - dependentClasses.push({ classNode: foundClassDeclaration, fileName: currentSource.fileName }); + dependentClasses.push({ + classNode: foundClassDeclaration, + sourceFile: foundSource, + fileName: currentSource.fileName, + }); + if (keepLooking) { buildExtendsTree( compilerCtx, @@ -320,7 +334,10 @@ export function mergeExtendedClassMeta( module.isExtended = true; doesExtend = true; - if ((mixinProps.length > 0 || mixinStates.length > 0) && !detectModernPropDeclarations(extendedClass.classNode)) { + if ( + (mixinProps.length > 0 || mixinStates.length > 0) && + !detectModernPropDeclarations(extendedClass.classNode, extendedClass.sourceFile) + ) { const err = buildWarn(buildCtx.diagnostics); const target = buildCtx.config.tsCompilerOptions?.target; err.messageText = `Component classes can only extend from other Stencil decorated base classes when targetting more modern JavaScript (ES2022 and above).