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-anti-slop

v0.5.0

Published

Config-driven anti-slop ESLint rules for React/TypeScript projects.

Downloads

493

Readme

eslint-plugin-anti-slop

eslint-plugin-anti-slop catches product and UI code patterns that make React/TypeScript apps feel unfinished: unjustified client components, placeholder copy, generic marketing text, demo data on primary routes, weak empty states, generic stat labels, defensive guard sprawl, low-value memoization, and deterministic structural UI tells such as gradient text, decorative grid backgrounds, side-stripe accents, excessive radii, arbitrary z-index values, missing reduced-motion fallbacks, hidden reveal defaults, and nested cards.

The plugin is intentionally opinionated. It focuses on rules that are specific enough to be useful in product repositories without turning lint into vague taste enforcement.

Compatibility

  • Node.js ^22.13.0 || >=24.
  • ESLint 9.x with flat config.
  • React/TypeScript projects using app, component, or library source paths.

The next pre-1.0 release drops Node 20 because that release line is End-of-Life. The 0.4.0 release line was the last one with Node 20 support.

ESLint 8 is not supported. A local compatibility audit showed that ESLint 8.57.1 can load the flat-config rule preset in a narrow lint smoke, but the package CLI fails under ESLint 8 because that major rejects the ESLint 9 overrideConfigFile: true option used by anti-slop check and anti-slop gate. Consumers should upgrade to ESLint 9 before adopting the plugin or CLI.

Upgrading from 0.3

Version 0.4 retains the published package subpaths and rule IDs, but it is a hardening release with deliberate behavior changes:

  • Use the supported Node range above; Node 20 is no longer supported by the next release.
  • Gate JSON output is schema 1.1 and audit events use schema 1.1; existing baseline formats remain accepted. See the audit section for retaining a single-version event log.
  • anti-slop/no-unjustified-use-client no longer autofixes directives. Review and remove a directive manually only after confirming the boundary is safe.
  • Configured gate baselinePath and outputPath values must resolve inside the project root.

See the changelog for the complete behavior and migration notes.

Install

From npm:

pnpm add -D eslint-plugin-anti-slop

Package page: eslint-plugin-anti-slop on npm.

Security

Report vulnerabilities through GitHub private vulnerability reporting. Do not put exploit details or live credentials in a public issue; see the security policy for the reporting contract.

Quick Start

// eslint.config.mjs
import antiSlop from "eslint-plugin-anti-slop";

export default [
  {
    files: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}", "lib/**/*.{ts,tsx}"],
    ...antiSlop.configs.recommended,
  },
];

Custom Configuration

Rules read project-specific vocabulary from settings["anti-slop"].

// eslint.config.mjs
import antiSlop from "eslint-plugin-anti-slop";

export default [
  {
    files: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}", "lib/**/*.{ts,tsx}"],
    plugins: {
      "anti-slop": antiSlop,
    },
    settings: {
      "anti-slop": {
        placeholderPatterns: ["coming soon", "todo", "tbd", "lorem ipsum", "placeholder"],
        marketingPatterns: ["powerful", "seamless", "unlock", "supercharge"],
        genericStatLabels: ["performance", "insights", "overview", "analytics", "usage", "activity"],
        actionWords: ["retry", "open", "create", "run", "fix", "clear filter"],
        demoDataModules: ["@/demo", "@/mocks", "@/fixtures"],
        realDataIndicators: ["fetch", "db", "prisma", "trpc"],
        clientOnlyImports: ["next/navigation", "@tanstack/react-query", "recharts"],
      },
    },
    rules: {
      "anti-slop/no-unjustified-use-client": "error",
      "anti-slop/no-useless-memo": "warn",
      "anti-slop/no-placeholder-copy": "error",
      "anti-slop/no-marketing-copy": "warn",
      "anti-slop/require-empty-state-action": "warn",
      "anti-slop/no-demo-data-primary-path": "error",
      "anti-slop/no-defensive-guard-sprawl": "warn",
      "anti-slop/no-generic-stat-label": "warn",
      "anti-slop/no-gradient-text": "warn",
      "anti-slop/no-decorative-grid-background": "warn",
      "anti-slop/no-side-stripe-accent": "warn",
      "anti-slop/no-excessive-radius": "warn",
      "anti-slop/no-arbitrary-z-index": "warn",
      "anti-slop/require-reduced-motion": "warn",
      "anti-slop/no-hidden-reveal-default": "warn",
      "anti-slop/no-nested-cards": "warn",
    },
  },
];

Presets

