npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@angular-modernizer/plugin-angular

v0.1.3

Published

Angular-specific analysis and performance rules for the Angular Modernization Platform

Readme

@angular-modernizer/plugin-angular

Angular-specific analysis rules for detecting anti-patterns, performance issues, and best practice violations in Angular applications.

Overview

This plugin provides 30 analysis rules and multiple transformation types for analyzing and modernizing Angular codebases, with a focus on:

  • Change Detection Strategy optimization
  • RxJS operator efficiency and memory leak prevention
  • Angular decorator validation
  • Component/Service architecture best practices
  • Automated code transformations (constructor injection, interface extraction, DI migration)

Installation

pnpm add @angular-modernizer/plugin-angular

Analysis Rules

RxJSOptimizationRule

Rule ID: plugin-angular:rxjs-optimization Severity: Warning

Detects inefficient RxJS operator usage across multiple anti-patterns with impact analysis and refactoring complexity estimation.

Detected Patterns

Multiple Chained Pipe Calls

Consolidate multiple .pipe().pipe() chains into a single pipe operator.

Before:

this.data$ = this.http
  .get('/api/data')
  .pipe(map((data) => data.items))
  .pipe(filter((items) => items.length > 0))
  .pipe(tap((items) => console.log(items)));

After:

this.data$ = this.http.get('/api/data').pipe(
  map((data) => data.items),
  filter((items) => items.length > 0),
  tap((items) => console.log(items)),
);

Impact: Low | Refactoring Complexity: Low


Missing shareReplay for Expensive Operations

HTTP calls or computed observables subscribed multiple times without caching.

Before:

// In component
this.userData$ = this.http.get<User>('/api/user');

// In template - triggers 3 HTTP requests
<div>{{ (userData$ | async)?.name }}</div>
<div>{{ (userData$ | async)?.email }}</div>
<div>{{ (userData$ | async)?.role }}</div>

After:

this.userData$ = this.http
  .get<User>('/api/user')
  .pipe(shareReplay({ bufferSize: 1, refCount: true }));

// Now only 1 HTTP request for all subscriptions

Impact: High | Refactoring Complexity: Low


Redundant Sequential Operators

Consecutive identical operators that should be merged.

Before:

this.filtered$ = this.items$.pipe(
  map((items) => items.filter((i) => i.active)),
  map((items) => items.sort((a, b) => a.name.localeCompare(b.name))),
);

After:

this.filtered$ = this.items$.pipe(
  map((items) =>
    items.filter((i) => i.active).sort((a, b) => a.name.localeCompare(b.name)),
  ),
);

Impact: Low | Refactoring Complexity: Low


Missing Debounce on User Input

FormControl valueChanges without debounceTime() or throttleTime().

Before:

this.searchControl.valueChanges
  .pipe(switchMap((term) => this.searchService.search(term)))
  .subscribe((results) => (this.results = results));

After:

this.searchControl.valueChanges
  .pipe(
    debounceTime(300),
    switchMap((term) => this.searchService.search(term)),
  )
  .subscribe((results) => (this.results = results));

Impact: High | Refactoring Complexity: Low


Logic in Subscribe Blocks

Business logic inside .subscribe() that should use pipe operators.

Before:

this.users$.subscribe((users) => {
  const activeUsers = users.filter((u) => u.active);
  const sortedUsers = activeUsers.sort((a, b) => a.name.localeCompare(b.name));
  this.displayUsers = sortedUsers.slice(0, 10);
});

After:

this.displayUsers$ = this.users$.pipe(
  map(users => users.filter(u => u.active)),
  map(users => users.sort((a, b) => a.name.localeCompare(b.name))),
  map(users => users.slice(0, 10))
);

// In template
<div *ngFor="let user of displayUsers$ | async">{{ user.name }}</div>

Impact: Medium | Refactoring Complexity: Medium


Subscribe in Subscribe (Nested Subscriptions)

