Checks a Next.js project against the Next.js Style Guide and reports every finding with its rule ID, linked to the rule:
src/features/invoices/queries.ts:1:1: MUST NEXTJS-09.1: mark queries.ts with import 'server-only' at the top [chopper]
src/features/invoices/components/invoice-actions.tsx:14:3: MUST NOT NEXTJS-07.8: do not fetch in useEffect; fetch in a Server Component (or a query) and pass the data down [chopper]
src/features/invoices/format.ts:1:1: MUST NEXTJS-05.5: All imports in the declaration are only used as types. Use `import type`. [eslint:@typescript-eslint/consistent-type-imports]
It runs four kinds of check:
- ESLint with the project's
eslint.config.mjs(--format json). Findings from rules that enforce a rule of the guide carry its ID (no-explicit-anyis NEXTJS-06.3,consistent-type-importsNEXTJS-05.5, the StyleX rules NEXTJS-11.9); the rest are NEXTJS-05, since the guide runs ESLint with--max-warnings 0. - Prettier with the project's config (
--list-different). - tsc --noEmit with
--typecheck, afternext typegensoPagePropsand typed routes exist. It is off by default because it is slow. - The guide's own checks for rules the tools do not express:
'use client'placement and scope,server-onlyon queries, the database client and env,process.envoutsidesrc/lib/env.ts,'use cache'scopes withoutcacheLife/cacheTagor readingcookies(),revalidateTagwithout a profile, default exports outside route files, kebab-case file names, components named after their file,<Component>Props, StyleX (noclassNamestrings, tokens instead of hex and px,stylex.createat module level),<img>and<script>, tests beside their source, TODO links, thenext.config.tsflags,.env.example, the security headers and more.checks.jsonlists them all.
The tools come from the project's node_modules/.bin, so install the project's dependencies first (pnpm install --frozen-lockfile): an ESLint flat config imports its plugins and only works where they are installed. The project's own eslint.config.mjs, prettier.config.mjs and tsconfig.json are used; a project without an ESLint or Prettier config gets the kit's copies (the guide's appendix, verbatim), resolved against its node_modules. When a tool is missing the kit says so and runs the rest.
Run it
Needs Python 3.9+, and Node.js with the project's dependencies installed for the tool checks.
python3 chopper_lint.py # the project in the current folder
python3 chopper_lint.py src e2e # some folders
python3 chopper_lint.py --root apps/web # a project in another folder
python3 chopper_lint.py --typecheck --strict # also tsc; fail on SHOULD too
python3 chopper_lint.py --list-checks # what it checks
Run it from the folder with package.json (or pass that folder as --root): file paths, the route checks under src/app, and the project checks (package.json, .nvmrc, tsconfig.json, next.config.ts, .env.example) are relative to it. Folders named node_modules, .next, out, build, dist and coverage are skipped, as are *.d.ts files.
Exit status: 1 when a MUST or MUST NOT rule is broken (any finding with --strict), else 0.
In GitHub Actions
- uses: actions/checkout@v4
- uses: carson-hopper/chopper.dev/content/styleguides/nextjs/lint@main
with:
working-directory: . # the folder with package.json
# typecheck: "true" # also run tsc --noEmit
The action sets up pnpm (from the packageManager field) and Node.js (from .nvmrc), and runs pnpm install --frozen-lockfile in working-directory. If the workflow has already installed the dependencies, pass install: "false". Findings appear as annotations on the pull request. Pin @main to a commit SHA for reproducible runs.
Silencing a finding
On the same line or the line above, with a reason:
const legacy = process.env.LEGACY_FLAG; // chopper-lint: allow NEXTJS-06.12 (read before env.ts loads)
In JSX, as a JSX comment on the line above:
{/* chopper-lint: allow NEXTJS-16.6 (an SVG sprite, not a photo) */}
<img src="/sprite.svg" alt="" />
For a whole file: // chopper-lint: allow-file NEXTJS-06.12. This silences tool findings mapped to that rule too.