Code style
The conventions a reviewer will hold you to. The named-props-interface rule, what the formatter owns, and the handful of choices that are not negotiable.
The short version
Formatting is not a discussion. Prettier owns it, ESLint owns correctness, and a review comment about a line break is a bug in the configuration rather than a bug in the change.
What is left is a short list of decisions the tools cannot make:
- A named exported props interface per component. Non-negotiable. The API tables are generated from it.
- Erasable-syntax-only TypeScript in
scripts/*.mts. No enums, no parameter properties, no namespaces; plainnoderuns these by type-stripping. - No raw colour values, anywhere.
- British spelling in prose, American in code identifiers and CSS.
- Named exports. One default export per module is one export you cannot find by searching.
How it works
What the tools own
- Prettier owns formatting, plus Tailwind class ordering. The Tailwind
plugin needs
"tailwindStylesheet": "./app/globals.css"in.prettierrcor class sorting silently does nothing under Tailwind v4. - ESLint runs
eslint-config-nextplus the project rules. Generated output is excluded:.source/,lib/generated/,registry/__index__.ts,public/r/. Without those exclusions a clean clone lints upstream's generated code and reports errors nobody can act on. - TypeScript runs with
strict: true.allowImportingTsExtensionsis on because the.mtsscripts importlib/color/*.tswith the explicit extension Node requires;noEmitmakes it legal.
app/globals.css and app/product.css are in .prettierignore deliberately.
The formatter reflows the linear() spring tokens to one stop per line and
rewrites the block the shadcn CLI produced, which destroys the ability to diff
that file against a fresh shadcn init. In a file whose order is its content,
that is a bad trade.
The conventions the tools cannot check
- One component per file, named for the file. See Naming conventions.
- Props destructured in the signature, with
classNameand...propslast, so a caller'sclassNamereachescn()in the right position. - Early return over nested conditionals. A component with three levels of ternary in its JSX is a component that wants to be two components.
- JSDoc on every public prop. It is not a comment; it becomes the description column of a published table.
- Comments explain why, not what. The what is in the code; the why is the thing that gets lost.
Do this
- Run
pnpm run checkbefore opening a pull request. It runs four things:check:generated,check:ia,check:a11yandcheck:llms. It does not runpnpm typecheckorpnpm lint, and CI runs those as separate steps along withpnpm build, so run those too. The four together are what the pull-request gate asks. - Name the interface
<Component>Propsand export it. This is the one convention that has a visible consequence in public if you get it wrong: an empty API table. - Put
classNamelast in a prop spread you are forwarding. - Prefer a plain function to a class. There are no classes in this
codebase, and adding one to a
.mtsscript would also break type-stripping. - Keep
.mtsscripts dependency-free. They run under plainnodewith no build step; that is a property worth protecting.
Not this
- Do not add a formatter or lint rule for personal preference. Every rule costs everyone.
- Do not use
any.unknownplus a narrowing is the same amount of typing and does not disable the compiler. - Do not use a default export. The exception is where a framework requires one: a Next.js page, a route handler, a config file.
- Do not use an
enum. It is not erasable syntax and it will fail at runtime in a.mtsscript, with an error that does not mention enums. - Do not disable a lint rule for a file. Disable it for a line, with a reason.
- Do not commit a generated file you edited by hand. CI regenerates and diffs.
Gotchas
- Tailwind class sorting no-ops without
tailwindStylesheet. No warning; the classes simply stay as typed and a reviewer notices before the tool does. eslint9.x prints a deprecation notice. Every 9.x release now does, because 10.x islatest. It is a deprecation notice, not a peer warning, and the pin is deliberate. See the note in the rootpackage.json.- A clean clone fails
typecheckbeforepnpm install..source/is gitignored and regenerated bypostinstall, so the type checker points atlib/source.tsfor a problem that is not there. allowImportingTsExtensionswithoutnoEmitis TS5097, and the error points at the import rather than at the configuration.- Prettier and the CSS import order disagree by design. That is why the two stylesheets are ignored; do not "fix" it by removing the ignore.
- A
.mtsscript on Node 20 fails with a syntax error. The version guard at the top of each script exists to turn that into a readable message.
Related
- Naming conventions is the published naming contract.
- TypeScript covers the named-props-interface rule and what the generator does with it.
- Component checklist is where these conventions appear as ship criteria.
- Contributing covers the routes in and the reviews each one needs.
Contributing tokens
Adding or changing a token, and the migration obligation it creates. That obligation exists because a CSS custom property is a public API covered by semver.
Content & language
Why the words are part of the design system when the reader is a patient, and the five rules that apply to every string in the product.