JSPM

eslint-plugin-client-creep

0.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 10
  • Score
    100M100P100Q40423F
  • License MIT

ESLint plugin to catch accidental client components in Next.js App Router

Package Exports

  • eslint-plugin-client-creep

Readme

eslint-plugin-client-creep

ESLint plugin to catch accidental client components in Next.js App Router.

Companion to client-creep — get the same insights inline in your editor and CI, one file at a time.


Rules

Rule What it catches Auto-detects
no-unnecessary-use-client "use client" files with no hooks, event handlers, or browser APIs ✅ No setup needed
no-client-creep Files dragged into the client graph with no client signals Needs client-creep installed or cache

Install

npm install -D eslint-plugin-client-creep

Setup

ESLint 9 (flat config — eslint.config.js)

import clientCreep from "eslint-plugin-client-creep";

export default [
  clientCreep.configs.recommended,
];

ESLint 8 (legacy config — .eslintrc.js)

module.exports = {
  plugins: ["client-creep"],
  extends: ["plugin:client-creep/recommended-legacy"],
};

Manual rule config

// eslint.config.js
import clientCreep from "eslint-plugin-client-creep";

export default [
  {
    plugins: { "client-creep": clientCreep },
    rules: {
      "client-creep/no-unnecessary-use-client": "warn",
      "client-creep/no-client-creep": "warn",
    },
  },
];

Rule details

no-unnecessary-use-client

Flags "use client" directives in files that have no detectable client-only signals. These boundaries are likely accidents — they force the entire component subtree into the browser bundle for no reason.

Detected signals:

  • React hooks (useState, useEffect, any use[A-Z]*)
  • React.useXxx() namespaced calls
  • Event handler props (onClick, onChange, etc.)
  • Browser globals (window, document, localStorage, etc.)
  • Known client-only packages (framer-motion, @radix-ui/*, @apollo/client, etc.)
// ❌ flagged — no client signals
"use client";
export function Badge({ label }: { label: string }) {
  return <span>{label}</span>;
}

// ✅ fine — has a hook
"use client";
import { useState } from "react";
export function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}

no-client-creep

Flags files that are in the client component graph but have no client-only signals — they're client purely because a parent imported them. These are candidates to be hoisted back to server components.

This rule needs the full import graph, which it gets by:

  1. Reading .client-creep-cache.json from the project root (fastest — pre-generate it)
  2. Running npx client-creep --json automatically if no cache is found

Pre-generate the cache (recommended for CI):

npx client-creep --json > .client-creep-cache.json

Add .client-creep-cache.json to .gitignore or commit it — your choice.

Or add to your package.json scripts:

{
  "scripts": {
    "lint": "npx client-creep --json > .client-creep-cache.json && eslint ."
  }
}

vs. running npx client-creep directly

npx client-creep eslint-plugin-client-creep
Full project summary
Import chain "why" trace
Inline editor warnings
CI lint failure via --ci flag via ESLint
Per-file feedback
Works with eslint --fix future

Use both — client-creep for the full picture, the ESLint plugin for inline feedback as you write.


License

MIT