Skip to content

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.

Terminal window
npm install --save-dev eslint-plugin-kerfjs
eslint.config.js
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.

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);

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>);

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 />);

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 };
}
}
}

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 both
const TOGGLE = attr("data-action", "toggle");
<button {...TOGGLE.attrs}>Toggle</button>;
delegate(root, "click", TOGGLE.selector, handler);

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.

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.