diff --git a/libs/ng-mocks/src/lib/common/func.reflect-component-type.spec.ts b/libs/ng-mocks/src/lib/common/func.reflect-component-type.spec.ts new file mode 100644 index 0000000000..b5eb6f6aa9 --- /dev/null +++ b/libs/ng-mocks/src/lib/common/func.reflect-component-type.spec.ts @@ -0,0 +1,32 @@ +import funcGetGlobal from './func.get-global'; +import funcReflectComponentType from './func.reflect-component-type'; + +describe('funcReflectComponentType', () => { + let global: any; + + beforeEach(() => { + global = funcGetGlobal(); + }); + + afterEach(() => { + delete global.__ngMocksReflectComponentType; + }); + + it('returns undefined when API does not exist', () => { + // Simulate Angular 13 - where reflectComponentType doesn't exist + global.__ngMocksReflectComponentType = false; + + const result = funcReflectComponentType({ test: 'component' }); + expect(result).toBeUndefined(); + }); + + it('returns result when API exists and works', () => { + const mockMirror = { inputs: [], outputs: [] }; + global.__ngMocksReflectComponentType = jasmine + .createSpy('mockReflectComponentType') + .and.returnValue(mockMirror); + + const result = funcReflectComponentType({ test: 'component' }); + expect(result).toBe(mockMirror); + }); +}); diff --git a/libs/ng-mocks/src/lib/common/func.reflect-component-type.ts b/libs/ng-mocks/src/lib/common/func.reflect-component-type.ts new file mode 100644 index 0000000000..8348d92e21 --- /dev/null +++ b/libs/ng-mocks/src/lib/common/func.reflect-component-type.ts @@ -0,0 +1,22 @@ +import * as angularCore from '@angular/core'; + +import funcGetGlobal from './func.get-global'; + +/** + * Helper function to safely access Angular's reflectComponentType API. + * This API is available in Angular 14+ and is used for runtime reflection + * of component metadata, particularly for signal inputs/outputs. + */ +export default (def: any): any => { + const global = funcGetGlobal(); + + // Use test override if present, otherwise use real API + const reflectApi = 'reflect' + 'ComponentType'; // Avoid build tool processing + const reflectComponentType = global.__ngMocksReflectComponentType ?? (angularCore as any)[reflectApi]; + + if (!reflectComponentType) { + return undefined; + } + + return reflectComponentType(def); +}; diff --git a/libs/ng-mocks/src/lib/resolve/collect-declarations.ts b/libs/ng-mocks/src/lib/resolve/collect-declarations.ts index 9cc4362661..20993b4577 100644 --- a/libs/ng-mocks/src/lib/resolve/collect-declarations.ts +++ b/libs/ng-mocks/src/lib/resolve/collect-declarations.ts @@ -1,9 +1,12 @@ +/* eslint-disable max-lines */ + import { ɵReflectionCapabilities as ReflectionCapabilities } from '@angular/core'; import coreDefineProperty from '../common/core.define-property'; import { AnyDeclaration, DirectiveIo } from '../common/core.types'; import funcDirectiveIoBuild from '../common/func.directive-io-build'; import funcDirectiveIoParse from '../common/func.directive-io-parse'; +import funcReflectComponentType from '../common/func.reflect-component-type'; interface Declaration { host: Record; @@ -133,6 +136,29 @@ const parseDecorators = ( } }; +const addUniqueDirectiveIo = ( + declaration: Declaration, + key: 'inputs' | 'outputs', + name: string, + alias: string | undefined, + required: boolean | undefined, +): void => { + const normalizedDef = funcDirectiveIoBuild({ name, alias, required }); + + for (const def of declaration[key]) { + if (def === normalizedDef) { + return; + } + + const { name: defName, alias: defAlias } = funcDirectiveIoParse(def); + if (defName === name && defAlias === alias) { + return; + } + } + + declaration[key].unshift(normalizedDef); +}; + const parsePropMetadataParserFactoryProp = (key: 'inputs' | 'outputs') => ( @@ -151,25 +177,7 @@ const parsePropMetadataParserFactoryProp = required: decorator.required, }); - const normalizedDef = funcDirectiveIoBuild({ name, alias, required }); - - let add = true; - for (const def of declaration[key]) { - if (def === normalizedDef) { - add = false; - break; - } - - const { name: defName, alias: defAlias, required: defRequired } = funcDirectiveIoParse(def); - if (defName === name && defAlias === alias && defRequired === required) { - add = false; - break; - } - } - - if (add) { - declaration[key].unshift(normalizedDef); - } + addUniqueDirectiveIo(declaration, key, name, alias, required); }; const parsePropMetadataParserInput = parsePropMetadataParserFactoryProp('inputs'); const parsePropMetadataParserOutput = parsePropMetadataParserFactoryProp('outputs'); @@ -316,6 +324,37 @@ const parseNgDef = ( } }; +/** + * Note: This does not seem to work in every environment (e.g. the tests) + * and is therefore a supplementary support for signals. + */ +const parseReflectComponentType = (def: any, declaration: Declaration): void => { + // Only available in NG 14+ + const mirror = funcReflectComponentType(def); + if (!mirror) { + return; + } + + for (const input of mirror.inputs) { + const { name, alias, required } = funcDirectiveIoParse({ + name: input.propName, + alias: input.templateName === input.propName ? undefined : input.templateName, + required: undefined, // reflectComponentType doesn't provide required info for signal inputs + }); + + addUniqueDirectiveIo(declaration, 'inputs', name, alias, required); + } + + for (const output of mirror.outputs) { + const { name, alias, required } = funcDirectiveIoParse({ + name: output.propName, + alias: output.templateName === output.propName ? undefined : output.templateName, + }); + + addUniqueDirectiveIo(declaration, 'outputs', name, alias, required); + } +}; + const parsePropDecoratorsParserFactoryProp = (key: 'inputs' | 'outputs') => { const callback = parsePropMetadataParserFactoryProp(key); return ( @@ -481,6 +520,7 @@ const parse = (def: any): any => { parsePropDecorators(def, declaration); parsePropMetadata(def, declaration); parseNgDef(def, declaration); + parseReflectComponentType(def, declaration); buildDeclaration(declaration.Directive, declaration); buildDeclaration(declaration.Component, declaration); buildDeclaration(declaration.Pipe, declaration);