Nested observable subscriptions without flattening operators.

Before:

this.route.params.subscribe((params) => {
  this.userService.getUser(params['id']).subscribe((user) => {
    this.user = user;
  });
});

After:

this.user$ = this.route.params.pipe(
  switchMap(params => this.userService.getUser(params['id']))
);

// In template
<div>{{ (user$ | async)?.name }}</div>

Impact: High | Refactoring Complexity: Medium


Silent HTTP catchError

HTTP observables using catchError that returns of(null), of([]), or EMPTY without re-throwing, preventing callers from distinguishing success from failure.

Impact: High | Refactoring Complexity: Medium


Incorrect Subject Usage for State

Using Subject instead of BehaviorSubject for state management.

Before:

export class StateService {
  private state$ = new Subject<AppState>();

  setState(state: AppState) {
    this.state$.next(state);
  }

  getState() {
    return this.state$.asObservable();
  }
}

After:

export class StateService {
  private state$ = new BehaviorSubject<AppState>(initialState);

  setState(state: AppState) {
    this.state$.next(state);
  }

  getState() {
    return this.state$.asObservable();
  }
}

Impact: High | Refactoring Complexity: Low


Configuration

{
  "rxjs-optimization": {
    "maxPipeChains": 1,              // Max allowed chained .pipe() calls
    "maxSubscribeComplexity": 2,     // Max statements in subscribe blocks
    "requireDebounceOnUserInput": true // Enforce debounce on FormControl valueChanges
  }
}

PerformanceViolationRule

Rule ID: angular:performance-pattern Severity: Warning/Error

Detects missing subscription cleanup patterns in component subscriptions and complex template getters.

Missing takeUntil Before:

export class UserComponent implements OnInit {
  ngOnInit() {
    this.userService.currentUser$.subscribe((user) => {
      this.user = user;
    }); // Memory leak - survives component destruction
  }
}

Missing takeUntil After:

