@smartive/eslint-config
v10.0.0
Published
ESLint configuration by smartive
Keywords
Readme
@smartive/eslint-config
This package provides smartive's default eslint configuration.
Installation
$ npm install eslint @smartive/eslint-config -DRequires ESLint 10.0.0 or newer — see ESLint 10 is required.
Usage
This package offers three different rule sets, one for plain TypeScript applications, a separate one for React applications and one that works well with Next.js applications (minimum supported version is Next.js v16).
Flat Config (eslint.config.mjs)
import { config } from '@smartive/eslint-config'
// For plain TS applications ..
export default config('typescript');
// .. or React applications
export default config('react');
// .. or Next.js applications
// make sure to add `@next/eslint-plugin-next@16`
// to your devDependencies
export default config('nextjs');NPM scripts
To use eslint add the following to your package.json:
"scripts": {
"lint": "eslint {your source directory}",
"lint:fix": "eslint {your source directory} --fix"
}Included plugins
typescript-eslinteslint-plugin-import-x@eslint-react/eslint-plugin(reactandnextjsrule sets)eslint-plugin-jsx-a11y-x(reactandnextjsrule sets)eslint-plugin-prettier@stylistic/eslint-plugin(reactandnextjsrule sets)@next/eslint-plugin-next(nextjsrule set, as an optional peer dependency)
eslint-config-next, eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-import and
eslint-plugin-jsx-a11y are no longer used.
ESLint 10 is required
@eslint-react/eslint-plugin dropped ESLint 9 in its v3.0.0, which declares a ^10.0.0 peer range. Note
that its peerDependencies say eslint: "*", so npm will not warn you about an older ESLint — it will
simply misbehave at some point.
Next.js without eslint-config-next
eslint-config-next does not work on ESLint 10: it bundles eslint-plugin-react, whose rules all throw
while loading because React version detection calls the context.getFilename() API that ESLint 10
removed, and it parses plain JavaScript with a Babel parser that calls the removed
scopeManager.addGlobals() (vercel/next.js#89764). This
package therefore rebuilds what it provides from ESLint 10 compatible parts, split by where each part
makes sense:
| From eslint-config-next/core-web-vitals | Here | Rule set |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------- |
| 6 jsx-a11y rules, all warn | eslint-plugin-jsx-a11y-x's recommended preset, all error — see Accessibility rules | react |
| import/no-anonymous-default-export | import-x/no-anonymous-default-export | react |
| Babel parser, JSX in .js | the typescript-eslint parser, with JSX enabled in .js, .jsx and .mjs | react |
| eslint-plugin-react, eslint-plugin-react-hooks | ESLint React (see below for what it does not cover) | react |
| @next/next rules, core-web-vitals severities | @next/eslint-plugin-next, taken from its own core-web-vitals config | nextjs |
| jsx-a11y/alt-text also checking <Image> | the same option | nextjs |
| ignores .next/, out/, build/, next-env.d.ts | the same ignores | nextjs |
The accessibility rules are named after the fork, so jsx-a11y/alt-text becomes jsx-a11y-x/alt-text.
@next/eslint-plugin-next is an optional
peer dependency, loaded only when config('nextjs') is called, so the other rule sets do not need it.
ESLint React covers most of eslint-plugin-react and eslint-plugin-react-hooks. What is genuinely lost:
| Rule | Replacement |
| ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| react/jsx-no-duplicate-props | TypeScript reports this as TS17001 |
| react/jsx-uses-react, react/jsx-uses-vars, react/jsx-no-undef | redundant — ESLint 10 tracks JSX references natively |
| react/no-is-mounted, react/require-render-return | class-component patterns, not relevant to modern React |
| react-hooks/config, react-hooks/gating, react-hooks/incompatible-library, react-hooks/preserve-manual-memoization | none — ESLint React does not implement these React Compiler rules |
| react/no-unescaped-entities | none |
Upgrading from v9
nextjs no longer loads eslint-config-next. Replace it in your devDependencies:
$ npm uninstall eslint-config-next
$ npm install -D @next/eslint-plugin-next@16The @next/next rules, their severities and the ignored paths are the same as before. What changes:
Accessibility checks are now errors, and there are more of them.
reactandnextjsswitch oneslint-plugin-jsx-a11y-x's recommended preset with a few rules turned off — 20 rules aterrorand 1 atwarn(see Accessibility rules). That includes the sixjsx-a11yrulesnextjshad aswarnin v9, so a Next.js project that carried those warnings now fails its lint run.reactgains the rest of whatnextjsalready had that is not Next.js-specific:import-x/no-anonymous-default-export(warn), and JSX in plain.jsfiles now parses.react/*,react-hooks/*andimport/*rule ids are gone fromnextjs. They came from the pluginseslint-config-nextregistered, and v9 switched every one of them off anyway. A consuming config that turns one of them back on now fails withCould not find plugin, and aneslint-disablecomment naming one reportsDefinition for rule … was not found. Use the@eslint-react/*orimport-x/*equivalent.Accessibility rules are renamed from
jsx-a11y/*tojsx-a11y-x/*, after the fork that provides them. v9'snextjsregistered them asjsx-a11y/*, so a config line naming one now fails withCould not find plugin "jsx-a11y", and aneslint-disablecomment naming one reportsDefinition for rule … was not found. Renaming the prefix is all it takes; this finds every mention:$ grep -rn 'jsx-a11y/' --exclude-dir=node_modules --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' .settings.react.versionis no longer set, since nothing reads it any more.
Upgrading from v8
The plugin namespace for this package's own rules changed from smartive to @smartive-eslint, so
smartive/forbid-component-props is now @smartive-eslint/forbid-component-props. Nothing about the
rule's behaviour or options changed. The shipped rule sets are updated; what needs changing is every
mention of the old id in consuming projects:
$ grep -rn 'smartive/forbid-component-props' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.mjs' .How each kind of mention fails is worth knowing, because only two of the three are loud:
| Mention | What happens on v9 |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| 'smartive/forbid-component-props': ['warn', …] in a config | ESLint refuses to start: Could not find plugin "smartive" in configuration |
| // eslint-disable-next-line smartive/forbid-component-props | Definition for rule 'smartive/forbid-component-props' was not found, and the suppressed warning comes back |
| 'smartive/forbid-component-props': 'off' in a config | silently ignored — the rule stays on at its default error |
The third is the one to search for: turning a rule off under a namespace that no longer exists is not an error, so the override simply stops taking effect.
The same release also adds three stylistic JSX rules to the react and nextjs
rule sets. They are new errors on code that passed on v8, and all three are auto-fixable.
@smartive-eslint/forbid-component-props is raised from warn to error in the same release. It
reports on exactly the code it reported on before, so nothing new is flagged — but a project that was
carrying those warnings will now fail its lint run. It is not auto-fixable: each report needs either the
prop removed, the component added to allowedFor/allowedForPatterns, or the rule turned down locally.
Upgrading from v7
Expect new errors on code that passed before. ESLint React's recommended-type-checked preset is
considerably broader than the eslint-plugin-react set it replaces, so upgrading turns on 44 checks
that had no predecessor — 14 as error, 30 as warn. These are new findings in unchanged code, not
regressions.
The 14 that fail a build:
| Rule | Catches |
| ----------------------------------------------------- | ------------------------------------------------------------- |
| @eslint-react/no-leaked-conditional-rendering | {count && <Foo />} rendering 0, or crashing on a non-node |
| @eslint-react/no-nested-component-definitions | components declared inside other components |
| @eslint-react/no-nested-lazy-component-declarations | lazy() called inside a component |
| @eslint-react/jsx-no-key-after-spread | key placed after {...props} |
| @eslint-react/jsx-no-namespace | namespaced JSX names |
| @eslint-react/jsx-no-children-prop-with-children | children passed both as prop and as JSX children |
| @eslint-react/dom-no-void-elements-with-children | children on <img>, <br> and friends |
| @eslint-react/dom-no-flush-sync | flushSync, which forces a synchronous re-render |
| @eslint-react/dom-no-use-form-state | the removed useFormState |
| @eslint-react/no-access-state-in-setstate | reading this.state inside setState |
| @eslint-react/rsc-function-definition | invalid server-component function forms |
| no-unassigned-vars | declared, never assigned, still read |
| no-useless-assignment | a value overwritten before it is read |
| preserve-caught-error | rethrowing without passing cause |
The last three are core ESLint rules, new to js.configs.recommended in @eslint/js v10.
The 30 warnings cluster into a few families: effect-cleanup leaks
(@eslint-react/web-api-no-leaked-timeout, -interval, -event-listener, -fetch, and the two
observer rules), React 19 deprecations (no-forward-ref, no-context-provider, no-use-context,
no-clone-element, the no-children-* set), naming conventions (naming-convention-context-name,
-id-name, -ref-name) and @eslint-react/no-array-index-key.
Rule ids also changed, so existing suppressions stop working — silently, since an eslint-disable
naming an unknown rule is simply inert. Anything mentioning react/*, react-hooks/* or import/*
needs rewriting to @eslint-react/*, import-x/* or @smartive-eslint/*.
Accessibility rules
The react and nextjs rule sets use
eslint-plugin-jsx-a11y-x's recommended
preset, with every rule at error. Its rules are named jsx-a11y-x/…, after the plugin. On top of it, two rules the preset leaves out are switched on:
| Rule | Level | Flags |
| ---------------------------------------- | ------- | ------------------------------------------------------- |
| jsx-a11y-x/lang | error | an invalid lang on <html>, such as lang="english" |
| jsx-a11y-x/no-aria-hidden-on-focusable | warn | aria-hidden on something the keyboard can still focus |
and these are switched off:
| Rule | Why |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| jsx-a11y-x/anchor-is-valid | <a onClick> and href="#" are common in existing code, and each fix is a restyle |
| jsx-a11y-x/no-static-element-interactions | a click-to-close modal backdrop is legitimate |
| jsx-a11y-x/click-events-have-key-events | flags the same code as the rule above |
| jsx-a11y-x/no-noninteractive-element-interactions | the same problem on <li>, <h2> and friends |
| jsx-a11y-x/no-noninteractive-tabindex | scrollable containers need tabIndex={0} for keyboard scrolling |
| jsx-a11y-x/media-has-caption | cannot see captions a video player adds |
| jsx-a11y-x/label-has-associated-control | only recognises native controls, so <label>Name <TextField /></label> is reported |
| jsx-a11y-x/no-autofocus | focusing the first field of a dialog is correct |
| jsx-a11y-x/img-redundant-alt | judges wording: alt="Photo booth at the party" is reported for containing "photo" |
| jsx-a11y-x/iframe-has-title | iframes are mostly third-party embeds, rare enough to catch in review |
| jsx-a11y-x/no-distracting-elements | <marquee> and <blink> do not appear in modern React code |
| jsx-a11y-x/mouse-events-have-key-events | also fires when hovering reveals nothing essential, e.g. analytics or prefetching |
| jsx-a11y-x/prefer-tag-over-role | suggests from a lookup table, so <svg role="img"> and <div role="presentation"> are told to become <img> |
control-has-associated-label and anchor-ambiguous-text are off in the preset already. In nextjs, alt-text also checks next/image's <Image>.
jsx-a11y-x/no-redundant-roles stays on, but allows role="list" on <ul> and <ol>: Safari drops the
list semantics of a list styled list-style: none (Tailwind's list-none, for instance), and
role="list" is the standard way to restore them.
Stylistic JSX rules
ESLint Stylistic is registered in the react and nextjs rule sets.
Three rules are enabled:
| Rule | Flags |
| ------------------------------------- | ------------------------------------------------------------------------------------ |
| @stylistic/jsx-curly-brace-presence | <Card label={'x'} /> and <span>{'x'}</span> — braces around a string literal |
| @stylistic/jsx-self-closing-comp | <Card></Card> — a component with no children that does not close itself |
| @stylistic/jsx-pascal-case | <Test_component /> — a component name that is neither PascalCase nor a DOM element |
All three are error and all three are auto-fixable, so eslint --fix clears them in one pass.
jsx-curly-brace-presence is configured with an object and propElementValues: 'always', which is what
its documentation recommends — the brace-less
prop=<Icon /> form the 'never' setting produces is "obscure, and intentionally undocumented". It
would also fight Prettier, which puts the braces straight back, and ESLint reports the pair as a circular
fix.
Custom rules
@smartive-eslint/forbid-component-props
Forbids the given props on components (<Foo />, <Foo.Bar />) while leaving intrinsic elements
(<div />) alone. It replaces react/forbid-component-props, which has no equivalent in ESLint React.
Enabled in the react and nextjs rule sets as:
'@smartive-eslint/forbid-component-props': ['error', { forbid: ['style', 'className'] }]Each entry in forbid is either a prop name or an object that narrows where the prop stays allowed:
| Key | Meaning |
| -------------------- | ---------------------------------------------------------------------------------------- |
| propName | The forbidden prop (required). |
| allowedFor | Component names the prop is still allowed on, matched exactly. |
| allowedForPatterns | Globs (* for any run of characters, ? for one) the component name may match instead. |
| message | Replaces the default report message. |
Component names are matched on their full JSX name, so <Mantine.Button /> matches Mantine.*.
'@smartive-eslint/forbid-component-props': [
'error',
{
forbid: [
{
propName: 'className',
allowedFor: ['NextImage', 'NextLink'],
allowedForPatterns: ['*Icon', 'Mantine*'],
message: 'Avoid using className except NextImage, NextLink, icons and Mantine components',
},
],
},
]Development
$ npm run check-types # type-check the config sources
$ npm run prettier # formatting
$ npm test # build, type-check the tests, then run themThe tests in test/ lint the fixtures in test/fixtures/ with each of the three rule sets and assert that
a given fixture line is flagged. They deliberately never assert on rule ids, so that swapping out a plugin
for an equivalent one does not require rewriting them.
npm testruns the TypeScript test files directly through Node's type stripping, which needs Node 22.18 or newer.
