@ttoss/eslint-config
@ttoss/eslint-config is a set of rules for ESLint to use on ttoss ecosystem. It uses the new ESLint flat configuration format.
Installation
pnpm add -D @ttoss/eslint-config
Usage
Add the following to your eslint.config.mjs file:
import ttossEslintConfig from '@ttoss/eslint-config';
export default [...ttossEslintConfig];
Relay projects
Relay rules are not in the base config: every one of them operates on
graphql\...`` tagged template literals, so a project without them pays to load
the plugin on every file and gets nothing back. Applications that do use Relay
compose the extra config on top:
import ttossEslintConfig from '@ttoss/eslint-config';
import ttossEslintConfigRelay from '@ttoss/eslint-config/relay';
export default [...ttossEslintConfig, ...ttossEslintConfigRelay];
ESLint 10
This config requires ESLint 10. eslint-plugin-react, eslint-plugin-import and eslint-plugin-jsx-a11y do not run on it (eslint-plugin-react#3977), so they are replaced by @eslint-react/eslint-plugin, eslint-plugin-import-x and eslint-plugin-jsx-a11y-x. When upgrading, rename the rules in your eslint-disable comments:
| Before | After |
|---|---|
import/* | import-x/* |
jsx-a11y/* | jsx-a11y-x/* |
react/display-name | @eslint-react/no-missing-component-display-name |
react/jsx-key | @eslint-react/no-missing-key |
react/no-children-prop | @eslint-react/jsx-no-children-prop |
react/no-unknown-property | @eslint-react/dom-no-unknown-property |
react/jsx-no-target-blank | @eslint-react/dom-no-unsafe-target-blank |
react/prop-types | removed — TypeScript checks props |
The full mapping is in config.js. ESLint 10 also resolves the config file closest to each linted file, so a nested eslint.config.* now applies when linting from the repository root.
react-refresh/only-export-components is no longer part of this config. Fast Refresh only exists in an app's dev server, and libraries export contexts, hooks and metadata next to their components by design. Apps with HMR (Vite, Next.js) can add eslint-plugin-react-refresh to their own config; remove any eslint-disable react-refresh/... comments otherwise.
TypeScript 7
TypeScript 7 is the native compiler, and its npm package no longer exposes the
JavaScript compiler API that typescript-eslint and eslint-plugin-sonarjs
read the AST through. This config therefore depends on TypeScript 6 and pins the
plugins that need that API to it, as the
side-by-side setup
recommends.
Nothing is required of the consumer: build with TypeScript 7 and lint with this
config, and the two resolve independently. Projects still on TypeScript 6 share
the single copy. Because linting parses with the TypeScript 6 API, syntax
introduced after TypeScript 6.0 is not understood until typescript-eslint
supports TypeScript 7 (typescript-eslint#10940).
Rules
Complexity, module sizes, and duplication
To keep code understandable and testable, this config limits cyclomatic complexity, cognitive complexity, and module size, and rejects duplicated branches, identical functions, and assignments that are never read. The rationale is explained in Cognitive Complexity — because testability, understandability, and changeability matter.
For the current rule values, see config.js. Test files (*.spec.ts, *.test.ts, *.spec.tsx, *.test.tsx) get thresholds calibrated for suites rather than a blanket opt-out: file length, callback nesting and cyclomatic complexity are all still enforced, just at limits picked so only genuine outliers report. Depth, parameter count and cognitive complexity apply unchanged. Only two rules are off there — max-lines-per-function, which counts a whole describe block as one function, and no-identical-functions, since repetitive arrange/assert blocks are how a suite stays readable.
Untranslated JSX text
formatjs/no-literal-string-in-jsx applies to packages/*/src/**/*.tsx only — what a package ships. Tests, Storybook stories and the docs sites are allowed hardcoded copy, and scoping it there is what makes it enforceable: repo-wide it reported 2216 times, every one of them hidden by eslint --quiet.
Message ids
formatjs/no-id rejects explicit message ids (and --fix deletes them), because ids are content hashes injected at build time. The exception is *.persisted.messages.{ts,tsx,js}: a message reference stored in a database must keep its id when the copy changes, so those files declare ids by hand (see @ttoss/i18n-core).
What this config cannot enforce
ESLint reads one file at a time and has no coverage or runtime data, so these quality gates need separate tooling: Halstead metrics (an escomplex-based reporter), test coverage and CRAP (Jest coverageThreshold), surviving mutants (Stryker), cross-file duplication (jscpd), and unused exports or modules (Knip).
any is rejected via @typescript-eslint/no-explicit-any. unknown is deliberately allowed — it is the safe counterpart to any, and TypeScript has no alternative for as unknown as T double assertions, Record<string, unknown>, or generic defaults.