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-mvvm

v1.0.2

Published

ESLint plugin enforcing MVVM architectural layer boundaries

Readme

eslint-plugin-mvvm

CI Release OpenSSF Scorecard npm npm downloads License: MIT

ESLint plugin that enforces MVVM architectural layer boundaries in React codebases. Data flows one way — Model → ViewModel → View — and these rules keep it that way: state usage and API calls in Views are flagged as ESLint errors and pushed into ViewModel hooks where they belong.

Installation

npm install --save-dev eslint-plugin-mvvm

Requires Node.js >= 18 and ESLint >= 8 (flat config, ESLint 9 recommended).

Usage

// eslint.config.js
import mvvm from 'eslint-plugin-mvvm';

export default [
  mvvm.configs.recommended, // or mvvm.configs.strict
];

Or configure rules individually:

import mvvm from 'eslint-plugin-mvvm';

export default [
  {
    plugins: { mvvm },
    rules: {
      'mvvm/no-api-in-view': 'error',
      'mvvm/no-state-in-view': ['error', { mode: 'strict' }],
      'mvvm/no-jsx-in-viewmodel': 'error',
      'mvvm/enforce-layer-boundaries': 'error',
    },
  },
];

Rules

| Rule | Description | | --- | --- | | mvvm/no-api-in-view | Disallows API calls (fetch, axios.*, TanStack Query, RTK Query, SWR, Apollo hooks) in View files. Data fetching belongs in ViewModel hooks. A callee that resolves to the ViewModel layer or is named like a ViewModel (e.g. useEditDialogViewModel) is never flagged; use the ignorePattern option as an extra escape hatch. | | mvvm/no-state-in-view | Disallows useState / useReducer in View files. strict mode bans all state; warn-business mode only flags state that coexists with an API call in the same file (pure UI state like isOpen stays legal). | | mvvm/no-jsx-in-viewmodel | ViewModel files must return data, not JSX. | | mvvm/enforce-layer-boundaries | Enforces import direction: Views import ViewModels, ViewModels import Models — never the reverse, and Views never import Models directly (optionally relaxed for import type). |

What counts as a View / ViewModel / Model?

  • View — any .tsx / .jsx file that isn't classified as a ViewModel.
  • ViewModeluseXxx.ts(x) hooks, *.vm.*, *.viewmodel.*, *ViewModel.*, or anything under hooks/, viewmodels/, view-models/.
  • Model — files under models/, stores/, api/, services/, repositories/, domain/, or *.model.ts / *.service.ts / *.store.ts.

enforce-layer-boundaries resolves relative imports to their file on disk so the real extension drives classification. The directory hints (viewDirPatterns) only apply to a path that can't be resolved to a concrete .ts/.tsx/.js/.jsx file, and are matched relative to the project root (see below) — so an extension-less ../useThing that points at a ViewModel hook is treated as a ViewModel, not misclassified as a View.

Conventions are overridable via shared settings:

export default [
  mvvm.configs.recommended,
  {
    settings: {
      mvvm: {
        viewModelPatterns: ['\\.vm\\.', 'ViewModel\\.'],
        viewDirPatterns: ['/components/', '/pages/'],
        modelPatterns: ['/api/', '\\.service\\.ts$'],
        // Base path for directory-hint matching. Defaults to the ESLint
        // working directory, so a hint like `/ui/` only fires on directories
        // *inside* the project, not an ancestor path that is named `ui/`.
        root: import.meta.dirname,
      },
    },
  },
];

Pass an explicit empty array to opt out of a category entirely — for example viewDirPatterns: [] disables the View directory hints (an omitted value falls back to the defaults above).

Configs

  • recommended — pragmatic defaults for incremental adoption. API calls and business state in Views are errors; no-state-in-view runs in warn-business mode so pure UI state is still allowed.
  • strict — zero tolerance. No state at all in Views, no import type exceptions from Model.

Development

bun install
bun run lint   # eslint
bun test       # bun's test runner
bun run build  # tsc → dist/

Releases are automated: merging a package.json version bump to master tags, signs, and publishes the package with npm provenance (see .github/workflows/release.yml).

License

MIT