@gaia-harness/lint 3.0.4 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,17 +67,17 @@ new source root: no per-config override blocks needed.
67
67
  | Property | Shape | Includes | Required? |
68
68
  | ---------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
69
69
  | `base` | `Linter.Config[]` | JS recommended, TypeScript (typescript-eslint), `import-x`, unused-disable-directive reporting at error, `eslint-comments` (every disable states a reason), `prefer-arrow-functions`, lodash/underscore guard, `no-restricted-imports` (bans bare `@conform-to/zod`) | required |
70
- | `react` | `Linter.Config[]` | `eslint-plugin-react`, `react-hooks`, `jsx-a11y`, `react-refresh` (Fast Refresh-safe exports), `react-you-might-not-need-an-effect` (Effect misuse), nine `@eslint-react` rules (Effect cleanup, React 19 forms), GAIA-specific React rules | required for React apps |
71
- | `reactRouter` | `Linter.Config[]` | Relaxations for React Router framework mode, route-module exports allowed by `react-refresh`, plus `no-imperative-query-fetch` (custom). Spread **after** `react`. See [Router-specific rules](#router-specific-rules) | React Router only |
70
+ | `react` | `Linter.Config[]` | `eslint-plugin-react`, `react-hooks`, `jsx-a11y`, `react-refresh` (Fast Refresh-safe exports), `react-you-might-not-need-an-effect` (Effect misuse), nine `@eslint-react` rules (Effect cleanup, React 19 forms), 77 curated `react-doctor` rules (SSR and hydration, HTML validity, accessibility, render and Effect correctness, performance, motion, component size and complexity), GAIA-specific React rules. See [react-doctor rules](#react-doctor-rules) | required for React apps |
71
+ | `reactRouter` | `Linter.Config[]` | Relaxations for React Router framework mode, route-module exports allowed by `react-refresh`, plus a TanStack Query block (`no-imperative-query-fetch` (custom), `@tanstack/eslint-plugin-query` recommended, five `react-doctor` Query rules) and 25 curated `react-doctor` router rules with the capability settings they need. Spread **after** `react`. See [Router-specific rules](#router-specific-rules) | React Router only |
72
72
  | `styleHygiene` | `Linter.Config[]` | `canonical`, `perfectionist`, `unicorn`, `unused-imports`, `check-file` | required |
73
- | `guardrails` | `Linter.Config[]` | `regexp` (recommended, ReDoS included), `no-enum` (custom), `no-switch` (custom), `no-jsx-iife` (custom), `no-null-render` (custom), `no-zod-enum` (custom), `cn-conditional` (custom), `no-relative-import-paths`, `sonarjs`, `import-x`, `prefer-arrow-functions` | required |
73
+ | `guardrails` | `Linter.Config[]` | `regexp` (recommended, ReDoS included), `no-enum` (custom), `no-switch` (custom), `no-jsx-iife` (custom), `no-null-render` (custom), `no-zod-enum` (custom), `cn-conditional` (custom), `no-relative-import-paths`, `sonarjs`, `import-x`, `prefer-arrow-functions`, 14 curated `react-doctor` security and defensive-JS rules | required |
74
74
  | `i18n` | `Linter.Config[]` | `i18next/no-literal-string` on raw JSX text under the source dir. See [i18n and Zod](#i18n-and-zod) | optional |
75
75
  | `zod` | `Linter.Config[]` | `eslint-plugin-zod` recommended, minus `consistent-import`. See [i18n and Zod](#i18n-and-zod) | optional |
76
76
  | `testing` | `Linter.Config[]` | Vitest + jest-dom config scoped to `*.test.*`, `*.stories.*` and `test/` | optional |
77
77
  | `storybook` | `Linter.Config[]` | `eslint-plugin-storybook` scoped to `*.stories.*` and `.storybook/main.*` | optional |
78
78
  | `playwright` | `Linter.Config[]` | `eslint-plugin-playwright` scoped to `.playwright/` | optional |
79
79
  | `prettier` | `Linter.Config[]` | `eslint-config-prettier`, must be **last** to disable formatting rules | required if using Prettier |
80
- | `shadcn` | `(opts?) => Linter.Config[]` | `@shadcn/lint` token rules plus the vendored-ui exemption; see [shadcn factory](#shadcn-factory). Spread **last**, after `prettier`, with the full GAIA composition before it | optional |
80
+ | `shadcn` | `(opts?) => Linter.Config[]` | `@shadcn/lint` token rules, four curated `react-doctor` composition rules, plus the vendored-ui exemption; see [shadcn factory](#shadcn-factory). Spread **last**, after `prettier`, with the full GAIA composition before it | optional |
81
81
  | `betterTailwind` | `(opts) => Linter.Config[]` | `eslint-plugin-better-tailwindcss` factory; takes `entryPoint` (path to Tailwind entry CSS) and optional `ignore` (class names to skip) | optional |
82
82
  | `ignores` | `Iterable<Linter.Config> & ((opts?) => Linter.Config[])` | `includeIgnoreFile` helper plus GAIA defaults. Spread directly for defaults (`...lint.ignores`) or call with options to override (`...lint.ignores({extra: ['.gaia/**']})`). | recommended |
83
83
 
@@ -287,6 +287,29 @@ The `/.react-router/**` glob stays in the `ignores` defaults. It names a
287
287
  directory that only exists in a React Router project, so it costs other
288
288
  projects nothing.
289
289
 
290
+ ## react-doctor rules
291
+
292
+ 125 rules from [`eslint-plugin-react-doctor`](https://www.react.doctor/docs/configuration/eslint-and-oxlint-plugins), chosen one by one; no preset is spread. The presets turn back on rules this config turns off on purpose and carry hundreds of rules for libraries it does not cover. The plugin is pinned to an exact version because its rule set changes between releases, and ESLint fails a config that names a rule the installed plugin no longer ships. Every curated id and severity lives in `src/configs/react-doctor.ts`, grouped with the reason for each group, and a snapshot test pins the full list.
293
+
294
+ | Block | Groups |
295
+ | --- | --- |
296
+ | `react` (77) | SSR and hydration, HTML validity, accessibility, render and state purity, Effect lifecycle, performance (including images and scripts), motion and font, component and hook size and complexity |
297
+ | `reactRouter` (30) | 24 framework-mode rules (loaders, actions, middleware, sessions, cookies, search params, navigation, error UI, the server entry); five Query rules in the Query block; `no-unguarded-browser-global-at-module-scope` on source files only, not tests or stories |
298
+ | `guardrails` (14) | auth tokens in web storage, secrets in client code, dynamic `RegExp` input, path containment, `window.open` without `noopener`, throwing and NaN-producing parses, and similar |
299
+ | `shadcn()` (4) | icon Button label, DialogContent title, InputGroup controls, TabsTrigger inside TabsList |
300
+
301
+ The vendored shadcn ui files skip the four shadcn rules (they check how GAIA code composes the primitives) and the three motion and font rules (registry classes).
302
+
303
+ `no-giant-component` reports a component over 300 lines. `no-high-complexity-react-function` reports a component or `use*` hook over 15 cyclomatic or cognitive complexity. Both thresholds are fixed upstream. They stand in for the sonarjs size and complexity rules, which stay off because they do not model components and hooks.
304
+
305
+ `reactRouter` also sets `settings['react-doctor'].capabilities`. The react-doctor CLI detects these per project; the standalone plugin reads them from settings, and without `react-router-framework` the router rules never recognize a route module's named exports. Once the list is set, a rule runs only when every capability it requires is listed verbatim, so an override that adds a react-doctor rule may need to extend the list.
306
+
307
+ ### TanStack Query
308
+
309
+ `reactRouter`'s Query block also enables `@tanstack/eslint-plugin-query`'s recommended rules, read from its flat preset, beside `no-imperative-query-fetch`. They match TanStack Query imports, so a project without Query sees nothing. react-doctor's `query-no-rest-destructuring`, `query-stable-query-client` and `query-no-void-query-fn` stay off because the official plugin owns them. TanStack's property-order rules need `onMutate` before `onError` and `queryFn` before the page-param getters, which alphabetical order breaks. So in the object passed straight to those calls, `perfectionist/sort-objects` keeps natural order but sorts those late-binding keys into a last group: `onError` and `onSettled` for `useMutation`; `getNextPageParam` and `getPreviousPageParam` for `useInfiniteQuery`, `useSuspenseInfiniteQuery` and `infiniteQueryOptions`. Its autofix lands on the order TanStack requires. `no-void-query-fn` needs type information.
310
+
311
+ `react` also turns `react/jsx-key` back on (error, with `checkFragmentShorthand` and `warnOnDuplicates`): Airbnb's non-strict React config turns it off, and nothing else here catches a list rendered without `key`.
312
+
290
313
  ## i18n and Zod
291
314
 
292
315
  Both are opt-in arrays: spread them only when the project uses the library.
@@ -1 +1 @@
1
- {"version":3,"file":"guardrails.d.ts","sourceRoot":"","sources":["../../src/configs/guardrails.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,QAAQ,CAAC;AAsRnC,eAAO,MAAM,eAAe,GAAI,WAAW,MAAM,KAAG,MAAM,CAAC,MAAM,EAWhE,CAAC"}
1
+ {"version":3,"file":"guardrails.d.ts","sourceRoot":"","sources":["../../src/configs/guardrails.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,QAAQ,CAAC;AA0SnC,eAAO,MAAM,eAAe,GAAI,WAAW,MAAM,KAAG,MAAM,CAAC,MAAM,EAYhE,CAAC"}
@@ -0,0 +1,61 @@
1
+ import type { ESLint, Linter } from 'eslint';
2
+ /**
3
+ * The one `eslint-plugin-react-doctor` registration every block that names a
4
+ * `react-doctor/*` rule spreads into its `plugins`. Flat config accepts the
5
+ * same plugin object in several blocks, so `react`, `reactRouter`, `shadcn()`
6
+ * and `guardrails` each stay self-contained without a redefinition error.
7
+ *
8
+ * Curated rules only, never a preset: the presets re-enable rules GAIA turns
9
+ * off on purpose and carry hundreds of rules for libraries GAIA does not use.
10
+ * The rules take no options, carry no autofix and need no type information,
11
+ * so the same rule ids port unchanged to Oxlint's `oxlint-plugin-react-doctor`.
12
+ *
13
+ * The package is pinned to an exact version. The rule set churns between
14
+ * releases, and ESLint fails a config that names a rule the installed plugin
15
+ * no longer ships, so a bump is a deliberate change with its tests. Every
16
+ * curated rule id lives in this file, grouped by the block that applies it,
17
+ * so a bump is checked against one list.
18
+ */
19
+ export declare const REACT_DOCTOR_PLUGINS: Record<string, ESLint.Plugin>;
20
+ /**
21
+ * `react`: motion that ignores reduced-motion and pixel font sizes that ignore
22
+ * user font scaling. Off in vendored shadcn ui (see `shadcn.ts`).
23
+ */
24
+ export declare const MOTION_FONT_RULES: Linter.RulesRecord;
25
+ export declare const REACT_DOCTOR_REACT_RULES: Linter.RulesRecord;
26
+ /**
27
+ * `reactRouter`: framework-mode mistakes in loaders, actions, middleware,
28
+ * sessions, cookies, search params, navigation and the server entry.
29
+ */
30
+ export declare const REACT_DOCTOR_REACT_ROUTER_RULES: Linter.RulesRecord;
31
+ /**
32
+ * `reactRouter`, in the Query block: TanStack Query misuse the official
33
+ * plugin does not check. Its `no-rest-destructuring`, `stable-query-client`
34
+ * and `no-void-query-fn` own those cases, so react-doctor's copies stay off.
35
+ */
36
+ export declare const REACT_DOCTOR_QUERY_RULES: Linter.RulesRecord;
37
+ /** `reactRouter`: source modules only; a module-scope browser global crashes SSR. */
38
+ export declare const REACT_DOCTOR_MODULE_SCOPE_RULES: Linter.RulesRecord;
39
+ /** `guardrails`: security and defensive-JS classes sonarjs and regexp miss. */
40
+ export declare const REACT_DOCTOR_GUARDRAIL_RULES: Linter.RulesRecord;
41
+ /**
42
+ * `shadcn()`: composition that breaks a shadcn primitive's accessible name,
43
+ * keyboard model or structure. Off in vendored ui, which defines the
44
+ * primitives these check.
45
+ */
46
+ export declare const REACT_DOCTOR_SHADCN_RULES: Linter.RulesRecord;
47
+ /**
48
+ * The capabilities `reactRouter` declares in `settings['react-doctor']`.
49
+ * react-doctor's CLI detects them per project; the standalone plugin reads
50
+ * them from settings. Without `react-router-framework` the react-router rules
51
+ * never recognize a route module's named `loader`, `action` or
52
+ * `ErrorBoundary` exports and report nothing. Once the list is set, a rule
53
+ * runs only when every capability it `requires` appears verbatim (a version
54
+ * entry such as `react-router:7.9` is a floor, not a range), so this names
55
+ * each capability the curated rules require that a React Router
56
+ * framework-mode project on React 19 and Tailwind has. `shadcn` and
57
+ * `tanstack-query` are listed too: their rules match the components and hooks
58
+ * they check by import, so they stay silent in a project without them.
59
+ */
60
+ export declare const REACT_DOCTOR_CAPABILITIES: string[];
61
+ //# sourceMappingURL=react-doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-doctor.d.ts","sourceRoot":"","sources":["../../src/configs/react-doctor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,MAAM,EAAE,MAAM,EAAC,MAAM,QAAQ,CAAC;AAE3C;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAE9D,CAAC;AA+GF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,WAItC,CAAC;AAaF,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,WAS7C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,+BAA+B,EAAE,MAAM,CAAC,WAyBpD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,WAM7C,CAAC;AAEF,qFAAqF;AACrF,eAAO,MAAM,+BAA+B,EAAE,MAAM,CAAC,WAEpD,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,EAAE,MAAM,CAAC,WAejD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC,WAK9C,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,UAerC,CAAC"}
@@ -21,7 +21,10 @@ import type { Linter } from 'eslint';
21
21
  * (`createFileRoute(...)({component: Named})`) and never produces that shape,
22
22
  * so a TanStack project wants the rule left on.
23
23
  *
24
- * It also carries one ADDITIVE rule, `no-imperative-query-fetch`, on every
24
+ * It also carries ADDITIVE rules on every TypeScript file under the source
25
+ * dir: the curated react-doctor React Router and Query rules with the
26
+ * capability settings they need, the `@tanstack/eslint-plugin-query`
27
+ * recommended rules, and `no-imperative-query-fetch`. That last one sits on every
25
28
  * TypeScript file under the source dir. It bans `ensureQueryData`,
26
29
  * `fetchQuery`, `prefetchQuery` and their infinite variants, so a route has
27
30
  * one fetch idiom: `queryClient.query` awaited in a clientLoader, read with
@@ -1 +1 @@
1
- {"version":3,"file":"react-router.d.ts","sourceRoot":"","sources":["../../src/configs/react-router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,QAAQ,CAAC;AA0BnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,gBAAgB,GAAI,WAAW,MAAM,KAAG,MAAM,CAAC,MAAM,EA2BjE,CAAC"}
1
+ {"version":3,"file":"react-router.d.ts","sourceRoot":"","sources":["../../src/configs/react-router.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAS,MAAM,EAAC,MAAM,QAAQ,CAAC;AA4C3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,gBAAgB,GAAI,WAAW,MAAM,KAAG,MAAM,CAAC,MAAM,EAsEjE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/configs/react.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAS,MAAM,EAAC,MAAM,QAAQ,CAAC;AAE3C;;;;;;;;;GASG;AACH;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B;;;CAGjC,CAAC;AAEX,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EA+GhC,CAAC"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/configs/react.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAS,MAAM,EAAC,MAAM,QAAQ,CAAC;AAM3C;;;;;;;;;GASG;AACH;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B;;;CAGjC,CAAC;AAEX,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAgIhC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"shadcn.d.ts","sourceRoot":"","sources":["../../src/configs/shadcn.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAS,MAAM,EAAC,MAAM,QAAQ,CAAC;AAG3C,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B;;;;;;OAMG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAoLF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,OAAO,qBAAqB,KAC3B,MAAM,CAAC,MAAM,EA4Df,CAAC"}
1
+ {"version":3,"file":"shadcn.d.ts","sourceRoot":"","sources":["../../src/configs/shadcn.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAS,MAAM,EAAC,MAAM,QAAQ,CAAC;AAQ3C,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B;;;;;;OAMG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAgMF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,OAAO,qBAAqB,KAC3B,MAAM,CAAC,MAAM,EAsEf,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"style-hygiene.d.ts","sourceRoot":"","sources":["../../src/configs/style-hygiene.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,QAAQ,CAAC;AAmTnC,eAAO,MAAM,iBAAiB,GAAI,WAAW,MAAM,KAAG,MAAM,CAAC,MAAM,EAMlE,CAAC"}
1
+ {"version":3,"file":"style-hygiene.d.ts","sourceRoot":"","sources":["../../src/configs/style-hygiene.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,QAAQ,CAAC;AAyVnC,eAAO,MAAM,iBAAiB,GAAI,WAAW,MAAM,KAAG,MAAM,CAAC,MAAM,EAMlE,CAAC"}
package/dist/index.js CHANGED
@@ -703,6 +703,189 @@ var plugin6 = {
703
703
  };
704
704
  var no_zod_enum_default = plugin6;
705
705
 
706
+ // src/configs/react-doctor.ts
707
+ import reactDoctor from "eslint-plugin-react-doctor";
708
+ var REACT_DOCTOR_PLUGINS = {
709
+ "react-doctor": reactDoctor
710
+ };
711
+ var SSR_RULES = {
712
+ "react-doctor/no-hydration-branch-on-browser-global": "error",
713
+ "react-doctor/no-locale-format-in-render": "warn",
714
+ "react-doctor/no-match-media-in-state-initializer": "warn",
715
+ "react-doctor/no-unguarded-browser-global-in-render-or-hook-init": "error"
716
+ };
717
+ var HTML_RULES = {
718
+ "react-doctor/form-control-requires-name": "warn",
719
+ "react-doctor/html-no-invalid-paragraph-child": "warn",
720
+ "react-doctor/html-no-invalid-table-nesting": "warn",
721
+ "react-doctor/html-no-nested-form": "warn",
722
+ "react-doctor/html-no-nested-interactive": "warn",
723
+ "react-doctor/no-inert-sticky-position": "warn"
724
+ };
725
+ var A11Y_RULES = {
726
+ "react-doctor/data-table-requires-accessible-name": "warn",
727
+ "react-doctor/details-requires-summary": "warn",
728
+ "react-doctor/dialog-has-accessible-name": "warn",
729
+ "react-doctor/empty-table-header": "warn",
730
+ "react-doctor/fieldset-requires-legend": "warn",
731
+ "react-doctor/html-label-has-single-control": "warn",
732
+ "react-doctor/no-aria-invalid-without-description": "warn",
733
+ "react-doctor/no-assertive-status": "warn",
734
+ "react-doctor/no-autoplay-without-muted": "warn",
735
+ "react-doctor/no-blocked-paste": "error",
736
+ "react-doctor/no-disabled-zoom": "error",
737
+ "react-doctor/no-duplicate-static-id-reference": "error",
738
+ "react-doctor/no-focusable-content-in-aria-hidden": "warn",
739
+ "react-doctor/no-hover-only-reveal": "warn",
740
+ "react-doctor/no-invalid-progress-range": "error",
741
+ "react-doctor/no-multiple-main-landmarks": "warn",
742
+ "react-doctor/no-multiple-unlabeled-navigation-landmarks": "warn",
743
+ "react-doctor/no-placeholder-only-field": "warn",
744
+ "react-doctor/no-presentation-role-conflict": "warn",
745
+ "react-doctor/no-responsive-hidden-accessible-name": "warn",
746
+ "react-doctor/no-skipped-heading-level": "warn",
747
+ "react-doctor/no-uninformative-aria-label": "warn",
748
+ "react-doctor/radio-input-missing-name": "warn",
749
+ "react-doctor/role-button-requires-complete-keyboard-activation": "warn"
750
+ };
751
+ var RENDER_STATE_RULES = {
752
+ "react-doctor/no-call-component-as-function": "warn",
753
+ "react-doctor/no-controlled-input-value-without-state-update": "warn",
754
+ "react-doctor/no-create-context-in-render": "error",
755
+ "react-doctor/no-create-ref-in-function-component": "warn",
756
+ "react-doctor/no-default-props": "warn",
757
+ "react-doctor/no-impure-call-at-module-scope": "warn",
758
+ "react-doctor/no-indeterminate-attribute": "warn",
759
+ "react-doctor/no-mutate-queried-dom-node-in-component": "warn",
760
+ "react-doctor/no-mutating-reducer-state": "error",
761
+ "react-doctor/no-nondeterministic-id-value-in-render-body": "warn",
762
+ "react-doctor/no-prop-callback-in-render": "error",
763
+ "react-doctor/no-side-effect-in-state-updater-function": "warn",
764
+ "react-doctor/no-uncontrolled-input": "warn",
765
+ "react-doctor/rerender-functional-setstate": "warn",
766
+ "react-doctor/waapi-animation-in-render": "error"
767
+ };
768
+ var EFFECT_RULES = {
769
+ "react-doctor/debounce-no-cleanup": "warn",
770
+ "react-doctor/effect-raf-loop-needs-cancel": "warn",
771
+ "react-doctor/no-create-object-url-without-revoke": "warn",
772
+ "react-doctor/no-fetch-in-effect": "warn",
773
+ "react-doctor/no-fetch-response-used-without-status-check": "warn",
774
+ "react-doctor/no-loading-flag-reset-outside-finally": "warn",
775
+ "react-doctor/no-promise-then-side-effect-in-effect-without-catch": "warn",
776
+ "react-doctor/no-set-state-after-await-in-effect": "warn",
777
+ "react-doctor/no-stale-timer-ref": "warn",
778
+ "react-doctor/no-unbounded-animation-frame-loop": "warn",
779
+ "react-doctor/no-unowned-async-error-clear": "warn",
780
+ "react-doctor/pointer-capture-needs-cancel-handler": "warn"
781
+ };
782
+ var PERFORMANCE_RULES = {
783
+ "react-doctor/async-parallel": "warn",
784
+ "react-doctor/js-hoist-intl": "warn",
785
+ "react-doctor/no-eager-new-in-use-state-initializer": "warn",
786
+ "react-doctor/no-img-lazy-with-high-fetchpriority": "warn",
787
+ "react-doctor/no-img-without-dimensions": "warn",
788
+ "react-doctor/no-mixed-srcset-descriptors": "warn",
789
+ "react-doctor/no-spread-accumulator-in-reduce": "warn",
790
+ "react-doctor/no-srcset-without-sizes": "warn",
791
+ "react-doctor/rendering-script-defer-async": "warn",
792
+ "react-doctor/rerender-lazy-ref-init": "warn",
793
+ "react-doctor/rerender-lazy-state-init": "warn"
794
+ };
795
+ var MOTION_FONT_RULES = {
796
+ "react-doctor/no-arbitrary-px-font-size": "warn",
797
+ "react-doctor/no-smooth-scroll-without-reduced-motion": "warn",
798
+ "react-doctor/no-ungated-tailwind-animation": "warn"
799
+ };
800
+ var SIZE_RULES = {
801
+ "react-doctor/no-giant-component": "warn",
802
+ "react-doctor/no-high-complexity-react-function": "warn"
803
+ };
804
+ var REACT_DOCTOR_REACT_RULES = {
805
+ ...SSR_RULES,
806
+ ...HTML_RULES,
807
+ ...A11Y_RULES,
808
+ ...RENDER_STATE_RULES,
809
+ ...EFFECT_RULES,
810
+ ...PERFORMANCE_RULES,
811
+ ...MOTION_FONT_RULES,
812
+ ...SIZE_RULES
813
+ };
814
+ var REACT_DOCTOR_REACT_ROUTER_RULES = {
815
+ "react-doctor/react-router-csp-nonce-consistency": "error",
816
+ "react-doctor/react-router-guard-aborted-handle-error": "warn",
817
+ "react-doctor/react-router-internal-route-anchor": "warn",
818
+ "react-doctor/react-router-loader-fetch-forwards-signal": "warn",
819
+ "react-doctor/react-router-loader-parallel-fetch": "warn",
820
+ "react-doctor/react-router-no-catch-middleware-next": "warn",
821
+ "react-doctor/react-router-no-client-module-in-server-render": "error",
822
+ "react-doctor/react-router-no-loader-request-body": "error",
823
+ "react-doctor/react-router-no-middleware-response-body-consumption": "warn",
824
+ "react-doctor/react-router-no-multiple-blockers": "error",
825
+ "react-doctor/react-router-no-multiple-middleware-next": "error",
826
+ "react-doctor/react-router-no-multiple-set-search-params-in-tick": "warn",
827
+ "react-doctor/react-router-no-navigate-in-render": "error",
828
+ "react-doctor/react-router-no-redirect-in-try-catch": "error",
829
+ "react-doctor/react-router-no-route-module-environment-suffix": "error",
830
+ "react-doctor/react-router-no-session-mutation-in-loader": "error",
831
+ "react-doctor/react-router-no-static-cookie-expires": "error",
832
+ "react-doctor/react-router-no-unsynchronized-search-params-mutation": "error",
833
+ "react-doctor/react-router-no-use-loader-data-in-error-ui": "error",
834
+ "react-doctor/react-router-require-root-error-boundary": "error",
835
+ "react-doctor/react-router-resource-link-requires-reload": "error",
836
+ "react-doctor/react-router-return-navigation-promise-in-transition": "warn",
837
+ "react-doctor/react-router-server-middleware-return-response": "error",
838
+ "react-doctor/react-router-session-mutation-requires-commit": "error"
839
+ };
840
+ var REACT_DOCTOR_QUERY_RULES = {
841
+ "react-doctor/query-destructure-result": "warn",
842
+ "react-doctor/query-mutation-missing-invalidation": "warn",
843
+ "react-doctor/query-no-mutation-in-effect-as-read": "warn",
844
+ "react-doctor/query-no-query-in-effect": "warn",
845
+ "react-doctor/query-no-usequery-for-mutation": "warn"
846
+ };
847
+ var REACT_DOCTOR_MODULE_SCOPE_RULES = {
848
+ "react-doctor/no-unguarded-browser-global-at-module-scope": "warn"
849
+ };
850
+ var REACT_DOCTOR_GUARDRAIL_RULES = {
851
+ "react-doctor/auth-token-in-web-storage": "warn",
852
+ "react-doctor/no-document-write": "warn",
853
+ "react-doctor/no-fill-map-element-as-key": "warn",
854
+ "react-doctor/no-non-literal-selector-query-without-try-catch": "warn",
855
+ "react-doctor/no-non-null-assertion-on-maybe-undefined-result": "warn",
856
+ "react-doctor/no-nullish-coalescing-arithmetic-precedence": "warn",
857
+ "react-doctor/no-path-prefix-containment": "warn",
858
+ "react-doctor/no-secrets-in-client-code": "warn",
859
+ "react-doctor/no-sync-xhr": "warn",
860
+ "react-doctor/no-unescaped-dynamic-string-in-regexp": "warn",
861
+ "react-doctor/no-unguarded-numeric-input-parse": "warn",
862
+ "react-doctor/no-unguarded-throwing-parse-call": "warn",
863
+ "react-doctor/no-whole-object-default-losing-per-key-defaults": "warn",
864
+ "react-doctor/window-open-without-noopener": "warn"
865
+ };
866
+ var REACT_DOCTOR_SHADCN_RULES = {
867
+ "react-doctor/shadcn-dialog-content-requires-title": "warn",
868
+ "react-doctor/shadcn-icon-button-requires-label": "warn",
869
+ "react-doctor/shadcn-input-group-no-raw-controls": "warn",
870
+ "react-doctor/shadcn-tabs-trigger-requires-list": "warn"
871
+ };
872
+ var REACT_DOCTOR_CAPABILITIES = [
873
+ "react",
874
+ "react:19",
875
+ "react-router",
876
+ "react-router:6.4",
877
+ "react-router:6.7",
878
+ "react-router:7",
879
+ "react-router:7.8",
880
+ "react-router:7.9",
881
+ "react-router:7.10",
882
+ "react-router-framework",
883
+ "shadcn",
884
+ "ssr",
885
+ "tailwind",
886
+ "tanstack-query"
887
+ ];
888
+
706
889
  // src/configs/guardrails.ts
707
890
  var buildSonarConfig = (sourceDir) => [
708
891
  sonarjs.configs.recommended,
@@ -747,6 +930,14 @@ var buildSonarConfig = (sourceDir) => [
747
930
  var regexpConfig = [
748
931
  { ...regexp.configs["flat/recommended"], name: "regexp" }
749
932
  ];
933
+ var reactDoctorGuardrailsConfig = [
934
+ {
935
+ files: ["**/*.ts?(x)", "**/*.js?(x)"],
936
+ name: "react-doctor/guardrails",
937
+ plugins: REACT_DOCTOR_PLUGINS,
938
+ rules: REACT_DOCTOR_GUARDRAIL_RULES
939
+ }
940
+ ];
750
941
  var cnConditionalConfig = [
751
942
  {
752
943
  files: ["**/*.ts?(x)", "**/*.js?(x)"],
@@ -900,6 +1091,7 @@ var buildNoRelativeImportPathsConfig = (sourceDir) => [
900
1091
  var buildGuardrails = (sourceDir) => [
901
1092
  ...buildSonarConfig(sourceDir),
902
1093
  ...regexpConfig,
1094
+ ...reactDoctorGuardrailsConfig,
903
1095
  ...cnConditionalConfig,
904
1096
  ...noEnumConfig,
905
1097
  ...noJsxIifeConfig,
@@ -1054,6 +1246,12 @@ var react = [
1054
1246
  "react/jsx-curly-brace-presence": "error",
1055
1247
  "react/jsx-filename-extension": "off",
1056
1248
  "react/jsx-fragments": "error",
1249
+ // airbnb-extended's non-strict react config turns this off, and nothing
1250
+ // else in the bundle catches a list rendered without `key`.
1251
+ "react/jsx-key": [
1252
+ "error",
1253
+ { checkFragmentShorthand: true, warnOnDuplicates: true }
1254
+ ],
1057
1255
  // off by default because it doesn't handle props onXyz function names correctly
1058
1256
  // turn this on from time to time to check for misnamed handlers elsewhere
1059
1257
  "react/jsx-handler-names": ["off", { checkLocalVariables: true }],
@@ -1127,6 +1325,17 @@ var react = [
1127
1325
  "@eslint-react/web-api-no-leaked-timeout": "error"
1128
1326
  }
1129
1327
  },
1328
+ // Curated react-doctor rules (see `react-doctor.ts`, which groups them with
1329
+ // the reason for each group): SSR and hydration hazards, invalid HTML,
1330
+ // accessibility jsx-a11y misses, side effects in render and state updates,
1331
+ // Effect lifecycle and teardown, render-path performance, reduced motion
1332
+ // and font scaling, and component and hook size and complexity.
1333
+ {
1334
+ files: ["**/*.ts?(x)", "**/*.js?(x)"],
1335
+ name: "react-doctor/react",
1336
+ plugins: REACT_DOCTOR_PLUGINS,
1337
+ rules: REACT_DOCTOR_REACT_RULES
1338
+ },
1130
1339
  {
1131
1340
  files: ["**/*.test.ts?(x)", "**/*.stories.ts?(x)"],
1132
1341
  name: "react/test-files",
@@ -1136,6 +1345,9 @@ var react = [
1136
1345
  }
1137
1346
  ];
1138
1347
 
1348
+ // src/configs/react-router.ts
1349
+ import tanstackQuery from "@tanstack/eslint-plugin-query";
1350
+
1139
1351
  // src/plugins/no-imperative-query-fetch.ts
1140
1352
  var BANNED_METHODS = /* @__PURE__ */ new Map([
1141
1353
  ["ensureInfiniteQueryData", "infiniteQuery"],
@@ -1195,6 +1407,12 @@ var plugin7 = {
1195
1407
  var no_imperative_query_fetch_default = plugin7;
1196
1408
 
1197
1409
  // src/configs/react-router.ts
1410
+ var TANSTACK_QUERY_RECOMMENDED_RULES = Object.assign(
1411
+ {},
1412
+ ...tanstackQuery.configs["flat/recommended"].map(
1413
+ (config) => config.rules
1414
+ )
1415
+ );
1198
1416
  var ROUTE_MODULE_EXPORTS = [
1199
1417
  "action",
1200
1418
  "clientAction",
@@ -1230,13 +1448,56 @@ var buildReactRouter = (sourceDir) => [
1230
1448
  ]
1231
1449
  }
1232
1450
  },
1451
+ // TanStack Query, one block: `no-imperative-query-fetch`, the official
1452
+ // `@tanstack/eslint-plugin-query` recommended rules, and the react-doctor
1453
+ // Query rules the official plugin does not cover (`react-doctor.ts`). The
1454
+ // official and react-doctor rules match hooks and clients imported from
1455
+ // TanStack Query, so they stay silent in a project without Query.
1456
+ // `no-imperative-query-fetch` matches by method name.
1233
1457
  {
1234
1458
  files: [`${sourceDir}/**/*.ts?(x)`],
1235
1459
  name: "react-router/no-imperative-query-fetch",
1236
- plugins: { "no-imperative-query-fetch": no_imperative_query_fetch_default },
1460
+ plugins: {
1461
+ "@tanstack/query": tanstackQuery,
1462
+ "no-imperative-query-fetch": no_imperative_query_fetch_default,
1463
+ ...REACT_DOCTOR_PLUGINS
1464
+ },
1237
1465
  rules: {
1466
+ ...TANSTACK_QUERY_RECOMMENDED_RULES,
1467
+ ...REACT_DOCTOR_QUERY_RULES,
1238
1468
  "no-imperative-query-fetch/no-imperative-query-fetch": "error"
1239
1469
  }
1470
+ },
1471
+ // react-doctor's CLI detects a project's capabilities and hands them to its
1472
+ // rules; the standalone plugin reads them from these settings instead (see
1473
+ // `REACT_DOCTOR_CAPABILITIES`). Route detection matches `app/routes/`,
1474
+ // `app/root.*` and `app/entry.*`.
1475
+ {
1476
+ files: [`${sourceDir}/**/*.ts?(x)`],
1477
+ name: "react-doctor/react-router-settings",
1478
+ settings: {
1479
+ "react-doctor": { capabilities: REACT_DOCTOR_CAPABILITIES }
1480
+ }
1481
+ },
1482
+ // Curated react-doctor rules (see `react-doctor.ts`) for React Router
1483
+ // framework-mode mistakes that throw or misbehave at runtime: loaders,
1484
+ // actions, middleware, sessions, cookies, search params, navigation, error
1485
+ // UI and the server entry.
1486
+ {
1487
+ files: [`${sourceDir}/**/*.ts?(x)`],
1488
+ name: "react-doctor/react-router",
1489
+ plugins: REACT_DOCTOR_PLUGINS,
1490
+ rules: REACT_DOCTOR_REACT_ROUTER_RULES
1491
+ },
1492
+ // A browser global read at module scope crashes the server render. Source
1493
+ // modules only: tests, stories and the helpers in `tests/` folders run in a
1494
+ // browser or a DOM environment, never through SSR.
1495
+ {
1496
+ files: [`${sourceDir}/**/*.ts?(x)`],
1497
+ ignores: ["**/*.test.ts?(x)", "**/*.stories.ts?(x)", "**/tests/**"],
1498
+ name: "react-doctor/react-router-module-scope",
1499
+ plugins: REACT_DOCTOR_PLUGINS,
1500
+ rules: REACT_DOCTOR_MODULE_SCOPE_RULES
1240
1501
  }
1241
1502
  ];
1242
1503
 
@@ -1361,6 +1622,18 @@ var VENDORED_UI_RULES_OFF = {
1361
1622
  "react-you-might-not-need-an-effect/no-external-store-subscription": "off",
1362
1623
  "react-you-might-not-need-an-effect/no-initialize-state": "off",
1363
1624
  "react-you-might-not-need-an-effect/no-pass-data-to-parent": "off",
1625
+ // The react-doctor shadcn rules check how GAIA code composes the primitives,
1626
+ // not the primitives themselves. toast.tsx: ToastClose's default
1627
+ // `render={<Button size="icon-sm" />}` reads as an unlabeled icon Button,
1628
+ // but the aria-label sits on the ToastPrimitive.Close that renders it.
1629
+ ...Object.fromEntries(
1630
+ Object.keys(REACT_DOCTOR_SHADCN_RULES).map((rule) => [rule, "off"])
1631
+ ),
1632
+ // Registry animation, scroll and font classes are shadcn's (toast.tsx
1633
+ // animates without a reduced-motion gate).
1634
+ ...Object.fromEntries(
1635
+ Object.keys(MOTION_FONT_RULES).map((rule) => [rule, "off"])
1636
+ ),
1364
1637
  // sidebar.tsx: SidebarRail's <button> carries no `type`.
1365
1638
  "react/button-has-type": "off",
1366
1639
  // carousel.tsx, chart.tsx, toggle-group.tsx pass an inline object to a
@@ -1435,6 +1708,16 @@ var buildShadcn = (sourceDir, opts) => {
1435
1708
  "shadcn/require-static-classes": "error"
1436
1709
  }
1437
1710
  },
1711
+ // Curated react-doctor rules (see `react-doctor.ts`) for shadcn
1712
+ // composition that breaks a primitive: an icon-only Button with no label,
1713
+ // a DialogContent with no title, a raw control in an InputGroup, a
1714
+ // TabsTrigger outside TabsList.
1715
+ {
1716
+ files: [`${sourceDir}/**/*.${SOURCE_EXTENSIONS}`],
1717
+ name: "react-doctor/shadcn",
1718
+ plugins: REACT_DOCTOR_PLUGINS,
1719
+ rules: REACT_DOCTOR_SHADCN_RULES
1720
+ },
1438
1721
  {
1439
1722
  files: uiFiles,
1440
1723
  name: "shadcn/vendored-ui",
@@ -1562,6 +1845,43 @@ var perfectionistConfig = [
1562
1845
  type: "natural"
1563
1846
  }
1564
1847
  ],
1848
+ // TanStack Query reads option order for type inference: the
1849
+ // `@tanstack/query/*-property-order` rules need `onMutate` before
1850
+ // `onError` and `onSettled`, and `queryFn` before the page-param
1851
+ // getters. Plain natural order puts those keys first, so in the object
1852
+ // passed straight to the TanStack call they sort into a last group
1853
+ // instead, natural order everywhere else in it. Every other object
1854
+ // keeps `recommended-natural`'s sort.
1855
+ "perfectionist/sort-objects": [
1856
+ "error",
1857
+ {
1858
+ customGroups: [
1859
+ {
1860
+ elementNamePattern: "^(onError|onSettled)$",
1861
+ groupName: "after-on-mutate"
1862
+ }
1863
+ ],
1864
+ groups: ["unknown", "after-on-mutate"],
1865
+ order: "asc",
1866
+ type: "natural",
1867
+ useConfigurationIf: { callingFunctionNamePattern: "^useMutation$" }
1868
+ },
1869
+ {
1870
+ customGroups: [
1871
+ {
1872
+ elementNamePattern: "^(getNextPageParam|getPreviousPageParam)$",
1873
+ groupName: "after-query-fn"
1874
+ }
1875
+ ],
1876
+ groups: ["unknown", "after-query-fn"],
1877
+ order: "asc",
1878
+ type: "natural",
1879
+ useConfigurationIf: {
1880
+ callingFunctionNamePattern: "^(infiniteQueryOptions|useInfiniteQuery|useSuspenseInfiniteQuery)$"
1881
+ }
1882
+ },
1883
+ { order: "asc", type: "natural" }
1884
+ ],
1565
1885
  "perfectionist/sort-jsx-props": [
1566
1886
  "error",
1567
1887
  {