build(frontend): add ESLint flat config as a gate lane
ESLint 9 flat config, pinned. Narrow by design: the load-bearing rule is react-hooks (rules-of-hooks + exhaustive-deps) — 33 disable comments existed for a rule nothing ran. tsc already owns unused vars / undefined / types, so those are turned off here to report each finding once. no-explicit-any is off: all 19 live in the api.ts god-module that R7 rewrites, and it flips the rule back on then. reportUnusedDisableDirectives is an error, which immediately caught two inert `eslint-disable` comments (App.tsx, AddToPlaylist.tsx) whose effects have stable deps — removed. no-unused-expressions allows the side-effect ternary idiom the codebase already uses (`v.paused ? v.play() : v.pause()`). Errors: 0. Remaining 38 are warnings (28 react-refresh DX, 10 exhaustive-deps) — pre-existing, visible in the lint output, tracked to their epics.
This commit is contained in:
@@ -0,0 +1,53 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import prettier from "eslint-config-prettier";
|
||||||
|
import globals from "globals";
|
||||||
|
|
||||||
|
// Flat config (ESLint 9). Deliberately NARROW: the high-value rule here is
|
||||||
|
// react-hooks/exhaustive-deps — 33 suppressions across the app exist for it, and until this
|
||||||
|
// sprint nothing ran to make them mean anything. tsc already owns unused vars, undefined names
|
||||||
|
// and types (noUnusedLocals/Parameters + strict), so typescript-eslint's non-type-checked
|
||||||
|
// `recommended` is layered on but its overlap with tsc is turned off below to avoid double-reporting
|
||||||
|
// and a churny diff. `eslint-config-prettier` is LAST so formatting rules never fight Prettier.
|
||||||
|
//
|
||||||
|
// reportUnusedDisableDirectives: a `// eslint-disable` that no longer suppresses anything is itself
|
||||||
|
// an error — that is what keeps inert disable comments from re-accumulating (the S1b mandate).
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ["dist", "node_modules", "playwright-report", "test-results", "e2e/.auth"] },
|
||||||
|
{
|
||||||
|
files: ["src/**/*.{ts,tsx}"],
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
globals: { ...globals.browser },
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
},
|
||||||
|
linterOptions: {
|
||||||
|
reportUnusedDisableDirectives: "error",
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||||
|
// tsc already enforces these under strict + noUnusedLocals/Parameters — let it own them so a
|
||||||
|
// finding is reported once, by one tool, and this config stays about hooks + refresh safety.
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"no-undef": "off",
|
||||||
|
// The 19 `any`s all live in the api.ts god-module, which R7 (Frontend API layer) rewrites.
|
||||||
|
// Turning the rule on now would either block the gate on pre-existing debt or scatter 19
|
||||||
|
// suppressions — R7 flips this back to "error" when it types that layer. Tracked, not ignored.
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
// Side-effect ternaries/short-circuits are an established idiom here (`cond ? a() : b()`,
|
||||||
|
// `v.paused ? v.play() : v.pause()`) — allow them rather than rewrite working code to if/else.
|
||||||
|
"@typescript-eslint/no-unused-expressions": [
|
||||||
|
"error",
|
||||||
|
{ allowShortCircuit: true, allowTernary: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prettier,
|
||||||
|
);
|
||||||
Generated
+1406
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,9 @@
|
|||||||
"build": "npm run typecheck:app && npm run typecheck:node && vite build",
|
"build": "npm run typecheck:app && npm run typecheck:node && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"knip": "knip",
|
"knip": "knip",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"e2e": "playwright test"
|
"e2e": "playwright test"
|
||||||
@@ -31,16 +34,24 @@
|
|||||||
"react-i18next": "^14.1.2"
|
"react-i18next": "^14.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "9.39.1",
|
||||||
"@playwright/test": "^1.61.1",
|
"@playwright/test": "^1.61.1",
|
||||||
"@types/node": "^26.1.1",
|
"@types/node": "^26.1.1",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
|
"eslint": "9.39.1",
|
||||||
|
"eslint-config-prettier": "10.1.8",
|
||||||
|
"eslint-plugin-react-hooks": "5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "0.4.24",
|
||||||
|
"globals": "16.5.0",
|
||||||
"knip": "6.27.0",
|
"knip": "6.27.0",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.39",
|
||||||
|
"prettier": "3.6.2",
|
||||||
"tailwindcss": "^3.4.6",
|
"tailwindcss": "^3.4.6",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
|
"typescript-eslint": "8.46.0",
|
||||||
"vite": "^5.3.4",
|
"vite": "^5.3.4",
|
||||||
"vitest": "^3.2.7"
|
"vitest": "^3.2.7"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export default function App() {
|
|||||||
// post-login effect (which knows the account id). Here we only clean the address bar.
|
// post-login effect (which knows the account id). Here we only clean the address bar.
|
||||||
stripUrlParams();
|
stripUrlParams();
|
||||||
}
|
}
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export default function AddToPlaylist({
|
|||||||
// its content — hence height — changes, so the flip-above decision uses the true size.
|
// its content — hence height — changes, so the flip-above decision uses the true size.
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (open) place();
|
if (open) place();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [open, membership.data]);
|
}, [open, membership.data]);
|
||||||
|
|
||||||
function place() {
|
function place() {
|
||||||
|
|||||||
Reference in New Issue
Block a user