style
esc
  • Loading…

Check itNext.js Style Guide lint kit

Conventions for writing, structuring, formatting and shipping Next.js 16 apps with the App Router, strict TypeScript, Cache Components and StyleX.

Next.js 16.4React 19.3StyleX
Download the kit 95 checks · rule IDs on every finding

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-any is NEXTJS-06.3, consistent-type-imports NEXTJS-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, after next typegen so PageProps and 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-only on queries, the database client and env, process.env outside src/lib/env.ts, 'use cache' scopes without cacheLife / cacheTag or reading cookies(), revalidateTag without a profile, default exports outside route files, kebab-case file names, components named after their file, <Component>Props, StyleX (no className strings, tokens instead of hex and px, stylex.create at module level), <img> and <script>, tests beside their source, TODO links, the next.config.ts flags, .env.example, the security headers and more. checks.json lists 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.