| Preset | Purpose | | --- | --- | | antiSlop.configs.recommended | Balanced defaults for product repositories. | | antiSlop.configs.strict | Escalates every rule to error. |

Rules

Each rule has a full docs page with rationale, examples, and options under docs/rules/, linked from editor tooltips via meta.docs.url.

| Rule | What it catches | | --- | --- | | anti-slop/no-unjustified-use-client | Client components without obvious client-only behavior; reports for manual review. | | anti-slop/no-useless-memo | Trivial useMemo and useCallback calls. | | anti-slop/no-placeholder-copy | Placeholder text in user-facing JSX and copy-bearing objects. | | anti-slop/no-marketing-copy | Generic product marketing language inside application UI. | | anti-slop/require-empty-state-action | High-confidence local empty states without wording or a usable control. | | anti-slop/no-demo-data-primary-path | Primary routes using fixture data outside a nullish fallback to an immutable same-scope real-data result. | | anti-slop/no-defensive-guard-sprawl | Repeated low-signal defensive checks that obscure the real contract. | | anti-slop/no-generic-stat-label | Dashboard labels such as "insights" or "performance" without domain meaning. | | anti-slop/no-gradient-text | Gradient-clipped text in static JSX class names or style objects. | | anti-slop/no-decorative-grid-background | Decorative two-axis one-pixel CSS gradient backgrounds. | | anti-slop/no-side-stripe-accent | Thick left/right border stripe accents on cards and callouts. | | anti-slop/no-excessive-radius | Oversized static radius values on framed UI surfaces. | | anti-slop/no-arbitrary-z-index | Arbitrary z-index values outside a semantic stacking scale. | | anti-slop/require-reduced-motion | Static motion code without a type-matched reduced-motion fallback. | | anti-slop/no-hidden-reveal-default | Reveal patterns that hide content by default. | | anti-slop/no-nested-cards | Nested Card components or nested card class containers. |

anti-slop/no-unjustified-use-client

Reports unnecessary "use client" directives for manual review when the file has no obvious client-only behavior. It deliberately does not autofix a server/client boundary.

Invalid:

"use client";

export function Header() {
  return <header>Account</header>;
}

Valid:

"use client";

import * as React from "react";

export function Toggle() {
  const [open, setOpen] = React.useState(false);
  return <button onClick={() => setOpen(!open)}>Toggle</button>;
}

anti-slop/no-useless-memo

Flags trivial useMemo and useCallback calls that add indirection without a clear memo-sensitive reason.

Invalid:

const value = useMemo(() => 1, []);
const onClick = useCallback(() => submit(), []);

Valid:

const filtered = useMemo(() => items.filter((item) => item.active), [items]);
const summary = useMemo(() => ({ total: items.length, items }), [items]);

anti-slop/no-placeholder-copy

Blocks placeholder text in user-facing JSX and common copy-bearing object properties.

Invalid:

export function EmptyState() {
  return <p>Coming soon</p>;
}

Valid:

const internalNote = "todo";

anti-slop/no-marketing-copy

Discourages generic marketing language inside product UI.

Invalid:

export function Banner() {
  return <p>Unlock powerful insights</p>;
}

Valid:

export function Banner() {
  return <p>Run payroll for contractors</p>;
}

anti-slop/require-empty-state-action

Requires high-confidence empty states to include action wording or a usable control on the same static render path in the nearest local message/container boundary. Disabled controls, href-less anchors without a handler, hidden inputs, native controls in a disabled fieldset other than its first legend, and actions elsewhere on the page do not satisfy the rule; native boolean attributes remain present even when written as strings such as disabled="false".

Invalid:

export function InvoiceList() {
  return (
    <section>
      <p>No invoices</p>
    </section>
  );
}

Valid:

export function InvoiceList() {
  return (
    <section>
      <p>No invoices</p>
      <button>Create invoice</button>
    </section>
  );
}

anti-slop/no-demo-data-primary-path

Prevents primary routes from using demo data except as the ?? fallback for an immutable same-scope configured real-data result. Direct fixture use, unrelated calls or imports, another fixture binding, and || remain invalid.

Invalid:

import { rows } from "@/fixtures/rows";

export function Page() {
  return rows;
}

Valid:

import { rows } from "@/fixtures/rows";
import { db } from "@/lib/db";

export async function Page() {
  const records = await db.report.findMany();
  return records ?? rows;
}

anti-slop/no-generic-stat-label

Discourages vague metric and section labels.

Invalid:

export function Dashboard() {
  return <h2>Analytics</h2>;
}

