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

v0.4.2

Published

ESLint plugin with rules for reducing AI-generated code smells

Readme

eslint-plugin-unslop

ESLint plugin for architecture enforcement and code quality. Define module boundaries, control imports and exports, catch false sharing, and fix common LLM-generated code smells - all from a single shared configuration.

Requires ESLint 9+ (flat config). TypeScript optional but recommended.

Installation

npm install --save-dev eslint-plugin-unslop

Quick Start

The full config enables the complete rule suite - architecture enforcement plus symbol fixers:

// eslint.config.mjs
import unslop from 'eslint-plugin-unslop'

export default [
  unslop.configs.full,
  {
    settings: {
      unslop: {
        sourceRoot: 'src',
        architecture: {
          utils: { shared: true },
          'repository/*': {
            imports: ['utils', 'models/*'],
            exports: ['^create\\w+Repo$', '^Repository[A-Z]\\w+$'],
          },
          'models/*': {
            imports: ['utils'],
          },
          app: {
            imports: ['*'],
          },
        },
      },
    },
  },
]

This turns on:

| Rule | Severity | What it does | | ---------------------------- | -------- | ---------------------------------------------------------------------- | | unslop/import-control | error | Enforces boundaries and forbids local namespace imports | | unslop/export-control | error | Restricts export patterns and forbids export * in module entrypoints | | unslop/no-false-sharing | error | Flags shared entrypoint symbols with fewer than two consumer groups | | unslop/no-special-unicode | error | Catches smart quotes, invisible spaces, and other unicode impostors | | unslop/no-unicode-escape | error | Prefers "©" over "\u00A9" | | unslop/read-friendly-order | error | Enforces top-down, dependency-friendly declaration order |

The configs.minimal config contains only the zero-config symbol fixers (no-special-unicode and no-unicode-escape). It is included automatically within configs.full, or can be used standalone for projects that don't need architecture enforcement:

// eslint.config.mjs
import unslop from 'eslint-plugin-unslop'

export default [unslop.configs.minimal]

Rules

unslop/import-control

Think of this as customs control for your modules - you declare which modules are allowed to import from which, and anything undeclared gets turned away at the border.

The rule reads from a shared policy in settings.unslop.architecture. It's deny-by-default for cross-module imports, which means forgetting to declare a dependency is a loud error rather than a silent free-for-all. It also enforces:

  • cross-module imports must arrive through the public gate (index.ts or types.ts)
  • local cross-module namespace imports are forbidden (import * as X from '<local-module>')
  • same-module relative imports can only go one level deeper - no tunnelling into internals
  • files that don't match any declared module are denied (fail-closed, not fail-silently)

Configuration

// eslint.config.mjs
import unslop from 'eslint-plugin-unslop'

export default [
  {
    plugins: { unslop },
    settings: {
      unslop: {
        sourceRoot: 'src',
        architecture: {
          utils: { shared: true },
          'repository/*': {
            imports: ['utils', 'models/*'],
            exports: ['^create\\w+Repo$', '^Repository[A-Z]\\w+$'],
          },
          'models/*': {
            imports: ['utils'],
          },
          app: {
            imports: ['*'],
          },
        },
      },
    },
    rules: {
      'unslop/import-control': 'error',
      'unslop/export-control': 'error',
      'unslop/no-false-sharing': 'error',
    },
  },
]

unslop/export-control

The customs declaration form for the other direction: what are you actually exporting from your module's public entrypoints?

When a module defines exports regex patterns in settings.unslop.architecture, every symbol exported from that module's index.ts or types.ts must match at least one pattern - otherwise it's stopped at the gate with an error at the export site. Modules without exports are waved through by default, so you can adopt this gradually. Regardless of module policy, export * from ... is rejected in index.ts and types.ts so symbol provenance stays explicit.

unslop/no-false-sharing

The "shared" folder anti-pattern detector. LLMs (and some humans also) love creating shared APIs that are only used by one consumer - or worse, by nobody at all. This rule evaluates symbols exported from shared module entrypoints (index.ts and types.ts) and requires each exported symbol to be imported by at least two separate directory-level consumer groups. If a symbol is used in only one place, it's not shared - it's misplaced.

Configuration

Shared modules are declared via shared: true on module policies in settings.unslop.architecture:

// eslint.config.mjs
import unslop from 'eslint-plugin-unslop'

