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 } }, { 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', }, }, { // The Playwright suite. Out of scope until #137, because tsconfig.json // includes only `src` and type-aware linting had no program for these // files. tsconfig.test.json is that program. // // `project` rather than `projectService`: the service resolves a file to the // nearest tsconfig.json, which for tests/ is the one that excludes them, and // every file then errors as not part of a project. files: ['tests/**/*.ts'], languageOptions: { globals: globals.node, parserOptions: { project: ['./tsconfig.test.json'], tsconfigRootDir: import.meta.dirname, }, }, rules: { // The one that matters most here. Playwright's API is almost entirely // promises, and a missing `await` on an assertion does not fail — it // passes, having asserted nothing, which is the worst outcome a test can // have. The project's own Playwright notes already warn about it; this // enforces it. '@typescript-eslint/no-floating-promises': 'error', // Switched off for tests rather than left as warnings. #60's whole // argument is that a gate nobody reads is not a gate, and bringing these // files in scope added 45 warnings of which none were defects. A rule // that cannot be true here is noise that hides the rules that can. // // There is no React in this directory. The hooks rules fire on ordinary // functions whose parameter happens to be named `use` — which Playwright // fixtures are, by its own API. 'react-hooks/rules-of-hooks': 'off', 'react-hooks/exhaustive-deps': 'off', 'react-hooks/set-state-in-effect': 'off', 'react-hooks/purity': 'off', // Test credentials are the point of a test, and the project's own rule is // that they must live only in test paths — which is here. Flagging them // where they belong trains the reader to ignore the rule where they do // not. 'sonarjs/no-hardcoded-passwords': 'off', // Math.random builds unique fixture names so parallel workers do not // collide. Nothing here is a secret, and a cryptographic generator would // say something untrue about what the value is for. 'sonarjs/pseudo-random': 'off', // Page objects hold locators built in the constructor and never // reassigned. Flagging them as mutable props does not apply to a class. 'sonarjs/prefer-read-only-props': 'off', }, } );