From 807f161adbb5ccbfbad47ea7fa357f0b2b215cb4 Mon Sep 17 00:00:00 2001 From: Matthew Spero Date: Sun, 20 Jul 2025 15:17:59 -0500 Subject: [PATCH 1/6] feat: partial(?) signal support via parseReflectComponentType --- .../src/lib/resolve/collect-declarations.ts | 84 ++++++++++++++----- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/libs/ng-mocks/src/lib/resolve/collect-declarations.ts b/libs/ng-mocks/src/lib/resolve/collect-declarations.ts index 9cc4362661..53791ebf27 100644 --- a/libs/ng-mocks/src/lib/resolve/collect-declarations.ts +++ b/libs/ng-mocks/src/lib/resolve/collect-declarations.ts @@ -1,4 +1,6 @@ -import { ɵReflectionCapabilities as ReflectionCapabilities } from '@angular/core'; +/* eslint-disable max-lines */ + +import { ɵReflectionCapabilities as ReflectionCapabilities, reflectComponentType } from '@angular/core'; import coreDefineProperty from '../common/core.define-property'; import { AnyDeclaration, DirectiveIo } from '../common/core.types'; @@ -133,6 +135,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 +176,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 +323,42 @@ 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 => { + if (typeof def === 'function') { + try { + const mirror = reflectComponentType(def); + if (mirror?.inputs) { + 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); + } + } + + if (mirror?.outputs) { + 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); + } + } + } catch { + // reflectComponentType may fail for non-components or incompatible types + } + } +}; + const parsePropDecoratorsParserFactoryProp = (key: 'inputs' | 'outputs') => { const callback = parsePropMetadataParserFactoryProp(key); return ( @@ -481,6 +524,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); From cc586bcacb1908405616f8018a3d668cf5991e15 Mon Sep 17 00:00:00 2001 From: Matthew Spero Date: Mon, 4 Aug 2025 00:31:40 -0500 Subject: [PATCH 2/6] feat: make sure it's compatible with older compiler versions --- .../src/lib/resolve/collect-declarations.ts | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/libs/ng-mocks/src/lib/resolve/collect-declarations.ts b/libs/ng-mocks/src/lib/resolve/collect-declarations.ts index 53791ebf27..0547862196 100644 --- a/libs/ng-mocks/src/lib/resolve/collect-declarations.ts +++ b/libs/ng-mocks/src/lib/resolve/collect-declarations.ts @@ -1,6 +1,7 @@ /* eslint-disable max-lines */ -import { ɵReflectionCapabilities as ReflectionCapabilities, reflectComponentType } from '@angular/core'; +import { ɵReflectionCapabilities as ReflectionCapabilities } from '@angular/core'; +import * as angularCore from '@angular/core'; import coreDefineProperty from '../common/core.define-property'; import { AnyDeclaration, DirectiveIo } from '../common/core.types'; @@ -328,33 +329,33 @@ const parseNgDef = ( * and is therefore a supplementary support for signals. */ const parseReflectComponentType = (def: any, declaration: Declaration): void => { - if (typeof def === 'function') { - try { - const mirror = reflectComponentType(def); - if (mirror?.inputs) { - 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); - } - } + // Only available in NG 14+ + if (!(angularCore as any).reflectComponentType) { + return; + } - if (mirror?.outputs) { - for (const output of mirror.outputs) { - const { name, alias, required } = funcDirectiveIoParse({ - name: output.propName, - alias: output.templateName === output.propName ? undefined : output.templateName, - }); + const mirror = (angularCore as any).reflectComponentType(def); - addUniqueDirectiveIo(declaration, 'outputs', name, alias, required); - } - } - } catch { - // reflectComponentType may fail for non-components or incompatible types + if (mirror?.inputs) { + 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); + } + } + + if (mirror?.outputs) { + 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); } } }; From fe60aaf03dd6d96ffd294c4d2ee366cbcf1a515d Mon Sep 17 00:00:00 2001 From: Matthew Spero Date: Mon, 4 Aug 2025 01:58:03 -0500 Subject: [PATCH 3/6] feat: Make Webpack not dig into reflectComponentType (at the cost of some tree-shaking, most likely) --- libs/ng-mocks/src/lib/resolve/collect-declarations.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/ng-mocks/src/lib/resolve/collect-declarations.ts b/libs/ng-mocks/src/lib/resolve/collect-declarations.ts index 0547862196..4b0af82f85 100644 --- a/libs/ng-mocks/src/lib/resolve/collect-declarations.ts +++ b/libs/ng-mocks/src/lib/resolve/collect-declarations.ts @@ -330,12 +330,11 @@ const parseNgDef = ( */ const parseReflectComponentType = (def: any, declaration: Declaration): void => { // Only available in NG 14+ - if (!(angularCore as any).reflectComponentType) { + const mirror = (angularCore as any)['reflectComponentType']?.(def); + if (!mirror) { return; } - const mirror = (angularCore as any).reflectComponentType(def); - if (mirror?.inputs) { for (const input of mirror.inputs) { const { name, alias, required } = funcDirectiveIoParse({ From 3e6ae389e1f2d51713e46f7c3c8647164a1c347c Mon Sep 17 00:00:00 2001 From: Matthew Spero Date: Thu, 6 Nov 2025 10:30:15 -0600 Subject: [PATCH 4/6] feat: Remove unnecessary conditional branches --- .../src/lib/resolve/collect-declarations.ts | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/libs/ng-mocks/src/lib/resolve/collect-declarations.ts b/libs/ng-mocks/src/lib/resolve/collect-declarations.ts index 4b0af82f85..b20086ffc7 100644 --- a/libs/ng-mocks/src/lib/resolve/collect-declarations.ts +++ b/libs/ng-mocks/src/lib/resolve/collect-declarations.ts @@ -335,27 +335,23 @@ const parseReflectComponentType = (def: any, declaration: Declaration): void => return; } - if (mirror?.inputs) { - 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 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); } - if (mirror?.outputs) { - for (const output of mirror.outputs) { - const { name, alias, required } = funcDirectiveIoParse({ - name: output.propName, - alias: output.templateName === output.propName ? undefined : output.templateName, - }); + 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); - } + addUniqueDirectiveIo(declaration, 'outputs', name, alias, required); } }; From b871a858feb53cb5d732f26816ca67c19f827630 Mon Sep 17 00:00:00 2001 From: Matthew Spero Date: Thu, 6 Nov 2025 12:28:49 -0600 Subject: [PATCH 5/6] feat: Add reflectComponentType testing shim --- .../func.reflect-component-type.spec.ts | 32 +++++++++++++++++++ .../lib/common/func.reflect-component-type.ts | 21 ++++++++++++ .../src/lib/resolve/collect-declarations.ts | 4 +-- 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 libs/ng-mocks/src/lib/common/func.reflect-component-type.spec.ts create mode 100644 libs/ng-mocks/src/lib/common/func.reflect-component-type.ts 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..ea2ea8507a --- /dev/null +++ b/libs/ng-mocks/src/lib/common/func.reflect-component-type.ts @@ -0,0 +1,21 @@ +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 reflectComponentType = global.__ngMocksReflectComponentType ?? (angularCore as any)['reflectComponentType']; + + 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 b20086ffc7..20993b4577 100644 --- a/libs/ng-mocks/src/lib/resolve/collect-declarations.ts +++ b/libs/ng-mocks/src/lib/resolve/collect-declarations.ts @@ -1,12 +1,12 @@ /* eslint-disable max-lines */ import { ɵReflectionCapabilities as ReflectionCapabilities } from '@angular/core'; -import * as angularCore 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; @@ -330,7 +330,7 @@ const parseNgDef = ( */ const parseReflectComponentType = (def: any, declaration: Declaration): void => { // Only available in NG 14+ - const mirror = (angularCore as any)['reflectComponentType']?.(def); + const mirror = funcReflectComponentType(def); if (!mirror) { return; } From 754e4ff209244d3ec5a6025f02f2f1bc024f6801 Mon Sep 17 00:00:00 2001 From: Matthew Spero Date: Mon, 10 Nov 2025 11:26:11 -0600 Subject: [PATCH 6/6] feat: Skip `reflectComponentType` from build tool processing --- libs/ng-mocks/src/lib/common/func.reflect-component-type.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index ea2ea8507a..8348d92e21 100644 --- a/libs/ng-mocks/src/lib/common/func.reflect-component-type.ts +++ b/libs/ng-mocks/src/lib/common/func.reflect-component-type.ts @@ -11,7 +11,8 @@ export default (def: any): any => { const global = funcGetGlobal(); // Use test override if present, otherwise use real API - const reflectComponentType = global.__ngMocksReflectComponentType ?? (angularCore as any)['reflectComponentType']; + const reflectApi = 'reflect' + 'ComponentType'; // Avoid build tool processing + const reflectComponentType = global.__ngMocksReflectComponentType ?? (angularCore as any)[reflectApi]; if (!reflectComponentType) { return undefined;