@gaia-harness/lint 3.0.4 → 3.1.1

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,70 @@
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.
53
+ *
54
+ * Once the list is set, a rule runs only when every entry in its `requires`
55
+ * appears here verbatim. A versioned entry such as `react-router:7.9` is a
56
+ * minimum-version claim ("at least 7.9"), matched as a literal string, never
57
+ * as a range: a project on React Router 8 must still list each lower floor a
58
+ * rule names. So the list carries every versioned entry the plugin uses that
59
+ * GAIA's stack floors satisfy (React Router 8, React 19.3, Tailwind 4, Zod 4),
60
+ * whether or not a curated rule needs it today, so a rule added later finds
61
+ * its floor. A rule whose `requires` is missing from this list stays silent
62
+ * with no error; a test in `shadcn.test.ts` reads each enabled rule's
63
+ * `requires` from the plugin's metadata and fails on any gap.
64
+ *
65
+ * `shadcn`, `base-ui` and `tanstack-query` are listed too: their rules match
66
+ * the components and hooks they check by import, so they stay silent in a
67
+ * project without that library.
68
+ */
69
+ export declare const REACT_DOCTOR_CAPABILITIES: string[];
70
+ //# 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;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,yBAAyB,UAwBrC,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,198 @@ 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
+ "base-ui",
874
+ "react",
875
+ "react:18",
876
+ "react:19",
877
+ "react:19.2",
878
+ "react-router",
879
+ "react-router:6.4",
880
+ "react-router:6.7",
881
+ "react-router:6.9",
882
+ "react-router:7",
883
+ "react-router:7.8",
884
+ "react-router:7.9",
885
+ "react-router:7.10",
886
+ "react-router:8",
887
+ "react-router-framework",
888
+ "shadcn",
889
+ "ssr",
890
+ "tailwind",
891
+ "tailwind:3.4",
892
+ "tailwind:4",
893
+ "tanstack-query",
894
+ "zod",
895
+ "zod:4"
896
+ ];
897
+
706
898
  // src/configs/guardrails.ts
