ESLint & Oxlint
Detect unstable element families, incomplete host props and legacy declarations.
Optional, syntax-aware rules for ESLint and Oxlint. The package is independent of
the Toned runtime, TypeScript and React. One toned plugin provides all rules;
React-specific rule names start with react/, giving IDs such as
toned/react/no-create-elements-in-render.
Installation does not enable any rule automatically.
pnpm add -D @toned/eslint-pluginFor ESLint flat config, add these entries alongside your existing TypeScript/JSX parser configuration:
import toned from '@toned/eslint-plugin'
export default [toned.configs.recommended]For Oxlint 1.69 or newer, configure the JavaScript plugins explicitly:
{
"jsPlugins": ["@toned/eslint-plugin"],
"rules": {
"toned/react/no-create-elements-in-render": "error",
"toned/react/no-partial-host-bag": "error",
"toned/prefer-canonical-declarations": "warn"
}
}| Rule | Checks | Automatic fix |
|---|---|---|
toned/react/no-create-elements-in-render | Proven createElements calls inside named components, hooks, React memo/forwardRef callbacks, and render-time useMemo/useState callbacks | No: hoisting captured values needs a design decision |
toned/react/no-partial-host-bag | A proven useStyles part's style, className, or ref passed alone to an intrinsic or known native host | No: merge caller props with withProps deliberately |
toned/prefer-canonical-declarations | Declaration style/$$type aliases and explicitly generic .variants<Mods>(...) | Renames ordinary keys only; skips duplicate keys, spreads, computed keys, and shorthand |
toned/no-global-config | Proven process-global setConfig calls | No: install an explicit renderer/provider at the application boundary |
toned/prefer-semantic-tokens | Opt-in static raw style properties with configured semantic alternatives | No: lint cannot prove visual equivalence |
Canonical authoring keeps component identities stable and preserves Toned's host ref and interaction props:
import { createElements, useStyles } from '@toned/react'
import type { Variants } from '@toned/core'
import { ui } from './system'
const styles = ui
.stylesheet({ Root: { $kind: 'pressable', opacity: 1 } })
.variants(($: Variants<{ size: 's' | 'l' }>) => ({
[$.size('s')]: { Root: { padding: 2 } },
}))
const S = createElements(styles)
function Button({ size }: { size: 's' | 'l' }) {
return (
<S size={size}>
<S.Root as="button" />
</S>
)
}
function RawButton({ onClick }: { onClick: () => void }) {
const s = useStyles(styles)
return <button {...s.Root.withProps({ onClick })} />
}A standalone <S.Root /> intentionally uses the family's defaults. t(),
useStyles(styles, variants), dynamic/native style expressions, and extracting
.style for a non-host integration such as a date picker's styles API remain
supported. Unknown custom components are not assumed to be hosts. These rules do
not duplicate stylesheet type errors or compiler semantic diagnostics.
The two policy rules are not part of the recommended config. An application can opt in while a compatibility library continues to support the global API:
{
"toned/no-global-config": "error",
"toned/prefer-semantic-tokens": [
"warn",
{
"properties": { "color": "textColor", "fontSize": "typography" }
}
]
}Semantic alternatives are hints supplied by the application, not a universal Toned token vocabulary. Raw values with no equivalent token stay valid; zero, inherited/current/transparent paint, CSS variables, expressions, and native transform objects are excluded. Keep the rule scoped to governed components, or suppress a specific diagnostic with a reason when the raw value is intentional.
Provenance and bounded analysis
Imports from public Toned entries, namespace imports, immutable local aliases, and destructuring are resolved through ESLint's lexical scope graph. Shadowed bindings and unrelated APIs with the same spelling are ignored. Relative reexports are not loaded or guessed. Supply explicit contracts per rule:
{
"toned/prefer-canonical-declarations": [
"warn",
{
"modules": { "./design-system": { "stylesheet": "stylesheet" } }
}
]
}createTonedPlugin(options) accepts a
configuration object or (context) => options for a host-specific wrapper.
Such a wrapper can map known files lexically without evaluating their modules.
Only configure exports whose provenance the application owns.
Alias and declaration traversal stop at 32 levels; declaration walks inspect at most 4096 objects per call. Caches are per rule/file and weakly keyed by lexical bindings. No TypeScript program, cross-file cache, filesystem read, or application execution occurs during lint. Dynamic factories, object mutation, nonliteral rule fragments, and unconfigured reexports are intentionally outside this syntax analysis; the compiler and runtime remain responsible for those cases.
oxlint --format=json emits stable rule IDs and source locations for automated
tooling.
These diagnostics should guide targeted changes, not blind mass replacements.
Tests invoke the pinned Oxlint CLI with isolated source/config files. Run
pnpm --filter @toned/eslint-plugin test from the repository root after
installing the workspace dependencies.