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

eslint-plugin-ng-onpush-migration

v1.0.0

Published

ESLint plugin that flags Angular upgrade risk and OnPush performance debt — nested state mutation, RxJS/async callback reassignment, and change-detection hazards — across Angular v6 through v22.

Readme

ng-onpush-migration-tool

An ESLint plugin that flags Angular upgrade risk and performance debt in component classes — starting with the change-detection hazards that break silently the moment a component moves to OnPush.

What this project does

Angular codebases accumulate the same handful of problems: state mutated in place, change detection quietly skipped, and code that relies on patterns Angular is moving away from. This plugin makes those problems visible with AST-based rules that work across Angular v6 through v22, without branching on Angular version.

The first rule category — nested mutation and OnPush-unsafe reassignment — is what's implemented today, as four focused rules that can be enabled independently:

  • nested-object-mutation

    • Example: this.state.user.name = value
    • Example: this.items.push(value) — not an object mutation itself, but a mutating array method call that Angular's OnPush change detection won't see either, so it's flagged the same way
  • rxjs-callback-reassignment

    • Example: of(1).pipe(map(() => { this.value = ... }))
  • async-callback-reassignment

    • Example: setTimeout(() => { this.value = 1 }, 0)
  • component-state-reassignment

    • Example: this.value = name — a direct reassignment of a component class property holding a primitive value (string, number, boolean); reassigning to a new object or array literal is handled by nested-object-mutation instead

Installation

Install the plugin as a dev dependency:

npm install --save-dev eslint-plugin-ng-onpush-migration

Minimum supported versions

The rules themselves are Angular-version-agnostic — they check plain TypeScript AST shapes (decorators, assignment expressions, member/call expressions) that have been stable since early TypeScript, so they apply equally to an Angular v6 codebase and a v22 one. What actually gates support is the linting toolchain, independent of whatever Angular/TypeScript version the application itself builds with:

| Tool | Minimum version | Why | | --- | --- | --- | | Node.js | 18.18.0 | Required by @typescript-eslint/utils v8's engines field — the plugin's rules require() it at runtime | | npm | 9.8.1 | Bundled with Node 18.18.0; not an independent constraint of its own, just the practical floor that comes with the Node requirement above | | TypeScript | 4.8.4 | Lower bound of @typescript-eslint/utils v8's peer range (>=4.8.4 <6.1.0). This is only a linting-time devDependency — a project can keep an older TypeScript for its actual build and still add a newer one solely for eslint to use | | ESLint | 8.57.0 | Lower bound of @typescript-eslint/utils v8's peer range on eslint (^8.57.0 \|\| ^9.0.0); both the flat-config and .eslintrc.json paths documented below work at this floor |

If a project can't clear the Node 18.18.0 floor at all — not even in a dedicated CI lint step or a local shell with a second Node binary available — none of the above can be worked around with dependency versions alone, since it's an engines requirement on the package itself, not just a peer-range compatibility question.

Linting with an old pinned Node version

Plenty of enterprise Angular v6/v7/v8 codebases are pinned to Node 12 or 14 project-wide because bumping Node — not TypeScript — is what breaks the build (native node-gyp addons, node-sass, webpack 4, and similar). This plugin doesn't need the build to move to Node 18.18+; it only needs whatever process actually executes eslint to satisfy that floor. That process is usually one of three things, and each has a different, low-friction answer — no company-wide Node upgrade, and no Node version manager (nvm, volta, etc.) required anywhere:

  • CI. Give the lint step its own job/stage with its own Node version, separate from the build job. Every major CI platform already supports this natively — GitHub Actions' setup-node, GitLab's per-job image:, Azure Pipelines' UseNode@N — each just fetches/caches the requested Node binary for that one job. It has no effect on the build job's Node version, and angular.json/project.json's lint architect target, the ESLint config, and the npm run lint script itself don't change at all — only the CI YAML gains a Node-version step scoped to that job.

  • Editor (VS Code and similar). The ESLint extension runs its own language-server process using the editor's own bundled Node runtime (or an explicit eslint.runtime/eslint.nodePath override), not the project's pinned Node. Since editors have shipped a modern bundled Node for a while now, linting-on-keystroke is typically unaffected by an old project-wide Node pin — verify this once on a real machine, but it's usually already fine with no configuration changes.

  • Local terminal and pre-commit hooks (git hooks, Husky, etc.). This is the one place a genuinely older Node is likely to be the active one. Rather than installing a version manager, vendor a second, standalone Node binary as a plain unzipped folder (Node's official downloads are a zip/tarball with no installer and no admin rights required), and point the lint script at it by explicit path instead of the bare eslint binary, e.g.:

    {
      "scripts": {
        "lint": "./.tools/node18/bin/node ./node_modules/.bin/eslint ."
      }
    }

    This changes nothing about the default node on any developer's PATH, requires no policy exception (it's just a folder containing an executable, not an installed tool), and doesn't touch angular.json/project.json's architect configuration or the ESLint config itself — only the one script line that invokes the linter.

