Skip to content

Commit

Permalink
Angular: Provide alternative implementation of checking the instance …
Browse files Browse the repository at this point in the history
…of a decorator
  • Loading branch information
valentinpalkovic committed Oct 16, 2023
1 parent 202d918 commit 418ff82
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -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();

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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<unknown> => {
Expand All @@ -107,7 +106,7 @@ export const isComponent = (component: any): component is Type<unknown> => {

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<unknown> => {
Expand All @@ -117,10 +116,12 @@ export const isStandaloneComponent = (component: any): component is Type<unknown

const decorators = reflectionCapabilities.annotations(component);

// TODO: `standalone` is only available in Angular v14. Remove cast to `any` once
// Angular deps are updated to v14.x.x.
return (decorators || []).some(
(d) => (d instanceof Component || d instanceof Directive || d instanceof Pipe) && d.standalone
(d) =>
(isDecoratorInstanceOf(d, 'Component') ||
isDecoratorInstanceOf(d, 'Directive') ||
isDecoratorInstanceOf(d, 'Pipe')) &&
d.standalone
);
};

Expand All @@ -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'));
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NgModule, ɵReflectionCapabilities as ReflectionCapabilities } from '@angular/core';
import { isDecoratorInstanceOf } from './isDecoratorInstanceOf';

const reflectionCapabilities = new ReflectionCapabilities();

Expand Down Expand Up @@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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');
Expand Down Expand Up @@ -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;
};
}
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function isDecoratorInstanceOf(
decorator: any,
name: 'Component' | 'Directive' | 'Pipe' | 'Input' | 'Output' | 'NgModule'
) {
return decorator?.ngMetadataName === name;
}

0 comments on commit 418ff82

Please sign in to comment.