Valid:

export function Dashboard() {
  return <h2>Failed payments</h2>;
}

anti-slop/no-defensive-guard-sprawl

Flags ordinary functions that stack more than two leading nullish or isRecord(...) guards. Centralized validators and type guards such as isRecord, assertPayload, ensurePayload, and validatePayload are allowed to own repeated shape checks.

Invalid:

function parseUser(input) {
  if (!isRecord(input)) return null;
  if (input.id == null) return null;
  if (input.email == null) return null;

  return { id: input.id, email: input.email };
}

Valid:

function assertUserPayload(input) {
  if (!isRecord(input)) throw new Error("Invalid user");
  if (input.id == null) throw new Error("Invalid user");
  if (input.email == null) throw new Error("Invalid user");
}

anti-slop/no-gradient-text

Discourages gradient-clipped text from static JSX class names and style objects.

Invalid:

<h1 className="bg-gradient-to-r from-red-500 to-blue-500 bg-clip-text text-transparent">
  Revenue
</h1>

Valid:

<h1 className="text-brand">Revenue</h1>

anti-slop/no-decorative-grid-background

Flags two-axis one-pixel CSS gradient backgrounds used as decoration.

Invalid:

<div style={{ backgroundImage: "linear-gradient(#eee 1px, transparent 1px), linear-gradient(90deg, #eee 1px, transparent 1px)" }} />

Valid:

<div className="grid grid-cols-2 gap-4" />

anti-slop/no-side-stripe-accent

Flags thick left/right border accents such as border-l-4 or borderLeftWidth: 6.

Invalid:

<aside className="border-l-4 border-red-500" />

Valid:

<aside className="border border-red-200 bg-red-50" />

anti-slop/no-excessive-radius

Flags static radius values of 32px or larger on framed surfaces.

Invalid:

<section className="rounded-[40px]" />

Valid:

<section className="rounded-xl" />

anti-slop/no-arbitrary-z-index

Flags arbitrary z-index values of 999 or higher.

Invalid:

<div className="z-[9999]" />

Valid:

<div className="z-50" />

anti-slop/require-reduced-motion

Requires a type-matched reduced-motion fallback when static JSX/CSS-in-JS code declares transitions or animations: animate-* uses motion-reduce:animate-none, and transition* uses motion-reduce:transition-none. CSS fallbacks must target the same selector in a direct top-level reduced-motion media block, retain the source's static non-motion media scope, and win the cascade; nested, scoped, conditional, or layered CSS is reported rather than guessed at.

Invalid:

<button className="transition-opacity">Save</button>
<button className="animate-spin motion-reduce:transition-none">Load</button>

Valid:

<button className="transition-opacity motion-reduce:transition-none">Save</button>
<button className="animate-spin motion-reduce:animate-none">Load</button>

anti-slop/no-hidden-reveal-default

Flags reveal patterns that combine hidden default content with static motion.

Invalid:

<section className="opacity-0 transition-opacity">Hidden until reveal</section>

Valid:

<section className="opacity-100 transition-opacity">Visible content</section>

anti-slop/no-nested-cards

Flags nested Card components or nested containers with a card class.

Invalid:

<Card>
  <MetricCard />
</Card>

Valid:

<Card>
  <section>Details</section>
</Card>

Development

For local sibling-project development, install the checkout directly:

pnpm add -D file:../eslint-plugin-anti-slop
pnpm install
pnpm test
pnpm verify
pnpm smoke:published:eslint9-floor

Rule tests use ESLint RuleTester through Node's built-in test runner. pnpm coverage:report writes LCOV to coverage/lcov.info, and pnpm coverage:check enforces the .pre-cr.json threshold against src/ records.

pnpm verify is the deterministic pre-PR gate. It runs formatting, syntax, dead-code and circular-import checks, secret, test, coverage, pack, and local-consumer checks without a fresh registry resolution. pnpm verify:consumer-online separately packs the artifact and installs it in a fresh temporary consumer. That online ecosystem check verifies the CLI, flat config, formatter, all public runtime exports, and TypeScript declarations. pnpm verify:ci requires both groups plus a registry dependency audit; pnpm verify:release also requires the release tag to match package.json. The publish workflow separately verifies that the tag resolves to a commit reachable from main. pnpm verify:local additionally runs Pre-CR changed-line readiness and requires a globally installed pre-cr.

pnpm smoke:eslint9 links the current eslint-plugin-anti-slop checkout into smoke-consumer/ with pnpm, then runs ESLint 9 against a small JSX fixture, the anti-slop CLI, and the audit formatter. pnpm smoke:consumer is kept as an alias for the same supported-major smoke.

