mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
c266a5e1a7
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.
149 lines
5.7 KiB
JavaScript
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>.',
|
|
}],
|
|
},
|
|
},
|
|
);
|