@@ -3,9 +3,7 @@ import tseslint from 'typescript-eslint';
33import eslint from '@eslint/js' ;
44import { FlatCompat } from '@eslint/eslintrc' ;
55import eslintPluginUnicorn from 'eslint-plugin-unicorn' ;
6- import eslintPluginReact from 'eslint-plugin-react' ;
7- import eslintPluginReactHooks from 'eslint-plugin-react-hooks' ;
8- import eslintPluginJsxA11y from 'eslint-plugin-jsx-a11y' ;
6+ import eslintReact from '@eslint-react/eslint-plugin' ;
97import eslintPluginVitest from '@vitest/eslint-plugin' ;
108import eslintPluginImportX from 'eslint-plugin-import-x' ;
119import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' ;
@@ -14,16 +12,19 @@ import { getDefaultCallees } from 'eslint-plugin-better-tailwindcss/defaults';
1412import noBarrelFiles from 'eslint-plugin-no-barrel-files' ;
1513import eslintPluginLodash from 'eslint-plugin-lodash' ;
1614import eslintPluginPlaywright from 'eslint-plugin-playwright' ;
15+ import eslintPluginReactHooks from 'eslint-plugin-react-hooks' ;
16+ import reactYouMightNotNeedAnEffect from 'eslint-plugin-react-you-might-not-need-an-effect' ;
1717import eslintPluginStorybook from 'eslint-plugin-storybook' ;
1818
19+
1920const compat = new FlatCompat ( {
2021 baseDirectory : import . meta. dirname ,
2122} ) ;
2223
2324/** @type {import("eslint").Linter.Config['languageOptions'] } */
2425const languageOptions = {
2526 parserOptions : {
26- project : true ,
27+ projectService : true ,
2728 tsconfigRootDir : import . meta. dirname ,
2829 } ,
2930 ecmaVersion : 'latest' ,
@@ -38,26 +39,14 @@ export default defineConfig(
3839 '.next/**' ,
3940 '.github/**' ,
4041 'bin/**' ,
41- 'static/**' ,
4242 'cypress-coverage/**' ,
4343 'vitest-coverage/**' ,
44- 'src/.storybook/**' ,
45- '.storybook-dist/**' ,
46- 'storybook-static/**' ,
4744 'playwright-report/**' ,
4845 'test-results/**' ,
4946 'public/**' ,
47+ 'src/.storybook/**' ,
5048 '*.svg' ,
51- 'vitest.setup.tsx' ,
52- 'vitest.config.mts' ,
5349 'prettier.config.js' ,
54- 'postcss.config.js' ,
55- 'next-sitemap.config.js' ,
56- 'next.config.ts' ,
57- 'next.config.js' ,
58- 'playwright.config.ts' ,
59- 'sentry.client.config.js' ,
60- 'sentry.server.config.js' ,
6150 ] ) ,
6251
6352 // ── Base configs ──
@@ -98,64 +87,34 @@ export default defineConfig(
9887 } ,
9988 } ,
10089
101- // ── React ──
102- eslintPluginReact . configs . flat [ 'jsx-runtime' ] ,
90+ // ── React (via @eslint-react) ──
10391 {
104- plugins : { 'react-hooks' : eslintPluginReactHooks } ,
105- rules : eslintPluginReactHooks . configs . recommended . rules ,
92+ files : [ '**/*.{ts,tsx}' ] ,
93+ extends : [ eslintReact . configs [ ' recommended-typescript' ] ] ,
10694 } ,
95+
96+ // ── React Hooks / React Compiler (compiler-specific rules only; overlaps with @eslint-react disabled) ──
10797 {
108- files : [ '**/*.{ts,tsx,js,jsx }' ] ,
109- settings : { react : { version : 'detect' } } ,
98+ files : [ '**/*.{ts,tsx}' ] ,
99+ ... eslintPluginReactHooks . configs . flat [ 'recommended-latest' ] ,
110100 rules : {
111- 'react/function-component-definition' : [
112- 'error' ,
113- {
114- namedComponents : [ 'arrow-function' , 'function-declaration' ] ,
115- unnamedComponents : [ 'arrow-function' , 'function-expression' ] ,
116- } ,
117- ] ,
118- 'react/forbid-prop-types' : [ 'error' , { forbid : [ 'any' ] } ] ,
119- 'react/jsx-curly-brace-presence' : [ 'error' , { props : 'never' , children : 'never' } ] ,
120- 'react/jsx-filename-extension' : [ 'error' , { extensions : [ '.js' , '.tsx' ] } ] ,
121- 'react/jsx-max-props-per-line' : [ 'error' , { maximum : 1 , when : 'multiline' } ] ,
122- 'react/no-unescaped-entities' : 'off' ,
123- 'react/jsx-no-target-blank' : 'off' ,
124- 'react/jsx-no-useless-fragment' : [ 'error' , { allowExpressions : true } ] ,
125- 'react/jsx-one-expression-per-line' : 'off' ,
126- 'react/jsx-props-no-spreading' : 'off' ,
127- 'react/no-did-mount-set-state' : 'off' ,
128- 'react/no-unused-prop-types' : 'error' ,
129- 'react/no-unused-state' : 'error' ,
130- 'react/prefer-stateless-function' : 'off' ,
131- 'react/react-in-jsx-scope' : 'off' ,
132- 'react/state-in-constructor' : [ 'error' , 'never' ] ,
133- 'react/static-property-placement' : 'off' ,
101+ ...eslintPluginReactHooks . configs . flat [ 'recommended-latest' ] . rules ,
102+ 'react-hooks/rules-of-hooks' : 'off' ,
103+ 'react-hooks/exhaustive-deps' : 'off' ,
104+ 'react-hooks/use-memo' : 'off' ,
105+ 'react-hooks/component-hook-factories' : 'off' ,
106+ 'react-hooks/set-state-in-effect' : 'off' ,
107+ 'react-hooks/error-boundaries' : 'off' ,
108+ 'react-hooks/purity' : 'off' ,
109+ 'react-hooks/set-state-in-render' : 'off' ,
110+ 'react-hooks/unsupported-syntax' : 'off' ,
134111 } ,
135112 } ,
136113
137- // ── JSX Accessibility ──
138- eslintPluginJsxA11y . flatConfigs . recommended ,
114+ // ── React "You Might Not Need an Effect" ──
139115 {
140- files : [ '**/*.{ts,tsx,js,jsx}' ] ,
141- rules : {
142- 'jsx-a11y/anchor-is-valid' : [
143- 'error' ,
144- {
145- components : [ 'Link' ] ,
146- specialLink : [ 'hrefLeft' , 'hrefRight' ] ,
147- aspects : [ 'invalidHref' , 'preferButton' ] ,
148- } ,
149- ] ,
150- 'jsx-a11y/label-has-associated-control' : [
151- 2 ,
152- {
153- labelComponents : [ 'Label' ] ,
154- labelAttributes : [ 'for' ] ,
155- controlComponents : [ 'Input' , 'Select' ] ,
156- } ,
157- ] ,
158- } ,
116+ files : [ '**/*.{ts,tsx}' ] ,
117+ ...reactYouMightNotNeedAnEffect . configs . recommended ,
159118 } ,
160119
161120 // ── Import-X (replaces eslint-plugin-import) ──
@@ -173,7 +132,6 @@ export default defineConfig(
173132 json : 'always' ,
174133 png : 'always' ,
175134 svg : 'always' ,
176- stories : 'always' ,
177135 } ,
178136 ] ,
179137 'import-x/no-unresolved' : 'off' ,
@@ -232,18 +190,15 @@ export default defineConfig(
232190 } ,
233191 settings : {
234192 'better-tailwindcss' : {
235- entryPoint : './src/common /styles/globals.css' ,
236- callees : [ ...getDefaultCallees ( ) , 'cx ' , 'cva' ] ,
193+ entryPoint : './src/lib /styles/globals.css' ,
194+ callees : [ ...getDefaultCallees ( ) , 'cn ' , 'cva' ] ,
237195 } ,
238196 } ,
239197 } ,
240198
241199 // ── No Barrel Files ──
242200 noBarrelFiles . flat ,
243201
244- // ── Storybook (flat config) ──
245- ...eslintPluginStorybook . configs [ 'flat/recommended' ] ,
246-
247202 // ── CommonJS files ──
248203 {
249204 files : [ '**/*.js' ] ,
@@ -296,12 +251,6 @@ export default defineConfig(
296251 message :
297252 'Please use named imports of "prop-types".\n Example: "import { func } from \'prop-types\';"' ,
298253 } ,
299- {
300- name : 'formik' ,
301- importNames : [ 'Form' ] ,
302- message :
303- 'Please use our Form component to have good defaults defined.\n "import Form from \'@/components/Form/Form\';"' ,
304- } ,
305254 {
306255 name : 'react' ,
307256 importNames : [ 'default' ] ,
@@ -311,13 +260,13 @@ export default defineConfig(
311260 name : 'tailwind-merge' ,
312261 importNames : [ 'twMerge' ] ,
313262 message :
314- 'Please import `cx ` from `@/common /utils/cva .ts` instead of directly from tailwind-merge.' ,
263+ 'Please import `cn ` from `@/lib /utils.ts` instead of directly from tailwind-merge.' ,
315264 } ,
316265 {
317266 name : 'class-variance-authority' ,
318267 importNames : [ 'cx' , 'cva' ] ,
319268 message :
320- 'Please import from `@/common /utils/cva .ts` instead of directly from class-variance-authority.' ,
269+ 'Please import from `@/lib /utils.ts` instead of directly from class-variance-authority.' ,
321270 } ,
322271 ] ,
323272 } ,
@@ -332,10 +281,6 @@ export default defineConfig(
332281 rules : {
333282 'no-restricted-imports' : 'off' ,
334283
335- 'react/prop-types' : 'off' ,
336- 'react/no-array-index-key' : 'off' ,
337- 'react/require-default-props' : 'off' ,
338-
339284 '@typescript-eslint/consistent-type-imports' : 'error' ,
340285 '@typescript-eslint/explicit-module-boundary-types' : 'off' ,
341286 '@typescript-eslint/naming-convention' : [
@@ -377,12 +322,6 @@ export default defineConfig(
377322 name : 'react-select' ,
378323 message : 'Please use `@/components/Form/Select/ThemedReactSelect` instead.' ,
379324 } ,
380- {
381- name : 'formik' ,
382- importNames : [ 'Form' ] ,
383- message :
384- 'Please use our Form component to have good defaults defined.\n "import { Form } from \'@/components/Form/Form\';"' ,
385- } ,
386325 {
387326 name : 'react' ,
388327 importNames : [ 'default' ] ,
@@ -392,13 +331,13 @@ export default defineConfig(
392331 name : 'tailwind-merge' ,
393332 importNames : [ 'twMerge' ] ,
394333 message :
395- 'Please import `cx ` from `@/common /utils/cva .ts` instead of directly from tailwind-merge.' ,
334+ 'Please import `cn ` from `@/lib /utils.ts` instead of directly from tailwind-merge.' ,
396335 } ,
397336 {
398337 name : 'class-variance-authority' ,
399338 importNames : [ 'cx' , 'cva' ] ,
400339 message :
401- 'Please import from `@/common /utils/cva .ts` instead of directly from class-variance-authority.' ,
340+ 'Please import from `@/lib /utils.ts` instead of directly from class-variance-authority.' ,
402341 } ,
403342 ] ,
404343 } ,
@@ -448,6 +387,7 @@ export default defineConfig(
448387 files : [ '**/*.test.ts' , '**/*.test.tsx' ] ,
449388 rules : {
450389 '@typescript-eslint/no-non-null-assertion' : 'off' ,
390+ '@eslint-react/component-hook-factories' : 'off' ,
451391 } ,
452392 } ,
453393
@@ -474,6 +414,9 @@ export default defineConfig(
474414 } ,
475415 } ,
476416
417+ // ── Storybook stories ──
418+ ...eslintPluginStorybook . configs [ 'flat/recommended' ] ,
419+
477420 // ── Prettier (must be last) ──
478421 eslintPluginPrettierRecommended ,
479422) ;
0 commit comments