Skip to content

Tooling

ESLint & Oxlint

Detect unstable element families, incomplete host props and legacy declarations.

packages/toned-eslint-plugin/README.md ↗Rendered from the package’s Markdown

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.

Terminal
pnpm add -D @toned/eslint-plugin

For ESLint flat config, add these entries alongside your existing TypeScript/JSX parser configuration:

TypeScript
import toned from '@toned/eslint-plugin'

export default [toned.configs.recommended]

For Oxlint 1.69 or newer, configure the JavaScript plugins explicitly:

JSON
{
  "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"
  }
}
RuleChecksAutomatic fix
toned/react/no-create-elements-in-renderProven createElements calls inside named components, hooks, React memo/forwardRef callbacks, and render-time useMemo/useState callbacksNo: hoisting captured values needs a design decision
toned/react/no-partial-host-bagA proven useStyles part's style, className, or ref passed alone to an intrinsic or known native hostNo: merge caller props with withProps deliberately
toned/prefer-canonical-declarationsDeclaration style/$$type aliases and explicitly generic .variants<Mods>(...)Renames ordinary keys only; skips duplicate keys, spreads, computed keys, and shorthand
toned/no-global-configProven process-global setConfig callsNo: install an explicit renderer/provider at the application boundary
toned/prefer-semantic-tokensOpt-in static raw style properties with configured semantic alternativesNo: lint cannot prove visual equivalence

Canonical authoring keeps component identities stable and preserves Toned's host ref and interaction props:

TypeScript
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:

JSON
{
  "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:

JSON
{
  "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.