707
899
  var buildSonarConfig = (sourceDir) => [
708
900
  sonarjs.configs.recommended,
@@ -747,6 +939,14 @@ var buildSonarConfig = (sourceDir) => [
747
939
  var regexpConfig = [
748
940
  { ...regexp.configs["flat/recommended"], name: "regexp" }
749
941
  ];
942
+ var reactDoctorGuardrailsConfig = [
943
+ {
944
+ files: ["**/*.ts?(x)", "**/*.js?(x)"],
945
+ name: "react-doctor/guardrails",
946
+ plugins: REACT_DOCTOR_PLUGINS,
947
+ rules: REACT_DOCTOR_GUARDRAIL_RULES
948
+ }
949
+ ];
750
950
  var cnConditionalConfig = [
751
951
  {
752
952
  files: ["**/*.ts?(x)", "**/*.js?(x)"],
@@ -900,6 +1100,7 @@ var buildNoRelativeImportPathsConfig = (sourceDir) => [
900
1100
  var buildGuardrails = (sourceDir) => [
901
1101
  ...buildSonarConfig(sourceDir),
902
1102
  ...regexpConfig,
1103
+ ...reactDoctorGuardrailsConfig,
903
1104
  ...cnConditionalConfig,
904
1105
  ...noEnumConfig,
905
1106
  ...noJsxIifeConfig,
@@ -1054,6 +1255,12 @@ var react = [
1054
1255
  "react/jsx-curly-brace-presence": "error",
1055
1256
  "react/jsx-filename-extension": "off",
1056
1257
  "react/jsx-fragments": "error",
1258
+ // airbnb-extended's non-strict react config turns this off, and nothing
1259
+ // else in the bundle catches a list rendered without `key`.
1260
+ "react/jsx-key": [
1261
+ "error",
1262
+ { checkFragmentShorthand: true, warnOnDuplicates: true }
1263
+ ],
1057
1264
  // off by default because it doesn't handle props onXyz function names correctly
1058
1265
  // turn this on from time to time to check for misnamed handlers elsewhere
1059
1266
  "react/jsx-handler-names": ["off", { checkLocalVariables: true }],
@@ -1127,6 +1334,17 @@ var react = [
1127
1334
  "@eslint-react/web-api-no-leaked-timeout": "error"
1128
1335
  }
1129
1336
  },
1337
+ // Curated react-doctor rules (see `react-doctor.ts`, which groups them with
1338
+ // the reason for each group): SSR and hydration hazards, invalid HTML,
1339
+ // accessibility jsx-a11y misses, side effects in render and state updates,
1340
+ // Effect lifecycle and teardown, render-path performance, reduced motion
1341
+ // and font scaling, and component and hook size and complexity.
1342
+ {
1343
+ files: ["**/*.ts?(x)", "**/*.js?(x)"],
1344
+ name: "react-doctor/react",
1345
+ plugins: REACT_DOCTOR_PLUGINS,
1346
+ rules: REACT_DOCTOR_REACT_RULES
1347
+ },
1130
1348
  {
1131
1349
  files: ["**/*.test.ts?(x)", "**/*.stories.ts?(x)"],
1132
1350
  name: "react/test-files",
@@ -1136,6 +1354,9 @@ var react = [
1136
1354
  }
1137
1355
  ];
1138
1356
 
1357
+ // src/configs/react-router.ts
1358
+ import tanstackQuery from "@tanstack/eslint-plugin-query";
1359
+
1139
1360
  // src/plugins/no-imperative-query-fetch.ts
1140
1361
  var BANNED_METHODS = /* @__PURE__ */ new Map([
1141
1362
  ["ensureInfiniteQueryData", "infiniteQuery"],
@@ -1195,6 +1416,12 @@ var plugin7 = {
1195
1416
  var no_imperative_query_fetch_default = plugin7;
1196
1417
 
1197
1418
  // src/configs/react-router.ts
1419
+ var TANSTACK_QUERY_RECOMMENDED_RULES = Object.assign(
1420
+ {},
1421
+ ...tanstackQuery.configs["flat/recommended"].map(
1422
+ (config) => config.rules
1423
+ )
1424
+ );
1198
1425
  var ROUTE_MODULE_EXPORTS = [
1199
1426
  "action",
1200
1427
  "clientAction",
@@ -1230,13 +1457,56 @@ var buildReactRouter = (sourceDir) => [
1230
1457
  ]
1231
1458
  }
1232
1459
  },
1460
+ // TanStack Query, one block: `no-imperative-query-fetch`, the official
1461
+ // `@tanstack/eslint-plugin-query` recommended rules, and the react-doctor
1462
+ // Query rules the official plugin does not cover (`react-doctor.ts`). The
1463
+ // official and react-doctor rules match hooks and clients imported from
1464
+ // TanStack Query, so they stay silent in a project without Query.
1465
+ // `no-imperative-query-fetch` matches by method name.
1233
1466
  {
1234
1467
  files: [`${sourceDir}/**/*.ts?(x)`],
1235
1468
  name: "react-router/no-imperative-query-fetch",
1236
- plugins: { "no-imperative-query-fetch": no_imperative_query_fetch_default },
1469
+ plugins: {
1470
+ "@tanstack/query": tanstackQuery,
1471
+ "no-imperative-query-fetch": no_imperative_query_fetch_default,
1472
+ ...REACT_DOCTOR_PLUGINS
1473
+ },
1237
1474
  rules: {
1475
+ ...TANSTACK_QUERY_RECOMMENDED_RULES,
1476
+ ...REACT_DOCTOR_QUERY_RULES,
1238
1477
  "no-imperative-query-fetch/no-imperative-query-fetch": "error"
1239
1478
  }
1479
+ },
1480
+ // react-doctor's CLI detects a project's capabilities and hands them to its
1481
+ // rules; the standalone plugin reads them from these settings instead (see
1482
+ // `REACT_DOCTOR_CAPABILITIES`). Route detection matches `app/routes/`,
1483
+ // `app/root.*` and `app/entry.*`.
1484
+ {
1485
+ files: [`${sourceDir}/**/*.ts?(x)`],
1486
+ name: "react-doctor/react-router-settings",
1487
+ settings: {
1488
+ "react-doctor": { capabilities: REACT_DOCTOR_CAPABILITIES }
1489
+ }
1490
+ },
1491
+ // Curated react-doctor rules (see `react-doctor.ts`) for React Router
1492
+ // framework-mode mistakes that throw or misbehave at runtime: loaders,
1493
+ // actions, middleware, sessions, cookies, search params, navigation, error
1494
+ // UI and the server entry.
1495
+ {
1496
+ files: [`${sourceDir}/**/*.ts?(x)`],
1497
+ name: "react-doctor/react-router",
1498
+ plugins: REACT_DOCTOR_PLUGINS,
1499
+ rules: REACT_DOCTOR_REACT_ROUTER_RULES
1500
+ },
1501
+ // A browser global read at module scope crashes the server render. Source
1502
+ // modules only: tests, stories and the helpers in `tests/` folders run in a
1503
+ // browser or a DOM environment, never through SSR.
1504
+ {
1505
+ files: [`${sourceDir}/**/*.ts?(x)`],
1506
+ ignores: ["**/*.test.ts?(x)", "**/*.stories.ts?(x)", "**/tests/**"],
1507
+ name: "react-doctor/react-router-module-scope",
1508
+ plugins: REACT_DOCTOR_PLUGINS,
1509
+ rules: REACT_DOCTOR_MODULE_SCOPE_RULES
1240
1510
  }
1241
1511
  ];
1242
1512
 
@@ -1361,6 +1631,18 @@ var VENDORED_UI_RULES_OFF = {
1361
1631
  "react-you-might-not-need-an-effect/no-external-store-subscription": "off",
1362
1632
  "react-you-might-not-need-an-effect/no-initialize-state": "off",
1363
1633
  "react-you-might-not-need-an-effect/no-pass-data-to-parent": "off",
1634
+ // The react-doctor shadcn rules check how GAIA code composes the primitives,
1635
+ // not the primitives themselves. toast.tsx: ToastClose's default
1636
+ // `render={<Button size="icon-sm" />}` reads as an unlabeled icon Button,
1637
+ // but the aria-label sits on the ToastPrimitive.Close that renders it.
1638
+ ...Object.fromEntries(
1639
+ Object.keys(REACT_DOCTOR_SHADCN_RULES).map((rule) => [rule, "off"])
1640
+ ),
1641
+ // Registry animation, scroll and font classes are shadcn's (toast.tsx
1642
+ // animates without a reduced-motion gate).
1643
+ ...Object.fromEntries(
1644
+ Object.keys(MOTION_FONT_RULES).map((rule) => [rule, "off"])
1645
+ ),
1364
1646
  // sidebar.tsx: SidebarRail's <button> carries no `type`.
1365
1647
  "react/button-has-type": "off",
1366
1648
  // carousel.tsx, chart.tsx, toggle-group.tsx pass an inline object to a
@@ -1435,6 +1717,16 @@ var buildShadcn = (sourceDir, opts) => {
1435
1717
  "shadcn/require-static-classes": "error"
1436
1718
  }
1437
1719
  },
1720
+ // Curated react-doctor rules (see `react-doctor.ts`) for shadcn
1721
+ // composition that breaks a primitive: an icon-only Button with no label,
1722
+ // a DialogContent with no title, a raw control in an InputGroup, a
1723
+ // TabsTrigger outside TabsList.
1724
+ {
1725
+ files: [`${sourceDir}/**/*.${SOURCE_EXTENSIONS}`],
1726
+ name: "react-doctor/shadcn",
1727
+ plugins: REACT_DOCTOR_PLUGINS,
1728
+ rules: REACT_DOCTOR_SHADCN_RULES
1729
+ },
1438
1730
  {
1439
1731
  files: uiFiles,
1440
1732
  name: "shadcn/vendored-ui",
@@ -1562,6 +1854,43 @@ var perfectionistConfig = [
1562
1854
  type: "natural"
1563
1855
  }
1564
1856
  ],
1857
+ // TanStack Query reads option order for type inference: the
1858
+ // `@tanstack/query/*-property-order` rules need `onMutate` before
1859
+ // `onError` and `onSettled`, and `queryFn` before the page-param
1860
+ // getters. Plain natural order puts those keys first, so in the object
1861
+ // passed straight to the TanStack call they sort into a last group
1862
+ // instead, natural order everywhere else in it. Every other object
1863
+ // keeps `recommended-natural`'s sort.
1864
+ "perfectionist/sort-objects": [
1865
+ "error",
1866
+ {
1867
+ customGroups: [
1868
+ {
1869
+ elementNamePattern: "^(onError|onSettled)$",
1870
+ groupName: "after-on-mutate"
1871
+ }
1872
+ ],
1873
+ groups: ["unknown", "after-on-mutate"],
1874
+ order: "asc",
1875
+ type: "natural",
1876
+ useConfigurationIf: { callingFunctionNamePattern: "^useMutation$" }
1877
+ },
1878
+ {
1879
+ customGroups: [
1880
+ {
1881
+ elementNamePattern: "^(getNextPageParam|getPreviousPageParam)$",
1882
+ groupName: "after-query-fn"
1883
+ }
1884
+ ],
1885
+ groups: ["unknown", "after-query-fn"],
1886
+ order: "asc",
1887
+ type: "natural",
1888
+ useConfigurationIf: {
1889
+ callingFunctionNamePattern: "^(infiniteQueryOptions|useInfiniteQuery|useSuspenseInfiniteQuery)$"
1890
+ }
1891
+ },
1892
+ { order: "asc", type: "natural" }
1893
+ ],
1565
1894
  "perfectionist/sort-jsx-props": [
1566
1895
  "error",
1567
1896
  {