Configuring the plugin

You can enable a curated preset or opt into individual rules.

CommonJS / legacy config

For older ESLint setups that use a CommonJS configuration file, you can register the plugin like this:

module.exports = {
  plugins: ['eslint-plugin-ng-onpush-migration'],
  rules: {
    'eslint-plugin-ng-onpush-migration/nested-object-mutation': 'warn',
    'eslint-plugin-ng-onpush-migration/rxjs-callback-reassignment': 'error',
  },
};

.eslintrc.json (JSON config, legacy)

This format predates ESLint's flat config system and is no longer the recommended approach, but it's still supported by ESLint for projects that haven't migrated. In the plugins array, use the package's short name — drop the eslint-plugin- prefix, since ESLint's legacy config resolver adds it back automatically when looking up the module. Rule IDs in rules then use that same short name as their prefix:

{
  "plugins": ["ng-onpush-migration"],
  "rules": {
    "ng-onpush-migration/nested-object-mutation": "warn",
    "ng-onpush-migration/rxjs-callback-reassignment": "error"
  }
}

To pass options such as allowFix, use the [severity, options] array form, same as in any other config format:

{
  "plugins": ["ng-onpush-migration"],
  "rules": {
    "ng-onpush-migration/nested-object-mutation": ["warn", { "allowFix": true, "injectMissingDependency": true }]
  }
}

If your .eslintrc.json uses overrides (for example, to scope rules to *.ts files), declare plugins and rules inside the relevant override block rather than at the top level.

Flat config with ESLint 9+ and ES modules

If you are using a flat config in eslint.config.js, the plugin can be imported and registered like this:

import tseslint from 'typescript-eslint';
import angular from 'angular-eslint';
import ngOnpushMigration from 'eslint-plugin-ng-onpush-migration';

export default tseslint.config(
  {
    files: ['**/*.ts'],
    extends: [...angular.configs.tsRecommended],
    plugins: {
      'ng-onpush-migration': ngOnpushMigration,
    },
    rules: {
      'ng-onpush-migration/nested-object-mutation': 'error',
      'ng-onpush-migration/rxjs-callback-reassignment': 'error',
    },
  },
);

A plain severity string ('error' / 'warn') only turns a rule on for reporting — it leaves that rule's allowFix option at its default (false), so no suggestion is offered for it. See Enabling the fix suggestion (allowFix) below to opt a rule into it.

Enabling the fix suggestion (allowFix)

Every fixable rule (nested-object-mutation, rxjs-callback-reassignment, async-callback-reassignment, component-state-reassignment) accepts an allowFix boolean option:

  • allowFix: false (the default) — the rule still reports violations, but offers no suggestion.
  • allowFix: true — the rule attaches an ESLint suggestion that inserts this.changeDetectorRef.markForCheck(); after the offending statement. The rule still reports the same diagnostic afterward (the fix does not remove the mutation itself, it only wires up change detection).