export default [
  unslop.configs.full,
  {
    settings: {
      unslop: {
        sourceRoot: 'src',
        architecture: {
          utils: { shared: true },
          'shared/*': { shared: true },
        },
      },
    },
  },
]

The rule takes no options - all configuration comes from the shared architecture settings, consistent with import-control and export-control.

Consumer counting is always at the directory level: the importer file path relative to sourceRoot, minus filename. Both value imports and import type imports count as consumers, and alias imports such as @/shared/index are resolved the same as relative imports.

What it catches

src/shared/index.ts
  export const formatDate = ...
  -> imported only by src/features/calendar/view.ts
  -> error: symbol "formatDate" has 1 consumer group(s) (group: features/calendar)

src/shared/types.ts
  export type LegacyOptions = ...
  -> not imported by anyone
  -> error: symbol "LegacyOptions" has 0 consumer group(s) (no consumers found)

unslop/read-friendly-order

Enforces a top-down reading order for your code. The idea: when someone opens a file, they should see the important stuff first and the helpers below. LLM-generated code often scatters declarations in random order, making files harder to follow.

This rule covers three areas:

Top-level ordering - Public/exported symbols should come before the private helpers they use. Read the API first, implementation details second.

// Bad - helper defined before its consumer
function formatName(name) {
  return name.trim().toLowerCase()
}

export function createUser(name) {
  return { name: formatName(name) }
}

// Good - consumer first, helper below
export function createUser(name) {
  return { name: formatName(name) }
}

function formatName(name) {
  return name.trim().toLowerCase()
}

Class member ordering - Constructor first, public fields next, then other members ordered by dependency.

// Bad
class UserService {
  private format() { /* ... */ }
  name = 'default'
  constructor() { /* ... */ }
}

// Good
class UserService {
  constructor() { /* ... */ }
  name = 'default'
  private format() { /* ... */ }
}

Test file ordering - Setup hooks (beforeEach, beforeAll) before teardown hooks (afterEach, afterAll), and both before test cases.

// Bad - setup and tests buried between helpers
function buildFixture(overrides) {
  return { id: 1, ...overrides }
}
it('works', () => {
  /* ... */
})
function assertCorrect(value) {
  expect(value).toBe(1)
}
beforeEach(() => {
  buildFixture()
})

// Good - setup first, then tests, helpers at the bottom
beforeEach(() => {
  buildFixture()
})
it('works', () => {
  /* ... */
})
function buildFixture(overrides) {
  return { id: 1, ...overrides }
}
function assertCorrect(value) {
  expect(value).toBe(1)
}

unslop/no-special-unicode

Disallows special unicode punctuation and whitespace characters in string literals and template literals. LLMs love to sprinkle in smart quotes (“like this”), non-breaking spaces, and other invisible gremlins that look fine in a PR review but cause fun bugs at runtime.

Caught characters include: left/right smart quotes (“” ‘’), non-breaking space, en/em dash, horizontal ellipsis, zero-width space, and various other exotic whitespace.

// Bad - these contain invisible special characters that look normal
const greeting = 'Hello World' // a non-breaking space (U+00A0) is hiding between the words
const quote = 'He said “hello”' // smart double quotes (U+201C, U+201D)

// Good
const greeting = 'Hello World' // regular ASCII space
const quote = 'He said "hello"' // plain ASCII quotes

Note: the bad examples above contain actual unicode characters that may be indistinguishable from their ASCII counterparts in your font - that's exactly the problem this rule catches.

unslop/no-unicode-escape

Prefers actual characters over \uXXXX escape sequences. If your string says \u00A9, just write © - your coworkers will thank you. LLM-generated code sometimes encodes characters as escape sequences for no good reason.

// Bad
const copyright = '\u00A9 2025'
const arrow = '\u2192'

// Good
const copyright = '© 2025'
const arrow = '→'

A Note on Provenance

Yes, a fair amount of this was vibe-coded with LLM assistance - which is fitting, since that's exactly the context this plugin is designed for. That said, the ideas behind these rules, the decisions about what to catch and how to catch it, and the overall design are mine. Every piece of code went through human review, and the test cases in particular were written and verified with deliberate care.

The project also dogfoods itself: eslint-plugin-unslop is linted using eslint-plugin-unslop.

Contributing

See AGENTS.md for development setup and guidelines.

License

MIT