From 418ff821e4b6d045dd5622638af400b19852a9f2 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Mon, 16 Oct 2023 11:36:55 +0200 Subject: [PATCH] Angular: Provide alternative implementation of checking the instance of a decorator --- .../angular-beta/utils/NgComponentAnalyzer.ts | 41 +++++++-------- .../angular-beta/utils/NgModulesAnalyzer.ts | 9 ++-- .../utils/PropertyExtractor.test.ts | 3 +- .../angular-beta/utils/PropertyExtractor.ts | 40 ++------------- .../utils/isDecoratorInstanceOf.test.ts | 50 +++++++++++++++++++ .../utils/isDecoratorInstanceOf.ts | 6 +++ 6 files changed, 88 insertions(+), 61 deletions(-) create mode 100644 code/frameworks/angular/src/client/angular-beta/utils/isDecoratorInstanceOf.test.ts create mode 100644 code/frameworks/angular/src/client/angular-beta/utils/isDecoratorInstanceOf.ts diff --git a/code/frameworks/angular/src/client/angular-beta/utils/NgComponentAnalyzer.ts b/code/frameworks/angular/src/client/angular-beta/utils/NgComponentAnalyzer.ts index 09b1f63211ce..d9782a712547 100644 --- a/code/frameworks/angular/src/client/angular-beta/utils/NgComponentAnalyzer.ts +++ b/code/frameworks/angular/src/client/angular-beta/utils/NgComponentAnalyzer.ts @@ -1,12 +1,5 @@ -import { - Type, - Component, - Directive, - Input, - Output, - Pipe, - ɵReflectionCapabilities as ReflectionCapabilities, -} from '@angular/core'; +import { Type, Component, ɵReflectionCapabilities as ReflectionCapabilities } from '@angular/core'; +import { isDecoratorInstanceOf } from './isDecoratorInstanceOf'; const reflectionCapabilities = new ReflectionCapabilities(); @@ -55,8 +48,10 @@ export const getComponentInputsOutputs = (component: any): ComponentInputsOutput // Browses component properties to extract I/O // Filters properties that have the same name as the one present in the @Component property return Object.entries(componentPropsMetadata).reduce((previousValue, [propertyName, values]) => { - const value = values.find((v) => v instanceof Input || v instanceof Output); - if (value instanceof Input) { + const value = values.find( + (v) => isDecoratorInstanceOf(v, 'Input') || isDecoratorInstanceOf(v, 'Output') + ); + if (isDecoratorInstanceOf(value, 'Input')) { const inputToAdd = { propName: propertyName, templateName: value.bindingPropertyName ?? value.alias ?? propertyName, @@ -70,7 +65,7 @@ export const getComponentInputsOutputs = (component: any): ComponentInputsOutput inputs: [...previousInputsFiltered, inputToAdd], }; } - if (value instanceof Output) { + if (isDecoratorInstanceOf(value, 'Output')) { const outputToAdd = { propName: propertyName, templateName: value.bindingPropertyName ?? value.alias ?? propertyName, @@ -95,9 +90,13 @@ export const isDeclarable = (component: any): boolean => { const decorators = reflectionCapabilities.annotations(component); - return !!(decorators || []).find( - (d) => d instanceof Directive || d instanceof Pipe || d instanceof Component - ); + return !!(decorators || []).find((d) => { + return ( + isDecoratorInstanceOf(d, 'Directive') || + isDecoratorInstanceOf(d, 'Pipe') || + isDecoratorInstanceOf(d, 'Component') + ); + }); }; export const isComponent = (component: any): component is Type => { @@ -107,7 +106,7 @@ export const isComponent = (component: any): component is Type => { const decorators = reflectionCapabilities.annotations(component); - return (decorators || []).some((d) => d instanceof Component); + return (decorators || []).some((d) => isDecoratorInstanceOf(d, 'Component')); }; export const isStandaloneComponent = (component: any): component is Type => { @@ -117,10 +116,12 @@ export const isStandaloneComponent = (component: any): component is Type (d instanceof Component || d instanceof Directive || d instanceof Pipe) && d.standalone + (d) => + (isDecoratorInstanceOf(d, 'Component') || + isDecoratorInstanceOf(d, 'Directive') || + isDecoratorInstanceOf(d, 'Pipe')) && + d.standalone ); }; @@ -138,5 +139,5 @@ export const getComponentPropsDecoratorMetadata = (component: any) => { export const getComponentDecoratorMetadata = (component: any): Component | undefined => { const decorators = reflectionCapabilities.annotations(component); - return decorators.reverse().find((d) => d instanceof Component); + return decorators.reverse().find((d) => isDecoratorInstanceOf(d, 'Component')); }; diff --git a/code/frameworks/angular/src/client/angular-beta/utils/NgModulesAnalyzer.ts b/code/frameworks/angular/src/client/angular-beta/utils/NgModulesAnalyzer.ts index a63fdee1822b..d0a80e2bcc74 100644 --- a/code/frameworks/angular/src/client/angular-beta/utils/NgModulesAnalyzer.ts +++ b/code/frameworks/angular/src/client/angular-beta/utils/NgModulesAnalyzer.ts @@ -1,4 +1,5 @@ import { NgModule, ɵReflectionCapabilities as ReflectionCapabilities } from '@angular/core'; +import { isDecoratorInstanceOf } from './isDecoratorInstanceOf'; const reflectionCapabilities = new ReflectionCapabilities(); @@ -45,11 +46,13 @@ const extractNgModuleMetadata = (importItem: any): NgModule => { return null; } - const ngModuleDecorator: NgModule | undefined = decorators.find( - (decorator) => decorator instanceof NgModule - ); + const ngModuleDecorator: NgModule | undefined = decorators.find((decorator) => { + return isDecoratorInstanceOf(decorator, 'NgModule'); + }); + if (!ngModuleDecorator) { return null; } + return ngModuleDecorator; }; diff --git a/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.test.ts b/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.test.ts index 4c8778cdc31a..c373cc1cd6b4 100644 --- a/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.test.ts +++ b/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.test.ts @@ -8,7 +8,7 @@ import { provideNoopAnimations, } from '@angular/platform-browser/animations'; import { NgModuleMetadata } from '../../types'; -import { PropertyExtractor, REMOVED_MODULES } from './PropertyExtractor'; +import { PropertyExtractor } from './PropertyExtractor'; import { WithOfficialModule } from '../__testfixtures__/test.module'; const TEST_TOKEN = new InjectionToken('testToken'); @@ -17,7 +17,6 @@ const TestService = Injectable()(class {}); const TestComponent1 = Component({})(class {}); const TestComponent2 = Component({})(class {}); const StandaloneTestComponent = Component({ standalone: true })(class {}); -const TestDirective = Directive({})(class {}); const StandaloneTestDirective = Directive({ standalone: true })(class {}); const TestModuleWithDeclarations = NgModule({ declarations: [TestComponent1] })(class {}); const TestModuleWithImportsAndProviders = NgModule({ diff --git a/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.ts b/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.ts index e6db7384488f..757d9ea06483 100644 --- a/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.ts +++ b/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.ts @@ -1,15 +1,9 @@ /* eslint-disable no-console */ import { CommonModule } from '@angular/common'; import { - Component, - Directive, importProvidersFrom, - Injectable, InjectionToken, - Input, NgModule, - Output, - Pipe, Provider, ɵReflectionCapabilities as ReflectionCapabilities, } from '@angular/core'; @@ -23,6 +17,7 @@ import { import dedent from 'ts-dedent'; import { NgModuleMetadata } from '../../types'; import { isComponentAlreadyDeclared } from './NgModulesAnalyzer'; +import { isDecoratorInstanceOf } from './isDecoratorInstanceOf'; export const reflectionCapabilities = new ReflectionCapabilities(); export const REMOVED_MODULES = new InjectionToken('REMOVED_MODULES'); @@ -168,40 +163,13 @@ export class PropertyExtractor implements NgModuleMetadata { static analyzeDecorators = (component: any) => { const decorators = reflectionCapabilities.annotations(component); - const isComponent = decorators.some((d) => this.isDecoratorInstanceOf(d, 'Component')); - const isDirective = decorators.some((d) => this.isDecoratorInstanceOf(d, 'Directive')); - const isPipe = decorators.some((d) => this.isDecoratorInstanceOf(d, 'Pipe')); + const isComponent = decorators.some((d) => isDecoratorInstanceOf(d, 'Component')); + const isDirective = decorators.some((d) => isDecoratorInstanceOf(d, 'Directive')); + const isPipe = decorators.some((d) => isDecoratorInstanceOf(d, 'Pipe')); const isDeclarable = isComponent || isDirective || isPipe; const isStandalone = (isComponent || isDirective) && decorators.some((d) => d.standalone); return { isDeclarable, isStandalone }; }; - - static isDecoratorInstanceOf = (decorator: any, name: string) => { - let factory; - switch (name) { - case 'Component': - factory = Component; - break; - case 'Directive': - factory = Directive; - break; - case 'Pipe': - factory = Pipe; - break; - case 'Injectable': - factory = Injectable; - break; - case 'Input': - factory = Input; - break; - case 'Output': - factory = Output; - break; - default: - throw new Error(`Unknown decorator type: ${name}`); - } - return decorator instanceof factory || decorator.ngMetadataName === name; - }; } diff --git a/code/frameworks/angular/src/client/angular-beta/utils/isDecoratorInstanceOf.test.ts b/code/frameworks/angular/src/client/angular-beta/utils/isDecoratorInstanceOf.test.ts new file mode 100644 index 000000000000..01a7709e8009 --- /dev/null +++ b/code/frameworks/angular/src/client/angular-beta/utils/isDecoratorInstanceOf.test.ts @@ -0,0 +1,50 @@ +import { Component, Directive, Pipe, Input, Output, NgModule } from '@angular/core'; +import { isDecoratorInstanceOf } from './isDecoratorInstanceOf'; + +// Simulate Angular's behavior by manually adding the ngMetadataName property, since this information is added during compile time. +const MockComponentDecorator = { ...Component, ngMetadataName: 'Component' }; +const MockDirectiveDecorator = { ...Directive, ngMetadataName: 'Directive' }; +const MockPipeDecorator = { ...Pipe, ngMetadataName: 'Pipe' }; +const MockInputDecorator = { ...Input, ngMetadataName: 'Input' }; +const MockOutputDecorator = { ...Output, ngMetadataName: 'Output' }; +const MockNgModuleDecorator = { ...NgModule, ngMetadataName: 'NgModule' }; + +describe('isDecoratorInstanceOf', () => { + it('should correctly identify a Component', () => { + expect(isDecoratorInstanceOf(MockComponentDecorator, 'Component')).toBe(true); + }); + + it('should correctly identify a Directive', () => { + expect(isDecoratorInstanceOf(MockDirectiveDecorator, 'Directive')).toBe(true); + }); + + it('should correctly identify a Pipe', () => { + expect(isDecoratorInstanceOf(MockPipeDecorator, 'Pipe')).toBe(true); + }); + + it('should correctly identify an Input', () => { + expect(isDecoratorInstanceOf(MockInputDecorator, 'Input')).toBe(true); + }); + + it('should correctly identify an Output', () => { + expect(isDecoratorInstanceOf(MockOutputDecorator, 'Output')).toBe(true); + }); + + it('should correctly identify an NgModule', () => { + expect(isDecoratorInstanceOf(MockNgModuleDecorator, 'NgModule')).toBe(true); + }); + + it('should return false for mismatched metadata names', () => { + expect(isDecoratorInstanceOf(MockComponentDecorator, 'Directive')).toBe(false); + }); + + it('should handle null or undefined decorators gracefully', () => { + expect(isDecoratorInstanceOf(null, 'Component')).toBe(false); + expect(isDecoratorInstanceOf(undefined, 'Component')).toBe(false); + }); + + it('should handle decorators without ngMetadataName property', () => { + const mockDecoratorWithoutMetadata = {}; + expect(isDecoratorInstanceOf(mockDecoratorWithoutMetadata, 'Component')).toBe(false); + }); +}); diff --git a/code/frameworks/angular/src/client/angular-beta/utils/isDecoratorInstanceOf.ts b/code/frameworks/angular/src/client/angular-beta/utils/isDecoratorInstanceOf.ts new file mode 100644 index 000000000000..4ff27c48bd2d --- /dev/null +++ b/code/frameworks/angular/src/client/angular-beta/utils/isDecoratorInstanceOf.ts @@ -0,0 +1,6 @@ +export function isDecoratorInstanceOf( + decorator: any, + name: 'Component' | 'Directive' | 'Pipe' | 'Input' | 'Output' | 'NgModule' +) { + return decorator?.ngMetadataName === name; +}