eslint-plugin-client-creep
v0.1.0
Published
ESLint plugin to catch accidental client components in Next.js App Router
Maintainers
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-creepSetup
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, anyuse[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:
- Reading
.client-creep-cache.jsonfrom the project root (fastest — pre-generate it) - Running
npx client-creep --jsonautomatically if no cache is found
Pre-generate the cache (recommended for CI):
npx client-creep --json > .client-creep-cache.jsonAdd .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
