ESLint plugin
eslint-plugin-kerfjs is a companion ESLint plugin that catches kerf hard-rule violations at edit time, before they reach tsc or the runtime dev-warns. It sits alongside two earlier defense layers shipped by kerf:
| Layer | Catches | When |
|---|---|---|
tsc --noEmit with strict typings |
Most type-shaped bugs (e.g. partial-set against multi-key store state) | Build time |
Opt-in dev-warns (KERF_DEV_WARN_*) |
Rebuilt listeners, untracked signals, narrow set | Runtime |
eslint-plugin-kerfjs |
Inline JSX handlers, missing data-key, nested mount(), global JSX augmentation |
Edit time |
The core syntax rules analyze the AST without TypeScript parser services. The AI-config rule reads the filesystem, and the optional UI rules also resolve catalog and profile artifacts. Consumers configure their own parser; no TypeScript parser service is required.
Install
Section titled “Install”npm install --save-dev eslint-plugin-kerfjsConfigure — flat config (ESLint v9+)
Section titled “Configure — flat config (ESLint v9+)”import kerfjs from "eslint-plugin-kerfjs";import tsParser from "@typescript-eslint/parser";
export default [ { files: ["**/*.ts", "**/*.tsx"], languageOptions: { parser: tsParser, parserOptions: { ecmaFeatures: { jsx: true } }, }, }, kerfjs.configs.recommended,];The package is ESM-only and supports flat config. Legacy .eslintrc plugin
loading is not supported. For UI components, install @kerfjs/ui and add
kerfjs.configs['recommended-ui'] or kerfjs.configs['strict-ui']; these
presets add six catalog, ownership, composition, CSS-value, preference, and
wiring rules to the eight core rules. See the
package README
for their configuration and complete rule table.
Most rules ship as error in the recommended preset (AST-shaped antipatterns are bugs). prefer-attr-selector, no-raw-with-dynamic-arg, and ai-assistant-configs ship as warn — they’re nudges, audit trails, or project-hygiene checks, not correctness bugs.
kerfjs/no-inline-jsx-event-handlers
Section titled “kerfjs/no-inline-jsx-event-handlers”Disallow inline onClick-style JSX event handler attributes on intrinsic (lowercase-tag) elements. Use a data-action attribute and delegate() from the mount root instead.
// ❌<button onClick={save}>Save</button>
// ✅<button data-action="save">Save</button>delegate(rootEl, 'click', '[data-action="save"]', save);kerfjs/require-data-key-in-each
Section titled “kerfjs/require-data-key-in-each”Require data-key (or id) on the root element returned from an each() row render. Without a key, the keyed reconciler matches by position and loses identity, focus, and cursor position on insert / delete.
// ❌each(items, (item) => <li>{item.name}</li>);
// ✅each(items, (item) => <li data-key={item.id}>{item.name}</li>);kerfjs/no-nested-mount
Section titled “kerfjs/no-nested-mount”Disallow mount() calls inside another mount()’s render callback. Composition is via plain functions that return JSX, not nested mount trees.
// ❌mount(root, () => { mount(otherRoot, () => <div />); return <div />;});
// ✅mount(headerRoot, () => <Header />);mount(bodyRoot, () => <Body />);kerfjs/prefer-module-jsx-augmentation
Section titled “kerfjs/prefer-module-jsx-augmentation”Disallow declaration-merging JSX.IntrinsicElements into the global namespace. kerf’s JSX runtime reads custom-element typings from its own module’s JSX namespace, so global augmentations don’t flow through.
// ❌declare global { namespace JSX { interface IntrinsicElements { "my-tag": { foo?: string }; } }}
// ✅declare module "kerfjs/jsx-runtime" { namespace JSX { interface IntrinsicElements { "my-tag": KerfCustomElement & { foo?: string }; } }}kerfjs/prefer-attr-selector
Section titled “kerfjs/prefer-attr-selector”When delegate() / delegateCapture() is called with a literal [name="value"] selector string, nudge toward defining attr('name', 'value') once and passing its .selector — so the JSX ({...spec.attrs}) and the delegate target stay synchronized through a single typed source. Rename-safety; not a correctness rule. Severity: warn.
// ❌ — JSX attribute and selector string are independent literals<button data-action="toggle">Toggle</button>;delegate(root, "click", '[data-action="toggle"]', handler);
// ✅ — one typed constant drives bothconst TOGGLE = attr("data-action", "toggle");<button {...TOGGLE.attrs}>Toggle</button>;delegate(root, "click", TOGGLE.selector, handler);Why these rules, and not more
Section titled “Why these rules, and not more”Rules that need flow analysis (signal reads outside render), call-graph analysis (addEventListener inside the mount tree), or type information (partial-set against multi-key state) are already covered by the opt-in dev-warns and strict TS. Duplicating them in lint would mean either high false-positive rates without type info, or a parserServices dependency that complicates consumer setup.
Source + per-rule docs
Section titled “Source + per-rule docs”eslint-plugin-kerfjs lives in the kerf monorepo under eslint-plugin/. Each rule has a longer docs page with edge cases and what-it-doesn’t-catch notes.