pnpm smoke:published:eslint9-floor runs the packed runtime surface against ESLint 9.0.0, including JavaScript and TypeScript/TSX lint paths. The ordinary packed smoke additionally compiles public TypeScript declarations against the current ESLint 9.x consumer; ESLint 9.0.0 itself does not expose declarations.

Quality Gate CLI

The package includes a gate runner for CI, local hooks, and Pre-CR-adjacent checks:

pnpm exec anti-slop check .
pnpm exec anti-slop gate --changed --mode block --format pre-cr

Run anti-slop --help after installing the package to inspect the available commands.

anti-slop check defaults to human-readable output. anti-slop gate defaults to line-delimited Pre-CR-compatible records. Both commands run ESLint with a built-in Anti-Slop flat config, normalize anti-slop/* findings, apply the configured gate policy, and exit nonzero only when the effective policy blocks new error findings. The built-in config supports JavaScript, JSX, TypeScript, and TSX, so backfill scans can run before a target repo has adopted an Anti-Slop ESLint config.

A successful gate result means the selected source was analyzed using a valid configuration and baseline. Invalid configuration or baseline input exits 2 before ESLint runs. Fatal ESLint or parser failures exit 1 in every policy mode and are reported as failed analysis rather than as a clean gate.

Gate policy modes:

  • --mode auto: block on protected branches or known dev/production gate envs, warn on detected feature branches.
  • --mode block: fail the process when new error findings exist.
  • --mode warn: report findings without failing the process.
  • --mode audit: report findings without blocking them. It exits zero only after successful analysis; invalid input and fatal analysis failures still fail clearly.

Output formats:

  • text: concise terminal summary.
  • json: full report with requested mode, effective policy, analysis status, summary, new findings, and baselined findings.
  • jsonl: one machine-readable finding per line; skipped or failed analysis emits a single analysis-status record instead of an empty success stream.
  • pre-cr: JSONL records shaped for quality-gate ingestion, including explicit skipped or failed analysis records.
  • sarif: SARIF 2.1.0 for CI code-scanning systems, with a synthetic status result when analysis fails or is skipped.

The full JSON gate report uses schemaVersion: "1.1". Generated baseline files remain schemaVersion: "1.0" and retain their existing compatibility forms.

Optional anti-slop.config.json:

{
  "files": ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}", "lib/**/*.{ts,tsx}"],
  "ignores": ["dist/**"],
  "mode": "auto",
  "baselinePath": ".anti-slop-baseline.json",
  "outputPath": ".aios/audit/anti-slop.json"
}

ignores entries use standard glob semantics (via picomatch), matching what ESLint accepts: dist/**, **/generated/**, src/**/*.fixture.ts, and basename patterns such as *.stories.tsx all work.

The config is strict: its only supported keys are files, ignores, mode, baselinePath, and outputPath. files must contain at least one non-empty path; each ignore-list entry must also be a non-empty string. Unsupported keys or malformed JSON are configuration errors rather than silently ignored settings. When configured, baselinePath and outputPath must be non-empty project-relative paths that resolve inside the project root.

In --mode auto without an explicit --branch, the CLI resolves the current branch from CI environment variables (GITHUB_REF_NAME, GITHUB_HEAD_REF, BRANCH_NAME, VERCEL_GIT_COMMIT_REF, AIOS_BRANCH) or git rev-parse, the same way audit events do, so feature branches warn instead of blocking.

Use changed-file mode for fast local gates:

pnpm exec anti-slop gate --changed --mode block --format pre-cr

When Git reports no changed files, --changed performs no full-repository fallback scan and returns a visible skipped-analysis result. If changed-file discovery itself fails, the command exits 2 instead of treating the failure as an empty scan.

Use audit mode for first-pass repo adoption/backfill evidence:

pnpm exec anti-slop check . --mode audit --format json

Use a baseline to adopt the gate in existing codebases without blocking on known findings:

pnpm exec anti-slop check . --update-baseline
pnpm exec anti-slop gate . --baseline .anti-slop-baseline.json --mode block

Existing raw fingerprint arrays and unversioned { "findings": [...] } baseline files remain accepted alongside generated versioned baselines. A malformed baseline is never treated as empty, and --update-baseline will not overwrite it.

For Pre-CR, keep Anti-Slop as a separate quality command alongside pre-cr run --workspace . until Pre-CR grows a first-class external-gate adapter:

