Skip to content
HomePagesHomePages template kit

ESLint preset

The authoring-lint preset shipped as `@homepages/eslint-plugin-template` — setup and scope. Per-rule reference lives in `rules/`.

@homepages/eslint-plugin-template is a flat-config ESLint preset carrying every authoring rule the kit enforces one file at a time, under the template-kit/* rule namespace.

Terminal window
npm i -D @homepages/eslint-plugin-template eslint @typescript-eslint/parser
eslint.config.mjs
import kit from "@homepages/eslint-plugin-template";
export default kit;

That is the whole setup — no plugin registration, no parser wiring, no tsconfig. The preset registers its rules and sets languageOptions (parser included) itself, so those two lines are a complete config.

The preset ships as its own package rather than a kit subpath because it needs a different dependency tree: ESLint and the parser, not React. That keeps a linter out of the production install of every app that renders a template — the kit’s main entry is a set of React primitives, and it must not drag one in.

ESLint and the parser are peer dependencies, so you install them yourself; the preset resolves them from your workspace. If you see Cannot find package '@typescript-eslint/parser', that is the install line above missing.

Setup stays two lines, but the preset is not syntax-only: one entry, the correctness tier, sets parserOptions.projectService and reads your workspace’s tsconfig.json. That asks for nothing you do not already have — template-kit check refuses to run without a tsconfig.json extending the kit’s — but it has one consequence worth knowing: a .ts/.tsx file that your tsconfig.json does not include is reported as a parsing error rather than being skipped, and no other rule runs on it. Scaffolded workspaces include templates/**, which is where section code lives; tooling config files (*.config.ts) are excluded from the preset already.

The authoring rules themselves stay syntactic, so they still fire on a file whose types cannot be resolved.

The preset is consumed once at a workspace root and lints every section nested under templates/**; there are no per-template config files. It ships three config entries, matched by glob rather than by directory depth:

Entry Files Rules
template-kit/authoring **/*.ts, **/*.tsx the broadly file-scoped rules — see rules/
template-kit/contract-files **/Renderer.tsx, **/section.ts, **/fixtures.ts no-client-directive-in-contract, no-node-builtins-in-contract
template-kit/renderer **/Renderer.tsx props-from-schema
template-kit/theme-source **/theme.ts, **/theme.test.ts turns no-hex off — the palette file is where a raw colour belongs
template-kit/correctness **/*.ts, **/*.tsx the type-aware correctness tier, below

The contract-files entry lists the three files a section’s contract lives in (Sections and declarations).

Server-vs-client scoping is decided per file, by the "use client" directive prologue — see server vs client for what that means and exactly which rules stop at that boundary.

Eight rules from @typescript-eslint that are not about templates. They are here because each one can put a wrong byte in the bundle a visitor downloads while tsc --strict stays green:

Rule What it stops reaching the page
no-base-to-string String(value) rendering the literal text [object Object]
restrict-template-expressions the same defect inside `${value}` (numbers and booleans are allowed)
restrict-plus-operands "1" + 1 rendering 11
no-misused-promises an async event handler passed where a void return is expected, whose rejection has nowhere to go
no-floating-promises a promise nobody awaits or catches, failing silently in a published page
await-thenable await on a non-promise, so the next line reads a value that is not ready
no-for-in-array for...in over an array, yielding string indices and inherited keys
no-implied-eval code passed as a string to a timer or constructed at runtime, which a real deployment’s CSP blocks

These are the same rules a submitted template is judged by. That is deliberate: what gates a pack on the way in has to be something its author could already run.

House style — import order, naming conventions, banned identifiers — is not here and never will be. Your codebase’s conventions are yours.

Each rule is documented upstream by typescript-eslint; they keep their own @typescript-eslint/* ids in output, so a message resolves to that documentation rather than to this corpus.

Every rule the preset enforces — what it bans, why, and a before/after fix — is documented once, per id, in rules/. Rules scoped to a single file are enforced here, by this preset; rules scoped to the tree — a missing contract file, a slot declared in the schema but unmarked in Renderer.tsx, a CSS budget — cannot be expressed one-file-at-a-time and belong to template-kit check under the same template-kit/<id> namespace. One namespace, two venues: the id printed by either failure resolves to exactly one page.

  • Rule index — every rule id, with a page each, across both venues.
  • Server vs client — how a file is scoped, and which rules stop at that boundary.
  • Run check — the tree-scoped half of the same rule namespace.
  • Versioning — what this package’s version number means, and why it always matches the kit’s.