Files
TREK/client/eslint.config.mjs
T
Maurice c266a5e1a7 chore(client): freeze typed promise, hook, size, store and global rules in the ESLint baseline
The client lint ran untyped with two react-hooks rules, so nothing saw a
floating promise, an async handler where a void one is expected, a whole-store
Zustand subscription or a window event bus, all of which client/CLAUDE.md
forbids. eslint.config.mjs now lints with the project service and adds, as
warnings: no-floating-promises, no-misused-promises (void returns in JSX
attributes included), the rest of react-hooks v7's recommended set,
complexity 20, max-depth 4, max-params 5 and max-lines-per-function (120 in a
.ts function, 300 in a .tsx one, whose body also holds its markup),
no-console and no-non-null-assertion. Three local rules in
scripts/lib/eslint-rules.mjs cover what no stock rule sees: an empty
.catch() callback, use*Store() without a selector (the message points to
useShallow, since an object selector without it loops on Zustand 5), and
window.__* globals and window.dispatchEvent with an event of our own.
scripts/eslint-baseline.json takes today's count of each new rule, so the
existing code is frozen, not changed, and each new instance fails
lint:warnings. The CI step gets the 8 GB heap typed linting needs.
2026-10-10 21:32:23 +02:00

149 lines
5.7 KiB
JavaScript

import js from '@eslint/js';
import gitignore from 'eslint-config-flat-gitignore';
import eslintConfigPrettier from 'eslint-config-prettier';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
import trek from './scripts/lib/eslint-rules.mjs';
// Minimal stub so the existing `// eslint-disable-next-line react/no-danger`
// directive in src/i18n/TransHtml.tsx resolves without pulling in the full
// eslint-plugin-react (not a dependency here). The rule is a no-op.
const reactStub = {
rules: {
'no-danger': {
meta: { schema: [] },
create() {
return {};
},
},
},
};
export default tseslint.config(
gitignore({ strict: false }),
{
ignores: [
'node_modules',
'dist',
'coverage',
'public',
'test-results',
'playwright-report',
'e2e/**',
'scripts/**',
'**/*.config.js',
'**/*.config.ts',
'**/*.config.mjs',
],
},
js.configs.recommended,
...tseslint.configs.recommended,
eslintConfigPrettier,
{
files: ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'],
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
react: reactStub,
trek,
},
languageOptions: {
// Typed linting, for the rules that need to know a value is a promise.
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
},
rules: {
'react/no-danger': 'off',
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
// --- Severities tuned to keep CI green on a codebase that was never linted ---
// (each rule below has pre-existing violations; surfaced as warnings, not blockers)
// A hook called conditionally is a crash waiting for the render that skips it.
// The rest of react-hooks v7's recommended set (the React Compiler's checks:
// state set in effects or render, refs read in render, impure renders,
// mutated props) as warnings, each count held by scripts/eslint-baseline.json.
...Object.fromEntries(Object.keys(reactHooks.configs['recommended-latest'].rules).map((rule) => [rule, 'warn'])),
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
],
'@typescript-eslint/no-unused-expressions': 'warn',
'@typescript-eslint/no-unsafe-function-type': 'warn',
'@typescript-eslint/no-this-alias': 'warn',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'warn',
// js.recommended rules with pre-existing hits.
'no-empty': 'warn',
'no-useless-escape': 'warn',
'no-useless-assignment': 'warn',
'preserve-caught-error': 'warn',
// --- Added later as warnings; lint:warnings freezes what exists and fails on each new one ---
// A promise nobody awaits or catches loses its failure, and an async
// handler where a void one is expected (onClick={async () => ...})
// rejects into nowhere.
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-misused-promises': ['warn', { checksVoidReturn: { attributes: true } }],
'trek/no-swallowed-catch': 'warn',
// A `!` silences the null the type admits instead of handling it.
'@typescript-eslint/no-non-null-assertion': 'warn',
// Whole-store subscriptions; the message points to useShallow.
'trek/store-without-selector': 'warn',
// window.__dragData, window.__addToast and window.dispatchEvent buses.
'trek/no-window-globals': 'warn',
'no-console': 'warn',
},
},
{
// Size and branching, in app code only. ESLint's defaults for complexity
// (20) and max-depth (4). Five parameters: a hook with more takes an
// options object. A function body may hold 120 lines in a .ts file, which
// is logic only; a .tsx component also holds its markup, which is long by
// nature, so it gets 300, still well under the god components this is
// meant to stop (LoginPage was one function of 1300 lines).
files: ['src/**/*.{ts,tsx}'],
ignores: ['src/**/*.test.{ts,tsx}'],
rules: {
complexity: ['warn', 20],
'max-depth': ['warn', 4],
'max-params': ['warn', 5],
'max-lines-per-function': ['warn', { max: 120, skipBlankLines: true, skipComments: true, IIFEs: true }],
},
},
{
files: ['src/**/*.tsx'],
ignores: ['src/**/*.test.tsx'],
rules: {
'max-lines-per-function': ['warn', { max: 300, skipBlankLines: true, skipComments: true, IIFEs: true }],
},
},
{
// react-dom/server was worth ~190 KB raw / 57 KB gzip in a chunk three lazy
// routes share — including the Leaflet renderer, which is the default — for
// output that is always a single <svg>. utils/iconMarkup.ts does that job
// without Fizz, and this keeps the import from creeping back: nothing else
// would notice, the build stays green and the app keeps working.
files: ['src/**/*.{ts,tsx}'],
ignores: ['src/**/*.test.{ts,tsx}'],
rules: {
'no-restricted-imports': ['error', {
paths: [{
name: 'react-dom/server',
message: 'Use renderIconMarkup from utils/iconMarkup — Fizz is ~190 KB for one <svg>.',
}],
}],
'no-restricted-syntax': ['error', {
selector: "ImportExpression > Literal[value=/^react-dom\\u002F(server|static)/]",
message: 'Use renderIconMarkup from utils/iconMarkup — Fizz is ~190 KB for one <svg>.',
}],
},
},
);