@gaia-harness/lint 0.0.0-stage → 3.0.4

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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +514 -2
  3. package/dist/configs/base.d.ts +3 -0
  4. package/dist/configs/base.d.ts.map +1 -0
  5. package/dist/configs/better-tailwind.d.ts +17 -0
  6. package/dist/configs/better-tailwind.d.ts.map +1 -0
  7. package/dist/configs/guardrails.d.ts +3 -0
  8. package/dist/configs/guardrails.d.ts.map +1 -0
  9. package/dist/configs/i18n.d.ts +17 -0
  10. package/dist/configs/i18n.d.ts.map +1 -0
  11. package/dist/configs/ignores.d.ts +25 -0
  12. package/dist/configs/ignores.d.ts.map +1 -0
  13. package/dist/configs/playwright.d.ts +10 -0
  14. package/dist/configs/playwright.d.ts.map +1 -0
  15. package/dist/configs/prettier.d.ts +11 -0
  16. package/dist/configs/prettier.d.ts.map +1 -0
  17. package/dist/configs/react-router.d.ts +37 -0
  18. package/dist/configs/react-router.d.ts.map +1 -0
  19. package/dist/configs/react.d.ts +23 -0
  20. package/dist/configs/react.d.ts.map +1 -0
  21. package/dist/configs/restricted-imports.d.ts +29 -0
  22. package/dist/configs/restricted-imports.d.ts.map +1 -0
  23. package/dist/configs/shadcn.d.ts +59 -0
  24. package/dist/configs/shadcn.d.ts.map +1 -0
  25. package/dist/configs/storybook.d.ts +9 -0
  26. package/dist/configs/storybook.d.ts.map +1 -0
  27. package/dist/configs/style-hygiene.d.ts +3 -0
  28. package/dist/configs/style-hygiene.d.ts.map +1 -0
  29. package/dist/configs/testing.d.ts +10 -0
  30. package/dist/configs/testing.d.ts.map +1 -0
  31. package/dist/configs/zod.d.ts +16 -0
  32. package/dist/configs/zod.d.ts.map +1 -0
  33. package/dist/index.d.ts +62 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +1938 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/plugins/cn-conditional.d.ts +4 -0
  38. package/dist/plugins/cn-conditional.d.ts.map +1 -0
  39. package/dist/plugins/no-enum.d.ts +4 -0
  40. package/dist/plugins/no-enum.d.ts.map +1 -0
  41. package/dist/plugins/no-imperative-query-fetch.d.ts +4 -0
  42. package/dist/plugins/no-imperative-query-fetch.d.ts.map +1 -0
  43. package/dist/plugins/no-jsx-iife.d.ts +4 -0
  44. package/dist/plugins/no-jsx-iife.d.ts.map +1 -0
  45. package/dist/plugins/no-null-render.d.ts +4 -0
  46. package/dist/plugins/no-null-render.d.ts.map +1 -0
  47. package/dist/plugins/no-switch.d.ts +4 -0
  48. package/dist/plugins/no-switch.d.ts.map +1 -0
  49. package/dist/plugins/no-zod-enum.d.ts +4 -0
  50. package/dist/plugins/no-zod-enum.d.ts.map +1 -0
  51. package/dist/prettier.d.ts +4 -0
  52. package/dist/prettier.d.ts.map +1 -0
  53. package/dist/prettier.js +15 -0
  54. package/dist/prettier.js.map +1 -0
  55. package/dist/stylelint.d.ts +4 -0
  56. package/dist/stylelint.d.ts.map +1 -0
  57. package/dist/stylelint.js +36 -0
  58. package/dist/stylelint.js.map +1 -0
  59. package/package.json +116 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steven Sacks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,515 @@