export class UserComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    this.userService.currentUser$
      .pipe(takeUntil(this.destroy$))
      .subscribe((user) => {
        this.user = user;
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

MissingOnPushRule

Rule ID: angular:missing-onpush Severity: High

Detects components with @Input() properties using default change detection.

Before:

@Component({
  selector: 'app-user-card',
  // Missing: changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
  @Input() user!: User;
}

After:

@Component({
  selector: 'app-user-card',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  @Input() user!: User;
}

ServiceWithoutInjectableRule

Rule ID: angular:service-without-injectable Severity: High

Detects services missing @Injectable() decorator.


ComponentWithoutSelectorRule

Rule ID: angular:component-without-selector Severity: Medium

Detects components missing selector property.


DirectiveWithoutSelectorRule

Rule ID: angular:directive-without-selector Severity: Medium

Detects directives missing selector property.


PipeWithoutNameRule

Rule ID: angular:pipe-without-name Severity: Medium

Detects pipes missing name property.


AsyncPipeMisuseRule

Rule ID: angular:async-pipe-misuse Severity: Warning

Detects incorrect usage of the async pipe in templates. Two violation types:

  • double-async-subscription: observable used with | async in template AND manually subscribed in a lifecycle hook
  • multiple-async-subscriptions: same observable used with | async two or more times in the same template

UnnecessaryChangeDetectionRule

Rule ID: angular:unnecessary-change-detection Severity: Low

Detects components using Default change detection when OnPush would suffice, and components calling detectChanges() without using OnPush strategy.


MissingReturnTypeRule

Rule ID: angular:missing-return-type Severity: Low

Detects methods in Angular classes that are missing explicit return type annotations.

Before:

@Component({...})
export class UserComponent {
  // Missing return type
  getUserName() {
    return this.user?.name || 'Unknown';
  }
}

After:

@Component({...})
export class UserComponent {
  // Explicit return type
  getUserName(): string {
    return this.user?.name || 'Unknown';
  }
}

TooManyInputsRule

Rule ID: angular:too-many-inputs Severity: Low

Detects components with excessive @Input() properties (default threshold: 10).

Configuration:

{
  "too-many-inputs": {
    "maxInputs": 5
  }
}

LifecycleHookRule

Rule ID: angular:lifecycle-hook-violation Severity: Warning/Error

Detects improper lifecycle hook usage and missing cleanup. Also detects heavy ngOnInit with 5 or more service calls (warning), escalating to error at 8 or more.

Before:

@Component({...})
export class TimerComponent {
  private intervalId: number;

  ngOnInit() {
    // Missing cleanup - interval continues after component destruction
    this.intervalId = window.setInterval(() => {
      console.log('tick');
    }, 1000);
  }
}

After:

@Component({...})
export class TimerComponent implements OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(() => console.log('tick'));
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

TemplateComplexityRule

Rule ID: angular:template-complexity Severity: Medium

Detects complex operations in templates that cause side effects on every change detection cycle. Also detects component getters with 3 or more non-trivial statements (complex-template-getter violation type).

Before:

@Component({
  template: `
    <div>{{ getExpensiveComputation() }}</div>
    <button (click)="users.push(newUser)">Add User</button>
  `,
})
export class UserListComponent {
  getExpensiveComputation() {
    return this.users.filter((u) => u.active).length * 42;
  }
}

After:

@Component({
  template: `
    <div>{{ activeUserCount }}</div>
    <button (click)="addUser()">Add User</button>
  `,
})
export class UserListComponent {
  activeUserCount = this.users.filter((u) => u.active).length;

  addUser() {
    this.users.push(this.newUser);
    this.activeUserCount = this.users.filter((u) => u.active).length;
  }
}

ImmutableInputViolationRule

Rule ID: angular:immutable-input-violation Severity: High

Detects mutation of @Input properties that break OnPush change detection.

Before:

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  @Input() user: User;

  updateUser() {
    this.user.name = 'New Name'; // Mutates input - breaks OnPush
  }
}

After:

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  @Input() user: User;

  updateUser() {
    this.userChange.emit({ ...this.user, name: 'New Name' });
  }

  @Output() userChange = new EventEmitter<User>();
}

MissingTrackByRule

Rule ID: angular:missing-trackby Severity: Medium

Detects ngFor directives without trackBy functions.

Before:

@Component({
  template: `
    <li *ngFor="let user of users">{{ user.name }}</li>
  `,
})
export class UserListComponent {
  users: User[] = [];
}

After:

@Component({
  template: `
    <li *ngFor="let user of users; trackBy: trackByUserId">{{ user.name }}</li>
  `,
})
export class UserListComponent {
  users: User[] = [];

  trackByUserId(index: number, user: User): string {
    return user.id;
  }
}

PresentationalComponentViolationRule

Rule ID: angular:presentational-component-violation Severity: Warning

Detects presentational (dumb) components that have taken on responsibilities belonging to container components:

  • service-dependency: constructor or inject() service dependency
  • subscription-in-lifecycle: subscriptions without takeUntilDestroyed
  • missing-output: service call without a corresponding @Output
  • business-logic: keyword prefix combined with cyclomatic complexity above 3
  • complex-state-management: 3 or more non-Input array/Map/Set properties

Configurable dialog class detection via .angular-modernizer.json rules section.


ServiceMutableStateRule

Rule ID: angular:service-mutable-state Severity: Warning

Detects @Injectable services that mutate public array, Map, or Set fields via push, splice, pop, sort, or reverse.


PromiseVsObservableRule

Rule ID: angular:promise-vs-observable-anti-pattern Severity: Warning

Detects 5 anti-patterns:

  • .toPromise() calls
  • Async component methods
  • Promise-wrapping of HttpClient services
  • Sequential awaits that could run in parallel
  • Manual detectChanges() calls

ComponentWrapperInstantiationRule

Rule ID: angular:component-wrapper-instantiation Severity: Warning

Detects direct new X() instantiation of wrapper or adapter classes that should be injected. Configurable class name patterns via .angular-modernizer.json.


DtoDirectInstantiationRule

Rule ID: angular:dto-direct-instantiation Severity: Warning

Detects direct new X() instantiation of DTO, Response, or Result classes. Configurable via .angular-modernizer.json.


MagicStringSelectorRule

Rule ID: angular:magic-string-selector Severity: Warning

Detects string-literal selector arguments passed to dynamic component loading methods. Target method patterns are configurable via .angular-modernizer.json.


Transformation Rules

ConstructorToInjectRule

Transformation Type: constructor-to-inject

Converts Angular constructor injection to the modern inject() function.

Before:

@Component({...})
export class UserComponent {
  constructor(
    private userService: UserService,
    private router: Router,
    @Inject(APP_CONFIG) private config: AppConfig
  ) {}
}

After:

@Component({...})
export class UserComponent {
  private userService = inject(UserService);
  private router = inject(Router);
  private config = inject(APP_CONFIG);
}

Features:

  • Converts all constructor parameters to inject() calls
  • Handles @Inject() decorators with string tokens
  • Automatically adds inject import from @angular/core
  • Preserves parameter access modifiers and names

InterfaceExtractionRule

Transformation Type: interface-extraction

Extracts inline type literals into named interfaces.

Before:

@Component({...})
export class UserFormComponent {
  @Input() user: {
    id: number;
    name: string;
    email: string;
    profile: {
      avatar: string;
      bio?: string;
    };
  };
}

After:

interface UserProfile {
  avatar: string;
  bio?: string;
}

interface User {
  id: number;
  name: string;
  email: string;
  profile: UserProfile;
}

@Component({...})
export class UserFormComponent {
  @Input() user: User;
}

Features:

  • Extracts complex inline type literals
  • Handles nested object types and arrays
  • Supports optional properties and union types
  • Generates unique interface names to avoid conflicts

DependencyInjectionMigrationRule

Transformation Type: dependency-injection-migration

Modernizes legacy Angular dependency injection patterns including string tokens and ReflectiveInjector.

Before:

// String token injection
@Injectable()
export class ApiService {
  constructor(@Inject('API_URL') private apiUrl: string) {}
}

// ReflectiveInjector usage
const injector = ReflectiveInjector.resolveAndCreate([
  { provide: 'API_CONFIG', useValue: config },
]);

After:

// Modern injection token
export const API_URL = new InjectionToken<string>('API_URL');

@Injectable()
export class ApiService {
  private apiUrl = inject(API_URL);
}

// Modern injector usage
const injector = Injector.create([{ provide: API_CONFIG, useValue: config }]);

ConstructorInjectionTransformRule

Transformation Type: constructor-injection-transform

Converts manual service instantiation (new Service()) to proper inject(Service) calls.

Before:

@Component({...})
export class UserComponent {
  constructor() {
    this.httpClient = new HttpClient();
    this.userService = new UserService();
  }

  private httpClient: HttpClient;
  private userService: UserService;
}

After:

@Component({...})
export class UserComponent {
  private httpClient = inject(HttpClient);
  private userService = inject(UserService);
}

Configuration:

{
  "constructor-injection-transform": {
    "enabled": true,
    "serviceSuffixes": ["Service", "Repository", "Client", "Store"],
    "handleComplexExpressions": true
  }
}

ComponentInputsInterfaceExtractionRule

Transformation Type: component-inputs-interface-extraction

Extracts component input properties into dedicated interfaces.

Before:

@Component({...})
export class UserCardComponent {
  @Input() userId: number;
  @Input() userName: string;
  @Input() isActive: boolean;
  @Input() avatarUrl?: string;
}

After:

interface UserCardInputs {
  userId: number;
  userName: string;
  isActive: boolean;
  avatarUrl?: string;
}

@Component({...})
export class UserCardComponent {
  @Input() inputs: UserCardInputs;
}

Configuration:

{
  "component-inputs-interface-extraction": {
    "enabled": true,
    "minInputsThreshold": 3,
    "generateUniqueNames": true,
    "addToSeparateFile": false
  }
}

ServiceInjectionCleanupRule

Transformation Type: service-injection-cleanup

Converts manual service instantiation in property initializers and constructors to inject() calls.

Before:

@Component({...})
export class DataComponent {
  private http = new HttpClient();
  private router: Router = new Router();

  constructor() {
    this.http = new HttpClient();
  }
}

After:

@Component({...})
export class DataComponent {
  private http = inject(HttpClient);
  private router = inject(Router);
}

Configuration:

{
  "service-injection-cleanup": {
    "enabled": true,
    "servicePatterns": ["Service$", "Client$", "Repository$"],
    "handleComplexExpressions": true
  }
}

Additional Transformation Types

The plugin also exposes the following transformation types via the transform-code MCP tool:

  • promise-service-to-observable - Converts Promise-based service methods to Observable equivalents
  • promise-component-to-reactive - Converts async component methods to reactive patterns
  • promise-cleanup - Removes .toPromise() and related promise bridge code
  • sequential-await-to-forkjoin - Converts independent sequential awaits to parallel forkJoin()
  • missing-output-transform - Adds output() signals to presentational components that call services directly
  • subscription-transform - Converts raw .subscribe() in lifecycle hooks to takeUntilDestroyed() pattern
  • library-extraction - Executes a 7-phase library extraction workflow (use after extract-libraries detection)
  • static-class-transform - Converts static utility classes to standalone functions

Usage

With MCP Tools

# Start MCP server
pnpm run start:mcp

# Transform constructor injection
transform-code --filePath src/app/user.component.ts --transformation constructor-to-inject

# Extract interfaces
transform-code --filePath src/app/data.models.ts --transformation interface-extraction

# Migrate DI patterns
transform-code --filePath src/app/legacy.service.ts --transformation dependency-injection-migration

# Convert subscriptions to takeUntilDestroyed
transform-code --filePath src/app/user.component.ts --transformation subscription-transform

# Parallelize sequential awaits
transform-code --filePath src/app/data.service.ts --transformation sequential-await-to-forkjoin

With Direct API

import { Kernel } from '@angular-modernizer/core';
import { AngularPlugin } from '@angular-modernizer/plugin-angular';
import { ContextFactory } from '@angular-modernizer/plugin-system';

const kernel = new Kernel({
  plugins: [new AngularPlugin()],
});

await kernel.initialize();

const plugin = kernel.getPlugin('@angular-modernizer/plugin-angular');
const transformRules = plugin.getTransformRules();

// Get specific transformation rule
const constructorToInjectRule = transformRules.find(
  (rule) => rule.id === 'angular:constructor-to-inject',
);

// Transform a file
const sourceFile = kernel.getProject().addSourceFileAtPath('user.component.ts');
const context = ContextFactory.createTransformContext({
  sourceFile,
  project: kernel.getProject(),
  api: createPublicApi(),
  config: {},
});

const result = await constructorToInjectRule.transform(context);

if (result.modified) {
  await sourceFile.save();
}

Testing

# Run all plugin tests (analysis + transformation)
pnpm test

# Run transformation rule tests specifically
pnpm test transform-rules/

# Run with coverage
pnpm test:coverage

Performance

Tested on production Angular codebases:

| Metric | Analysis | Transformation | |-|-|-| | Files Processed | 2,753 | 500+ | | Execution Time | ~3.2s | ~1.5s | | Memory Usage | ~185 MB | ~95 MB |


Contributing

See DEVELOPMENT_GUIDE.md for architecture details and TESTING_GUIDE.md for testing conventions.


License

MIT