{
  "scripts": {
    "quality:anti-slop": "anti-slop gate --changed --mode block --format pre-cr",
    "quality": "pnpm quality:anti-slop && pre-cr run --workspace ."
  }
}

Gate Audit Output

Anti-Slop ESLint runners can emit AIOS-compatible audit artifacts with the package formatter. For a repo that does not already have Anti-Slop in its ESLint config, add the package config and keep the formatter in a dedicated script.

Install in the consumer repo:

pnpm add -D eslint eslint-plugin-anti-slop @typescript-eslint/parser

Add eslint.aios-audit.config.mjs:

import antiSlopAiosAuditConfig from "eslint-plugin-anti-slop/aios-audit-config";

export default antiSlopAiosAuditConfig;

Add the package script:

{
  "scripts": {
    "audit:anti-slop": "eslint . --config eslint.aios-audit.config.mjs --format ./node_modules/eslint-plugin-anti-slop/audit-formatter.mjs"
  }
}

Use the ./node_modules/.../audit-formatter.mjs file path in --format; ESLint's formatter loader treats package subpaths as local filesystem paths.

Run it with pnpm:

pnpm audit:anti-slop

If the repo already has an ESLint config with anti-slop/* rules enabled, keep that config and add only the formatter script:

{
  "scripts": {
    "audit:anti-slop": "eslint . --format ./node_modules/eslint-plugin-anti-slop/audit-formatter.mjs"
  }
}

The packaged config covers JavaScript, JSX, TypeScript, and TSX files and ignores .next, build, coverage, dist, and node_modules output. To add repo-specific ignores:

import { antiSlopAiosAuditConfig } from "eslint-plugin-anti-slop/aios-audit-config";

export default antiSlopAiosAuditConfig({
  ignores: ["generated/**"],
});

The formatter records severity-2 anti-slop/* findings in .aios/audit/gate-events.jsonl and refreshes .aios/audit/gate-summary.md plus .aios/audit/learning-lessons.md. Each JSONL event uses the current AIOS gate event envelope: schema_version, event_id, timestamp, repo, branch, commit_sha, run_id, actor_type, gate, gate_version, event_type, severity, category, rule_id, rule_name, decision, summary, evidence, failure_pattern, root_cause_hypothesis, required_fix, actual_fix, learning_lesson, dedupe_fingerprint, related_event_ids, blocked_duration_seconds, tokens_wasted_estimate, and notes.

Current formatter output uses audit event schema_version: "1.1". Its fingerprints match the gate identity, including the source snippet, so they are not comparable to historical 1.0 audit fingerprints. Existing valid 1.0 lines remain as history; summaries keep 1.0 and 1.1 fingerprint groups separate. For a single-version downstream log, archive the old file before the first upgraded audit:

mv .aios/audit/gate-events.jsonl .aios/audit/gate-events.v1.jsonl

The formatter then creates a fresh 1.1 gate-events.jsonl. Fatal analysis still replaces the active artifacts with its explicit failure event.

Findings are recorded as blocks on main, master, dev, develop, development, or when AIOS_DEV_ENVIRONMENT, AIOS_DEV_ENV, QUALITY_GATE_DEV_ENV, or GATE_CONNECTED_DEV_ENV is set; detected unprotected feature branches are recorded as neutral finding_observed warnings rather than claimed blocks. Protected-branch findings use commit_blocked. Fatal ESLint or parser results produce a single analysis_failed event and replace stale audit artifacts, so downstream readers do not mistake earlier findings for a successful current analysis. Audit findings use the same stable identity as gate findings. AIOS_BRANCH and AIOS_RUN_ID are used when present, and otherwise the formatter falls back to the current git branch. ESLint process exit behavior still depends on the runner's rule severity and CLI settings.

Design Principles

  • Prefer high-confidence findings over style nits.
  • Keep rules configurable by project vocabulary.
  • Use warnings for judgment-heavy signals and errors for release-blocking patterns.
  • Make violations actionable enough that an engineer knows what to change without reading the rule source.

Release Checklist

  1. Run pnpm install --frozen-lockfile.
  2. Run pnpm verify:ci (and pnpm verify:local on machines with pre-cr).
  3. Update CHANGELOG.md.
  4. Confirm package.json version and package metadata.
  5. Create a GitHub release whose v<package-version> tag points to a reviewed commit reachable from main. .github/workflows/publish.yml validates that ancestry and the tag/version match before publishing with npm provenance through trusted publishing (the repo must be configured as a trusted publisher for the package on npmjs.com — no long-lived npm token).