1
- # Temporary Holding Version
1
+ # @gaia-harness/lint
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ GAIA's lint configuration.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pnpm add -D @gaia-harness/lint eslint prettier typescript
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ The block below is the actual `eslint.config.mjs` shipped by GAIA. Copy it
14
+ verbatim and adjust the override block at the bottom for your project.
15
+
16
+ ```js
17
+ import gaiaLint from '@gaia-harness/lint';
18
+ import {defineConfig} from 'eslint/config';
19
+
20
+ const lint = gaiaLint();
21
+
22
+ export default defineConfig([
23
+ ...lint.ignores,
24
+ ...lint.base,
25
+ ...lint.react,
26
+ ...lint.reactRouter,
27
+ ...lint.testing,
28
+ ...lint.storybook,
29
+ ...lint.playwright,
30
+ ...lint.styleHygiene,
31
+ ...lint.guardrails,
32
+ ...lint.i18n, // opt-in; drop if the UI is not translated
33
+ ...lint.zod, // opt-in; Zod 4 projects
34
+ ...lint.betterTailwind({entryPoint: './app/styles/tailwind.css'}),
35
+ ...lint.prettier,
36
+ ...lint.shadcn({ui: '~/components/ui'}), // opt-in; must be last
37
+ ]);
38
+ ```
39
+
40
+ Drop the `reactRouter` line if you are not on React Router framework mode.
41
+ See [Router-specific rules](#router-specific-rules). Drop the `shadcn` line if
42
+ you do not use shadcn/ui; see [shadcn factory](#shadcn-factory). Drop the
43
+ `i18n` or `zod` line if the project does not translate its UI or does not use
44
+ Zod; see [i18n and Zod](#i18n-and-zod).
45
+
46
+ ## Factory options
47
+
48
+ `gaiaLint(opts?)` returns a bundle of config blocks. Call it once at the
49
+ top of `eslint.config.mjs` and spread the returned configs into
50
+ `defineConfig`.
51
+
52
+ | Option | Type | Default | Description |
53
+ | ----------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
54
+ | `sourceDir` | `string` | `'app'` | Project source directory (relative to repo root). Used to scope filename conventions, hook-folder rules, and the `no-relative-import-paths` root path. |
55
+
56
+ Non-GAIA projects that store source under `src/` (or any other path):
57
+
58
+ ```js
59
+ const lint = gaiaLint({sourceDir: 'src'});
60
+ ```
61
+
62
+ That single call rebinds `base`, `reactRouter`, `styleHygiene`, and `guardrails` to the
63
+ new source root: no per-config override blocks needed.
64
+
65
+ ## Bundle shape
66
+
67
+ | Property | Shape | Includes | Required? |
68
+ | ---------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
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 |
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 |
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
+ | `zod` | `Linter.Config[]` | `eslint-plugin-zod` recommended, minus `consistent-import`. See [i18n and Zod](#i18n-and-zod) | optional |
76
+ | `testing` | `Linter.Config[]` | Vitest + jest-dom config scoped to `*.test.*`, `*.stories.*` and `test/` | optional |
77
+ | `storybook` | `Linter.Config[]` | `eslint-plugin-storybook` scoped to `*.stories.*` and `.storybook/main.*` | optional |
78
+ | `playwright` | `Linter.Config[]` | `eslint-plugin-playwright` scoped to `.playwright/` | optional |
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 |
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
+ | `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
+
84
+ ## Override patterns
85
+
86
+ Flat config is last-write-wins. Append override blocks **after** the
87
+ gaia-lint spreads to disable, change, or scope rules.
88
+
89
+ ### Disable a rule globally
90
+
91
+ ```js
92
+ export default defineConfig([
93
+ ...lint.base,
94
+ ...lint.react,
95
+ {rules: {'sonarjs/cognitive-complexity': 'off'}},
96
+ ]);
97
+ ```
98
+
99
+ ### Override on specific globs
100
+
101
+ ```js
102
+ export default defineConfig([
103
+ ...lint.base,
104
+ ...lint.react,
105
+ {
106
+ files: ['app/legacy/**'],
107
+ rules: {'sonarjs/cognitive-complexity': 'off'},
108
+ },
109
+ ]);
110
+ ```
111
+
112
+ ### Swap parser options
113
+
114
+ ```js
115
+ export default defineConfig([
116
+ ...lint.base,
117
+ {
118
+ languageOptions: {
119
+ parserOptions: {project: './tsconfig.eslint.json'},
120
+ },
121
+ },
122
+ ]);
123
+ ```
124
+
125
+ ### Add an extra plugin
126
+
127
+ ```js
128
+ import myPlugin from 'eslint-plugin-my-plugin';
129
+
130
+ export default defineConfig([
131
+ ...lint.base,
132
+ ...lint.react,
133
+ {
134
+ plugins: {'my-plugin': myPlugin},
135
+ rules: {'my-plugin/some-rule': 'error'},
136
+ },
137
+ ]);
138
+ ```
139
+
140
+ ## Peer dependencies
141
+
142
+ `eslint`, `prettier`, and `typescript` are declared as **peer
143
+ dependencies** so consumers control their own versions and a single resolved
144
+ copy of each is installed in `node_modules`. Every other plugin
145
+ (`eslint-plugin-react`, `typescript-eslint`, `eslint-plugin-import-x`, etc.)
146
+ ships as a direct `dependency` of `@gaia-harness/lint` so consumers don't
147
+ have to install or upgrade them individually.
148
+
149
+ Supported versions:
150
+
151
+ - `eslint ^9.30.0` (`@shadcn/lint` needs 9.30 or later)
152
+ - `prettier ^3.0.0`
153
+ - `typescript ^5.0.0 || ^6.0.0`
154
+
155
+ ## Custom rules included
156
+
157
+ These rules are implemented inside this package and ship as part of
158
+ `guardrails`.
159
+
160
+ ### `no-enum`
161
+
162
+ Forbids TypeScript `enum` declarations. Enums emit runtime code, are not
163
+ tree-shakeable, conflate value and type space, and have well-known footguns
164
+ around numeric vs string enums and reverse mappings. Use a `const` object
165
+ plus a derived union type instead.
166
+
167
+ Opt out for a file or block:
168
+
169
+ ```js
170
+ {files: ['src/legacy/**'], rules: {'no-enum/no-enum': 'off'}}
171
+ ```
172
+
173
+ ### `no-switch`
174
+
175
+ Forbids `switch` statements. They are an early-return / lookup-map / `if`
176
+ chain in disguise and are easy to misuse (fallthrough, missing `default`,
177
+ shadowed locals across cases). Replace with a lookup object, an early
178
+ `return` chain, or a discriminated-union exhaustiveness check.
179
+
180
+ Opt out for a file or block:
181
+
182
+ ```js
183
+ {files: ['src/parser/**'], rules: {'no-switch/no-switch': 'off'}}
184
+ ```
185
+
186
+ ### `no-jsx-iife`
187
+
188
+ Forbids IIFEs (`{(() => { ... })()}`) inside JSX expression containers in `.tsx` and `.jsx` files. IIFEs obscure intent and allocate a new function on every render. Compute the value in a variable before the return statement, or use an inline `&&` expression instead.
189
+
190
+ Opt out for a file or block:
191
+
192
+ ```js
193
+ {files: ['src/legacy/**'], rules: {'no-jsx-iife/no-jsx-iife': 'off'}}
194
+ ```
195
+
196
+ ### `no-null-render`
197
+
198
+ Standardizes the empty render on `undefined` instead of `null` in `.tsx` and
199
+ `.jsx` files. A `return null` inside a function that provably renders JSX (it
200
+ returns a JSX element elsewhere in the same scope) is rewritten to
201
+ `return undefined`. `null` and `undefined` are identical to React's reconciler
202
+ (both, with `false`/`true`, are the same empty slot); this is a consistency
203
+ convention that picks one of two equivalent forms, not a correctness or
204
+ performance fix.
205
+
206
+ Autofixable: `--fix` rewrites `return null` → `return undefined`. The fix is
207
+ deliberately conservative and only touches a `return null` whose enclosing
208
+ function is provably a render function, so a `return null` in a loader, action,
209
+ or plain utility is never altered. `: null` ternary arms are out of scope (the
210
+ report-only `no-restricted-syntax` selectors cover those).
211
+
212
+ Opt out for a file or block:
213
+
214
+ ```js
215
+ {files: ['src/legacy/**'], rules: {'no-null-render/no-null-render': 'off'}}
216
+ ```
217
+
218
+ ### `cn-conditional`
219
+
220
+ Holds one form for a conditional class passed to `cn` from the
221
+ [`cn`](https://www.npmjs.com/package/cn) package: `cond && 'class'` (or
222
+ `!cond && 'class'`). It reports an object argument (`cn('a', {b: c})`) and a
223
+ ternary with an empty branch (`undefined`, `null`, `false`, `''`, or an empty
224
+ template), at any depth inside array arguments, ternary branches, and `&&`,
225
+ `||`, or `??` operands. Each offending node is reported once.
226
+
227
+ Allowed: `cn('a', c && 'b')`, `cn('a', !c && 'b')`, a two-class ternary
228
+ (`cn('a', c ? 'b' : 'd')`), `||` and `??` of values, strings, identifiers,
229
+ lookups (`VARIANTS[v]`), arrays of those, spreads, and template literals. The
230
+ condition side of `&&` and a ternary's test are never inspected.
231
+
232
+ It only fires on a call to the named import `cn` from `'cn'` (an aliased import
233
+ counts). A locally declared `cn`, or a `cn` imported from any other module, is
234
+ never reported. It is not autofixable, and it is active in test, story, and
235
+ Playwright files as well as source.
236
+
237
+ Opt out for a file or block:
238
+
239
+ ```js
240
+ {files: ['src/legacy/**'], rules: {'cn-conditional/cn-conditional': 'off'}}
241
+ ```
242
+
243
+ ## Router-specific rules
244
+
245
+ `reactRouter` is opt-in because it is **subtractive**. `storybook` and
246
+ `playwright` only add rules, so spreading them can never weaken anything.
247
+ `reactRouter` turns `no-empty-pattern` **off**, and its glob
248
+ (`**/routes/**/*.tsx`) is not unique to React Router, since TanStack Router
249
+ uses a `routes/` directory too. Shipping it inside `react` silently relaxed
250
+ the rule for every file-based router that isn't React Router.
251
+
252
+ ```js
253
+ ...lint.react,
254
+ ...lint.reactRouter, // React Router framework mode only
255
+ ```
256
+
257
+ **On React Router?** Spread it. A route module destructures nothing from its
258
+ typed props (`({}: Route.ComponentProps)`), which `no-empty-pattern` reads as
259
+ an empty pattern.
260
+
261
+ **On TanStack Router, or any other router?** Omit it, and keep the rule.
262
+ `createFileRoute(...)({component: Named})` never produces the empty-pattern
263
+ shape. You will most likely also want to ignore your generated route tree:
264
+
265
+ ```js
266
+ ...lint.ignores({extra: ['**/routeTree.gen.ts']}),
267
+ ```
268
+
269
+ `reactRouter` also adds one rule, `no-imperative-query-fetch/no-imperative-query-fetch`,
270
+ on every TypeScript file under the source dir. It bans `ensureQueryData`,
271
+ `fetchQuery`, `prefetchQuery` and their infinite variants on any member call,
272
+ so a route has one fetch idiom: `await queryClient.query(options)` in a
273
+ clientLoader, read with `useSuspenseQuery(options)` in the component. It
274
+ matches by method name, so it fires whether or not TanStack Query is
275
+ installed.
276
+
277
+ `react` turns on `react-refresh/only-export-components`, because a module that
278
+ exports anything besides components cannot be hot-swapped and Fast Refresh
279
+ falls back to a full reload. `reactRouter` lets route modules (`**/routes/**`
280
+ and `root.tsx`) export React Router's route-module names beside their
281
+ component: `loader`, `clientLoader`, `action`, `clientAction`, `middleware`,
282
+ `clientMiddleware`, `meta`, `links`, `headers`, `handle`, `shouldRevalidate`,
283
+ `ErrorBoundary`, `HydrateFallback` and `Layout`. Any other export from a route
284
+ module is still reported; move it to its own module.
285
+
286
+ The `/.react-router/**` glob stays in the `ignores` defaults. It names a
287
+ directory that only exists in a React Router project, so it costs other
288
+ projects nothing.
289
+
290
+ ## i18n and Zod
291
+
292
+ Both are opt-in arrays: spread them only when the project uses the library.
293
+
294
+ `lint.i18n` turns on `i18next/no-literal-string` in `jsx-text-only` mode for
295
+ `.tsx`/`.jsx` under the source dir. It reports raw text between JSX tags
296
+ (`<p>Hello</p>`), which should come from `t()`. Attributes are not checked,
297
+ since `data-slot`, `htmlFor` and `value` strings are not UI copy. Stories,
298
+ tests, `tests/` helpers and the vendored `components/ui/*.tsx` files are
299
+ exempt. Text that must stay untranslated, such as an error boundary that has to
300
+ render without i18n, takes an inline disable with a reason. A project that
301
+ drops i18n removes the line, and adds it back if it adds a language later.
302
+
303
+ `lint.zod` turns on `eslint-plugin-zod`'s recommended set for Zod 4:
304
+ conflicting or duplicate checks, `.optional()` with `.default()`, a `throw`
305
+ inside `.refine()`, a refine without an error message, `z.nativeEnum`,
306
+ `z.any()`, and the Zod 3 forms Zod 4 deprecates. `consistent-import` is off
307
+ because it wants `import * as z from 'zod'`, and GAIA writes
308
+ `import {z} from 'zod'`.
309
+
310
+ ## Tailwind / better-tailwindcss factory
311
+
312
+ `betterTailwind` is a factory because the underlying plugin needs to know
313
+ where your Tailwind entry CSS file lives.
314
+
315
+ ```js
316
+ ...lint.betterTailwind({
317
+ entryPoint: './app/styles/tailwind.css',
318
+ ignore: ['plain-link', 'plain-table'],
319
+ }),
320
+ ```
321
+
322
+ | Option | Type | Required | Description |
323
+ | ------------ | ---------- | -------- | -------------------------------------------------------------------------------------------- |
324
+ | `entryPoint` | `string` | yes | Path to your Tailwind entry CSS, used by the plugin to resolve the active class set. |
325
+ | `ignore` | `string[]` | no | Class names the plugin should ignore in `better-tailwindcss/no-unknown-classes` (e.g. design-system tokens, `plain-*` utility shims). |
326
+
327
+ ## shadcn factory
328
+
329
+ `shadcn()` bundles [`@shadcn/lint`](https://github.com/shadcn-ui/lint) (an
330
+ exact-pinned dependency) so role tokens are the only color vocabulary, and
331
+ exempts the vendored shadcn ui files from house style. It is opt-in: omit it
332
+ and nothing shadcn-related loads.
333
+
334
+ ```js
335
+ ...lint.shadcn({ui: '~/components/ui'}), // the LAST entry, after ...lint.prettier
336
+ ```
337
+
338
+ | Option | Type | Default | Description |
339
+ | ----------------- | ---------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
340
+ | `ui` | `string` | `'~/components/ui'` | Import alias of the vendored ui folder, written to the root-level `settings.shadcn.ui` (never a rule option). |
341
+ | `uiFiles` | `string[]` | `[`${sourceDir}/components/ui/*.tsx`]` plus the registry helpers | Globs of the vendored files: the ui folder plus the helpers `shadcn add` writes outside it (`hooks/use-mobile.ts`, `hooks/use-highlighted-code.ts`, `utils/highlighter.tsx`). Never matches `ui/tests/**`. |
342
+ | `restyleOffFiles` | `string[]` | `[]` | Fallback: globs outside ui where `shadcn/no-restyle` is off. Its block always ignores `${sourceDir}/components/ui/**`, so `ui/tests/**` stays covered. |
343
+
344
+ Blocks it returns:
345
+
346
+ - `shadcn/settings`: no `files`; registers the plugin and sets `settings.shadcn.ui`.
347
+ - `shadcn/rules`: source files. `shadcn/no-raw-colors`, `shadcn/require-static-classes`
348
+ and `shadcn/no-inline-styles` are `error`; `shadcn/no-restyle` is
349
+ `['error', {allow: ['layout']}]`. `shadcn/no-arbitrary-values` is `off`: an
350
+ arbitrary value is allowed where no scale step or token fits (`grid-cols-[auto_1fr]`),
351
+ and `better-tailwindcss/enforce-canonical-classes` (with `rootFontSize: 16`)
352
+ rewrites one that has a scale equivalent (`w-[16px]` to `w-4`). An arbitrary
353
+ color (hex, a CSS color function, a `color:` hint) is an error through
354
+ `better-tailwindcss/no-restricted-classes`, since colors are role tokens.
355
+ `shadcn/no-unknown-classes` is `off` because `better-tailwindcss/no-unknown-classes` (from `betterTailwind`)
356
+ stays the only unknown-class rule. A marker class a library applies, such as
357
+ sonner's `toaster`, is cleared through `betterTailwind({ignore: ['toaster']})`.
358
+ - `shadcn/vendored-ui`: the `uiFiles` only. See below.
359
+ - `shadcn/restyle-fallback`: only when `restyleOffFiles` is non-empty.
360
+
361
+ **Ordering.** The vendored-ui block turns off rules owned by plugins the other
362
+ GAIA blocks register (prettier, prefer-arrow-functions, perfectionist, unicorn,
363
+ better-tailwindcss, and more), so `shadcn()` assumes the full GAIA composition
364
+ is spread before it. Spread it last, after `...lint.prettier`, so its
365
+ `prettier/prettier: off` wins on vendored files.
366
+
367
+ ### Vendored ui policy
368
+
369
+ Files in `components/ui/*.tsx`, and the helpers the registry writes outside
370
+ that folder, are registry output kept byte-identical to `shadcn add`. The list
371
+ of rules off for them covers the full shadcn registry: every rule that fires on
372
+ unedited `shadcn add --all` output. Prettier and the house-style rules are off,
373
+ so lint never rewrites a vendored file, and so are the correctness-class rules
374
+ the shadcn source itself trips, each commented in `src/configs/shadcn.ts` with
375
+ the stock file it covers. `jsx-a11y/anchor-has-content` is one of them:
376
+ `PaginationLink` passes its anchor content through spread `children`, which the
377
+ rule cannot see.
378
+
379
+ Everything else stays on in vendored ui: `shadcn/no-raw-colors`,
380
+ `react-hooks/rules-of-hooks`, `react-hooks/exhaustive-deps`, and every other
381
+ correctness rule. `components/ui/tests/**` is GAIA-authored and stays fully
382
+ linted, and so does any hook or util beside the registry helpers. If a later
383
+ registry item trips a rule not in the list, extend the block rather than
384
+ editing the vendored file.
385
+
386
+ ## `FC` and `FunctionComponent` are banned
387
+
388
+ Importing `FC` or `FunctionComponent` from `'react'` is an error everywhere,
389
+ including test and story files (a core `no-restricted-imports` entry in the
390
+ shared path list). Type props inline instead:
391
+
392
+ ```tsx
393
+ type ButtonProps = {label: string};
394
+
395
+ const Button = ({label}: ButtonProps) => <button>{label}</button>;
396
+ ```
397
+
398
+ ## Ignores factory
399
+
400
+ `ignores` produces a leading flat-config block that merges your
401
+ `.gitignore` plus GAIA defaults. `.gitignore` is picked up automatically
402
+ if it exists at the project root; no need to declare it.
403
+
404
+ `lint.ignores` is dual-shape: spread it directly for the default case
405
+ (no call), or call it with options to override.
406
+
407
+ ```js
408
+ ...lint.ignores, // default: auto .gitignore + GAIA defaults
409
+ ...lint.ignores({extra: ['coverage/**']}), // + extra globs
410
+ ...lint.ignores({gitignore: 'config/.gitignore'}), // override the path
411
+ ...lint.ignores({gitignore: false}), // opt out of the merge
412
+ ```
413
+
414
+ | Option | Type | Required | Description |
415
+ | ---------- | ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
416
+ | `gitignore`| `string \| false` | no | Path to a `.gitignore` file (relative to `cwd` or absolute). Defaults to `'.gitignore'`. Silently skipped if the resolved path does not exist. Pass `false` to opt out entirely. |
417
+ | `extra` | `string[]` | no | Extra ignore globs merged with GAIA defaults. |
418
+
419
+ ## GAIA folder conventions baked into `check-file`
420
+
421
+ The `check-file` rules in `styleHygiene` encode GAIA's folder layout (with
422
+ `sourceDir` substituted for the literal `app/` prefix). Every folder and file
423
+ name is kebab-case:
424
+
425
+ ```
426
+ <sourceDir>/
427
+ components/
428
+ ui/ flat kebab-case files (button.tsx); tests/ is the only subfolder
429
+ <name>/index.tsx one folder per component, default export in PascalCase
430
+ tests/ index.stories.tsx, index.test.tsx
431
+ hooks/ use-<name>.ts
432
+ <sub-name>/index.tsx nested component
433
+ pages/
434
+ <route path>/page.tsx route page, default export <Name>Page
435
+ tests/ page.stories.tsx, page.test.tsx
436
+ <name>/index.tsx component used only by this page
437
+ hooks/
438
+ use-<name>.ts camelCase export (useTheme)
439
+ tests/use-<name>.test.ts
440
+ ```
441
+
442
+ - `<sourceDir>/components/**`: kebab-case folders; component files are named
443
+ `index.tsx`, except the flat `components/ui/` files; no component file sits
444
+ directly in `components/`
445
+ - `<sourceDir>/pages/**`: kebab-case folders; a `.tsx` file is `page.tsx` or
446
+ `index.tsx`
447
+ - `**/hooks/*`: hook files are kebab-case with a `use-` prefix (`use-*.ts`)
448
+ - `assets/`, `hooks/`, `state/`, `tests/`, and `utils/` are the reserved
449
+ subfolders of a component or page folder; files in them are kebab-case
450
+ - `test/**`: test harness naming
451
+
452
+ `canonical/filename-match-exported` compares a component or page file's
453
+ default export against its kebab-case folder (`theme-switch/index.tsx` exports
454
+ `ThemeSwitch`). Outside `components/` and `pages/` it keeps exact matching, so
455
+ `i18n.ts` exports `i18n`. It is off for `pages/**/page.tsx`, stories, tests,
456
+ routes, and hook files.
457
+
458
+ For most non-GAIA layouts, passing `sourceDir` to the factory is enough:
459
+
460
+ ```js
461
+ const lint = gaiaLint({sourceDir: 'src'});
462
+ ```
463
+
464
+ If your project uses a different layout (for example, a non-GAIA project
465
+ that names component files in PascalCase, `Button.tsx`, instead of GAIA's
466
+ kebab-case folders with `index.tsx`), override the relevant `check-file/*` rules **after** the
467
+ `styleHygiene` spread:
468
+
469
+ ```js
470
+ const lint = gaiaLint({sourceDir: 'src'});
471
+
472
+ export default defineConfig([
473
+ ...lint.base,
474
+ ...lint.styleHygiene,
475
+ {
476
+ files: ['src/components/**'],
477
+ rules: {
478
+ 'check-file/filename-naming-convention': [
479
+ 'error',
480
+ {'src/components/**/*.{ts,tsx}': 'PASCAL_CASE'},
481
+ ],
482
+ },
483
+ },
484
+ ]);
485
+ ```
486
+
487
+ Or disable the GAIA `check-file` block entirely and reapply your own:
488
+
489
+ ```js
490
+ {rules: {'check-file/folder-naming-convention': 'off'}}
491
+ ```
492
+
493
+ ## Versioning policy
494
+
495
+ This package follows SemVer.
496
+
497
+ - **Patch.** Plugin version bumps that do not change rule defaults, internal
498
+ refactors, README fixes.
499
+ - **Minor.** New rules added, new options exposed on factories, new named
500
+ exports, opt-in changes that do not break existing consumer configs.
501
+ - **Major.** Engine swaps (ESLint → Biome), `eslint` major version bumps,
502
+ removal/rename of named exports, default-rule changes that introduce new
503
+ errors in previously-clean code.
504
+
505
+ ## Prereleases
506
+
507
+ 3.0.0 ships first as `3.0.0-rc.N` on the `rc` dist-tag, for GAIA 2.0. Install
508
+ it with `pnpm add -D @gaia-harness/lint@rc`. The `latest` dist-tag stays on 2.x
509
+ until GAIA 2.0.0 ships. The Release workflow publishes from `main` only, so a
510
+ 2.x hotfix is cut and published manually by the maintainer from the
511
+ `release/2.x` branch.
512
+
513
+ ## License
514
+
515
+ MIT
@@ -0,0 +1,3 @@
1
+ import type { Linter } from 'eslint';
2
+ export declare const buildBase: (sourceDir: string) => Linter.Config[];
3
+ //# sourceMappingURL=base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/configs/base.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAS,MAAM,EAAC,MAAM,QAAQ,CAAC;AA+Y3C,eAAO,MAAM,SAAS,GAAI,WAAW,MAAM,KAAG,MAAM,CAAC,MAAM,EAY1D,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { Linter } from 'eslint';
2
+ export type GaiaLintBetterTailwindOptions = {
3
+ /** Path to the Tailwind entry CSS file (e.g. './app/styles/tailwind.css'). */
4
+ entryPoint: string;
5
+ /** Class names to ignore in `better-tailwindcss/no-unknown-classes`. */
6
+ ignore?: string[];
7
+ };
8
+ /**
9
+ * Better-Tailwind flat-config factory.
10
+ *
11
+ * Ports `betterTailwindConfig` from GAIA's `eslint.config.mjs`. The
12
+ * `entryPoint` (consumer's Tailwind CSS entry) and `ignore` list are
13
+ * project-specific, so this is shipped as a factory rather than a static
14
+ * array.
15
+ */
16
+ export declare const betterTailwind: (opts: GaiaLintBetterTailwindOptions) => Linter.Config[];
17
+ //# sourceMappingURL=better-tailwind.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"better-tailwind.d.ts","sourceRoot":"","sources":["../../src/configs/better-tailwind.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,QAAQ,CAAC;AAEnC,MAAM,MAAM,6BAA6B,GAAG;IAC1C,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,6BAA6B,KAClC,MAAM,CAAC,MAAM,EA8Bf,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Linter } from 'eslint';
2
+ export declare const buildGuardrails: (sourceDir: string) => Linter.Config[];
3
+ //# sourceMappingURL=guardrails.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,17 @@
1
+ import type { Linter } from 'eslint';
2
+ /**
3
+ * i18n flat-config block (opt-in), for projects that translate their UI.
4
+ *
5
+ * `i18next/no-literal-string` in `jsx-text-only` mode reports raw text between
6
+ * JSX tags (`<p>Hello</p>`), which should come from `t()`. Attributes and
7
+ * plain string literals are not checked: `jsx-only` mode flags `data-slot`,
8
+ * `htmlFor`, `value` and the like, which are not UI copy.
9
+ *
10
+ * Stories, tests, `tests/` helpers and the vendored shadcn ui files are
11
+ * exempt. Text that must stay untranslated (an error boundary that renders
12
+ * without i18n) takes an inline disable with a reason.
13
+ *
14
+ * A project that drops i18n removes `...lint.i18n` from its config.
15
+ */
16
+ export declare const buildI18n: (sourceDir: string) => Linter.Config[];
17
+ //# sourceMappingURL=i18n.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../src/configs/i18n.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,QAAQ,CAAC;AAEnC;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,GAAI,WAAW,MAAM,KAAG,MAAM,CAAC,MAAM,EAe1D,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { Linter } from 'eslint';
2
+ export type GaiaLintIgnoresOptions = {
3
+ /**
4
+ * Path to a `.gitignore` file to merge in. Resolved from `process.cwd()`
5
+ * if relative. Pass `false` to skip the gitignore merge entirely.
6
+ *
7
+ * If the resolved path does not exist, the merge is silently skipped —
8
+ * projects without a `.gitignore` don't need to opt out explicitly.
9
+ *
10
+ * @default '.gitignore'
11
+ */
12
+ gitignore?: string | false;
13
+ /** Extra ignore globs to merge with GAIA defaults. */
14
+ extra?: string[];
15
+ };
16
+ /**
17
+ * Ignores flat-config factory.
18
+ *
19
+ * Ports the `includeIgnoreFile(gitignorePath)` + static `ignored-files`
20
+ * block from GAIA's `eslint.config.mjs`. The `gitignore` path must be
21
+ * resolved at the consumer's cwd (config-load time), not at gaia-lint's
22
+ * install location, so this is shipped as a factory.
23
+ */
24
+ export declare const ignores: (opts?: GaiaLintIgnoresOptions) => Linter.Config[];
25
+ //# sourceMappingURL=ignores.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ignores.d.ts","sourceRoot":"","sources":["../../src/configs/ignores.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,QAAQ,CAAC;AAInC,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC3B,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAyBF;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,GAAI,OAAO,sBAAsB,KAAG,MAAM,CAAC,MAAM,EAmBpE,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { Linter } from 'eslint';
2
+ /**
3
+ * Playwright flat-config block.
4
+ *
5
+ * Ports `playwrightConfig` from GAIA's `eslint.config.mjs` — scopes
6
+ * `eslint-plugin-playwright`'s `flat/recommended` config to `.playwright/`
7
+ * test files (GAIA's e2e directory convention).
8
+ */
9
+ export declare const playwright: Linter.Config[];
10
+ //# sourceMappingURL=playwright.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright.d.ts","sourceRoot":"","sources":["../../src/configs/playwright.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,QAAQ,CAAC;AAEnC;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAsBrC,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { Linter } from 'eslint';
2
+ /**
3
+ * Prettier flat-config block.
4
+ *
5
+ * Ports `prettierConfig` from GAIA's `eslint.config.mjs`. Layers
6
+ * `eslint-config-prettier`'s rule disablers, then re-enables a few
7
+ * `@stylistic/*` rules GAIA wants to keep (padding lines, single quotes),
8
+ * and finally turns on the `prettier/prettier` rule itself.
9
+ */
10
+ export declare const prettier: Linter.Config[];
11
+ //# sourceMappingURL=prettier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prettier.d.ts","sourceRoot":"","sources":["../../src/configs/prettier.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,QAAQ,CAAC;AAEnC;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EA6BnC,CAAC"}