import js from '@eslint/js'; import tseslint from 'typescript-eslint'; import reactHooks from 'eslint-plugin-react-hooks'; import jsxA11y from 'eslint-plugin-jsx-a11y'; import sonarjs from 'eslint-plugin-sonarjs'; import globals from 'globals'; // Named `.mjs` because this package is CommonJS — `eslint.config.js` would be // parsed as CJS and the imports above would fail. // // Policy: every preset is downgraded to advisory, and the rules that actually // fail the build are listed once at the bottom. That way the CI gate is // readable in one place rather than inferred from four presets' defaults. // The reasoning behind the split, and the measurements it rests on, are in // docs/superpowers/specs/2026-08-19-eslint-design.md. /** * Rewrites a preset's enabled rules to `warn`, preserving each rule's options. * Rules the preset explicitly turned off stay off — a preset that disables a * rule means it, and flipping those to `warn` turns the whole of SonarJS's * opt-in catalogue (file headers, naming conventions) into daily noise. */ const advisory = (config) => ({ ...config, rules: Object.fromEntries( Object.entries(config.rules ?? {}).map(([rule, level]) => { const severity = Array.isArray(level) ? level[0] : level; if (severity === 'off' || severity === 0) return [rule, level]; return [rule, Array.isArray(level) ? ['warn', ...level.slice(1)] : 'warn']; }) ), }); export default tseslint.config( { ignores: ['dist/**', 'playwright-report/**', 'test-results/**', 'eslint.config.mjs'] }, ...[ js.configs.recommended, ...tseslint.configs.recommended, sonarjs.configs.recommended, // v7 of this plugin ships the React Compiler rule set alongside the two // classic rules. This is React 18 with no compiler in the build, so those // extra rules advise against a stricter model than the code was written // for — worth seeing (`purity` catches a real `Date.now()` in render), not // worth failing a build over. reactHooks.configs.flat['recommended-latest'], { rules: jsxA11y.flatConfigs.recommended.rules }, ].map(advisory), { plugins: { 'jsx-a11y': jsxA11y } }, { // `tests/` and the Playwright specs are deliberately out of scope for now: // tsconfig.json only includes `src`, so type-aware linting has no program // for them, and widening it is a separate change with its own count. files: ['src/**/*.{ts,tsx}'], languageOptions: { globals: globals.browser, parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname, }, }, rules: { // Hooks called conditionally break React outright. 'react-hooks/rules-of-hooks': 'error', // The stale-closure rule. PR #11 fixed a cart badge that did not update // after account creation, which is precisely what this catches. 'react-hooks/exhaustive-deps': 'error', // An unawaited promise fails silently — the same class of defect as the // unwrapped async routes in #59, and what the project's Playwright notes // already warn about for missing `await`. '@typescript-eslint/no-floating-promises': 'error', // `attributes: false` because `onClick={async () => ...}` is idiomatic // React and safe when the handler catches its own errors. Left at the // default this rule flags every antd button in the admin screens — 25 of // its 28 hits here — and a rule that is 89% noise gets switched off. '@typescript-eslint/no-misused-promises': [ 'error', { checksVoidReturn: { attributes: false } }, ], // A storefront image with no alt text is unusable in a screen reader, and // the fix is one attribute. 'jsx-a11y/alt-text': 'error', }, } );