@gaia-harness/lint 0.0.0-stage → 3.0.3

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 +119 -4
package/dist/index.js ADDED
@@ -0,0 +1,1938 @@
1
+ // src/configs/base.ts
2
+ import eslintComments from "@eslint-community/eslint-plugin-eslint-comments/configs";
3
+ import js from "@eslint/js";
4
+ import { configs, plugins, rules } from "eslint-config-airbnb-extended";
5
+ import preferArrowFunctions from "eslint-plugin-prefer-arrow-functions";
6
+ import lodashUnderscore from "eslint-plugin-you-dont-need-lodash-underscore";
7
+
8
+ // src/configs/restricted-imports.ts
9
+ var RESTRICTED_IMPORT_PATHS = [
10
+ {
11
+ importNames: ["FC", "FunctionComponent"],
12
+ message: "Do not import FC or FunctionComponent from 'react'. Type the props inline: `type ButtonProps = {...}; const Button = ({...}: ButtonProps) => ...`.",
13
+ name: "react"
14
+ },
15
+ {
16
+ message: "Import from '@conform-to/zod/v4'. The bare '@conform-to/zod' specifier targets Zod v3 and throws at runtime, uncaught by typecheck, lint, and build.",
17
+ name: "@conform-to/zod"
18
+ }
19
+ ];
20
+
21
+ // src/configs/base.ts
22
+ var jsConfig = [
23
+ // ESLint recommended config
24
+ {
25
+ name: "js/config",
26
+ ...js.configs.recommended
27
+ },
28
+ // Stylistic plugin
29
+ plugins.stylistic,
30
+ // Import X plugin
31
+ plugins.importX,
32
+ // Airbnb base recommended config
33
+ ...configs.base.recommended,
34
+ // Strict import rules
35
+ rules.base.importsStrict
36
+ ];
37
+ var jsCustomConfig = [
38
+ {
39
+ name: "js-custom",
40
+ rules: {
41
+ "consistent-return": "off",
42
+ curly: ["error", "all"],
43
+ "max-params": "error",
44
+ "no-param-reassign": "off",
45
+ // handled by sonarjs
46
+ "no-undef": "off",
47
+ "no-unused-vars": "off"
48
+ // handled by sonarjs
49
+ }
50
+ },
51
+ {
52
+ files: ["**/*.test.ts?(x)", "**/*.stories.ts?(x)"],
53
+ name: "test-files/all",
54
+ rules: {
55
+ "guard-for-in": "off",
56
+ "max-params": "off",
57
+ "no-await-in-loop": "off",
58
+ "no-plusplus": "off",
59
+ "no-restricted-syntax": "off"
60
+ }
61
+ },
62
+ {
63
+ files: [".playwright/**/*.ts?(x)"],
64
+ name: "playwright/all",
65
+ rules: {
66
+ "no-await-in-loop": "off",
67
+ "no-restricted-syntax": "off"
68
+ }
69
+ }
70
+ ];
71
+ var typescriptConfig = [
72
+ // TypeScript ESLint plugin
73
+ plugins.typescriptEslint,
74
+ // Airbnb base TypeScript config
75
+ ...configs.base.typescript,
76
+ // Strict TypeScript rules
77
+ rules.typescript.typescriptEslintStrict,
78
+ rules.typescript.stylistic,
79
+ // Airbnb React TypeScript config
80
+ ...configs.react.typescript,
81
+ {
82
+ files: ["./*.ts"],
83
+ name: "root-ts-files",
84
+ rules: {
85
+ "global-require": "off",
86
+ "no-void": "off"
87
+ }
88
+ },
89
+ {
90
+ files: ["**/*.ts?(x)"],
91
+ name: "no-void",
92
+ rules: {
93
+ "no-void": ["error", { allowAsStatement: true }]
94
+ }
95
+ }
96
+ ];
97
+ var buildTsEslintConfig = (sourceDir) => [
98
+ {
99
+ files: ["**/*.ts?(x)"],
100
+ name: "typescript/config",
101
+ rules: {
102
+ "@typescript-eslint/array-type": ["error", { default: "array" }],
103
+ "@typescript-eslint/ban-ts-comment": [
104
+ "error",
105
+ {
106
+ minimumDescriptionLength: 10,
107
+ "ts-check": false,
108
+ "ts-expect-error": "allow-with-description",
109
+ "ts-ignore": true,
110
+ "ts-nocheck": true
111
+ }
112
+ ],
113
+ "@typescript-eslint/consistent-type-definitions": ["error", "type"],
114
+ "@typescript-eslint/consistent-type-imports": "error",
115
+ "@typescript-eslint/explicit-module-boundary-types": "off",
116
+ "@typescript-eslint/method-signature-style": "error",
117
+ "@typescript-eslint/no-confusing-void-expression": [
118
+ "error",
119
+ { ignoreArrowShorthand: true }
120
+ ],
121
+ "@typescript-eslint/no-deprecated": "error",
122
+ "@typescript-eslint/no-floating-promises": [
123
+ "error",
124
+ { ignoreIIFE: true, ignoreVoid: true }
125
+ ],
126
+ "@typescript-eslint/no-misused-promises": [
127
+ "error",
128
+ { checksVoidReturn: false }
129
+ ],
130
+ "@typescript-eslint/no-non-null-assertion": "off",
131
+ "@typescript-eslint/no-throw-literal": "off",
132
+ "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error",
133
+ "@typescript-eslint/no-unnecessary-condition": "error",
134
+ "@typescript-eslint/no-unnecessary-type-conversion": "error",
135
+ "@typescript-eslint/no-unnecessary-type-parameters": "off",
136
+ "@typescript-eslint/no-unsafe-argument": "error",
137
+ "@typescript-eslint/no-unsafe-assignment": "error",
138
+ "@typescript-eslint/no-unsafe-call": "error",
139
+ "@typescript-eslint/no-unsafe-member-access": "error",
140
+ "@typescript-eslint/no-unsafe-return": "error",
141
+ "@typescript-eslint/only-throw-error": "off",
142
+ "@typescript-eslint/prefer-nullish-coalescing": "error",
143
+ "@typescript-eslint/prefer-promise-reject-errors": "off",
144
+ "@typescript-eslint/restrict-template-expressions": [
145
+ "error",
146
+ { allowBoolean: true, allowNumber: true }
147
+ ],
148
+ "@typescript-eslint/return-await": "off",
149
+ "@typescript-eslint/unbound-method": "off"
150
+ }
151
+ },
152
+ {
153
+ files: [
154
+ `${sourceDir}/utils/**`,
155
+ `${sourceDir}/services/**`,
156
+ `${sourceDir}/hooks/**`
157
+ ],
158
+ name: "typescript/explicit-return-types",
159
+ rules: {
160
+ "@typescript-eslint/explicit-module-boundary-types": "error"
161
+ }
162
+ },
163
+ {
164
+ files: ["**/*.test.ts?(x)", "**/*.stories.ts?(x)"],
165
+ name: "typescript/test-files",
166
+ rules: {
167
+ "@typescript-eslint/no-explicit-any": "off",
168
+ "@typescript-eslint/no-unsafe-argument": "off",
169
+ "@typescript-eslint/no-unsafe-assignment": "off",
170
+ "@typescript-eslint/no-unsafe-call": "off",
171
+ "@typescript-eslint/no-unsafe-member-access": "off"
172
+ }
173
+ },
174
+ {
175
+ files: ["test/**/*.ts?(x)"],
176
+ name: "typescript/test-config",
177
+ rules: {
178
+ "@typescript-eslint/no-var-requires": "off",
179
+ "no-plusplus": "off"
180
+ }
181
+ },
182
+ {
183
+ files: ["**/*.d.ts"],
184
+ name: "typescript/type-definitions",
185
+ rules: {
186
+ "@typescript-eslint/consistent-type-definitions": "off",
187
+ "@typescript-eslint/method-signature-style": "error",
188
+ "@typescript-eslint/no-unused-vars": "off"
189
+ }
190
+ }
191
+ ];
192
+ var NO_UNRESOLVED = [
193
+ "error",
194
+ { caseSensitive: true, commonjs: true, ignore: ["^virtual:"] }
195
+ ];
196
+ var buildImportXConfig = (sourceDir) => [
197
+ {
198
+ name: "import-x-all-files",
199
+ rules: {
200
+ "import-x/consistent-type-specifier-style": ["error", "prefer-top-level"],
201
+ "import-x/extensions": "off",
202
+ "import-x/no-anonymous-default-export": [
203
+ "error",
204
+ {
205
+ allowArray: true,
206
+ allowLiteral: true,
207
+ allowObject: true
208
+ }
209
+ ],
210
+ "import-x/no-namespace": "off",
211
+ "import-x/no-rename-default": "off",
212
+ "import-x/no-useless-path-segments": "off",
213
+ "import-x/order": "off",
214
+ "import-x/prefer-default-export": "off"
215
+ }
216
+ },
217
+ {
218
+ files: ["./*.ts"],
219
+ name: "import-x/root-ts-files",
220
+ rules: {
221
+ "import-x/no-extraneous-dependencies": "off",
222
+ "import-x/no-unresolved": NO_UNRESOLVED,
223
+ "import-x/prefer-default-export": "off"
224
+ }
225
+ },
226
+ {
227
+ files: ["**/*.test.ts?(x)", "**/*.stories.ts?(x)"],
228
+ name: "import-x/test-files",
229
+ rules: {
230
+ "import-x/extensions": "off",
231
+ "import-x/no-extraneous-dependencies": "off"
232
+ }
233
+ },
234
+ {
235
+ files: [`${sourceDir}/**/*.ts?(x)`],
236
+ name: "import-x/app-test-files",
237
+ rules: {
238
+ "import-x/no-unresolved": NO_UNRESOLVED
239
+ }
240
+ },
241
+ {
242
+ files: [`${sourceDir}/**/hooks/*.ts?(x)`],
243
+ name: "import-x/hooks",
244
+ rules: {
245
+ "import-x/no-default-export": "error"
246
+ }
247
+ },
248
+ {
249
+ files: ["test/**/*.ts?(x)"],
250
+ name: "import-x/test-config-files",
251
+ rules: {
252
+ "import-x/no-extraneous-dependencies": "off",
253
+ "import-x/prefer-default-export": "off"
254
+ }
255
+ },
256
+ {
257
+ files: [".storybook/**/*.ts?(x)", ".playwright/**/*.ts?(x)"],
258
+ name: "import-x/storybook-playwright",
259
+ rules: {
260
+ "import-x/no-extraneous-dependencies": "off",
261
+ "import-x/no-unresolved": "off"
262
+ }
263
+ }
264
+ ];
265
+ var unusedDisableDirectivesConfig = [
266
+ {
267
+ linterOptions: { reportUnusedDisableDirectives: "error" },
268
+ name: "gaia/unused-disable-directives"
269
+ }
270
+ ];
271
+ var eslintCommentsConfig = [
272
+ {
273
+ ...eslintComments.recommended,
274
+ name: "gaia/eslint-comments",
275
+ rules: {
276
+ ...eslintComments.recommended.rules,
277
+ "@eslint-community/eslint-comments/disable-enable-pair": [
278
+ "error",
279
+ { allowWholeFile: true }
280
+ ],
281
+ "@eslint-community/eslint-comments/require-description": [
282
+ "error",
283
+ { ignore: ["eslint-enable"] }
284
+ ]
285
+ }
286
+ }
287
+ ];
288
+ var preferArrowFunctionsConfig = [
289
+ {
290
+ name: "prefer-arrow",
291
+ plugins: {
292
+ "prefer-arrow-functions": preferArrowFunctions
293
+ },
294
+ rules: {
295
+ "prefer-arrow-functions/prefer-arrow-functions": "error"
296
+ }
297
+ },
298
+ {
299
+ files: ["**/*.d.ts"],
300
+ name: "ts-definition-files/prefer-arrow-off",
301
+ rules: {
302
+ "prefer-arrow-functions/prefer-arrow-functions": "off"
303
+ }
304
+ }
305
+ ];
306
+ var restrictedSyntaxConfig = [
307
+ {
308
+ ignores: ["**/*.d.ts"],
309
+ name: "gaia/no-restricted-syntax",
310
+ rules: {
311
+ "no-restricted-syntax": [
312
+ "error",
313
+ {
314
+ // `prefer-arrow-functions` has a hardcoded exemption for named
315
+ // default-exported declarations
316
+ // (`guard.js:hasNameAndIsExportedAsDefaultExport`), so
317
+ // `export default function Foo() {}` slips through. This selector
318
+ // closes that gap.
319
+ message: "Use `const Name = () => {}; export default Name;` instead. The prefer-arrow-functions plugin exempts named default-exported declarations upstream.",
320
+ selector: "ExportDefaultDeclaration > FunctionDeclaration"
321
+ },
322
+ {
323
+ // GAIA never renders `null`. `cond ? <JSX/> : null` is the
324
+ // numeric-0-safe `&&` guard written the long way. Both JSX-null
325
+ // ternary selectors only match a JSXElement/JSXFragment branch, so
326
+ // they are inert outside `.tsx`/`.jsx`.
327
+ message: "Do not render `null` from a ternary. Use a boolean-guarded `&&`: `cond && <JSX/>` (coerce a numeric/falsy guard with `!!cond`).",
328
+ selector: "ConditionalExpression[alternate.raw='null']:matches([consequent.type='JSXElement'], [consequent.type='JSXFragment'])"
329
+ },
330
+ {
331
+ message: "Do not render `null` from a ternary. Invert the condition and use `&&`: `!cond && <JSX/>`.",
332
+ selector: "ConditionalExpression[consequent.raw='null']:matches([alternate.type='JSXElement'], [alternate.type='JSXFragment'])"
333
+ },
334
+ {
335
+ // A `.length` left operand is always numeric, so `&&` returns the
336
+ // literal `0` (a renderable value) when the list is empty, leaking
337
+ // "0" into the DOM. `.length` is the single most common numeric-0
338
+ // leak and flagging it has zero false positives. The general
339
+ // `count && <JSX/>` case is caught by `sonarjs/jsx-no-leaked-render`
340
+ // in `guardrails`, which needs type information; this selector
341
+ // needs none, so it still holds where types are unavailable.
342
+ // Report-only: a blind rewrite is unsafe in general.
343
+ message: 'A numeric `.length` left operand leaks "0" into the DOM when empty. Force a real boolean: `items.length > 0 && <JSX/>`.',
344
+ selector: "LogicalExpression[operator='&&'][left.property.name='length']:matches([right.type='JSXElement'],[right.type='JSXFragment'])"
345
+ }
346
+ ]
347
+ }
348
+ }
349
+ ];
350
+ var restrictedImportsConfig = [
351
+ {
352
+ files: ["**/*.ts?(x)"],
353
+ name: "gaia/no-restricted-imports",
354
+ rules: {
355
+ "no-restricted-imports": ["error", { paths: RESTRICTED_IMPORT_PATHS }]
356
+ }
357
+ }
358
+ ];
359
+ var lodashUnderscoreConfig = [
360
+ {
361
+ name: "you-dont-need-lodash-underscore",
362
+ plugins: {
363
+ "you-dont-need-lodash-underscore": lodashUnderscore
364
+ },
365
+ rules: {
366
+ ...lodashUnderscore.configs.compatible.rules
367
+ }
368
+ }
369
+ ];
370
+ var buildBase = (sourceDir) => [
371
+ ...jsConfig,
372
+ ...jsCustomConfig,
373
+ ...typescriptConfig,
374
+ ...buildTsEslintConfig(sourceDir),
375
+ ...buildImportXConfig(sourceDir),
376
+ ...unusedDisableDirectivesConfig,
377
+ ...eslintCommentsConfig,
378
+ ...preferArrowFunctionsConfig,
379
+ ...restrictedSyntaxConfig,
380
+ ...restrictedImportsConfig,
381
+ ...lodashUnderscoreConfig
382
+ ];
383
+
384
+ // src/configs/better-tailwind.ts
385
+ import betterTailwindPlugin from "eslint-plugin-better-tailwindcss";
386
+ var betterTailwind = (opts) => [
387
+ {
388
+ name: "better-tailwindcss",
389
+ plugins: {
390
+ "better-tailwindcss": betterTailwindPlugin
391
+ },
392
+ rules: {
393
+ // rootFontSize lets a px arbitrary value canonicalize to its scale step
394
+ // (`w-[16px]` to `w-4`).
395
+ "better-tailwindcss/enforce-canonical-classes": [
396
+ "error",
397
+ { rootFontSize: 16 }
398
+ ],
399
+ "better-tailwindcss/enforce-consistent-important-position": "error",
400
+ "better-tailwindcss/enforce-consistent-variable-syntax": "error",
401
+ "better-tailwindcss/enforce-shorthand-classes": "error",
402
+ "better-tailwindcss/no-conflicting-classes": "error",
403
+ "better-tailwindcss/no-deprecated-classes": "error",
404
+ "better-tailwindcss/no-unknown-classes": [
405
+ "error",
406
+ { ignore: opts.ignore ?? [] }
407
+ ]
408
+ },
409
+ settings: {
410
+ "better-tailwindcss": {
411
+ detectComponentClasses: true,
412
+ entryPoint: opts.entryPoint
413
+ }
414
+ }
415
+ }
416
+ ];
417
+
418
+ // src/configs/guardrails.ts
419
+ import path from "path";
420
+ import noRelativeImportPaths from "eslint-plugin-no-relative-import-paths";
421
+ import * as regexp from "eslint-plugin-regexp";
422
+ import sonarjs from "eslint-plugin-sonarjs";
423
+
424
+ // src/plugins/cn-conditional.ts
425
+ var findVariable = (scope, name) => {
426
+ let current = scope;
427
+ while (current) {
428
+ const variable = current.set.get(name);
429
+ if (variable) return variable;
430
+ current = current.upper;
431
+ }
432
+ return void 0;
433
+ };
434
+ var isCnImport = (variable) => variable !== void 0 && variable.defs.length === 1 && variable.defs.every((definition) => {
435
+ if (definition.type !== "ImportBinding") return false;
436
+ const specifier = definition.node;
437
+ return specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === "cn" && definition.parent?.type === "ImportDeclaration" && definition.parent.source.value === "cn";
438
+ });
439
+ var isEmptyBranch = (node) => {
440
+ if (!node) return false;
441
+ switch (node.type) {
442
+ case "Identifier":
443
+ return node.name === "undefined";
444
+ case "Literal":
445
+ return node.value === null || node.value === false || node.value === "";
446
+ case "TemplateLiteral":
447
+ return node.expressions.length === 0 && node.quasis.every((quasi) => quasi.value.cooked === "");
448
+ default:
449
+ return false;
450
+ }
451
+ };
452
+ var cnConditionalRule = {
453
+ create: (context) => {
454
+ const visit = (node) => {
455
+ if (!node) return;
456
+ switch (node.type) {
457
+ case "ArrayExpression":
458
+ node.elements.forEach((element) => visit(element));
459
+ break;
460
+ case "ConditionalExpression":
461
+ if (isEmptyBranch(node.consequent) || isEmptyBranch(node.alternate)) {
462
+ context.report({ messageId: "useLogical", node });
463
+ } else {
464
+ visit(node.consequent);
465
+ visit(node.alternate);
466
+ }
467
+ break;
468
+ case "LogicalExpression":
469
+ if (node.operator === "&&") {
470
+ visit(node.right);
471
+ } else {
472
+ visit(node.left);
473
+ visit(node.right);
474
+ }
475
+ break;
476
+ case "ObjectExpression":
477
+ context.report({ messageId: "useLogical", node });
478
+ break;
479
+ default:
480
+ break;
481
+ }
482
+ };
483
+ return {
484
+ CallExpression: (node) => {
485
+ if (node.callee.type !== "Identifier") return;
486
+ const variable = findVariable(
487
+ context.sourceCode.getScope(node),
488
+ node.callee.name
489
+ );
490
+ if (!isCnImport(variable)) return;
491
+ node.arguments.forEach((argument) => visit(argument));
492
+ }
493
+ };
494
+ },
495
+ meta: {
496
+ docs: {
497
+ description: "Require conditional classes passed to cn to be written as `cond && 'class'`"
498
+ },
499
+ messages: {
500
+ useLogical: "Write a conditional class as `cond && 'class'` (or `!cond && 'class'`). `cn` does not accept object conditionals or a ternary with an empty branch in GAIA."
501
+ },
502
+ schema: [],
503
+ type: "problem"
504
+ }
505
+ };
506
+ var plugin = {
507
+ meta: {
508
+ name: "cn-conditional",
509
+ version: "0.1.0"
510
+ },
511
+ rules: {
512
+ "cn-conditional": cnConditionalRule
513
+ }
514
+ };
515
+ var cn_conditional_default = plugin;
516
+
517
+ // src/plugins/no-enum.ts
518
+ var noEnumRule = {
519
+ create: (context) => ({
520
+ TSEnumDeclaration: (node) => {
521
+ context.report({ messageId: "noEnum", node });
522
+ }
523
+ }),
524
+ meta: {
525
+ docs: { description: "Disallow TypeScript enums" },
526
+ messages: {
527
+ noEnum: "Do not use TypeScript enums. Use an object with `as const` instead."
528
+ },
529
+ schema: [],
530
+ type: "problem"
531
+ }
532
+ };
533
+ var plugin2 = {
534
+ meta: {
535
+ name: "no-enum",
536
+ version: "0.1.0"
537
+ },
538
+ rules: {
539
+ "no-enum": noEnumRule
540
+ }
541
+ };
542
+ var no_enum_default = plugin2;
543
+
544
+ // src/plugins/no-jsx-iife.ts
545
+ var noJsxIifeRule = {
546
+ create: (context) => ({
547
+ "JSXExpressionContainer > CallExpression > ArrowFunctionExpression.callee": (node) => {
548
+ context.report({ messageId: "noJsxIife", node });
549
+ },
550
+ "JSXExpressionContainer > CallExpression > FunctionExpression.callee": (node) => {
551
+ context.report({ messageId: "noJsxIife", node });
552
+ }
553
+ }),
554
+ meta: {
555
+ docs: { description: "Disallow IIFEs in JSX expressions" },
556
+ messages: {
557
+ noJsxIife: "Do not use IIFEs in JSX. Use a computed variable before the return or extract a component instead."
558
+ },
559
+ schema: [],
560
+ type: "problem"
561
+ }
562
+ };
563
+ var plugin3 = {
564
+ meta: {
565
+ name: "no-jsx-iife",
566
+ version: "0.1.0"
567
+ },
568
+ rules: {
569
+ "no-jsx-iife": noJsxIifeRule
570
+ }
571
+ };
572
+ var no_jsx_iife_default = plugin3;
573
+
574
+ // src/plugins/no-null-render.ts
575
+ var isJsxNode = (node) => node.type === "JSXElement" || node.type === "JSXFragment";
576
+ var noNullRenderRule = {
577
+ create: (context) => {
578
+ const stack = [];
579
+ const enterFunction = (node) => {
580
+ const frame = { candidates: [], rendersJsx: false };
581
+ if (node.type === "ArrowFunctionExpression" && isJsxNode(node.body)) {
582
+ frame.rendersJsx = true;
583
+ }
584
+ stack.push(frame);
585
+ };
586
+ const exitFunction = () => {
587
+ const frame = stack.pop();
588
+ if (!frame || !frame.rendersJsx) {
589
+ return;
590
+ }
591
+ for (const candidate of frame.candidates) {
592
+ const { argument } = candidate;
593
+ if (!argument) {
594
+ continue;
595
+ }
596
+ context.report({
597
+ fix: (fixer) => fixer.replaceText(argument, "undefined"),
598
+ messageId: "nullRender",
599
+ node: candidate
600
+ });
601
+ }
602
+ };
603
+ return {
604
+ ArrowFunctionExpression: enterFunction,
605
+ "ArrowFunctionExpression:exit": exitFunction,
606
+ FunctionDeclaration: enterFunction,
607
+ "FunctionDeclaration:exit": exitFunction,
608
+ FunctionExpression: enterFunction,
609
+ "FunctionExpression:exit": exitFunction,
610
+ ReturnStatement: (node) => {
611
+ const frame = stack[stack.length - 1];
612
+ if (!frame) {
613
+ return;
614
+ }
615
+ const { argument } = node;
616
+ if (!argument) {
617
+ return;
618
+ }
619
+ if (isJsxNode(argument)) {
620
+ frame.rendersJsx = true;
621
+ return;
622
+ }
623
+ if (argument.type === "Literal" && argument.value === null && argument.raw === "null") {
624
+ frame.candidates.push(node);
625
+ }
626
+ }
627
+ };
628
+ },
629
+ meta: {
630
+ docs: {
631
+ description: "Render nothing with `undefined`, not `null`, in render functions"
632
+ },
633
+ fixable: "code",
634
+ messages: {
635
+ nullRender: "Render nothing with `undefined`, not `null`. GAIA standardizes on `undefined` for the empty render (both are identical to React; this is a consistency convention)."
636
+ },
637
+ schema: [],
638
+ type: "suggestion"
639
+ }
640
+ };
641
+ var plugin4 = {
642
+ meta: {
643
+ name: "no-null-render",
644
+ version: "0.1.0"
645
+ },
646
+ rules: {
647
+ "no-null-render": noNullRenderRule
648
+ }
649
+ };
650
+ var no_null_render_default = plugin4;
651
+
652
+ // src/plugins/no-switch.ts
653
+ var noSwitchRule = {
654
+ create: (context) => ({
655
+ SwitchStatement: (node) => {
656
+ context.report({ messageId: "noSwitch", node });
657
+ }
658
+ }),
659
+ meta: {
660
+ docs: { description: "Disallow switch statements" },
661
+ messages: {
662
+ noSwitch: "Do not use switch statements. Use an object map or if/else instead."
663
+ },
664
+ schema: [],
665
+ type: "problem"
666
+ }
667
+ };
668
+ var plugin5 = {
669
+ meta: {
670
+ name: "no-switch",
671
+ version: "0.1.0"
672
+ },
673
+ rules: {
674
+ "no-switch": noSwitchRule
675
+ }
676
+ };
677
+ var no_switch_default = plugin5;
678
+
679
+ // src/plugins/no-zod-enum.ts
680
+ var noZodEnumRule = {
681
+ create: (context) => ({
682
+ 'CallExpression > MemberExpression.callee[object.name="z"][property.name="enum"]': (node) => {
683
+ context.report({ messageId: "noZodEnum", node });
684
+ }
685
+ }),
686
+ meta: {
687
+ docs: { description: "Disallow z.enum(); use z.literal([...]) instead" },
688
+ messages: {
689
+ noZodEnum: "Do not use z.enum(). Use z.literal([...]) for string unions (sort values alphanumerically)."
690
+ },
691
+ schema: [],
692
+ type: "problem"
693
+ }
694
+ };
695
+ var plugin6 = {
696
+ meta: {
697
+ name: "no-zod-enum",
698
+ version: "0.1.0"
699
+ },
700
+ rules: {
701
+ "no-zod-enum": noZodEnumRule
702
+ }
703
+ };
704
+ var no_zod_enum_default = plugin6;
705
+
706
+ // src/configs/guardrails.ts
707
+ var buildSonarConfig = (sourceDir) => [
708
+ sonarjs.configs.recommended,
709
+ {
710
+ name: "sonarjs",
711
+ rules: {
712
+ "sonarjs/cognitive-complexity": "error",
713
+ "sonarjs/fixme-tag": "off",
714
+ "sonarjs/no-commented-code": "off",
715
+ "sonarjs/no-nested-conditional": "off",
716
+ "sonarjs/no-nested-functions": "off",
717
+ "sonarjs/no-selector-parameter": "off",
718
+ "sonarjs/regex-complexity": "off",
719
+ "sonarjs/todo-tag": "off"
720
+ }
721
+ },
722
+ {
723
+ files: ["**/*.tsx", "**/hooks/*.ts?(x)"],
724
+ name: "sonarjs/react-files",
725
+ rules: {
726
+ "sonarjs/cognitive-complexity": "off",
727
+ "sonarjs/function-return-type": "off"
728
+ }
729
+ },
730
+ {
731
+ files: ["**/*.test.ts?(x)", "**/*.stories.ts?(x)"],
732
+ name: "sonarjs/test-files",
733
+ rules: {
734
+ "sonarjs/no-duplicate-string": "off",
735
+ "sonarjs/no-identical-functions": "off"
736
+ }
737
+ },
738
+ {
739
+ files: [`${sourceDir}/languages/**/*.ts`, "eslint.config.mjs"],
740
+ name: "sonarjs/credential-checks",
741
+ rules: {
742
+ "sonarjs/no-hardcoded-credentials": "off",
743
+ "sonarjs/no-hardcoded-passwords": "off"
744
+ }
745
+ }
746
+ ];
747
+ var regexpConfig = [
748
+ { ...regexp.configs["flat/recommended"], name: "regexp" }
749
+ ];
750
+ var cnConditionalConfig = [
751
+ {
752
+ files: ["**/*.ts?(x)", "**/*.js?(x)"],
753
+ name: "cn-conditional",
754
+ plugins: { "cn-conditional": cn_conditional_default },
755
+ rules: { "cn-conditional/cn-conditional": "error" }
756
+ }
757
+ ];
758
+ var noEnumConfig = [
759
+ {
760
+ files: ["**/*.ts?(x)"],
761
+ name: "no-enum",
762
+ plugins: { "no-enum": no_enum_default },
763
+ rules: {
764
+ "no-enum/no-enum": "error"
765
+ }
766
+ }
767
+ ];
768
+ var noJsxIifeConfig = [
769
+ {
770
+ files: ["**/*.tsx", "**/*.jsx"],
771
+ name: "no-jsx-iife",
772
+ plugins: { "no-jsx-iife": no_jsx_iife_default },
773
+ rules: { "no-jsx-iife/no-jsx-iife": "error" }
774
+ }
775
+ ];
776
+ var noNullRenderConfig = [
777
+ {
778
+ files: ["**/*.tsx", "**/*.jsx"],
779
+ name: "no-null-render",
780
+ plugins: { "no-null-render": no_null_render_default },
781
+ rules: { "no-null-render/no-null-render": "error" }
782
+ }
783
+ ];
784
+ var noSwitchConfig = [
785
+ {
786
+ files: ["**/*.ts?(x)", "**/*.js?(x)"],
787
+ name: "no-switch",
788
+ plugins: { "no-switch": no_switch_default },
789
+ rules: { "no-switch/no-switch": "error" }
790
+ }
791
+ ];
792
+ var noZodEnumConfig = [
793
+ {
794
+ files: ["**/*.ts?(x)"],
795
+ name: "no-zod-enum",
796
+ plugins: { "no-zod-enum": no_zod_enum_default },
797
+ rules: { "no-zod-enum/no-zod-enum": "error" }
798
+ }
799
+ ];
800
+ var buildNoRestrictedPathsConfig = (sourceDir) => {
801
+ const dir = (layer) => `./${sourceDir}/${layer}`;
802
+ const globDir = (layer) => `./${sourceDir}/${layer}/**`;
803
+ const routesDir = path.resolve(process.cwd(), sourceDir, "routes").split(path.sep).join("/");
804
+ const dataEndpoints = [
805
+ `${routesDir}/actions.*`,
806
+ `${routesDir}/resources.*`,
807
+ // Older `+` group folders stay exempt through the 2.x line.
808
+ `${routesDir}/actions+/**`,
809
+ `${routesDir}/resources+/**`
810
+ ];
811
+ return [
812
+ {
813
+ files: [`${sourceDir}/**/!(*.test|*.stories).ts?(x)`],
814
+ name: "import-x/architecture-boundaries",
815
+ rules: {
816
+ "import-x/no-restricted-paths": [
817
+ "error",
818
+ {
819
+ zones: [
820
+ {
821
+ except: dataEndpoints,
822
+ from: [globDir("routes")],
823
+ message: "Pages may only be imported by routes; a page must not import a route (import direction is routes -> pages -> components). Typed `actions.*`/`resources.*` data-endpoint routes are exempt (the older `actions+`/`resources+` folders are still accepted).",
824
+ target: dir("pages")
825
+ },
826
+ {
827
+ except: dataEndpoints,
828
+ from: [globDir("routes"), globDir("pages")],
829
+ message: "Reusable components must not depend on page- or route-level code (import direction is routes -> pages -> components). Typed `actions.*`/`resources.*` data-endpoint routes are exempt (the older `actions+`/`resources+` folders are still accepted).",
830
+ target: dir("components")
831
+ },
832
+ {
833
+ except: dataEndpoints,
834
+ from: [globDir("routes"), globDir("pages"), globDir("components")],
835
+ message: "Hooks and state sit below the UI tree; they must not import components, pages, or routes. Typed `actions.*`/`resources.*` data-endpoint routes are exempt (the older `actions+`/`resources+` folders are still accepted).",
836
+ target: [dir("hooks"), dir("state")]
837
+ },
838
+ {
839
+ from: [
840
+ dir("routes"),
841
+ dir("pages"),
842
+ dir("components"),
843
+ dir("hooks"),
844
+ dir("state")
845
+ ],
846
+ message: "The service/data layer sits below the UI and orchestration layers; it must not import components, pages, routes, hooks, or state.",
847
+ target: dir("services")
848
+ },
849
+ {
850
+ from: [
851
+ dir("routes"),
852
+ dir("pages"),
853
+ dir("components"),
854
+ dir("hooks"),
855
+ dir("state"),
856
+ dir("services")
857
+ ],
858
+ message: "Utils are near-leaves; they may import only types and other utils.",
859
+ target: dir("utils")
860
+ },
861
+ {
862
+ from: [
863
+ dir("routes"),
864
+ dir("pages"),
865
+ dir("components"),
866
+ dir("hooks"),
867
+ dir("state"),
868
+ dir("services"),
869
+ dir("utils")
870
+ ],
871
+ message: "Types are a pure leaf; they must not import any other app layer.",
872
+ target: dir("types")
873
+ }
874
+ ]
875
+ }
876
+ ]
877
+ }
878
+ }
879
+ ];
880
+ };
881
+ var buildNoRelativeImportPathsConfig = (sourceDir) => [
882
+ {
883
+ name: "no-relative-import-paths",
884
+ plugins: {
885
+ "no-relative-import-paths": noRelativeImportPaths
886
+ },
887
+ rules: {
888
+ "no-relative-import-paths/no-relative-import-paths": [
889
+ "error",
890
+ {
891
+ allowedDepth: 2,
892
+ allowSameFolder: true,
893
+ prefix: "~",
894
+ rootDir: sourceDir
895
+ }
896
+ ]
897
+ }
898
+ }
899
+ ];
900
+ var buildGuardrails = (sourceDir) => [
901
+ ...buildSonarConfig(sourceDir),
902
+ ...regexpConfig,
903
+ ...cnConditionalConfig,
904
+ ...noEnumConfig,
905
+ ...noJsxIifeConfig,
906
+ ...noNullRenderConfig,
907
+ ...noSwitchConfig,
908
+ ...noZodEnumConfig,
909
+ ...buildNoRestrictedPathsConfig(sourceDir),
910
+ ...buildNoRelativeImportPathsConfig(sourceDir)
911
+ ];
912
+
913
+ // src/configs/i18n.ts
914
+ import i18next from "eslint-plugin-i18next";
915
+ var buildI18n = (sourceDir) => [
916
+ {
917
+ files: [`${sourceDir}/**/*.tsx`, `${sourceDir}/**/*.jsx`],
918
+ ignores: [
919
+ "**/*.test.tsx",
920
+ "**/*.stories.tsx",
921
+ "**/tests/**",
922
+ `${sourceDir}/components/ui/*.tsx`
923
+ ],
924
+ name: "i18n/no-literal-string",
925
+ plugins: { i18next },
926
+ rules: {
927
+ "i18next/no-literal-string": ["error", { mode: "jsx-text-only" }]
928
+ }
929
+ }
930
+ ];
931
+
932
+ // src/configs/ignores.ts
933
+ import { includeIgnoreFile } from "@eslint/config-helpers";
934
+ import fs from "fs";
935
+ import path2 from "path";
936
+ var defaultIgnores = [
937
+ "/.react-router/**",
938
+ ".claude/**/*.js",
939
+ ".claude/**/*.cjs",
940
+ "scripts",
941
+ "public/**",
942
+ "**/*.css",
943
+ "**/*.svg",
944
+ "**/*.md"
945
+ ];
946
+ var ignores = (opts) => {
947
+ const out = [];
948
+ const gitignore = opts?.gitignore ?? ".gitignore";
949
+ if (gitignore !== false) {
950
+ const resolved = path2.isAbsolute(gitignore) ? gitignore : path2.resolve(process.cwd(), gitignore);
951
+ if (fs.existsSync(resolved)) {
952
+ out.push(includeIgnoreFile(resolved));
953
+ }
954
+ }
955
+ out.push({
956
+ ignores: [...defaultIgnores, ...opts?.extra ?? []],
957
+ name: "ignored-files"
958
+ });
959
+ return out;
960
+ };
961
+
962
+ // src/configs/playwright.ts
963
+ import playwrightPlugin from "eslint-plugin-playwright";
964
+ var playwright = [
965
+ {
966
+ name: "playwright",
967
+ ...playwrightPlugin.configs["flat/recommended"],
968
+ files: [".playwright/**/*.ts?(x)"],
969
+ rules: {
970
+ ...playwrightPlugin.configs["flat/recommended"].rules,
971
+ // Every test must assert something. `assertFunctionPatterns` lets custom
972
+ // `expect*()` helpers count as the assertion. Error, never warn: a test
973
+ // that asserts nothing is a false green.
974
+ "playwright/expect-expect": [
975
+ "error",
976
+ { assertFunctionPatterns: ["^expect[A-Z]"] }
977
+ ],
978
+ // A bare `test.skip()` is a test switched off at author time and stays
979
+ // flagged at the recommended set's severity. `test.skip(condition, reason)`
980
+ // is a different thing: Playwright's own API for a test that does not
981
+ // apply to the environment it just found (a feature the project did not
982
+ // enable, a browser without the capability). Allow the conditional form.
983
+ "playwright/no-skipped-test": ["warn", { allowConditional: true }]
984
+ }
985
+ }
986
+ ];
987
+
988
+ // src/configs/prettier.ts
989
+ import { rules as prettierConfigRules } from "eslint-config-prettier";
990
+ import prettierPlugin from "eslint-plugin-prettier";
991
+ var prettier = [
992
+ {
993
+ name: "prettier/plugin/config",
994
+ plugins: { prettier: prettierPlugin }
995
+ },
996
+ {
997
+ name: "prettier/config",
998
+ rules: {
999
+ ...prettierConfigRules,
1000
+ "@stylistic/padding-line-between-statements": [
1001
+ "error",
1002
+ {
1003
+ blankLine: "always",
1004
+ next: ["block-like", "export", "return", "throw"],
1005
+ prev: "*"
1006
+ }
1007
+ ],
1008
+ "@stylistic/quotes": [
1009
+ "error",
1010
+ "single",
1011
+ {
1012
+ allowTemplateLiterals: "avoidEscape",
1013
+ avoidEscape: true
1014
+ }
1015
+ ],
1016
+ "@stylistic/spaced-comment": "off",
1017
+ "prettier/prettier": ["error", { endOfLine: "auto" }]
1018
+ }
1019
+ }
1020
+ ];
1021
+
1022
+ // src/configs/react.ts
1023
+ import eslintReact from "@eslint-react/eslint-plugin";
1024
+ import { configs as configs3, plugins as plugins2 } from "eslint-config-airbnb-extended";
1025
+ import reactRefresh from "eslint-plugin-react-refresh";
1026
+ import youMightNotNeedAnEffect from "eslint-plugin-react-you-might-not-need-an-effect";
1027
+ var ONLY_EXPORT_COMPONENTS_OPTIONS = {
1028
+ allowCompoundComponents: true,
1029
+ allowConstantExport: true
1030
+ };
1031
+ var react = [
1032
+ // React plugin
1033
+ plugins2.react,
1034
+ // React hooks plugin
1035
+ plugins2.reactHooks,
1036
+ // React JSX A11y plugin
1037
+ plugins2.reactA11y,
1038
+ // Airbnb React recommended config
1039
+ ...configs3.react.recommended,
1040
+ {
1041
+ name: "react-custom",
1042
+ rules: {
1043
+ "jsx-a11y/control-has-associated-label": "off",
1044
+ "jsx-a11y/no-autofocus": "off",
1045
+ "react/boolean-prop-naming": [
1046
+ "error",
1047
+ {
1048
+ propTypeNames: ["bool", "mutuallyExclusiveTrueProps"],
1049
+ rule: "^((can|has|hide|is|show)[A-Z]([A-Za-z0-9]?)+|(checked|disabled|hide|required|show))"
1050
+ }
1051
+ ],
1052
+ "react/function-component-definition": "off",
1053
+ "react/jsx-boolean-value": ["error", "always"],
1054
+ "react/jsx-curly-brace-presence": "error",
1055
+ "react/jsx-filename-extension": "off",
1056
+ "react/jsx-fragments": "error",
1057
+ // off by default because it doesn't handle props onXyz function names correctly
1058
+ // turn this on from time to time to check for misnamed handlers elsewhere
1059
+ "react/jsx-handler-names": ["off", { checkLocalVariables: true }],
1060
+ "react/jsx-newline": ["error", { prevent: true }],
1061
+ "react/jsx-no-useless-fragment": ["error", { allowExpressions: true }],
1062
+ "react/no-children-prop": "off",
1063
+ "react/no-danger": "off",
1064
+ "react/prop-types": "off",
1065
+ "react/react-in-jsx-scope": "off",
1066
+ "react/require-default-props": "off"
1067
+ }
1068
+ },
1069
+ // A module that exports anything besides components cannot be hot-swapped:
1070
+ // Fast Refresh falls back to a full reload and drops component state. Test
1071
+ // and story files, and the helpers beside them in `tests/` folders, are never
1072
+ // hot-swapped as components, so they are exempt.
1073
+ {
1074
+ files: ["**/*.tsx", "**/*.jsx"],
1075
+ ignores: ["**/*.test.ts?(x)", "**/*.stories.ts?(x)", "**/tests/**"],
1076
+ name: "react-refresh",
1077
+ plugins: { "react-refresh": reactRefresh },
1078
+ rules: {
1079
+ "react-refresh/only-export-components": [
1080
+ "error",
1081
+ ONLY_EXPORT_COMPONENTS_OPTIONS
1082
+ ]
1083
+ }
1084
+ },
1085
+ // Effects that should not be Effects: state derived from other state or
1086
+ // props, user-event logic, chained updates, notifying a parent, resetting
1087
+ // state on a prop change (use `key`), and hand-rolled store subscriptions
1088
+ // (use `useSyncExternalStore`). Complements `react-hooks/set-state-in-effect`.
1089
+ {
1090
+ files: ["**/*.ts?(x)", "**/*.js?(x)"],
1091
+ name: "react-you-might-not-need-an-effect",
1092
+ plugins: {
1093
+ "react-you-might-not-need-an-effect": youMightNotNeedAnEffect
1094
+ },
1095
+ rules: {
1096
+ "react-you-might-not-need-an-effect/no-adjust-state-on-prop-change": "error",
1097
+ "react-you-might-not-need-an-effect/no-chain-state-updates": "error",
1098
+ "react-you-might-not-need-an-effect/no-derived-state": "error",
1099
+ "react-you-might-not-need-an-effect/no-event-handler": "error",
1100
+ "react-you-might-not-need-an-effect/no-external-store-subscription": "error",
1101
+ "react-you-might-not-need-an-effect/no-initialize-state": "error",
1102
+ "react-you-might-not-need-an-effect/no-pass-data-to-parent": "error",
1103
+ "react-you-might-not-need-an-effect/no-pass-live-state-to-parent": "error",
1104
+ "react-you-might-not-need-an-effect/no-reset-all-state-on-prop-change": "error"
1105
+ }
1106
+ },
1107
+ // Nine rules from @eslint-react, not its preset: the preset re-enables hook
1108
+ // rules `react-hooks` already owns. An Effect that adds a listener, timer,
1109
+ // interval, observer or fetch must tear it down in its cleanup (a fetch
1110
+ // through an AbortController). React 19 forms only: `ref` as a prop instead
1111
+ // of `forwardRef`, `use(Context)` instead of `useContext`, and `<Context>`
1112
+ // instead of `<Context.Provider>`. `no-context-provider` matches by name, so
1113
+ // it reports `ThemeContext.Provider` but not `Ctx.Provider`.
1114
+ {
1115
+ files: ["**/*.ts?(x)", "**/*.js?(x)"],
1116
+ name: "eslint-react",
1117
+ plugins: { "@eslint-react": eslintReact },
1118
+ rules: {
1119
+ "@eslint-react/no-context-provider": "error",
1120
+ "@eslint-react/no-forward-ref": "error",
1121
+ "@eslint-react/no-use-context": "error",
1122
+ "@eslint-react/web-api-no-leaked-event-listener": "error",
1123
+ "@eslint-react/web-api-no-leaked-fetch": "error",
1124
+ "@eslint-react/web-api-no-leaked-intersection-observer": "error",
1125
+ "@eslint-react/web-api-no-leaked-interval": "error",
1126
+ "@eslint-react/web-api-no-leaked-resize-observer": "error",
1127
+ "@eslint-react/web-api-no-leaked-timeout": "error"
1128
+ }
1129
+ },
1130
+ {
1131
+ files: ["**/*.test.ts?(x)", "**/*.stories.ts?(x)"],
1132
+ name: "react/test-files",
1133
+ rules: {
1134
+ "react/jsx-props-no-spreading": "off"
1135
+ }
1136
+ }
1137
+ ];
1138
+
1139
+ // src/plugins/no-imperative-query-fetch.ts
1140
+ var BANNED_METHODS = /* @__PURE__ */ new Map([
1141
+ ["ensureInfiniteQueryData", "infiniteQuery"],
1142
+ ["ensureQueryData", "query"],
1143
+ ["fetchInfiniteQuery", "infiniteQuery"],
1144
+ ["fetchQuery", "query"],
1145
+ ["prefetchInfiniteQuery", "infiniteQuery"],
1146
+ ["prefetchQuery", "query"]
1147
+ ]);
1148
+ var getPropertyName = (callee) => {
1149
+ if (callee.type !== "MemberExpression") {
1150
+ return void 0;
1151
+ }
1152
+ const { computed, property } = callee;
1153
+ if (!computed && property.type === "Identifier") {
1154
+ return property.name;
1155
+ }
1156
+ if (computed && property.type === "Literal" && typeof property.value === "string") {
1157
+ return property.value;
1158
+ }
1159
+ return void 0;
1160
+ };
1161
+ var noImperativeQueryFetchRule = {
1162
+ create: (context) => ({
1163
+ CallExpression: (node) => {
1164
+ const method = getPropertyName(node.callee);
1165
+ const replacement = method && BANNED_METHODS.get(method);
1166
+ if (method && replacement) {
1167
+ context.report({
1168
+ data: { method, replacement },
1169
+ messageId: "banned",
1170
+ node
1171
+ });
1172
+ }
1173
+ }
1174
+ }),
1175
+ meta: {
1176
+ docs: {
1177
+ description: "Disallow imperative TanStack Query fetches; await queryClient.query in a clientLoader and read with useSuspenseQuery"
1178
+ },
1179
+ messages: {
1180
+ banned: "`{{method}}` is banned. Await `queryClient.{{replacement}}(options)` in a clientLoader and read the data with `useSuspenseQuery(options)` in the component."
1181
+ },
1182
+ schema: [],
1183
+ type: "problem"
1184
+ }
1185
+ };
1186
+ var plugin7 = {
1187
+ meta: {
1188
+ name: "no-imperative-query-fetch",
1189
+ version: "0.1.0"
1190
+ },
1191
+ rules: {
1192
+ "no-imperative-query-fetch": noImperativeQueryFetchRule
1193
+ }
1194
+ };
1195
+ var no_imperative_query_fetch_default = plugin7;
1196
+
1197
+ // src/configs/react-router.ts
1198
+ var ROUTE_MODULE_EXPORTS = [
1199
+ "action",
1200
+ "clientAction",
1201
+ "clientLoader",
1202
+ "clientMiddleware",
1203
+ "ErrorBoundary",
1204
+ "handle",
1205
+ "headers",
1206
+ "HydrateFallback",
1207
+ "Layout",
1208
+ "links",
1209
+ "loader",
1210
+ "meta",
1211
+ "middleware",
1212
+ "shouldRevalidate"
1213
+ ];
1214
+ var buildReactRouter = (sourceDir) => [
1215
+ {
1216
+ files: ["**/routes/**/*.tsx"],
1217
+ name: "react-router/routes",
1218
+ rules: {
1219
+ "no-empty-pattern": "off"
1220
+ }
1221
+ },
1222
+ {
1223
+ files: ["**/routes/**/*.tsx", `${sourceDir}/root.tsx`],
1224
+ ignores: ["**/*.test.tsx", "**/*.stories.tsx", "**/tests/**"],
1225
+ name: "react-router/route-module-exports",
1226
+ rules: {
1227
+ "react-refresh/only-export-components": [
1228
+ "error",
1229
+ { ...ONLY_EXPORT_COMPONENTS_OPTIONS, allowExportNames: ROUTE_MODULE_EXPORTS }
1230
+ ]
1231
+ }
1232
+ },
1233
+ {
1234
+ files: [`${sourceDir}/**/*.ts?(x)`],
1235
+ name: "react-router/no-imperative-query-fetch",
1236
+ plugins: { "no-imperative-query-fetch": no_imperative_query_fetch_default },
1237
+ rules: {
1238
+ "no-imperative-query-fetch/no-imperative-query-fetch": "error"
1239
+ }
1240
+ }
1241
+ ];
1242
+
1243
+ // src/configs/shadcn.ts
1244
+ import { plugin as shadcnPlugin } from "@shadcn/lint";
1245
+ var SOURCE_EXTENSIONS = "{js,jsx,ts,tsx}";
1246
+ var ARBITRARY_COLOR_PATTERN = String.raw`^.*\[(#|rgba?\(|hsla?\(|oklch\(|oklab\(|lab\(|lch\(|color:).*\]$`;
1247
+ var VENDORED_HELPER_FILES = [
1248
+ "hooks/use-highlighted-code.ts",
1249
+ "hooks/use-mobile.ts",
1250
+ "utils/highlighter.tsx"
1251
+ ];
1252
+ var VENDORED_UI_RULES_OFF = {
1253
+ // shadcn renders `<Context.Provider>` and calls `useContext` (carousel,
1254
+ // chart, drawer, input-otp, sidebar, toggle-group).
1255
+ "@eslint-react/no-context-provider": "off",
1256
+ "@eslint-react/no-use-context": "off",
1257
+ "@stylistic/padding-line-between-statements": "off",
1258
+ // shadcn emits double-quoted strings.
1259
+ "@stylistic/quotes": "off",
1260
+ // shadcn writes `Array<T>`.
1261
+ "@typescript-eslint/array-type": "off",
1262
+ // Helper exports carry inferred return types (use-mobile, highlighter).
1263
+ "@typescript-eslint/explicit-module-boundary-types": "off",
1264
+ // toggle-group.tsx: `import { type VariantProps } from ...`.
1265
+ "@typescript-eslint/no-import-type-side-effects": "off",
1266
+ // chart.tsx: `payload[key as keyof typeof payload] as string`.
1267
+ "@typescript-eslint/no-unnecessary-type-assertion": "off",
1268
+ // shadcn function declarations use a variable before its declaration
1269
+ // (hoisted, no runtime effect), e.g. select.tsx.
1270
+ "@typescript-eslint/naming-convention": "off",
1271
+ "@typescript-eslint/no-use-before-define": "off",
1272
+ // toggle-group.tsx falls back from context to props with `||`.
1273
+ "@typescript-eslint/prefer-nullish-coalescing": "off",
1274
+ // toast.tsx re-exports namespace members as `const a = Namespace.a`.
1275
+ "@typescript-eslint/prefer-destructuring": "off",
1276
+ // The useMemo callback in field.tsx is flagged; a stylistic async annotation.
1277
+ "@typescript-eslint/promise-function-async": "off",
1278
+ // calendar.tsx and sidebar.tsx arrow bodies are blocks.
1279
+ "arrow-body-style": "off",
1280
+ // Tailwind class rewrites would change vendored class strings.
1281
+ "better-tailwindcss/enforce-canonical-classes": "off",
1282
+ "better-tailwindcss/enforce-shorthand-classes": "off",
1283
+ "better-tailwindcss/no-deprecated-classes": "off",
1284
+ // shadcn writes `export { A, B }` on one line.
1285
+ "canonical/export-specifier-newline": "off",
1286
+ // chart.tsx passes an object to `cn`.
1287
+ "cn-conditional/cn-conditional": "off",
1288
+ // shadcn uses inline `type` specifiers (`import {cva, type VariantProps}`).
1289
+ "import-x/consistent-type-specifier-style": "off",
1290
+ // shadcn renders null from field.tsx.
1291
+ "no-null-render/no-null-render": "off",
1292
+ // `_values` in slider.tsx.
1293
+ "no-underscore-dangle": "off",
1294
+ // carousel.tsx: `{ api: api, ... }`.
1295
+ "object-shorthand": "off",
1296
+ "perfectionist/sort-imports": "off",
1297
+ "perfectionist/sort-intersection-types": "off",
1298
+ "perfectionist/sort-jsx-props": "off",
1299
+ "perfectionist/sort-modules": "off",
1300
+ "perfectionist/sort-named-exports": "off",
1301
+ "perfectionist/sort-named-imports": "off",
1302
+ "perfectionist/sort-object-types": "off",
1303
+ "perfectionist/sort-objects": "off",
1304
+ "perfectionist/sort-union-types": "off",
1305
+ // shadcn components are `function` declarations.
1306
+ "prefer-arrow-functions/prefer-arrow-functions": "off",
1307
+ // Vendored output is never reformatted; it stays byte-identical to
1308
+ // `shadcn add`.
1309
+ "prettier/prettier": "off",
1310
+ // Prop names such as `inset` (dropdown-menu).
1311
+ "react/boolean-prop-naming": "off",
1312
+ // pagination.tsx: a bare `aria-hidden`.
1313
+ "react/jsx-boolean-value": "off",
1314
+ // button.tsx exports `buttonVariants` and toast.tsx exports its toast
1315
+ // manager beside their components. A hot update of either falls back to a
1316
+ // full reload, which costs dev time only.
1317
+ "react-refresh/only-export-components": "off",
1318
+ // `prop?: T | undefined` (calendar, carousel).
1319
+ "sonarjs/no-redundant-optional": "off",
1320
+ "sonarjs/prefer-read-only-props": "off",
1321
+ // chart.tsx: `!payload?.length`, `.replace(/.../g)`, negated ternaries.
1322
+ "unicorn/explicit-length-check": "off",
1323
+ "unicorn/no-negated-condition": "off",
1324
+ "unicorn/prefer-string-replace-all": "off",
1325
+ // The `e` handler parameter in input-group.tsx.
1326
+ "unicorn/prevent-abbreviations": "off",
1327
+ // Sanctioned correctness-class exemptions for vendored output. Each one
1328
+ // trips a pattern in the shadcn source that GAIA does not edit.
1329
+ // sidebar.tsx: `toggleSidebar` returns the setter's void result.
1330
+ "@typescript-eslint/no-confusing-void-expression": "off",
1331
+ // calendar.tsx, carousel.tsx, chart.tsx, sidebar.tsx reuse an outer name
1332
+ // (`props`, `className`, `api`, `config`, `open`) in a nested callback.
1333
+ "@typescript-eslint/no-shadow": "off",
1334
+ // field.tsx FieldError: `uniqueErrors?.length` optional chain on a non-nullish array.
1335
+ "@typescript-eslint/no-unnecessary-condition": "off",
1336
+ // chart.tsx reads recharts payload items, typed `any` upstream.
1337
+ "@typescript-eslint/no-unsafe-argument": "off",
1338
+ "@typescript-eslint/no-unsafe-assignment": "off",
1339
+ "@typescript-eslint/no-unsafe-member-access": "off",
1340
+ "@typescript-eslint/restrict-template-expressions": "off",
1341
+ // field.tsx FieldError: `uniqueErrors?.length == 1`.
1342
+ eqeqeq: "off",
1343
+ // pagination.tsx: PaginationLink's anchor gets its content through spread
1344
+ // `children`, which the rule cannot see.
1345
+ "jsx-a11y/anchor-has-content": "off",
1346
+ // input-group.tsx: InputGroupAddon div with an onClick that focuses the input.
1347
+ "jsx-a11y/click-events-have-key-events": "off",
1348
+ // label.tsx: the generic Label primitive renders a <label> with no static control.
1349
+ "jsx-a11y/label-has-associated-control": "off",
1350
+ // input-group.tsx: the role="group" div with an onClick.
1351
+ "jsx-a11y/no-noninteractive-element-interactions": "off",
1352
+ // code-block.tsx: a scrollable <pre> takes `tabIndex={0}` so the keyboard
1353
+ // can scroll it.
1354
+ "jsx-a11y/no-noninteractive-tabindex": "off",
1355
+ // carousel.tsx syncs Embla's selection into state from an effect;
1356
+ // use-mobile.ts reads `matchMedia` into state on mount.
1357
+ "react-hooks/set-state-in-effect": "off",
1358
+ // calendar.tsx and carousel.tsx call a prop callback from an effect;
1359
+ // use-mobile.ts subscribes to `matchMedia` and initializes state from it.
1360
+ "react-you-might-not-need-an-effect/no-event-handler": "off",
1361
+ "react-you-might-not-need-an-effect/no-external-store-subscription": "off",
1362
+ "react-you-might-not-need-an-effect/no-initialize-state": "off",
1363
+ "react-you-might-not-need-an-effect/no-pass-data-to-parent": "off",
1364
+ // sidebar.tsx: SidebarRail's <button> carries no `type`.
1365
+ "react/button-has-type": "off",
1366
+ // carousel.tsx, chart.tsx, toggle-group.tsx pass an inline object to a
1367
+ // context provider.
1368
+ "react/jsx-no-constructed-context-values": "off",
1369
+ // field.tsx FieldError: `<li key={index}>` over a static deduped error list.
1370
+ "react/no-array-index-key": "off",
1371
+ // calendar.tsx defines DayPicker's `components` slots inline.
1372
+ "react/no-unstable-nested-components": "off",
1373
+ // sidebar.tsx: SidebarMenuSkeleton picks a random skeleton width.
1374
+ "sonarjs/pseudo-random": "off",
1375
+ // sidebar.tsx persists the open state in a cookie.
1376
+ "unicorn/no-document-cookie": "off",
1377
+ // input-group.tsx: a `<Button>` className built dynamically. `no-raw-colors`
1378
+ // still reads those files.
1379
+ "shadcn/require-static-classes": "off",
1380
+ // shadcn writes `import * as React from 'react'`, which the core rule
1381
+ // reports against the `FC` ban because a namespace import could reach those
1382
+ // names. The block re-declares the shared bans minus the `react` entry
1383
+ // (options replace wholesale across blocks), so the other bans hold.
1384
+ "no-restricted-imports": [
1385
+ "error",
1386
+ { paths: RESTRICTED_IMPORT_PATHS.filter((entry) => entry.name !== "react") }
1387
+ ],
1388
+ // Registry items may carry an arbitrary color the token contract bans.
1389
+ "better-tailwindcss/no-restricted-classes": "off",
1390
+ // input-otp.tsx and navigation-menu.tsx carry registry classes GAIA's theme
1391
+ // does not define (`cn-input-otp`, `xs:`, `easing-[ease]`).
1392
+ "better-tailwindcss/no-unknown-classes": "off",
1393
+ // sonner.tsx sets CSS variables through a `style` object.
1394
+ "shadcn/no-inline-styles": "off",
1395
+ // The vendored files compose each other with `className`.
1396
+ "shadcn/no-restyle": "off"
1397
+ };
1398
+ var buildShadcn = (sourceDir, opts) => {
1399
+ const ui = opts?.ui ?? "~/components/ui";
1400
+ const uiFiles = opts?.uiFiles ?? [
1401
+ `${sourceDir}/components/ui/*.tsx`,
1402
+ ...VENDORED_HELPER_FILES.map((file) => `${sourceDir}/${file}`)
1403
+ ];
1404
+ const restyleOffFiles = opts?.restyleOffFiles ?? [];
1405
+ const configs4 = [
1406
+ {
1407
+ name: "shadcn/settings",
1408
+ plugins: { shadcn: shadcnPlugin },
1409
+ settings: { shadcn: { ui } }
1410
+ },
1411
+ {
1412
+ files: [`${sourceDir}/**/*.${SOURCE_EXTENSIONS}`],
1413
+ name: "shadcn/rules",
1414
+ rules: {
1415
+ // An arbitrary value is allowed where no scale step or token fits;
1416
+ // `enforce-canonical-classes` still rewrites one that has a scale
1417
+ // equivalent. An arbitrary color is banned: colors are role tokens.
1418
+ "better-tailwindcss/no-restricted-classes": [
1419
+ "error",
1420
+ {
1421
+ restrict: [
1422
+ {
1423
+ message: "Arbitrary colors are not allowed; use a theme role token.",
1424
+ pattern: ARBITRARY_COLOR_PATTERN
1425
+ }
1426
+ ]
1427
+ }
1428
+ ],
1429
+ "shadcn/no-arbitrary-values": "off",
1430
+ "shadcn/no-inline-styles": "error",
1431
+ "shadcn/no-raw-colors": "error",
1432
+ "shadcn/no-restyle": ["error", { allow: ["layout"] }],
1433
+ // better-tailwindcss/no-unknown-classes stays the only unknown-class rule.
1434
+ "shadcn/no-unknown-classes": "off",
1435
+ "shadcn/require-static-classes": "error"
1436
+ }
1437
+ },
1438
+ {
1439
+ files: uiFiles,
1440
+ name: "shadcn/vendored-ui",
1441
+ rules: VENDORED_UI_RULES_OFF
1442
+ }
1443
+ ];
1444
+ if (restyleOffFiles.length > 0) {
1445
+ configs4.push({
1446
+ files: restyleOffFiles,
1447
+ // The vendored ui folder (and with it `ui/tests/**`) is never reached.
1448
+ ignores: [`${sourceDir}/components/ui/**`],
1449
+ name: "shadcn/restyle-fallback",
1450
+ rules: { "shadcn/no-restyle": "off" }
1451
+ });
1452
+ }
1453
+ return configs4;
1454
+ };
1455
+
1456
+ // src/configs/storybook.ts
1457
+ import storybookPlugin from "eslint-plugin-storybook";
1458
+ var storybook = [
1459
+ ...storybookPlugin.configs["flat/recommended"]
1460
+ ];
1461
+
1462
+ // src/configs/style-hygiene.ts
1463
+ import canonical from "eslint-plugin-canonical";
1464
+ import checkFile from "eslint-plugin-check-file";
1465
+ import perfectionist from "eslint-plugin-perfectionist";
1466
+ import unicorn from "eslint-plugin-unicorn";
1467
+ import unusedImports from "eslint-plugin-unused-imports";
1468
+ var buildCanonicalConfig = (sourceDir) => [
1469
+ canonical.configs["flat/recommended"],
1470
+ {
1471
+ name: "canonical",
1472
+ rules: {
1473
+ "canonical/destructuring-property-newline": "off",
1474
+ "canonical/filename-match-exported": "error",
1475
+ "canonical/id-match": "off",
1476
+ "canonical/import-specifier-newline": "off"
1477
+ }
1478
+ },
1479
+ {
1480
+ // Component and page folders are kebab-case while their default export is
1481
+ // PascalCase, so `theme-switch/index.tsx` exports `ThemeSwitch`. Scoped to
1482
+ // these folders only: a global kebab transform would reject `i18n.ts`
1483
+ // exporting `i18n` (lodash kebabs it to `i-18-n`).
1484
+ files: [`${sourceDir}/components/**/*.tsx`, `${sourceDir}/pages/**/*.tsx`],
1485
+ name: "canonical/filename-match-exported-kebab",
1486
+ rules: {
1487
+ "canonical/filename-match-exported": ["error", { transforms: ["kebab"] }]
1488
+ }
1489
+ },
1490
+ {
1491
+ files: ["**/*.tsx", "**/hooks/*.ts?(x)"],
1492
+ name: "canonical/sort-react-dependencies",
1493
+ rules: {
1494
+ "canonical/sort-react-dependencies": "error"
1495
+ }
1496
+ },
1497
+ {
1498
+ files: [
1499
+ `${sourceDir}/root.tsx`,
1500
+ `${sourceDir}/entry.server.tsx`,
1501
+ // A route page exports `<Name>Page` from `page.tsx`
1502
+ `${sourceDir}/pages/**/page.tsx`,
1503
+ `${sourceDir}/**/tests/*`,
1504
+ "test/**/*.ts?(x)",
1505
+ "**/*.stories.tsx",
1506
+ "**/routes/**/*.tsx",
1507
+ "**/hooks/*.ts?(x)",
1508
+ ".storybook/**/*.ts?(x)",
1509
+ ".playwright/**/*.ts?(x)"
1510
+ ],
1511
+ name: "canonical/filename-match-exported-disabled",
1512
+ rules: {
1513
+ "canonical/filename-match-exported": "off"
1514
+ }
1515
+ }
1516
+ ];
1517
+ var perfectionistConfig = [
1518
+ perfectionist.configs["recommended-natural"],
1519
+ {
1520
+ name: "perfectionist",
1521
+ rules: {
1522
+ "perfectionist/sort-imports": [
1523
+ "error",
1524
+ {
1525
+ customGroups: [
1526
+ {
1527
+ elementNamePattern: "^react$",
1528
+ groupName: "react-type",
1529
+ selector: "type"
1530
+ },
1531
+ {
1532
+ elementNamePattern: "^react$",
1533
+ groupName: "react"
1534
+ },
1535
+ {
1536
+ elementNamePattern: "^react-.+",
1537
+ groupName: "react-other-type",
1538
+ selector: "type"
1539
+ },
1540
+ {
1541
+ elementNamePattern: "^react-.+",
1542
+ groupName: "react-other"
1543
+ }
1544
+ ],
1545
+ groups: [
1546
+ "react-type",
1547
+ "react",
1548
+ "react-other-type",
1549
+ "react-other",
1550
+ ["type-external", "external"],
1551
+ ["type-builtin", "builtin"],
1552
+ ["type-internal", "internal"],
1553
+ ["type-parent", "parent"],
1554
+ ["type-sibling", "sibling"],
1555
+ ["type-index", "index"],
1556
+ "unknown",
1557
+ "style",
1558
+ "side-effect",
1559
+ "side-effect-style"
1560
+ ],
1561
+ newlinesBetween: 0,
1562
+ type: "natural"
1563
+ }
1564
+ ],
1565
+ "perfectionist/sort-jsx-props": [
1566
+ "error",
1567
+ {
1568
+ customGroups: [
1569
+ {
1570
+ elementNamePattern: ["^key$", "^ref$"],
1571
+ groupName: "reserved"
1572
+ }
1573
+ ],
1574
+ groups: ["reserved"],
1575
+ type: "natural"
1576
+ }
1577
+ ]
1578
+ }
1579
+ }
1580
+ ];
1581
+ var unicornConfig = [
1582
+ unicorn.configs.recommended,
1583
+ {
1584
+ name: "unicorn",
1585
+ rules: {
1586
+ "unicorn/consistent-destructuring": "error",
1587
+ "unicorn/consistent-template-literal-escape": "off",
1588
+ "unicorn/filename-case": "off",
1589
+ "unicorn/new-for-builtins": "off",
1590
+ "unicorn/no-array-callback-reference": "off",
1591
+ "unicorn/no-array-for-each": "off",
1592
+ "unicorn/no-array-reduce": "off",
1593
+ "unicorn/no-nested-ternary": "off",
1594
+ "unicorn/no-null": "off",
1595
+ "unicorn/no-useless-undefined": "off",
1596
+ "unicorn/prefer-export-from": "off",
1597
+ "unicorn/prefer-global-this": "off",
1598
+ // `Array#includes` returns a plain boolean, not a type predicate, so it
1599
+ // cannot narrow a union the way an `===` comparison chain does. In a typed
1600
+ // codebase the chain is the type-safe idiom; forcing `.includes()` discards
1601
+ // the narrowing and the value's refined type along with it.
1602
+ "unicorn/prefer-includes-over-repeated-comparisons": "off",
1603
+ "unicorn/prefer-set-has": "off",
1604
+ // Both gate on `definition.kind === 'const'`, which
1605
+ // @typescript-eslint/scope-manager never sets (the `const` lives on
1606
+ // `definition.parent.kind`), so under the TypeScript parser they cannot
1607
+ // report. Off states that instead of implying coverage.
1608
+ "unicorn/no-blob-to-file": "off",
1609
+ "unicorn/prefer-set-size": "off",
1610
+ "unicorn/prefer-switch": "off",
1611
+ "unicorn/prefer-ternary": "off",
1612
+ "unicorn/prevent-abbreviations": [
1613
+ "error",
1614
+ {
1615
+ ignore: [
1616
+ "acc",
1617
+ "ctx",
1618
+ "e2e",
1619
+ "env",
1620
+ "obj",
1621
+ "prev",
1622
+ "req",
1623
+ "res",
1624
+ /args/i,
1625
+ /fn/i,
1626
+ /param/i,
1627
+ /params/i,
1628
+ /props/i,
1629
+ /ref/i,
1630
+ /src/i,
1631
+ /utils/i
1632
+ ]
1633
+ }
1634
+ ],
1635
+ "unicorn/text-encoding-identifier-case": "off"
1636
+ }
1637
+ },
1638
+ {
1639
+ files: ["./*.ts"],
1640
+ name: "unicorn/root-ts-files",
1641
+ rules: {
1642
+ "unicorn/prefer-module": "off",
1643
+ "unicorn/prevent-abbreviations": "off"
1644
+ }
1645
+ }
1646
+ ];
1647
+ var unusedImportsConfig = [
1648
+ {
1649
+ name: "unused-imports",
1650
+ plugins: {
1651
+ "unused-imports": unusedImports
1652
+ },
1653
+ rules: {
1654
+ "@typescript-eslint/no-unused-vars": "off",
1655
+ "no-unused-vars": "off",
1656
+ "sonarjs/no-unused-vars": "off",
1657
+ "sonarjs/unused-import": "off",
1658
+ "unused-imports/no-unused-imports": "error",
1659
+ "unused-imports/no-unused-vars": [
1660
+ "error",
1661
+ {
1662
+ args: "after-used",
1663
+ argsIgnorePattern: "^_",
1664
+ ignoreRestSiblings: true,
1665
+ vars: "all",
1666
+ varsIgnorePattern: "^_"
1667
+ }
1668
+ ]
1669
+ }
1670
+ },
1671
+ {
1672
+ files: ["**/*.d.ts"],
1673
+ name: "unused-imports/ts-definition-files",
1674
+ rules: {
1675
+ "unused-imports/no-unused-vars": "off"
1676
+ }
1677
+ }
1678
+ ];
1679
+ var RESERVED_FOLDERS = "(assets|hooks|state|tests|utils)";
1680
+ var RESERVED_AND_UI_FOLDERS = "(assets|hooks|state|tests|ui|utils)";
1681
+ var notExactly = (names) => `{!${names},@${names}+([a-z0-9-])}`;
1682
+ var buildCheckFileConfig = (sourceDir) => [
1683
+ {
1684
+ plugins: {
1685
+ "check-file": checkFile
1686
+ }
1687
+ },
1688
+ {
1689
+ files: [`${sourceDir}/**/*`],
1690
+ name: "check-file",
1691
+ rules: {
1692
+ "check-file/filename-naming-convention": [
1693
+ "error",
1694
+ {
1695
+ // React hook files are kebab-case with a `use-` prefix (use-theme.ts)
1696
+ "**/hooks/*.{ts,tsx}": "use-+([a-z0-9])*(-+([a-z0-9]))",
1697
+ [`${sourceDir}/state/*.tsx`]: "KEBAB_CASE",
1698
+ // Components live in folders, never directly in components/
1699
+ [`${sourceDir}/components/*.tsx`]: "!(*)",
1700
+ // components/ui/ holds flat kebab-case component files (button.tsx)
1701
+ [`${sourceDir}/components/ui/*.tsx`]: "KEBAB_CASE",
1702
+ // Every other component file is named index.tsx
1703
+ [`${sourceDir}/components/**/${notExactly(RESERVED_AND_UI_FOLDERS)}/*.tsx`]: "index+()",
1704
+ // A page folder holds its route page.tsx, or index.tsx for colocated components
1705
+ [`${sourceDir}/pages/**/${notExactly(RESERVED_FOLDERS)}/*.tsx`]: "@(index|page)",
1706
+ // Generally, non-component files must be named kebab-case
1707
+ [`${sourceDir}/{components,pages}/**/!(hooks)/*.ts`]: "KEBAB_CASE",
1708
+ // Non-component files inside specific components folders must be kebab-case
1709
+ [`${sourceDir}/{components,pages}/**/(assets|state|tests|utils)/*.{ts,tsx}`]: "KEBAB_CASE",
1710
+ "test/**/*.ts?(x)": "KEBAB_CASE"
1711
+ },
1712
+ {
1713
+ ignoreMiddleExtensions: true
1714
+ }
1715
+ ],
1716
+ "check-file/folder-match-with-fex": [
1717
+ "error",
1718
+ {
1719
+ // require stories and test files to be inside tests folders
1720
+ "*.(stories|test).{ts,tsx}": `${sourceDir}/**/tests/`
1721
+ }
1722
+ ],
1723
+ "check-file/folder-naming-convention": [
1724
+ "error",
1725
+ {
1726
+ // enforce kebab-case component and page folders; the reserved
1727
+ // assets, hooks, state, tests, and utils subfolders are kebab-case too
1728
+ [`${sourceDir}/components/**/`]: "KEBAB_CASE",
1729
+ [`${sourceDir}/pages/**/`]: "KEBAB_CASE",
1730
+ // components/ui/ is flat: tests/ is its only subfolder
1731
+ [`${sourceDir}/components/ui/*/`]: "@(tests)"
1732
+ }
1733
+ ]
1734
+ }
1735
+ },
1736
+ {
1737
+ files: ["test/**/*.*"],
1738
+ name: "check-file/test-files",
1739
+ rules: {
1740
+ "check-file/filename-naming-convention": [
1741
+ "error",
1742
+ {
1743
+ "test/**/*.ts?(x)": "KEBAB_CASE"
1744
+ },
1745
+ {
1746
+ ignoreMiddleExtensions: true
1747
+ }
1748
+ ]
1749
+ }
1750
+ }
1751
+ ];
1752
+ var buildStyleHygiene = (sourceDir) => [
1753
+ ...buildCanonicalConfig(sourceDir),
1754
+ ...perfectionistConfig,
1755
+ ...unicornConfig,
1756
+ ...unusedImportsConfig,
1757
+ ...buildCheckFileConfig(sourceDir)
1758
+ ];
1759
+
1760
+ // src/configs/testing.ts
1761
+ import vitest from "@vitest/eslint-plugin";
1762
+ import jestDom from "eslint-plugin-jest-dom";
1763
+ var testing = [
1764
+ {
1765
+ files: [
1766
+ "**/*.test.ts?(x)",
1767
+ "**/*.stories.ts?(x)",
1768
+ "test/**/*.ts?(x)"
1769
+ ],
1770
+ name: "vitest",
1771
+ plugins: {
1772
+ "jest-dom": jestDom,
1773
+ vitest
1774
+ },
1775
+ rules: {
1776
+ ...jestDom.configs["flat/recommended"].rules,
1777
+ ...vitest.configs.recommended.rules,
1778
+ "import-x/no-useless-path-segments": [
1779
+ "error",
1780
+ { noUselessIndex: true }
1781
+ ],
1782
+ "vitest/expect-expect": [
1783
+ "error",
1784
+ { assertFunctionNames: ["expect", "expect*"] }
1785
+ ],
1786
+ // Baseline bump: `recommended` ships no-disabled-tests at `warn`.
1787
+ // .skip is the single biggest gaming vector for a RED-verification
1788
+ // gate (it makes a failing test stop failing), so GAIA forbids it
1789
+ // outright. Park work with test.todo or an inline eslint-disable, not
1790
+ // a silent .skip.
1791
+ "vitest/no-disabled-tests": "error",
1792
+ // --- Hardening beyond the `recommended` preset ---------------------
1793
+ // Rule names and options verified against @vitest/eslint-plugin
1794
+ // v1.6.19. GAIA policy: every lint rule is `error`, never `warn`.
1795
+ //
1796
+ // Consistency: GAIA uses test() exclusively (120 vs 0 it() across the
1797
+ // gaia codebase). In 1.6.19 the `fn` option cascades to the
1798
+ // within-describe keyword, so this also governs test()/it() nested in
1799
+ // describe blocks. Auto-fixable.
1800
+ "vitest/consistent-test-it": ["error", { fn: "test" }],
1801
+ // Consistency: GAIA uses the `vi` mock API (37 vs 0 `vitest.` across the
1802
+ // gaia codebase). Auto-fixable.
1803
+ "vitest/consistent-vitest-vi": ["error", { fn: "vi" }],
1804
+ // RED-gate: a branch in a test body can assert nothing on one path.
1805
+ // Enable ONLY this one; `no-conditional-tests` is a byte-identical
1806
+ // duplicate in 1.6.19 and would double-report.
1807
+ "vitest/no-conditional-in-test": "error",
1808
+ // RED-gate: fdescribe/fit/xdescribe/xit bypass the active
1809
+ // no-focused-tests / no-disabled-tests rules; forbid the prefix
1810
+ // spellings so the focus/skip net stays airtight. Auto-fixable.
1811
+ "vitest/no-test-prefixes": "error",
1812
+ // RED-gate: a returned value/promise can resolve after the test
1813
+ // "passes", swallowing assertions and faking GREEN.
1814
+ "vitest/no-test-return-statement": "error",
1815
+ // RED-gate: empty test('...') stubs pass green and read as coverage;
1816
+ // rewrite to test.todo('...') so the gap is explicit. Contradicts
1817
+ // `warn-todo`, which stays off. Auto-fixable.
1818
+ "vitest/prefer-todo": "error",
1819
+ // Honesty (D-8 / SPEC-006 Rule 3, the cheap-win stand-in for
1820
+ // `gaia-test-honesty/no-call-through-only`). A bare
1821
+ // `toHaveBeenCalled()`/`toBeCalled()` proves the function ran but
1822
+ // asserts nothing about how (arguments) or how many (count) — a
1823
+ // call-through that re-proves the framework, not the behavior.
1824
+ //
1825
+ // SCOPE NOTE: `prefer-called-with` is BROADER than spec Rule 3. The spec
1826
+ // rule fires only when a bare `toHaveBeenCalled()` is the SOLE assertion
1827
+ // in a test; `prefer-called-with` fires on EVERY bare
1828
+ // `toHaveBeenCalled()`/`toBeCalled()` (it exempts only the `.not` form).
1829
+ // So this stands in for Rule 3 rather than matching it exactly.
1830
+ "vitest/prefer-called-with": "error",
1831
+ // A bare .toThrow() passes on ANY throw, masking the wrong error;
1832
+ // require an asserted message or matcher.
1833
+ "vitest/require-to-throw-message": "error"
1834
+ }
1835
+ },
1836
+ {
1837
+ // Honesty (D-8 / SPEC-006 Rule 4, the cheap-win stand-in for
1838
+ // `gaia-test-honesty/no-server-import-from-consumer`). A consumer test,
1839
+ // story, or harness file must not import a server-only (`*.server`) or
1840
+ // internal (`/internals/`) module: those are private surface, and
1841
+ // reaching into them couples the test to implementation the public
1842
+ // interface is meant to hide.
1843
+ //
1844
+ // Same testing glob as the `vitest` block above. The `*.server.test.*`
1845
+ // exemption is the point of Rule 4: a dedicated server-side test IS the
1846
+ // right place to import a `.server` module (mirrors the real
1847
+ // `app/utils/tests/http.server.test.ts` importing `../http.server`).
1848
+ //
1849
+ // Implemented with the core `no-restricted-imports` rule (config only, no
1850
+ // custom plugin) rather than a dedicated AST rule. Chosen over
1851
+ // `import-x/no-restricted-paths` because Rule 4 keys off the import
1852
+ // SPECIFIER shape (`*.server`, `/internals/`) plus a per-file filename
1853
+ // exemption, not the directory `from`/`target` zones that
1854
+ // `no-restricted-paths` models.
1855
+ //
1856
+ // LIMITATION vs the would-be custom rule: `no-restricted-imports` covers
1857
+ // static `import` / `export ... from` only, NOT dynamic
1858
+ // `import('./x.server')`. The dynamic case is intentionally out of scope
1859
+ // for this cheap-win form; the advisory worthiness audit backstops it.
1860
+ //
1861
+ // ERROR severity, per spec (Rule 4 is an error rule).
1862
+ files: [
1863
+ "**/*.test.ts?(x)",
1864
+ "**/*.stories.ts?(x)",
1865
+ "test/**/*.ts?(x)"
1866
+ ],
1867
+ ignores: ["**/*.server.test.*"],
1868
+ name: "gaia-test-honesty/no-server-import-from-consumer",
1869
+ rules: {
1870
+ "no-restricted-imports": [
1871
+ "error",
1872
+ {
1873
+ // `paths` re-declares the global `@conform-to/zod` ban from base.ts.
1874
+ // Flat config replaces a rule's options wholesale, so this later
1875
+ // block would otherwise drop the conform ban on test/story files.
1876
+ // Same shared const as base.ts, to stay DRY.
1877
+ paths: RESTRICTED_IMPORT_PATHS,
1878
+ patterns: [
1879
+ {
1880
+ group: ["**/*.server"],
1881
+ message: "Do not import a server-only ('*.server') module from a consumer test. Server-only modules are private surface: reach them through the public interface, or from a dedicated *.server.test.* file (which is exempt from this rule)."
1882
+ },
1883
+ {
1884
+ group: ["**/internals/**", "**/internals"],
1885
+ message: "Do not import an internal ('/internals/') module from a consumer test. Internal modules are private surface: importing them couples the test to implementation the public interface is meant to hide. Reach the behavior through the public interface instead."
1886
+ }
1887
+ ]
1888
+ }
1889
+ ]
1890
+ }
1891
+ }
1892
+ ];
1893
+
1894
+ // src/configs/zod.ts
1895
+ import zodPlugin from "eslint-plugin-zod";
1896
+ var zod = [
1897
+ {
1898
+ files: ["**/*.ts?(x)"],
1899
+ name: "zod",
1900
+ plugins: { zod: zodPlugin },
1901
+ rules: {
1902
+ ...zodPlugin.configs.recommended.rules,
1903
+ "zod/consistent-import": "off"
1904
+ }
1905
+ }
1906
+ ];
1907
+
1908
+ // src/index.ts
1909
+ var buildIgnoresAccessor = () => {
1910
+ const defaults = ignores();
1911
+ const accessor = ((opts) => ignores(opts));
1912
+ accessor[Symbol.iterator] = () => defaults[Symbol.iterator]();
1913
+ return accessor;
1914
+ };
1915
+ var gaiaLint = (opts) => {
1916
+ const sourceDir = opts?.sourceDir ?? "app";
1917
+ return {
1918
+ base: buildBase(sourceDir),
1919
+ betterTailwind,
1920
+ guardrails: buildGuardrails(sourceDir),
1921
+ i18n: buildI18n(sourceDir),
1922
+ ignores: buildIgnoresAccessor(),
1923
+ playwright,
1924
+ prettier,
1925
+ react,
1926
+ reactRouter: buildReactRouter(sourceDir),
1927
+ shadcn: (shadcnOpts) => buildShadcn(sourceDir, shadcnOpts),
1928
+ storybook,
1929
+ styleHygiene: buildStyleHygiene(sourceDir),
1930
+ testing,
1931
+ zod
1932
+ };
1933
+ };
1934
+ var index_default = gaiaLint;
1935
+ export {
1936
+ index_default as default
1937
+ };
1938
+ //# sourceMappingURL=index.js.map