This is a suggestion, not an autofix. Every fixable rule reports through ESLint's hasSuggestions mechanism, not fixable. That means the rewrite is only ever applied when a human explicitly picks "Apply suggestion" in their editor (or an equivalent tool that reads suggestions) — eslint --fix and --fix-dry-run never touch it, regardless of allowFix. The plugin is intentionally designed to never silently rewrite a file.

To opt a rule into the suggestion in a consumer config, use the [severity, options] array form instead of a bare severity string:

rules: {
  'ng-onpush-migration/nested-object-mutation': ['error', { allowFix: true }],
  'ng-onpush-migration/rxjs-callback-reassignment': ['error', { allowFix: true }],
  'ng-onpush-migration/component-state-reassignment': ['error', { allowFix: true }],
},

To turn the suggestion back off for a rule, either drop the options object or set allowFix: false explicitly:

rules: {
  'ng-onpush-migration/nested-object-mutation': 'error', // allowFix defaults to false
  'ng-onpush-migration/rxjs-callback-reassignment': ['error', { allowFix: false }], // explicit
},

If you adopt the recommended or all shared configs (below) instead of wiring rules by hand, allowFix: true is already baked in for every rule they enable — no extra configuration needed.

Injecting a missing ChangeDetectorRef (injectMissingDependency)

By default, if a component has no ChangeDetectorRef dependency at all, the suggestion withholds the fix rather than reference a property that doesn't exist — you still get the lint error, just no markForCheck() suggestion. If you'd rather the fix add the dependency for you, set injectMissingDependency: true alongside allowFix: true:

rules: {
  'ng-onpush-migration/nested-object-mutation': ['error', { allowFix: true, injectMissingDependency: true }],
},
  • If the class already injects a ChangeDetectorRef — under any name — that name is reused as-is; injectMissingDependency never renames an existing dependency.
  • Otherwise, a new one is added under the default name changeDetectorRef, using whichever DI style the class already uses elsewhere: inject(ChangeDetectorRef) if the class uses inject() for anything else, or a constructor parameter (constructor(private changeDetectorRef: ChangeDetectorRef) {}) if it doesn't. Constructor injection is also the fallback for a class with no existing dependencies of either style, since it's the one style that works across the whole Angular v6–v22 range.
  • If changeDetectorRef is already taken by something else on the class, or the existing @angular/core import can't be safely extended, the fix is withheld instead of guessing — same as the no-dependency case with injectMissingDependency off.

injectMissingDependency defaults to false and is not enabled by recommended or all — it's a separate opt-in on top of allowFix, since it can add a new class member (and possibly a new constructor) rather than only inserting a method call.

Recommended preset

The recommended config enables nested-object-mutation, rxjs-callback-reassignment, and async-callback-reassignment at warn, each with allowFix: true already set:

import ngOnpushMigration from 'eslint-plugin-ng-onpush-migration';

export default [
  {
    plugins: {
      'ng-onpush-migration': ngOnpushMigration,
    },
    ...ngOnpushMigration.configs.recommended,
  },
];

component-state-reassignment is not part of recommended — add it explicitly (see Select specific rules manually) if you want it included.

All rules

The all config enables every rule, including component-state-reassignment, at warn with allowFix: true:

import ngOnpushMigration from 'eslint-plugin-ng-onpush-migration';

export default [
  {
    plugins: {
      'ng-onpush-migration': ngOnpushMigration,
    },
    ...ngOnpushMigration.configs.all,
  },
];

Select specific rules manually

When wiring rules by hand instead of spreading a preset, each rule keeps allowFix: false by default — pass { allowFix: true } explicitly for any rule you want to offer the suggestion:

import ngOnpushMigration from 'eslint-plugin-ng-onpush-migration';

export default [
  {
    plugins: {
      'ng-onpush-migration': ngOnpushMigration,
    },
    rules: {
      'ng-onpush-migration/nested-object-mutation': ['warn', { allowFix: true }],
      'ng-onpush-migration/rxjs-callback-reassignment': 'error', // reports only, no suggestion
    },
  },
];

Technical documentation

For repository structure, design rationale, and implementation notes, see docs/DEVELOPMENT.md.