pasika 0.3.2 → 0.3.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 (189) hide show
  1. package/README.md +72 -20
  2. package/dist/cli/index.d.ts +0 -1
  3. package/dist/cli/index.js +4999 -103
  4. package/dist/eslint/pasika/index.d.ts +165 -53
  5. package/dist/eslint/pasika/index.js +4424 -89
  6. package/package.json +5 -6
  7. package/dist/cli/output.d.ts +0 -3
  8. package/dist/cli/output.js +0 -11
  9. package/dist/enforcement/coverage.d.ts +0 -51
  10. package/dist/enforcement/coverage.js +0 -206
  11. package/dist/enforcement/doctor.d.ts +0 -16
  12. package/dist/enforcement/doctor.js +0 -249
  13. package/dist/enforcement/normalize.d.ts +0 -11
  14. package/dist/enforcement/normalize.js +0 -21
  15. package/dist/enforcement/parse-docs.d.ts +0 -58
  16. package/dist/enforcement/parse-docs.js +0 -94
  17. package/dist/enforcement/types.d.ts +0 -54
  18. package/dist/enforcement/types.js +0 -50
  19. package/dist/eslint/pasika/ast-types.d.ts +0 -122
  20. package/dist/eslint/pasika/ast-types.js +0 -1
  21. package/dist/eslint/pasika/project/ccf.d.ts +0 -48
  22. package/dist/eslint/pasika/project/ccf.js +0 -119
  23. package/dist/eslint/pasika/project/index.d.ts +0 -21
  24. package/dist/eslint/pasika/project/index.js +0 -139
  25. package/dist/eslint/pasika/project/parse-module.d.ts +0 -27
  26. package/dist/eslint/pasika/project/parse-module.js +0 -128
  27. package/dist/eslint/pasika/rules/application-structure.d.ts +0 -2
  28. package/dist/eslint/pasika/rules/application-structure.js +0 -152
  29. package/dist/eslint/pasika/rules/component-conventions.d.ts +0 -16
  30. package/dist/eslint/pasika/rules/component-conventions.js +0 -145
  31. package/dist/eslint/pasika/rules/component-nesting.d.ts +0 -12
  32. package/dist/eslint/pasika/rules/component-nesting.js +0 -69
  33. package/dist/eslint/pasika/rules/component-placement.d.ts +0 -11
  34. package/dist/eslint/pasika/rules/component-placement.js +0 -75
  35. package/dist/eslint/pasika/rules/config-extraction.d.ts +0 -14
  36. package/dist/eslint/pasika/rules/config-extraction.js +0 -74
  37. package/dist/eslint/pasika/rules/cross-feature-import.d.ts +0 -2
  38. package/dist/eslint/pasika/rules/cross-feature-import.js +0 -75
  39. package/dist/eslint/pasika/rules/css/apply-usage.d.ts +0 -10
  40. package/dist/eslint/pasika/rules/css/apply-usage.js +0 -45
  41. package/dist/eslint/pasika/rules/css/base-layer-pair.d.ts +0 -10
  42. package/dist/eslint/pasika/rules/css/base-layer-pair.js +0 -49
  43. package/dist/eslint/pasika/rules/css/css-variable-naming.d.ts +0 -10
  44. package/dist/eslint/pasika/rules/css/css-variable-naming.js +0 -39
  45. package/dist/eslint/pasika/rules/css/custom-utility-apply.d.ts +0 -11
  46. package/dist/eslint/pasika/rules/css/custom-utility-apply.js +0 -52
  47. package/dist/eslint/pasika/rules/css/global-css-location.d.ts +0 -10
  48. package/dist/eslint/pasika/rules/css/global-css-location.js +0 -42
  49. package/dist/eslint/pasika/rules/css/helpers.d.ts +0 -29
  50. package/dist/eslint/pasika/rules/css/helpers.js +0 -93
  51. package/dist/eslint/pasika/rules/css/index.d.ts +0 -13
  52. package/dist/eslint/pasika/rules/css/index.js +0 -25
  53. package/dist/eslint/pasika/rules/css/root-variables.d.ts +0 -11
  54. package/dist/eslint/pasika/rules/css/root-variables.js +0 -53
  55. package/dist/eslint/pasika/rules/css/rule-tester.d.ts +0 -11
  56. package/dist/eslint/pasika/rules/css/rule-tester.js +0 -22
  57. package/dist/eslint/pasika/rules/css/stylesheet-ordering.d.ts +0 -11
  58. package/dist/eslint/pasika/rules/css/stylesheet-ordering.js +0 -76
  59. package/dist/eslint/pasika/rules/css/surface-utility.d.ts +0 -10
  60. package/dist/eslint/pasika/rules/css/surface-utility.js +0 -41
  61. package/dist/eslint/pasika/rules/css/theme-reset.d.ts +0 -9
  62. package/dist/eslint/pasika/rules/css/theme-reset.js +0 -36
  63. package/dist/eslint/pasika/rules/css/theme-variable-namespace.d.ts +0 -11
  64. package/dist/eslint/pasika/rules/css/theme-variable-namespace.js +0 -53
  65. package/dist/eslint/pasika/rules/cva-appearance-props.d.ts +0 -10
  66. package/dist/eslint/pasika/rules/cva-appearance-props.js +0 -77
  67. package/dist/eslint/pasika/rules/cva-boolean-variants.d.ts +0 -11
  68. package/dist/eslint/pasika/rules/cva-boolean-variants.js +0 -97
  69. package/dist/eslint/pasika/rules/data-testid-case.d.ts +0 -2
  70. package/dist/eslint/pasika/rules/data-testid-case.js +0 -76
  71. package/dist/eslint/pasika/rules/enforce-barrel-exports.d.ts +0 -9
  72. package/dist/eslint/pasika/rules/enforce-barrel-exports.js +0 -78
  73. package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +0 -21
  74. package/dist/eslint/pasika/rules/enforce-cn-merge.js +0 -86
  75. package/dist/eslint/pasika/rules/enforce-cva-variant-props.d.ts +0 -9
  76. package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +0 -86
  77. package/dist/eslint/pasika/rules/filename-case.d.ts +0 -2
  78. package/dist/eslint/pasika/rules/filename-case.js +0 -123
  79. package/dist/eslint/pasika/rules/hook-complexity.d.ts +0 -12
  80. package/dist/eslint/pasika/rules/hook-complexity.js +0 -114
  81. package/dist/eslint/pasika/rules/hook-extraction.d.ts +0 -11
  82. package/dist/eslint/pasika/rules/hook-extraction.js +0 -55
  83. package/dist/eslint/pasika/rules/import-boundaries.d.ts +0 -2
  84. package/dist/eslint/pasika/rules/import-boundaries.js +0 -167
  85. package/dist/eslint/pasika/rules/import-through-index.d.ts +0 -2
  86. package/dist/eslint/pasika/rules/import-through-index.js +0 -52
  87. package/dist/eslint/pasika/rules/interactive-component.d.ts +0 -10
  88. package/dist/eslint/pasika/rules/interactive-component.js +0 -76
  89. package/dist/eslint/pasika/rules/json/index.d.ts +0 -5
  90. package/dist/eslint/pasika/rules/json/index.js +0 -9
  91. package/dist/eslint/pasika/rules/json/no-cache-flag.d.ts +0 -10
  92. package/dist/eslint/pasika/rules/json/no-cache-flag.js +0 -45
  93. package/dist/eslint/pasika/rules/json/no-vulyk-dependency.d.ts +0 -10
  94. package/dist/eslint/pasika/rules/json/no-vulyk-dependency.js +0 -41
  95. package/dist/eslint/pasika/rules/json/rule-tester.d.ts +0 -11
  96. package/dist/eslint/pasika/rules/json/rule-tester.js +0 -21
  97. package/dist/eslint/pasika/rules/jsx-hygiene.d.ts +0 -9
  98. package/dist/eslint/pasika/rules/jsx-hygiene.js +0 -108
  99. package/dist/eslint/pasika/rules/locale-dotted-path.d.ts +0 -10
  100. package/dist/eslint/pasika/rules/locale-dotted-path.js +0 -65
  101. package/dist/eslint/pasika/rules/locale-placement.d.ts +0 -13
  102. package/dist/eslint/pasika/rules/locale-placement.js +0 -163
  103. package/dist/eslint/pasika/rules/locales-location.d.ts +0 -11
  104. package/dist/eslint/pasika/rules/locales-location.js +0 -56
  105. package/dist/eslint/pasika/rules/md/doc-kind-suffix.d.ts +0 -5
  106. package/dist/eslint/pasika/rules/md/doc-kind-suffix.js +0 -29
  107. package/dist/eslint/pasika/rules/md/example-heading-description.d.ts +0 -5
  108. package/dist/eslint/pasika/rules/md/example-heading-description.js +0 -30
  109. package/dist/eslint/pasika/rules/md/glossary-term-linking.d.ts +0 -2
  110. package/dist/eslint/pasika/rules/md/glossary-term-linking.js +0 -99
  111. package/dist/eslint/pasika/rules/md/guide-folder-entry-point.d.ts +0 -2
  112. package/dist/eslint/pasika/rules/md/guide-folder-entry-point.js +0 -46
  113. package/dist/eslint/pasika/rules/md/guide-link-anchors.d.ts +0 -5
  114. package/dist/eslint/pasika/rules/md/guide-link-anchors.js +0 -50
  115. package/dist/eslint/pasika/rules/md/guide-overview-no-links.d.ts +0 -5
  116. package/dist/eslint/pasika/rules/md/guide-overview-no-links.js +0 -51
  117. package/dist/eslint/pasika/rules/md/guide-states-no-requirement.d.ts +0 -5
  118. package/dist/eslint/pasika/rules/md/guide-states-no-requirement.js +0 -26
  119. package/dist/eslint/pasika/rules/md/guide-step-single-link.d.ts +0 -5
  120. package/dist/eslint/pasika/rules/md/guide-step-single-link.js +0 -48
  121. package/dist/eslint/pasika/rules/md/guide-step-single-sentence.d.ts +0 -5
  122. package/dist/eslint/pasika/rules/md/guide-step-single-sentence.js +0 -42
  123. package/dist/eslint/pasika/rules/md/helpers.d.ts +0 -25
  124. package/dist/eslint/pasika/rules/md/helpers.js +0 -37
  125. package/dist/eslint/pasika/rules/md/index.d.ts +0 -27
  126. package/dist/eslint/pasika/rules/md/index.js +0 -53
  127. package/dist/eslint/pasika/rules/md/no-cross-document-link.d.ts +0 -5
  128. package/dist/eslint/pasika/rules/md/no-cross-document-link.js +0 -36
  129. package/dist/eslint/pasika/rules/md/no-nested-how-to.d.ts +0 -5
  130. package/dist/eslint/pasika/rules/md/no-nested-how-to.js +0 -39
  131. package/dist/eslint/pasika/rules/md/no-template-prompt.d.ts +0 -5
  132. package/dist/eslint/pasika/rules/md/no-template-prompt.js +0 -22
  133. package/dist/eslint/pasika/rules/md/overview-length.d.ts +0 -5
  134. package/dist/eslint/pasika/rules/md/overview-length.js +0 -47
  135. package/dist/eslint/pasika/rules/md/overview-present.d.ts +0 -5
  136. package/dist/eslint/pasika/rules/md/overview-present.js +0 -41
  137. package/dist/eslint/pasika/rules/md/policy-no-examples.d.ts +0 -5
  138. package/dist/eslint/pasika/rules/md/policy-no-examples.js +0 -28
  139. package/dist/eslint/pasika/rules/md/policy-single-document.d.ts +0 -5
  140. package/dist/eslint/pasika/rules/md/policy-single-document.js +0 -34
  141. package/dist/eslint/pasika/rules/md/policy-subject-headings.d.ts +0 -5
  142. package/dist/eslint/pasika/rules/md/policy-subject-headings.js +0 -58
  143. package/dist/eslint/pasika/rules/md/project-index.d.ts +0 -17
  144. package/dist/eslint/pasika/rules/md/project-index.js +0 -67
  145. package/dist/eslint/pasika/rules/md/reference-block-headings.d.ts +0 -6
  146. package/dist/eslint/pasika/rules/md/reference-block-headings.js +0 -31
  147. package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.d.ts +0 -6
  148. package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.js +0 -26
  149. package/dist/eslint/pasika/rules/md/requirement-present.d.ts +0 -5
  150. package/dist/eslint/pasika/rules/md/requirement-present.js +0 -41
  151. package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.d.ts +0 -5
  152. package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.js +0 -46
  153. package/dist/eslint/pasika/rules/md/rule-paired-examples.d.ts +0 -5
  154. package/dist/eslint/pasika/rules/md/rule-paired-examples.js +0 -36
  155. package/dist/eslint/pasika/rules/md/rule-tester.d.ts +0 -11
  156. package/dist/eslint/pasika/rules/md/rule-tester.js +0 -21
  157. package/dist/eslint/pasika/rules/md/support-document-placement.d.ts +0 -2
  158. package/dist/eslint/pasika/rules/md/support-document-placement.js +0 -36
  159. package/dist/eslint/pasika/rules/md/title-matches-file-name.d.ts +0 -2
  160. package/dist/eslint/pasika/rules/md/title-matches-file-name.js +0 -46
  161. package/dist/eslint/pasika/rules/named-exports.d.ts +0 -2
  162. package/dist/eslint/pasika/rules/named-exports.js +0 -49
  163. package/dist/eslint/pasika/rules/no-arbitrary-tailwind.d.ts +0 -9
  164. package/dist/eslint/pasika/rules/no-arbitrary-tailwind.js +0 -97
  165. package/dist/eslint/pasika/rules/no-eslint-disable.d.ts +0 -10
  166. package/dist/eslint/pasika/rules/no-eslint-disable.js +0 -33
  167. package/dist/eslint/pasika/rules/no-mixed-concerns.d.ts +0 -9
  168. package/dist/eslint/pasika/rules/no-mixed-concerns.js +0 -71
  169. package/dist/eslint/pasika/rules/no-util-barrel.d.ts +0 -2
  170. package/dist/eslint/pasika/rules/no-util-barrel.js +0 -48
  171. package/dist/eslint/pasika/rules/pure-function-extract.d.ts +0 -9
  172. package/dist/eslint/pasika/rules/pure-function-extract.js +0 -92
  173. package/dist/eslint/pasika/rules/shared-style-dedup.d.ts +0 -13
  174. package/dist/eslint/pasika/rules/shared-style-dedup.js +0 -91
  175. package/dist/eslint/pasika/rules/stay-flat.d.ts +0 -12
  176. package/dist/eslint/pasika/rules/stay-flat.js +0 -81
  177. package/dist/eslint/pasika/rules/support-file-placement.d.ts +0 -15
  178. package/dist/eslint/pasika/rules/support-file-placement.js +0 -70
  179. package/dist/eslint/pasika/rules/support-folder-shape.d.ts +0 -2
  180. package/dist/eslint/pasika/rules/support-folder-shape.js +0 -59
  181. package/dist/eslint/pasika/rules/type-extraction.d.ts +0 -13
  182. package/dist/eslint/pasika/rules/type-extraction.js +0 -85
  183. package/dist/eslint/pasika/rules/ui-state.d.ts +0 -9
  184. package/dist/eslint/pasika/rules/ui-state.js +0 -99
  185. package/dist/eslint/pasika/rules/util-file-name.d.ts +0 -2
  186. package/dist/eslint/pasika/rules/util-file-name.js +0 -48
  187. package/dist/eslint/pasika/rules/value-extraction.d.ts +0 -13
  188. package/dist/eslint/pasika/rules/value-extraction.js +0 -48
  189. package/enforcement/registry.json +0 -1145
@@ -1,90 +1,4425 @@
1
- import { filenameCaseRule } from "./rules/filename-case.js";
2
- import { importBoundariesRule } from "./rules/import-boundaries.js";
3
- import { noMixedConcernsRule } from "./rules/no-mixed-concerns.js";
4
- import { noArbitraryTailwindRule } from "./rules/no-arbitrary-tailwind.js";
5
- import { enforceCnMergeRule } from "./rules/enforce-cn-merge.js";
6
- import { enforceCvaVariantPropsRule } from "./rules/enforce-cva-variant-props.js";
7
- import { enforceBarrelExportsRule } from "./rules/enforce-barrel-exports.js";
8
- import { componentPlacementRule } from "./rules/component-placement.js";
9
- import { supportFilePlacementRule } from "./rules/support-file-placement.js";
10
- import { applicationStructureRule } from "./rules/application-structure.js";
11
- import { namedExportsRule } from "./rules/named-exports.js";
12
- import { dataTestIdCaseRule } from "./rules/data-testid-case.js";
13
- import { supportFolderShapeRule } from "./rules/support-folder-shape.js";
14
- import { importThroughIndexRule } from "./rules/import-through-index.js";
15
- import { utilFileNameRule } from "./rules/util-file-name.js";
16
- import { noUtilBarrelRule } from "./rules/no-util-barrel.js";
17
- import { jsxHygieneRule } from "./rules/jsx-hygiene.js";
18
- import { interactiveComponentRule } from "./rules/interactive-component.js";
19
- import { uiStateRule } from "./rules/ui-state.js";
20
- import { cvaAppearancePropsRule } from "./rules/cva-appearance-props.js";
21
- import { cvaBooleanVariantsRule } from "./rules/cva-boolean-variants.js";
22
- import { crossFeatureImportRule } from "./rules/cross-feature-import.js";
23
- import { pureFunctionExtractRule } from "./rules/pure-function-extract.js";
24
- import { hookComplexityRule } from "./rules/hook-complexity.js";
25
- import { localeDottedPathRule } from "./rules/locale-dotted-path.js";
26
- import { localesLocationRule } from "./rules/locales-location.js";
27
- import { hookExtractionRule } from "./rules/hook-extraction.js";
28
- import { valueExtractionRule } from "./rules/value-extraction.js";
29
- import { configExtractionRule } from "./rules/config-extraction.js";
30
- import { componentNestingRule } from "./rules/component-nesting.js";
31
- import { stayFlatRule } from "./rules/stay-flat.js";
32
- import { typeExtractionRule } from "./rules/type-extraction.js";
33
- import { localePlacementRule } from "./rules/locale-placement.js";
34
- import { sharedStyleDedupRule } from "./rules/shared-style-dedup.js";
35
- import { noEslintDisableRule } from "./rules/no-eslint-disable.js";
36
- import { mdRules } from "./rules/md/index.js";
37
- import { cssRules } from "./rules/css/index.js";
38
- import { jsonRules } from "./rules/json/index.js";
39
- export const pasikaRules = {
40
- "component-placement": componentPlacementRule,
41
- "support-file-placement": supportFilePlacementRule,
42
- "application-structure": applicationStructureRule,
43
- "named-exports": namedExportsRule,
44
- "data-testid-case": dataTestIdCaseRule,
45
- "support-folder-shape": supportFolderShapeRule,
46
- "import-through-index": importThroughIndexRule,
47
- "util-file-name": utilFileNameRule,
48
- "no-util-barrel": noUtilBarrelRule,
49
- "jsx-hygiene": jsxHygieneRule,
50
- "interactive-component": interactiveComponentRule,
51
- "ui-state": uiStateRule,
52
- "filename-case": filenameCaseRule,
53
- "import-boundaries": importBoundariesRule,
54
- "no-mixed-concerns": noMixedConcernsRule,
55
- "no-arbitrary-tailwind": noArbitraryTailwindRule,
56
- "enforce-cn-merge": enforceCnMergeRule,
57
- "enforce-cva-variant-props": enforceCvaVariantPropsRule,
58
- "cva-appearance-props": cvaAppearancePropsRule,
59
- "cva-boolean-variants": cvaBooleanVariantsRule,
60
- "enforce-barrel-exports": enforceBarrelExportsRule,
61
- "cross-feature-import": crossFeatureImportRule,
62
- "pure-function-extract": pureFunctionExtractRule,
63
- "hook-complexity": hookComplexityRule,
64
- "locale-dotted-path": localeDottedPathRule,
65
- "locales-location": localesLocationRule,
66
- "hook-extraction": hookExtractionRule,
67
- "value-extraction": valueExtractionRule,
68
- "config-extraction": configExtractionRule,
69
- "component-nesting": componentNestingRule,
70
- "stay-flat": stayFlatRule,
71
- "type-extraction": typeExtractionRule,
72
- "locale-placement": localePlacementRule,
73
- "shared-style-dedup": sharedStyleDedupRule,
74
- "no-eslint-disable": noEslintDisableRule,
75
- };
76
- export { mdRules, cssRules, jsonRules };
77
- /** Rule ids as they appear in configuration and in lint output. */
78
- export const pasikaRuleIds = Object.keys(pasikaRules).map((name) => `pasika/${name}`);
79
- export const pasikaMdRuleIds = Object.keys(mdRules).map((name) => `pasika/${name}`);
80
- export const pasikaCssRuleIds = Object.keys(cssRules).map((name) => `pasika/${name}`);
81
- export const pasikaJsonRuleIds = Object.keys(jsonRules).map((name) => `pasika/${name}`);
82
- /** Every rule id, as they appear in configuration and in lint output. */
83
- export const allPasikaRuleIds = [...pasikaRuleIds, ...pasikaMdRuleIds, ...pasikaCssRuleIds, ...pasikaJsonRuleIds];
84
- export const pasikaConfig = {
85
- files: ["src/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}"],
86
- plugins: {
87
- pasika: { rules: pasikaRules },
88
- },
89
- rules: Object.fromEntries(pasikaRuleIds.map((id) => [id, "error"])),
1
+ // eslint/pasika/rules/filename-case.ts
2
+ import path2 from "path";
3
+
4
+ // eslint/pasika/rules/component-conventions.ts
5
+ import path from "path";
6
+ import ts from "typescript";
7
+ var isPascalCase = (name) => /^[A-Z][A-Za-z0-9]*$/.test(name);
8
+ var isHookCallName = (name) => /^use[A-Z]/.test(name);
9
+ function isExported(node) {
10
+ if (!ts.canHaveModifiers(node)) return false;
11
+ return (ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
12
+ }
13
+ function containsJsx(node) {
14
+ let found = false;
15
+ const visit = (child) => {
16
+ if (found) return;
17
+ if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {
18
+ found = true;
19
+ return;
20
+ }
21
+ ts.forEachChild(child, visit);
22
+ };
23
+ ts.forEachChild(node, visit);
24
+ return found;
25
+ }
26
+ function containsSmartCall(node) {
27
+ let found = false;
28
+ const visit = (child) => {
29
+ if (found) return;
30
+ if (ts.isCallExpression(child)) {
31
+ const expression = child.expression;
32
+ let name = "";
33
+ if (ts.isIdentifier(expression)) {
34
+ name = expression.text;
35
+ } else if (ts.isPropertyAccessExpression(expression)) {
36
+ name = expression.name.text;
37
+ }
38
+ if (isHookCallName(name)) {
39
+ found = true;
40
+ return;
41
+ }
42
+ }
43
+ ts.forEachChild(child, visit);
44
+ };
45
+ ts.forEachChild(node, visit);
46
+ return found;
47
+ }
48
+ function componentFromFunction(node) {
49
+ const name = node.name?.text;
50
+ if (!name || !isPascalCase(name) || !containsJsx(node)) return void 0;
51
+ return { name, declaration: node, smart: containsSmartCall(node) };
52
+ }
53
+ function componentFromVariable(node) {
54
+ if (!ts.isIdentifier(node.name) || !isPascalCase(node.name.text)) return void 0;
55
+ const initializer = node.initializer;
56
+ if (!initializer || !ts.isArrowFunction(initializer) && !ts.isFunctionExpression(initializer)) return void 0;
57
+ if (!containsJsx(initializer)) return void 0;
58
+ return {
59
+ name: node.name.text,
60
+ declaration: node,
61
+ smart: containsSmartCall(initializer)
62
+ };
63
+ }
64
+ function parseComponentInfo(text, filename) {
65
+ const sourceFile = ts.createSourceFile(path.resolve(filename), text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
66
+ const components = [];
67
+ for (const statement of sourceFile.statements) {
68
+ if (!isExported(statement)) continue;
69
+ if (ts.isFunctionDeclaration(statement)) {
70
+ const component = componentFromFunction(statement);
71
+ if (component) components.push(component);
72
+ continue;
73
+ }
74
+ if (ts.isVariableStatement(statement)) {
75
+ for (const declaration of statement.declarationList.declarations) {
76
+ const component = componentFromVariable(declaration);
77
+ if (component) components.push(component);
78
+ }
79
+ }
80
+ }
81
+ return components;
82
+ }
83
+ function jsxTagName(element) {
84
+ const tagName2 = ts.isJsxElement(element) ? element.openingElement.tagName : element.tagName;
85
+ return ts.isIdentifier(tagName2) ? tagName2.text : void 0;
86
+ }
87
+ function rootFromExpression(expression) {
88
+ if (ts.isJsxSelfClosingElement(expression)) {
89
+ const tagName2 = jsxTagName(expression);
90
+ if (!tagName2?.startsWith(tagName2[0]?.toLowerCase() ?? "")) return void 0;
91
+ return { tagName: tagName2, attributes: [...expression.attributes.properties] };
92
+ }
93
+ if (ts.isJsxElement(expression)) {
94
+ const tagName2 = jsxTagName(expression);
95
+ if (!tagName2?.startsWith(tagName2[0]?.toLowerCase() ?? "")) return void 0;
96
+ return { tagName: tagName2, attributes: [...expression.openingElement.attributes.properties] };
97
+ }
98
+ return void 0;
99
+ }
100
+ function findSimpleRoot(component, _text, _filename) {
101
+ const declaration = component.declaration;
102
+ let body;
103
+ if (ts.isFunctionDeclaration(declaration)) body = declaration.body;
104
+ else if (declaration.initializer && ts.isArrowFunction(declaration.initializer)) body = declaration.initializer.body;
105
+ else if (declaration.initializer && ts.isFunctionExpression(declaration.initializer)) {
106
+ body = declaration.initializer.body;
107
+ }
108
+ if (!body) return void 0;
109
+ if (!ts.isBlock(body)) return rootFromExpression(body);
110
+ const returns = [];
111
+ const visit = (node) => {
112
+ if (node !== body && (ts.isFunctionLike(node) || ts.isClassLike(node))) return;
113
+ if (ts.isReturnStatement(node)) returns.push(node);
114
+ ts.forEachChild(node, visit);
115
+ };
116
+ visit(body);
117
+ if (returns.length !== 1) return void 0;
118
+ const expression = returns[0]?.expression;
119
+ return expression && ts.isExpression(expression) ? rootFromExpression(expression) : void 0;
120
+ }
121
+ function getTestId(root) {
122
+ const attribute = root.attributes.find(
123
+ (candidate) => ts.isJsxAttribute(candidate) && ts.isIdentifier(candidate.name) && candidate.name.text === "data-testid"
124
+ );
125
+ if (!attribute) return {};
126
+ const value = attribute.initializer;
127
+ if (!value || !ts.isStringLiteral(value)) return { attribute };
128
+ return { value: value.text, attribute };
129
+ }
130
+
131
+ // eslint/pasika/rules/filename-case.ts
132
+ var NEXT_ROUTING_FILES = /* @__PURE__ */ new Set([
133
+ "page",
134
+ "layout",
135
+ "loading",
136
+ "error",
137
+ "not-found",
138
+ "route",
139
+ "template",
140
+ "default",
141
+ "middleware",
142
+ "instrumentation"
143
+ ]);
144
+ var COMPOUND_SUFFIXES = /* @__PURE__ */ new Set(["example", "test", "spec", "stories"]);
145
+ function isKebabCase(str) {
146
+ return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(str);
147
+ }
148
+ function isPascalCase2(str) {
149
+ return /^[A-Z][A-Za-z0-9]*$/.test(str);
150
+ }
151
+ var filenameCaseRule = {
152
+ meta: {
153
+ schema: [],
154
+ type: "problem",
155
+ docs: {
156
+ description: "Enforce pasika filename conventions: kebab-case by default, PascalCase for smart .tsx components."
157
+ }
158
+ },
159
+ create(context) {
160
+ const filename = context.filename;
161
+ if (!filename) {
162
+ return {};
163
+ }
164
+ const normalized = filename.replace(/\\/g, "/");
165
+ if (!normalized.includes("/src/")) {
166
+ return {};
167
+ }
168
+ const ext = path2.extname(filename);
169
+ let base = path2.basename(filename, ext);
170
+ const baseExt = path2.extname(base);
171
+ if (baseExt && COMPOUND_SUFFIXES.has(baseExt.slice(1))) {
172
+ base = path2.basename(base, baseExt);
173
+ }
174
+ if (NEXT_ROUTING_FILES.has(base)) {
175
+ return {};
176
+ }
177
+ if (ext === ".tsx") {
178
+ return {
179
+ Program(node) {
180
+ const components = parseComponentInfo(context.sourceCode.text, filename);
181
+ const component = components[0];
182
+ if (component?.smart && !isPascalCase2(base)) {
183
+ context.report({
184
+ node,
185
+ message: `Smart component files must use PascalCase.tsx. Filename "${path2.basename(filename)}" is not PascalCase.`
186
+ });
187
+ return;
188
+ }
189
+ if (component && !component.smart && !isKebabCase(base)) {
190
+ context.report({
191
+ node,
192
+ message: `Dumb component files must use kebab-case.tsx. Filename "${path2.basename(filename)}" is not kebab-case.`
193
+ });
194
+ return;
195
+ }
196
+ if (!component && !(isPascalCase2(base) || isKebabCase(base))) {
197
+ context.report({
198
+ node,
199
+ message: `Filename "${path2.basename(filename)}" does not match pasika conventions. Component files must be PascalCase.tsx (smart) or kebab-case.tsx (dumb).`
200
+ });
201
+ }
202
+ }
203
+ };
204
+ }
205
+ if (!isKebabCase(base)) {
206
+ return report(context, filename, ext);
207
+ }
208
+ return {};
209
+ }
210
+ };
211
+ function report(context, filename, ext) {
212
+ return {
213
+ Program(node) {
214
+ const convention = ext === ".tsx" ? "Component files must be PascalCase.tsx (smart) or kebab-case.tsx (dumb)." : "Non-component files must use kebab-case.";
215
+ context.report({
216
+ node,
217
+ loc: { line: 1, column: 0 },
218
+ message: `Filename "${path2.basename(filename)}" does not match pasika conventions. ${convention}`
219
+ });
220
+ }
221
+ };
222
+ }
223
+
224
+ // eslint/pasika/rules/import-boundaries.ts
225
+ import path3 from "path";
226
+ var sourceRoot = path3.resolve("src");
227
+ var rootSupportFolders = /* @__PURE__ */ new Set(["config", "constants", "hooks", "locales", "schemas", "types", "utils"]);
228
+ var moduleExtensions = /* @__PURE__ */ new Set([".cjs", ".cts", ".js", ".jsx", ".mjs", ".mts", ".ts", ".tsx"]);
229
+ var styleExtensions = /* @__PURE__ */ new Set([".css", ".less", ".sass", ".scss"]);
230
+ function resolveSourceImport(filename, importPath) {
231
+ if (importPath.startsWith("@/")) {
232
+ return path3.resolve(sourceRoot, importPath.slice(2));
233
+ }
234
+ if (importPath.startsWith(".")) {
235
+ return path3.resolve(path3.dirname(filename), importPath);
236
+ }
237
+ return void 0;
238
+ }
239
+ function sourceSegments(absolutePath) {
240
+ const relativePath = path3.relative(sourceRoot, absolutePath);
241
+ if (relativePath.startsWith("..") || path3.isAbsolute(relativePath)) {
242
+ return void 0;
243
+ }
244
+ return relativePath.split(path3.sep);
245
+ }
246
+ function relativeSpecifier(filename, resolvedPath) {
247
+ const relativePath = path3.relative(path3.dirname(filename), resolvedPath).split(path3.sep).join("/");
248
+ return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
249
+ }
250
+ function aliasSpecifier(resolvedPath) {
251
+ return `@/${(sourceSegments(resolvedPath) ?? []).join("/")}`;
252
+ }
253
+ function segmentCount(specifier) {
254
+ return specifier.replace(/^@\//, "").split("/").filter((segment) => segment !== "." && segment !== "").length;
255
+ }
256
+ function describeSegments(count) {
257
+ return `${String(count)} segment${count === 1 ? "" : "s"}`;
258
+ }
259
+ function prefersRelative(filename, resolvedPath) {
260
+ return segmentCount(relativeSpecifier(filename, resolvedPath)) <= segmentCount(aliasSpecifier(resolvedPath));
261
+ }
262
+ var importBoundariesRule = {
263
+ meta: {
264
+ schema: [],
265
+ type: "problem"
266
+ },
267
+ create(context) {
268
+ const filename = context.filename;
269
+ if (!filename) {
270
+ return {};
271
+ }
272
+ function reportImport(source) {
273
+ if (typeof source.value !== "string") {
274
+ return;
275
+ }
276
+ const importPath = source.value;
277
+ const resolvedPath = resolveSourceImport(filename, importPath);
278
+ if (!resolvedPath) {
279
+ return;
280
+ }
281
+ const importer = sourceSegments(filename);
282
+ const imported = sourceSegments(resolvedPath);
283
+ if (!importer || !imported || importer.length === 0 || imported.length === 0) {
284
+ return;
285
+ }
286
+ const [importerLayer = "", importerFeature] = importer;
287
+ const [importedLayer = "", importedFeature] = imported;
288
+ const extension = path3.extname(importPath);
289
+ const isCodeModule = !extension || moduleExtensions.has(extension);
290
+ const isAppLocalStyleImport = importerLayer === "app" && importedLayer === "app" && styleExtensions.has(extension);
291
+ const importedIsRootSupport = rootSupportFolders.has(importedLayer);
292
+ const importedIsSameConfig = importerLayer === "config" && importedLayer === "config" && importer[1] === imported[1];
293
+ const isAllowed = !isCodeModule && importerLayer === "app" && isAppLocalStyleImport || isCodeModule && importerLayer === "app" && (importedLayer === "compositions" || importedLayer === "features" || importedLayer === "shared" || importedIsRootSupport) || isCodeModule && importerLayer === "compositions" && (importedLayer === "compositions" || importedLayer === "features" || importedLayer === "shared" || importedIsRootSupport) || isCodeModule && importerLayer === "features" && (importedLayer === "features" && importerFeature === importedFeature || importedLayer === "shared" || importedIsRootSupport) || isCodeModule && importerLayer === "shared" && (importedLayer === "shared" || importedIsRootSupport) || isCodeModule && rootSupportFolders.has(importerLayer) && importedIsRootSupport && (importerLayer !== "config" || importedLayer !== "config" || importedIsSameConfig);
294
+ if (!isAllowed) {
295
+ context.report({
296
+ node: source,
297
+ message: "This import violates the src layer boundary."
298
+ });
299
+ return;
300
+ }
301
+ const relativeForm = relativeSpecifier(filename, resolvedPath);
302
+ const aliasForm = aliasSpecifier(resolvedPath);
303
+ const relativeSegments = segmentCount(relativeForm);
304
+ const aliasSegments = segmentCount(aliasForm);
305
+ function describeChoice(preferred, preferredSegments, other, otherSegments) {
306
+ const tie = preferredSegments === otherSegments ? ", and a tie goes to the relative path" : "";
307
+ return `Use "${preferred}" (${describeSegments(preferredSegments)}) instead of "${other}" (${describeSegments(otherSegments)})${tie}.`;
308
+ }
309
+ if (prefersRelative(filename, resolvedPath) && importPath.startsWith("@/")) {
310
+ context.report({
311
+ node: source,
312
+ message: describeChoice(relativeForm, relativeSegments, aliasForm, aliasSegments)
313
+ });
314
+ return;
315
+ }
316
+ if (!prefersRelative(filename, resolvedPath) && importPath.startsWith(".")) {
317
+ context.report({
318
+ node: source,
319
+ message: describeChoice(aliasForm, aliasSegments, relativeForm, relativeSegments)
320
+ });
321
+ }
322
+ }
323
+ return {
324
+ ImportDeclaration(node) {
325
+ reportImport(node.source);
326
+ },
327
+ ExportAllDeclaration(node) {
328
+ reportImport(node.source);
329
+ },
330
+ ExportNamedDeclaration(node) {
331
+ if (node.source) {
332
+ reportImport(node.source);
333
+ }
334
+ },
335
+ ImportExpression(node) {
336
+ reportImport(node.source);
337
+ },
338
+ CallExpression(node) {
339
+ if (node.callee.type === "Identifier" && node.callee.name === "require" && node.arguments[0]) {
340
+ reportImport(node.arguments[0]);
341
+ }
342
+ }
343
+ };
344
+ }
345
+ };
346
+
347
+ // eslint/pasika/rules/no-mixed-concerns.ts
348
+ function getExportName(declaration) {
349
+ if (declaration?.type === "FunctionDeclaration" && declaration.id) {
350
+ return declaration.id.name ?? null;
351
+ }
352
+ if (declaration?.type === "VariableDeclaration") {
353
+ const declarator = declaration.declarations?.[0];
354
+ if (declarator?.id?.type === "Identifier" && (declarator.init?.type === "ArrowFunctionExpression" || declarator.init?.type === "FunctionExpression")) {
355
+ return declarator.id.name ?? null;
356
+ }
357
+ }
358
+ if (declaration?.type === "ArrowFunctionExpression") {
359
+ return "default";
360
+ }
361
+ if (declaration?.type === "FunctionExpression" && declaration.id) {
362
+ return declaration.id.name ?? null;
363
+ }
364
+ return null;
365
+ }
366
+ var noMixedConcernsRule = {
367
+ meta: {
368
+ schema: [],
369
+ type: "problem",
370
+ docs: {
371
+ description: "Enforce one exported React component per .tsx file."
372
+ }
373
+ },
374
+ create(context) {
375
+ if (!context.filename.endsWith(".tsx")) return {};
376
+ let exportedComponentCount = 0;
377
+ const extraExports = [];
378
+ function registerExport(name) {
379
+ exportedComponentCount++;
380
+ if (exportedComponentCount > 1) {
381
+ extraExports.push({ name });
382
+ }
383
+ }
384
+ return {
385
+ ExportNamedDeclaration(node) {
386
+ const name = getExportName(node.declaration);
387
+ if (name) registerExport(name);
388
+ },
389
+ ExportDefaultDeclaration(node) {
390
+ const name = getExportName(node.declaration);
391
+ if (name) registerExport(name);
392
+ },
393
+ "Program:exit"() {
394
+ if (exportedComponentCount > 1) {
395
+ for (const extra of extraExports) {
396
+ context.report({
397
+ loc: { line: 1, column: 0 },
398
+ message: `File exports multiple components. "${extra.name}" is an extra component export. Move it to its own file. Each .tsx file MUST contain exactly one component. See docs/code-organization-guide/rules/no-mixed-concerns-rule.md`
399
+ });
400
+ }
401
+ }
402
+ }
403
+ };
404
+ }
405
+ };
406
+
407
+ // eslint/pasika/rules/no-arbitrary-tailwind.ts
408
+ var ARBITRARY_VALUE_RE = /(?:^|(?<=\s))(?:[a-z]+(?:-[a-z]+)*)-\[[^\]]+\](?![-\w]*:)/g;
409
+ var CLASS_HELPERS = /* @__PURE__ */ new Set(["cn", "clsx", "twMerge", "twJoin"]);
410
+ var noArbitraryTailwindRule = {
411
+ meta: {
412
+ schema: [],
413
+ type: "problem",
414
+ docs: {
415
+ description: "Disallow Tailwind arbitrary-value classes like rounded-[13px]."
416
+ }
417
+ },
418
+ create(context) {
419
+ function reportClassString(node, value) {
420
+ ARBITRARY_VALUE_RE.lastIndex = 0;
421
+ const match = ARBITRARY_VALUE_RE.exec(value);
422
+ if (!match) return;
423
+ context.report({
424
+ node,
425
+ message: `Tailwind arbitrary-value class "${match[0]}" is not allowed. Use a named token or custom utility. See docs/styling-guide/rules/arbitrary-value-rule.md`
426
+ });
427
+ }
428
+ function checkExpression(node, expression) {
429
+ if (!expression) return;
430
+ if (expression.type === "Literal") {
431
+ if (typeof expression.value === "string") reportClassString(node, expression.value);
432
+ return;
433
+ }
434
+ if (expression.type === "TemplateLiteral") {
435
+ for (const quasi of expression.quasis) reportClassString(node, quasi.value.raw);
436
+ return;
437
+ }
438
+ if (expression.type === "LogicalExpression") {
439
+ checkExpression(node, expression.left);
440
+ checkExpression(node, expression.right);
441
+ return;
442
+ }
443
+ if (expression.type === "ConditionalExpression") {
444
+ checkExpression(node, expression.consequent);
445
+ checkExpression(node, expression.alternate);
446
+ return;
447
+ }
448
+ if (expression.type === "ArrayExpression") {
449
+ for (const element of expression.elements) checkExpression(node, element);
450
+ return;
451
+ }
452
+ if (expression.type === "ObjectExpression") {
453
+ for (const property of expression.properties) {
454
+ if (property.type === "Property") checkExpression(node, property.key);
455
+ }
456
+ }
457
+ }
458
+ return {
459
+ JSXAttribute(node) {
460
+ const attributeName = node.name?.name;
461
+ if (attributeName !== "className" && attributeName !== "class") return;
462
+ const value = node.value;
463
+ if (!value) return;
464
+ checkExpression(node, value.type === "JSXExpressionContainer" ? value.expression : value);
465
+ },
466
+ CallExpression(node) {
467
+ if (node.callee.type !== "Identifier" || !CLASS_HELPERS.has(node.callee.name)) return;
468
+ for (const argument of node.arguments) {
469
+ checkExpression(node, argument);
470
+ }
471
+ }
472
+ };
473
+ }
474
+ };
475
+
476
+ // eslint/pasika/rules/enforce-cn-merge.ts
477
+ function classCount(str) {
478
+ return str.split(/\s+/).filter(Boolean).length;
479
+ }
480
+ function isComponentName(name) {
481
+ return name?.type === "JSXIdentifier" && /^[A-Z]/.test(name.name);
482
+ }
483
+ function isOuterLayoutClass(className) {
484
+ const utility = className.split(":").pop() ?? className;
485
+ return /^(?:-?m[trblsexy]?-.+|(?:min-|max-)?[wh]-.+|size-.+|(?:flex-(?:1|auto|initial|none|grow|shrink|basis-.+)|grow(?:-.+)?|shrink(?:-.+)?|basis-.+|order-.+|(?:col|row)-(?:auto|span-.+|start-.+|end-.+)|(?:self|place-self|justify-self)-.+|z-.+|gap-.+|space-[xy]-.+))$/.test(
486
+ utility
487
+ );
488
+ }
489
+ function stringArguments(node) {
490
+ if (node?.type === "Literal" && typeof node.value === "string") return [node.value];
491
+ if (node?.type === "TemplateLiteral") return node.quasis.map((quasi) => quasi.value.raw);
492
+ if (node?.type === "ConditionalExpression") return [...stringArguments(node.consequent), ...stringArguments(node.alternate)];
493
+ if (node?.type === "LogicalExpression") return [...stringArguments(node.left), ...stringArguments(node.right)];
494
+ if (node?.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "cn") {
495
+ return node.arguments.flatMap((argument) => stringArguments(argument));
496
+ }
497
+ return [];
498
+ }
499
+ var enforceCnMergeRule = {
500
+ meta: { schema: [], type: "problem", docs: { description: "Enforce cn() for conditional class merging." } },
501
+ create(context) {
502
+ return {
503
+ JSXAttribute(node) {
504
+ const attributeName = node.name?.name ?? "";
505
+ const element = node.parent.parent;
506
+ const isComponentProp = isComponentName(element.openingElement?.name);
507
+ if (isComponentProp && attributeName !== "className" && attributeName.endsWith("ClassName")) {
508
+ context.report({ node, message: "Expose appearance through typed variant props instead of separate internal class-name props. See docs/styling-guide/rules/class-composition-rule.md" });
509
+ return;
510
+ }
511
+ if (attributeName !== "className" && attributeName !== "class") return;
512
+ const valueNode = node.value;
513
+ if (!valueNode) return;
514
+ if (isComponentProp && attributeName === "className") {
515
+ const expression = valueNode.type === "JSXExpressionContainer" ? valueNode.expression : valueNode;
516
+ const invalidClass = stringArguments(expression).flatMap((value) => value.split(/\s+/).filter(Boolean)).find((className) => !isOuterLayoutClass(className));
517
+ if (invalidClass) context.report({ node, message: `Passed className contains non-layout utility "${invalidClass}". Expose appearance through typed variant props. See docs/styling-guide/rules/class-composition-rule.md` });
518
+ return;
519
+ }
520
+ if (valueNode.type === "Literal" && typeof valueNode.value === "string") {
521
+ if (classCount(valueNode.value) > 5) context.report({ node, message: "Static className with more than 5 classes must use cn() with grouped string literals. See docs/styling-guide/rules/class-composition-rule.md" });
522
+ return;
523
+ }
524
+ if (valueNode.type !== "JSXExpressionContainer") return;
525
+ const expr = valueNode.expression;
526
+ if (expr.type === "BinaryExpression" && expr.operator === "+") {
527
+ context.report({ node, message: "Use cn() instead of + operator for className. See docs/styling-guide/rules/class-composition-rule.md" });
528
+ return;
529
+ }
530
+ if (expr.type === "TemplateLiteral" && expr.expressions.length > 0) {
531
+ context.report({ node, message: "Use cn() instead of template literals with conditionals for className. See docs/styling-guide/rules/class-composition-rule.md" });
532
+ return;
533
+ }
534
+ if (expr.type === "Literal" && typeof expr.value === "string" && classCount(expr.value) > 5) {
535
+ context.report({ node, message: "Static className with more than 5 classes must use cn() with grouped string literals. See docs/styling-guide/rules/class-composition-rule.md" });
536
+ return;
537
+ }
538
+ if (expr.type === "CallExpression" && expr.callee.type === "Identifier" && expr.callee.name === "cn") {
539
+ for (const arg of expr.arguments) {
540
+ if (arg.type === "Literal" && typeof arg.value === "string" && classCount(arg.value) > 5) {
541
+ context.report({ node: arg, message: "Each cn() string argument must contain at most 5 class names. Group by styling concern. See docs/styling-guide/rules/class-composition-rule.md" });
542
+ }
543
+ }
544
+ }
545
+ }
546
+ };
547
+ }
548
+ };
549
+
550
+ // eslint/pasika/rules/enforce-cva-variant-props.ts
551
+ var enforceCvaVariantPropsRule = {
552
+ meta: {
553
+ schema: [],
554
+ type: "problem",
555
+ docs: {
556
+ description: "Enforce VariantProps from CVA instead of manual variant union types."
557
+ }
558
+ },
559
+ create(context) {
560
+ const cvaDefinitions = /* @__PURE__ */ new Map();
561
+ return {
562
+ VariableDeclarator(node) {
563
+ const init = node.init;
564
+ if (init?.type !== "CallExpression" || init.callee.type !== "Identifier" || init.callee.name !== "cva" || node.id.type !== "Identifier") {
565
+ return;
566
+ }
567
+ const args = init.arguments;
568
+ if (args.length < 2) return;
569
+ const options = args[1];
570
+ if (options?.type !== "ObjectExpression") return;
571
+ for (const prop of options.properties) {
572
+ if (prop.type !== "Property" || prop.key.type !== "Identifier" || prop.key.name !== "variants" || prop.value.type !== "ObjectExpression") {
573
+ continue;
574
+ }
575
+ const variantNames = prop.value.properties.filter(
576
+ (p) => p.type === "Property" && p.key.type === "Identifier"
577
+ ).map((p) => p.key.name);
578
+ if (variantNames.length > 0) {
579
+ cvaDefinitions.set(node.id.name, variantNames);
580
+ }
581
+ }
582
+ },
583
+ TSTypeAliasDeclaration(node) {
584
+ const aliasName = node.id?.name;
585
+ if (!aliasName?.endsWith("Props")) return;
586
+ if (node.typeAnnotation?.type !== "TSTypeLiteral") return;
587
+ for (const member of node.typeAnnotation.members ?? []) {
588
+ if (member.type !== "TSPropertySignature") continue;
589
+ const keyName = member.key?.type === "Identifier" ? member.key.name : void 0;
590
+ if (!keyName) continue;
591
+ const typeAnn = member.typeAnnotation?.typeAnnotation;
592
+ if (typeAnn?.type !== "TSUnionType") continue;
593
+ const allStringLiterals = (typeAnn.types ?? []).every(
594
+ (t) => t.type === "TSLiteralType" && (t.literal?.type === "StringLiteral" || t.literal?.type === "Literal" && typeof t.literal.value === "string")
595
+ );
596
+ if (allStringLiterals && (typeAnn.types?.length ?? 0) >= 2) {
597
+ for (const variantNames of cvaDefinitions.values()) {
598
+ if (variantNames.includes(keyName)) {
599
+ context.report({
600
+ node,
601
+ message: `Variant prop "${keyName}" duplicates CVA variant values. Use VariantProps<typeof variants> instead of manually writing union types. See docs/styling-guide/rules/component-variant-rule.md`
602
+ });
603
+ return;
604
+ }
605
+ }
606
+ }
607
+ }
608
+ }
609
+ };
610
+ }
611
+ };
612
+
613
+ // eslint/pasika/rules/enforce-barrel-exports.ts
614
+ import path4 from "path";
615
+ import fs from "fs";
616
+ function isPascalCase3(str) {
617
+ return /^[A-Z][A-Za-z0-9]*$/.test(str);
618
+ }
619
+ function isKebabCase2(str) {
620
+ return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(str);
621
+ }
622
+ var SUPPORT_FOLDERS = /* @__PURE__ */ new Set(["types", "schemas", "hooks", "constants", "utils", "config", "locales"]);
623
+ var enforceBarrelExportsRule = {
624
+ meta: {
625
+ schema: [],
626
+ type: "problem",
627
+ docs: {
628
+ description: "Enforce index.ts barrels only re-export the parent component."
629
+ }
630
+ },
631
+ create(context) {
632
+ const filename = context.filename;
633
+ if (!filename) return {};
634
+ const baseName = path4.basename(filename);
635
+ if (baseName !== "index.ts" && baseName !== "index.cts" && baseName !== "index.mts") return {};
636
+ const dirPath = path4.dirname(filename);
637
+ const folderName = path4.basename(dirPath);
638
+ const parentFolderName = path4.basename(path4.dirname(dirPath));
639
+ if (SUPPORT_FOLDERS.has(folderName)) return {};
640
+ if (!isPascalCase3(folderName) && !isKebabCase2(folderName)) return {};
641
+ const matchingTsx = fs.existsSync(path4.join(dirPath, `${folderName}.tsx`)) ? folderName : null;
642
+ if (!matchingTsx) return {};
643
+ if (!isPascalCase3(parentFolderName) && !isKebabCase2(parentFolderName)) return {};
644
+ const reExportedNames = /* @__PURE__ */ new Set();
645
+ return {
646
+ ExportNamedDeclaration(node) {
647
+ if (!node.source) return;
648
+ for (const spec of node.specifiers) {
649
+ if (spec.exported.type === "Identifier") {
650
+ reExportedNames.add(spec.exported.name);
651
+ }
652
+ }
653
+ },
654
+ "Program:exit"() {
655
+ if (reExportedNames.size === 0) return;
656
+ if (!reExportedNames.has(matchingTsx)) {
657
+ context.report({
658
+ loc: { line: 1, column: 0 },
659
+ message: `index.ts in "${folderName}/" must re-export "${matchingTsx}". See docs/code-organization-guide/rules/folder-nesting-rule.md`
660
+ });
661
+ return;
662
+ }
663
+ const nonParentExports = [...reExportedNames].filter((n) => n !== matchingTsx);
664
+ if (nonParentExports.length > 0) {
665
+ context.report({
666
+ loc: { line: 1, column: 0 },
667
+ message: `index.ts must not re-export exclusive children: ${nonParentExports.join(", ")}. Only "${matchingTsx}" may be re-exported. See docs/code-organization-guide/rules/folder-nesting-rule.md`
668
+ });
669
+ }
670
+ }
671
+ };
672
+ }
673
+ };
674
+
675
+ // eslint/pasika/rules/component-placement.ts
676
+ import path8 from "path";
677
+
678
+ // eslint/pasika/project/index.ts
679
+ import { readdirSync, statSync } from "fs";
680
+ import path6 from "path";
681
+
682
+ // eslint/pasika/project/parse-module.ts
683
+ import path5 from "path";
684
+ import { readFileSync } from "fs";
685
+ import ts2 from "typescript";
686
+ var isPascalCase4 = (name) => /^[A-Z][A-Za-z0-9]*$/.test(name);
687
+ var isHookName = (name) => /^use[A-Z]/.test(name);
688
+ var isSchemaName = (name) => /[Ss]chema$/.test(name);
689
+ function lineOf(sourceFile, node) {
690
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
691
+ }
692
+ function returnsJsx(node) {
693
+ let found = false;
694
+ const visit = (child) => {
695
+ if (found) return;
696
+ if (ts2.isJsxElement(child) || ts2.isJsxSelfClosingElement(child) || ts2.isJsxFragment(child) || ts2.isJsxOpeningFragment(child)) {
697
+ found = true;
698
+ return;
699
+ }
700
+ ts2.forEachChild(child, visit);
701
+ };
702
+ ts2.forEachChild(node, visit);
703
+ return found;
704
+ }
705
+ function classifyFunction(name, isTsx, hasJsx) {
706
+ if (isHookName(name)) return "hook";
707
+ if (isTsx && isPascalCase4(name) && hasJsx) return "component";
708
+ return "function";
709
+ }
710
+ function classifyValue(name, initializer, isTsx) {
711
+ const isFunctionLike = initializer !== void 0 && (ts2.isArrowFunction(initializer) || ts2.isFunctionExpression(initializer) || ts2.isFunctionDeclaration(initializer));
712
+ if (isHookName(name)) return "hook";
713
+ if (isSchemaName(name)) return "schema";
714
+ if (isTsx && isPascalCase4(name) && (initializer === void 0 || returnsJsx(initializer) || isFunctionLike)) {
715
+ return "component";
716
+ }
717
+ if (isFunctionLike) return "function";
718
+ return "constant";
719
+ }
720
+ function parseModule(file) {
721
+ const text = readFileSync(file, "utf8");
722
+ const isTsx = file.endsWith(".tsx") || file.endsWith(".jsx");
723
+ const sourceFile = ts2.createSourceFile(file, text, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TSX);
724
+ const imports = [];
725
+ const exports = [];
726
+ const addImport = (specifierNode, names, node) => {
727
+ if (!ts2.isStringLiteral(specifierNode)) return;
728
+ imports.push({ specifier: specifierNode.text, names, line: lineOf(sourceFile, node) });
729
+ };
730
+ for (const statement of sourceFile.statements) {
731
+ if (ts2.isImportDeclaration(statement)) {
732
+ const names = [];
733
+ const bindings = statement.importClause?.namedBindings;
734
+ if (statement.importClause?.name) names.push(statement.importClause.name.text);
735
+ if (bindings && ts2.isNamedImports(bindings)) {
736
+ for (const element of bindings.elements) names.push(element.propertyName?.text ?? element.name.text);
737
+ }
738
+ addImport(statement.moduleSpecifier, names, statement);
739
+ continue;
740
+ }
741
+ if (ts2.isExportDeclaration(statement)) {
742
+ if (statement.moduleSpecifier) {
743
+ const names = [];
744
+ if (statement.exportClause && ts2.isNamedExports(statement.exportClause)) {
745
+ for (const element of statement.exportClause.elements) {
746
+ names.push(element.propertyName?.text ?? element.name.text);
747
+ }
748
+ }
749
+ addImport(statement.moduleSpecifier, names, statement);
750
+ }
751
+ if (statement.exportClause && ts2.isNamedExports(statement.exportClause)) {
752
+ for (const element of statement.exportClause.elements) {
753
+ exports.push({ name: element.name.text, kind: "other", line: lineOf(sourceFile, element) });
754
+ }
755
+ }
756
+ continue;
757
+ }
758
+ const isExported2 = ts2.canHaveModifiers(statement) ? (ts2.getModifiers(statement) ?? []).some((modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword) : false;
759
+ if (!isExported2) continue;
760
+ if (ts2.isFunctionDeclaration(statement) && statement.name) {
761
+ const name = statement.name.text;
762
+ exports.push({
763
+ name,
764
+ kind: classifyFunction(name, isTsx, returnsJsx(statement)),
765
+ line: lineOf(sourceFile, statement)
766
+ });
767
+ continue;
768
+ }
769
+ if (ts2.isVariableStatement(statement)) {
770
+ for (const declaration of statement.declarationList.declarations) {
771
+ if (!ts2.isIdentifier(declaration.name)) continue;
772
+ exports.push({
773
+ name: declaration.name.text,
774
+ kind: classifyValue(declaration.name.text, declaration.initializer, isTsx),
775
+ line: lineOf(sourceFile, declaration)
776
+ });
777
+ }
778
+ continue;
779
+ }
780
+ if (ts2.isTypeAliasDeclaration(statement) || ts2.isInterfaceDeclaration(statement)) {
781
+ exports.push({ name: statement.name.text, kind: "type", line: lineOf(sourceFile, statement) });
782
+ }
783
+ }
784
+ return { file: path5.resolve(file), imports, exports };
785
+ }
786
+
787
+ // eslint/pasika/project/index.ts
788
+ var MODULE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
789
+ var INDEX_BASENAMES = ["index.ts", "index.tsx", "index.mts", "index.cts", "index.js", "index.jsx"];
790
+ var REVALIDATE_AFTER_MS = 2e3;
791
+ var symbolKey = (file, name) => `${file}\0${name}`;
792
+ function listSourceFiles(dir) {
793
+ let entries;
794
+ try {
795
+ entries = readdirSync(dir);
796
+ } catch {
797
+ return [];
798
+ }
799
+ return entries.flatMap((entry) => {
800
+ if (entry.startsWith(".") || entry === "node_modules") return [];
801
+ const entryPath = path6.join(dir, entry);
802
+ let stats;
803
+ try {
804
+ stats = statSync(entryPath);
805
+ } catch {
806
+ return [];
807
+ }
808
+ if (stats.isDirectory()) return listSourceFiles(entryPath);
809
+ return MODULE_EXTENSIONS.includes(path6.extname(entry)) ? [entryPath] : [];
810
+ });
811
+ }
812
+ function fingerprint(files) {
813
+ let total = 0;
814
+ for (const file of files) {
815
+ try {
816
+ total += statSync(file).mtimeMs;
817
+ } catch {
818
+ }
819
+ }
820
+ return `${String(files.length)}:${String(total)}`;
821
+ }
822
+ function resolveSpecifier(fromFile, specifier, sourceRoot2) {
823
+ let base;
824
+ if (specifier.startsWith("@/")) {
825
+ base = path6.resolve(sourceRoot2, specifier.slice(2));
826
+ } else if (specifier.startsWith(".")) {
827
+ base = path6.resolve(path6.dirname(fromFile), specifier);
828
+ } else {
829
+ return void 0;
830
+ }
831
+ const candidates = [
832
+ base,
833
+ ...MODULE_EXTENSIONS.map((extension) => `${base}${extension}`),
834
+ ...INDEX_BASENAMES.map((name) => path6.join(base, name))
835
+ ];
836
+ for (const candidate of candidates) {
837
+ try {
838
+ if (statSync(candidate).isFile()) return candidate;
839
+ } catch {
840
+ }
841
+ }
842
+ return void 0;
843
+ }
844
+ function build(sourceRoot2, files) {
845
+ const modules = /* @__PURE__ */ new Map();
846
+ const consumers = /* @__PURE__ */ new Map();
847
+ const symbolConsumers = /* @__PURE__ */ new Map();
848
+ for (const file of files) {
849
+ try {
850
+ modules.set(file, parseModule(file));
851
+ } catch {
852
+ }
853
+ }
854
+ for (const [file, module] of modules) {
855
+ for (const moduleImport of module.imports) {
856
+ const target = resolveSpecifier(file, moduleImport.specifier, sourceRoot2);
857
+ if (!target || !modules.has(target)) continue;
858
+ const fileConsumers = consumers.get(target) ?? /* @__PURE__ */ new Set();
859
+ fileConsumers.add(file);
860
+ consumers.set(target, fileConsumers);
861
+ for (const name of moduleImport.names) {
862
+ const key = symbolKey(target, name);
863
+ const nameConsumers = symbolConsumers.get(key) ?? /* @__PURE__ */ new Set();
864
+ nameConsumers.add(file);
865
+ symbolConsumers.set(key, nameConsumers);
866
+ }
867
+ }
868
+ }
869
+ return { sourceRoot: sourceRoot2, modules, consumers, symbolConsumers };
870
+ }
871
+ var cache;
872
+ function getProjectIndex(sourceRoot2) {
873
+ const now = Date.now();
874
+ if (cache?.index.sourceRoot === sourceRoot2 && now - cache.checkedAt < REVALIDATE_AFTER_MS) {
875
+ return cache.index;
876
+ }
877
+ try {
878
+ if (!statSync(sourceRoot2).isDirectory()) return void 0;
879
+ } catch {
880
+ return void 0;
881
+ }
882
+ const files = listSourceFiles(sourceRoot2).sort((left, right) => left.localeCompare(right));
883
+ const currentFingerprint = fingerprint(files);
884
+ if (cache?.index.sourceRoot === sourceRoot2 && cache.fingerprint === currentFingerprint) {
885
+ cache.checkedAt = now;
886
+ return cache.index;
887
+ }
888
+ const index = build(sourceRoot2, files);
889
+ cache = { index, checkedAt: now, fingerprint: currentFingerprint };
890
+ return index;
891
+ }
892
+
893
+ // eslint/pasika/project/ccf.ts
894
+ import path7 from "path";
895
+ var SUPPORT_FOLDERS2 = /* @__PURE__ */ new Set(["hooks", "types", "schemas", "constants", "utils"]);
896
+ function segmentsOf(file, sourceRoot2) {
897
+ const relative = path7.relative(sourceRoot2, file);
898
+ return relative.startsWith("..") ? [] : relative.split(path7.sep);
899
+ }
900
+ function folderSegmentsOf(file, sourceRoot2) {
901
+ return segmentsOf(file, sourceRoot2).slice(0, -1);
902
+ }
903
+ var isUnderApp = (segments) => segments[0] === "app";
904
+ var isConfigModule = (segments) => segments[0] === "config";
905
+ var isUnderCompositions = (segments) => segments[0] === "compositions";
906
+ function commonPrefix(folders) {
907
+ if (folders.length === 0) return [];
908
+ const [first = []] = folders;
909
+ const shared = [];
910
+ for (const [depth, segment] of first.entries()) {
911
+ if (!folders.every((folder) => folder[depth] === segment)) break;
912
+ shared.push(segment);
913
+ }
914
+ return shared;
915
+ }
916
+ function outOfSupportFolders(folder) {
917
+ const result = [...folder];
918
+ while (result.length > 0 && SUPPORT_FOLDERS2.has(result[result.length - 1] ?? "")) result.pop();
919
+ return result;
920
+ }
921
+ function resolveComponentPlacement(componentFile, index) {
922
+ const consumers = [...index.consumers.get(componentFile) ?? []];
923
+ const relevant = consumers.filter((consumer) => {
924
+ const segments = segmentsOf(consumer, index.sourceRoot);
925
+ return segments.length > 0 && !isUnderApp(segments) && !isConfigModule(segments);
926
+ });
927
+ if (relevant.length === 0) return void 0;
928
+ const outsideCompositions = relevant.filter(
929
+ (consumer) => !isUnderCompositions(segmentsOf(consumer, index.sourceRoot))
930
+ );
931
+ const counted = outsideCompositions.length > 0 ? outsideCompositions : relevant;
932
+ const shared = outOfSupportFolders(
933
+ commonPrefix(counted.map((consumer) => folderSegmentsOf(consumer, index.sourceRoot)))
934
+ );
935
+ if (shared.length === 1 && shared[0] === "features") {
936
+ return { countedConsumers: counted, expectedFolder: ["shared"], reason: "across-features" };
937
+ }
938
+ if (shared.length === 0) {
939
+ return { countedConsumers: counted, expectedFolder: ["shared"], reason: "across-layers" };
940
+ }
941
+ return { countedConsumers: counted, expectedFolder: shared, reason: "ccf" };
942
+ }
943
+ var formatFolder = (folder) => `src/${folder.join("/")}/`;
944
+ function owningFolderOf(consumer, sourceRoot2) {
945
+ return outOfSupportFolders(folderSegmentsOf(consumer, sourceRoot2));
946
+ }
947
+ var configModuleOf = (segments) => isConfigModule(segments) && segments.length >= 3 ? segments[1] : void 0;
948
+ function resolveSupportPlacement(supportFile, supportFolder, index) {
949
+ const consumers = [...index.consumers.get(supportFile) ?? []].filter(
950
+ (consumer) => segmentsOf(consumer, index.sourceRoot).length > 0
951
+ );
952
+ if (consumers.length === 0) return void 0;
953
+ const consumerSegments = consumers.map((consumer) => segmentsOf(consumer, index.sourceRoot));
954
+ if (consumerSegments.some((segments) => isUnderApp(segments))) {
955
+ return { countedConsumers: consumers, expectedFolder: [supportFolder], reason: "app-consumer" };
956
+ }
957
+ const configModules = new Set(consumerSegments.map((segments) => configModuleOf(segments)));
958
+ const [onlyConfigModule] = [...configModules];
959
+ if (configModules.size === 1 && onlyConfigModule !== void 0) {
960
+ return {
961
+ countedConsumers: consumers,
962
+ expectedFolder: ["config", onlyConfigModule, supportFolder],
963
+ reason: "config-module"
964
+ };
965
+ }
966
+ const shared = commonPrefix(consumers.map((consumer) => owningFolderOf(consumer, index.sourceRoot)));
967
+ if (shared.length === 1 && shared[0] === "features") {
968
+ return { countedConsumers: consumers, expectedFolder: [supportFolder], reason: "across-features" };
969
+ }
970
+ if (shared.length === 0) {
971
+ return { countedConsumers: consumers, expectedFolder: [supportFolder], reason: "across-layers" };
972
+ }
973
+ return { countedConsumers: consumers, expectedFolder: [...shared, supportFolder], reason: "ccf" };
974
+ }
975
+ function describeConsumers(consumers, sourceRoot2) {
976
+ const shown = 3;
977
+ const names = consumers.map((consumer) => path7.relative(path7.dirname(sourceRoot2), consumer).split(path7.sep).join("/")).sort((left, right) => left.localeCompare(right));
978
+ if (names.length <= shown) return names.join(", ");
979
+ return `${names.slice(0, shown).join(", ")} and ${String(names.length - shown)} more`;
980
+ }
981
+
982
+ // eslint/pasika/rules/component-placement.ts
983
+ var REASON_TEXT = {
984
+ ccf: "that is the closest folder its consumers share",
985
+ "across-features": "its consumers span more than one feature, and no feature may import from another",
986
+ "across-layers": "its consumers span more than one layer, so no feature can own it"
987
+ };
988
+ var sameFolder = (left, right) => left.length === right.length && left.every((segment, depth) => segment === right[depth]);
989
+ var componentPlacementRule = {
990
+ meta: {
991
+ schema: [],
992
+ type: "problem",
993
+ docs: {
994
+ description: "Enforce that a component lives in the folder its consumers imply."
995
+ }
996
+ },
997
+ create(context) {
998
+ const filename = context.filename;
999
+ if (!filename.endsWith(".tsx") && !filename.endsWith(".jsx")) return {};
1000
+ const sourceRoot2 = path8.resolve("src");
1001
+ const index = getProjectIndex(sourceRoot2);
1002
+ if (!index) return {};
1003
+ const componentFile = path8.resolve(filename);
1004
+ const segments = segmentsOf(componentFile, sourceRoot2);
1005
+ if (segments.length === 0) return {};
1006
+ if (isUnderApp(segments) || isConfigModule(segments)) return {};
1007
+ const module = index.modules.get(componentFile);
1008
+ if (!module?.exports.some((moduleExport) => moduleExport.kind === "component")) return {};
1009
+ const currentFolder = folderSegmentsOf(componentFile, sourceRoot2);
1010
+ const placement = resolveComponentPlacement(componentFile, index);
1011
+ return {
1012
+ Program(node) {
1013
+ if (!placement) {
1014
+ if (segments[0] !== "features" || segments.length < 3) {
1015
+ context.report({
1016
+ node,
1017
+ loc: { line: 1, column: 0 },
1018
+ message: `This component has no consumer outside src/app/ or a configuration module, so it belongs in the feature folder it represents, not in ${formatFolder(currentFolder)}. See docs/code-organization-guide/rules/component-placement-rule.md`
1019
+ });
1020
+ }
1021
+ return;
1022
+ }
1023
+ if (sameFolder(currentFolder, placement.expectedFolder)) return;
1024
+ const explanation = REASON_TEXT[placement.reason] ?? "that is where its consumers place it";
1025
+ context.report({
1026
+ node,
1027
+ loc: { line: 1, column: 0 },
1028
+ message: `Move this component to ${formatFolder(placement.expectedFolder)} \u2014 ${explanation}. Imported by ${describeConsumers(placement.countedConsumers, sourceRoot2)}. See docs/code-organization-guide/rules/component-placement-rule.md`
1029
+ });
1030
+ }
1031
+ };
1032
+ }
1033
+ };
1034
+
1035
+ // eslint/pasika/rules/support-file-placement.ts
1036
+ import path9 from "path";
1037
+ var CONFIG_OWNED_FOLDERS = /* @__PURE__ */ new Set(["types", "constants"]);
1038
+ var REASON_TEXT2 = {
1039
+ "app-consumer": "a file under src/app/ imports it, so it belongs to the app-wide support folder",
1040
+ "config-module": "every file that imports it belongs to that configuration module",
1041
+ ccf: "that is the closest folder its consumers share",
1042
+ "across-features": "its consumers span more than one feature, so no feature can own it",
1043
+ "across-layers": "its consumers span more than one layer, so no layer can own it"
1044
+ };
1045
+ var sameFolder2 = (left, right) => left.length === right.length && left.every((segment, depth) => segment === right[depth]);
1046
+ var supportFilePlacementRule = {
1047
+ meta: {
1048
+ schema: [],
1049
+ type: "problem",
1050
+ docs: {
1051
+ description: "Enforce that a support file lives in the folder its consumers imply."
1052
+ }
1053
+ },
1054
+ create(context) {
1055
+ const sourceRoot2 = path9.resolve("src");
1056
+ const supportFile = path9.resolve(context.filename);
1057
+ const currentFolder = folderSegmentsOf(supportFile, sourceRoot2);
1058
+ const supportFolder = currentFolder[currentFolder.length - 1];
1059
+ if (supportFolder === void 0 || !SUPPORT_FOLDERS2.has(supportFolder)) return {};
1060
+ const index = getProjectIndex(sourceRoot2);
1061
+ if (!index) return {};
1062
+ const placement = resolveSupportPlacement(supportFile, supportFolder, index);
1063
+ if (!placement || sameFolder2(currentFolder, placement.expectedFolder)) return {};
1064
+ if (isConfigModule(segmentsOf(supportFile, sourceRoot2)) && CONFIG_OWNED_FOLDERS.has(supportFolder)) {
1065
+ if (placement.reason !== "config-module") return {};
1066
+ }
1067
+ return {
1068
+ Program(node) {
1069
+ context.report({
1070
+ node,
1071
+ loc: { line: 1, column: 0 },
1072
+ message: `Move this file to ${formatFolder(placement.expectedFolder)} \u2014 ${REASON_TEXT2[placement.reason] ?? "that is where its consumers place it"}. Imported by ${describeConsumers(placement.countedConsumers, sourceRoot2)}.`
1073
+ });
1074
+ }
1075
+ };
1076
+ }
1077
+ };
1078
+
1079
+ // eslint/pasika/rules/application-structure.ts
1080
+ import fs2 from "fs";
1081
+ import path10 from "path";
1082
+ var MODULE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
1083
+ var ROUTING_FILES = /* @__PURE__ */ new Set([
1084
+ "default",
1085
+ "error",
1086
+ "instrumentation",
1087
+ "layout",
1088
+ "loading",
1089
+ "middleware",
1090
+ "not-found",
1091
+ "page",
1092
+ "route",
1093
+ "template"
1094
+ ]);
1095
+ var EXPECTED_SUPPORT_FOLDER = {
1096
+ hook: "hooks",
1097
+ type: "types",
1098
+ schema: "schemas",
1099
+ constant: "constants",
1100
+ function: "utils"
1101
+ };
1102
+ function report2(context, message) {
1103
+ return {
1104
+ Program(node) {
1105
+ context.report({ node, loc: { line: 1, column: 0 }, message });
1106
+ }
1107
+ };
1108
+ }
1109
+ function isCodeFile(filename) {
1110
+ return MODULE_EXTENSIONS2.has(path10.extname(filename));
1111
+ }
1112
+ function isRootSupportFolder(folder) {
1113
+ return SUPPORT_FOLDERS2.has(folder);
1114
+ }
1115
+ function exportedKinds(filename) {
1116
+ try {
1117
+ return new Set(parseModule(filename).exports.map((moduleExport) => moduleExport.kind));
1118
+ } catch {
1119
+ return /* @__PURE__ */ new Set();
1120
+ }
1121
+ }
1122
+ function expectedSupportFolder(kinds) {
1123
+ for (const kind of ["component", "hook", "type", "schema", "constant", "function"]) {
1124
+ if (kinds.has(kind)) return EXPECTED_SUPPORT_FOLDER[kind];
1125
+ }
1126
+ return void 0;
1127
+ }
1128
+ function configModuleRoot(filename, sourceRoot2) {
1129
+ const segments = segmentsOf(filename, sourceRoot2);
1130
+ if (segments[0] !== "config" || segments.length < 3) return void 0;
1131
+ return path10.join(sourceRoot2, "config", segments[1] ?? "");
1132
+ }
1133
+ function componentFolderStart(segments) {
1134
+ if (segments[0] === "features") return 2;
1135
+ if (segments[0] === "compositions" || segments[0] === "shared") return 1;
1136
+ return -1;
1137
+ }
1138
+ function componentFolderViolation(segments, sourceRoot2) {
1139
+ const start = componentFolderStart(segments);
1140
+ if (start < 0) return void 0;
1141
+ for (let depth = segments.length - 2; depth >= start; depth -= 1) {
1142
+ const folder = segments[depth];
1143
+ if (!folder || SUPPORT_FOLDERS2.has(folder)) continue;
1144
+ const folderPath = path10.join(sourceRoot2, ...segments.slice(0, depth + 1));
1145
+ const label = `src/${segments.slice(0, depth + 1).join("/")}/`;
1146
+ if (!fs2.existsSync(path10.join(folderPath, `${folder}.tsx`))) {
1147
+ return `A folder that is not a support folder must be a component folder; add "${folder}.tsx" to ${label} or move its files into a support folder.`;
1148
+ }
1149
+ if (!fs2.existsSync(path10.join(folderPath, "index.ts"))) {
1150
+ return `A component folder must have an index.ts that named-re-exports its component; add index.ts to ${label}.`;
1151
+ }
1152
+ }
1153
+ return void 0;
1154
+ }
1155
+ var applicationStructureRule = {
1156
+ meta: {
1157
+ schema: [],
1158
+ type: "problem",
1159
+ docs: {
1160
+ description: "Enforce the repository's application, configuration, and support-folder structure."
1161
+ }
1162
+ },
1163
+ create(context) {
1164
+ const filename = path10.resolve(context.filename);
1165
+ const sourceRoot2 = path10.resolve("src");
1166
+ const segments = segmentsOf(filename, sourceRoot2);
1167
+ if (segments.length === 0) return {};
1168
+ const [topLevel, secondLevel] = segments;
1169
+ if (topLevel === void 0) return {};
1170
+ if (segments.length > 1 && topLevel !== "app" && topLevel !== "compositions" && topLevel !== "features" && topLevel !== "shared" && topLevel !== "config" && topLevel !== "locales" && !isRootSupportFolder(topLevel)) {
1171
+ return report2(
1172
+ context,
1173
+ `Move this source file under an allowed src/ folder; "src/${topLevel}/" is not part of the application structure.`
1174
+ );
1175
+ }
1176
+ if (topLevel === "features" && segments.length >= 2 && secondLevel !== void 0) {
1177
+ if (SUPPORT_FOLDERS2.has(secondLevel)) {
1178
+ return report2(
1179
+ context,
1180
+ `Move this support folder inside a feature folder; "src/features/${secondLevel}/" does not belong to a feature.`
1181
+ );
1182
+ }
1183
+ if (segments.length === 2) {
1184
+ return report2(context, "src/features/ must contain only feature folders, not source files.");
1185
+ }
1186
+ }
1187
+ if (topLevel === "config") {
1188
+ if (segments.length === 2) {
1189
+ return report2(
1190
+ context,
1191
+ "A configuration module must be a src/config/<config-name>/ folder with index.ts as its entry point."
1192
+ );
1193
+ }
1194
+ const moduleRoot = configModuleRoot(filename, sourceRoot2);
1195
+ if (moduleRoot && !fs2.existsSync(path10.join(moduleRoot, "index.ts"))) {
1196
+ return report2(
1197
+ context,
1198
+ `Add src/config/${path10.basename(moduleRoot)}/index.ts as the configuration module entry point.`
1199
+ );
1200
+ }
1201
+ if (moduleRoot && segments.length === 3 && path10.basename(filename) !== "index.ts") {
1202
+ const kinds2 = exportedKinds(filename);
1203
+ const expected2 = expectedSupportFolder(kinds2);
1204
+ if (expected2 !== void 0) {
1205
+ return report2(
1206
+ context,
1207
+ `Move this configuration support file into src/config/${path10.basename(moduleRoot)}/${expected2}/.`
1208
+ );
1209
+ }
1210
+ }
1211
+ }
1212
+ if (topLevel === "app" && isCodeFile(filename)) {
1213
+ const basename = path10.basename(filename, path10.extname(filename));
1214
+ const currentFolder2 = path10.basename(path10.dirname(filename));
1215
+ if (SUPPORT_FOLDERS2.has(currentFolder2)) {
1216
+ return report2(
1217
+ context,
1218
+ "src/app/ may contain routing files and framework assets, but ordinary components and support files must live outside src/app/."
1219
+ );
1220
+ }
1221
+ if (!ROUTING_FILES.has(basename) && path10.extname(filename) !== ".css") {
1222
+ return report2(
1223
+ context,
1224
+ "src/app/ may contain routing files and framework assets, but ordinary components and support files must live outside src/app/."
1225
+ );
1226
+ }
1227
+ }
1228
+ const currentFolder = path10.basename(path10.dirname(filename));
1229
+ const kinds = exportedKinds(filename);
1230
+ const isConfigModuleRoot = topLevel === "config" && segments.length === 3 && path10.basename(filename) === "index.ts";
1231
+ if (isConfigModuleRoot) return {};
1232
+ if (!SUPPORT_FOLDERS2.has(currentFolder)) {
1233
+ const expected2 = expectedSupportFolder(kinds);
1234
+ if (expected2 !== void 0 && segments.length > 2) {
1235
+ return report2(
1236
+ context,
1237
+ `Move this file to a ${expected2}/ folder; ${currentFolder}/ is not a recognized support folder.`
1238
+ );
1239
+ }
1240
+ const violation2 = componentFolderViolation(segments, sourceRoot2);
1241
+ if (violation2 !== void 0) return report2(context, violation2);
1242
+ return {};
1243
+ }
1244
+ if (kinds.has("component")) {
1245
+ return report2(
1246
+ context,
1247
+ `A support folder must not contain a component; move ${path10.basename(filename)} beside ${currentFolder}/.`
1248
+ );
1249
+ }
1250
+ const expected = expectedSupportFolder(kinds);
1251
+ if (expected !== void 0 && expected !== currentFolder) {
1252
+ return report2(
1253
+ context,
1254
+ `Move this file to a ${expected}/ folder; ${currentFolder}/ is reserved for ${expected === "utils" ? "utilities" : expected}.`
1255
+ );
1256
+ }
1257
+ const violation = componentFolderViolation(segments, sourceRoot2);
1258
+ if (violation !== void 0) return report2(context, violation);
1259
+ return {};
1260
+ }
1261
+ };
1262
+
1263
+ // eslint/pasika/rules/named-exports.ts
1264
+ import path11 from "path";
1265
+ var FRAMEWORK_DEFAULT_EXPORT_FILES = /* @__PURE__ */ new Set([
1266
+ "default",
1267
+ "error",
1268
+ "layout",
1269
+ "loading",
1270
+ "not-found",
1271
+ "page",
1272
+ "route",
1273
+ "template"
1274
+ ]);
1275
+ function isFrameworkDefaultExportFile(filename) {
1276
+ const normalized = filename.replaceAll(path11.sep, "/");
1277
+ if (!normalized.includes("/src/app/")) return false;
1278
+ const basename = path11.basename(filename, path11.extname(filename));
1279
+ return FRAMEWORK_DEFAULT_EXPORT_FILES.has(basename);
1280
+ }
1281
+ var namedExportsRule = {
1282
+ meta: {
1283
+ schema: [],
1284
+ type: "problem",
1285
+ docs: {
1286
+ description: "Require named exports except where a framework requires a default export."
1287
+ }
1288
+ },
1289
+ create(context) {
1290
+ if (isFrameworkDefaultExportFile(context.filename)) return {};
1291
+ return {
1292
+ ExportDefaultDeclaration(node) {
1293
+ context.report({
1294
+ node,
1295
+ message: "Files that export values must use named exports unless a framework or third-party package requires a different export style for that file. See docs/code-organization-guide/rules/exports-and-imports-rule.md"
1296
+ });
1297
+ }
1298
+ };
1299
+ }
1300
+ };
1301
+
1302
+ // eslint/pasika/rules/data-testid-case.ts
1303
+ import path12 from "path";
1304
+ var NEXT_ROUTING_FILES2 = /* @__PURE__ */ new Set([
1305
+ "page",
1306
+ "layout",
1307
+ "loading",
1308
+ "error",
1309
+ "not-found",
1310
+ "route",
1311
+ "template",
1312
+ "default",
1313
+ "middleware",
1314
+ "instrumentation"
1315
+ ]);
1316
+ var isPascalCase5 = (value) => /^[A-Z][A-Za-z0-9]*$/.test(value);
1317
+ var isKebabCase3 = (value) => /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value);
1318
+ var dataTestIdCaseRule = {
1319
+ meta: {
1320
+ schema: [],
1321
+ type: "problem",
1322
+ docs: {
1323
+ description: "Enforce data-testid casing for smart and dumb components with a single intrinsic root."
1324
+ }
1325
+ },
1326
+ create(context) {
1327
+ const filename = path12.resolve(context.filename);
1328
+ if (!filename.endsWith(".tsx")) return {};
1329
+ const base = path12.basename(filename, path12.extname(filename));
1330
+ if (NEXT_ROUTING_FILES2.has(base)) return {};
1331
+ const text = context.sourceCode.text;
1332
+ const components = parseComponentInfo(text, filename);
1333
+ return {
1334
+ Program(node) {
1335
+ for (const component of components) {
1336
+ const root = findSimpleRoot(component, text, filename);
1337
+ if (!root) continue;
1338
+ const { value } = getTestId(root);
1339
+ const expected = component.smart ? component.name : toKebabCase(component.name);
1340
+ if (component.smart && value === void 0) {
1341
+ context.report({
1342
+ node,
1343
+ message: `Smart component "${component.name}" with one outer DOM element must set data-testid="${expected}".`
1344
+ });
1345
+ continue;
1346
+ }
1347
+ if (value === void 0 || value === expected) continue;
1348
+ if (component.smart ? !isPascalCase5(value) || value !== expected : !isKebabCase3(value)) {
1349
+ context.report({
1350
+ node,
1351
+ message: `data-testid for ${component.smart ? "smart" : "dumb"} component "${component.name}" must be ${component.smart ? "PascalCase" : "kebab-case"}: expected "${expected}".`
1352
+ });
1353
+ }
1354
+ }
1355
+ }
1356
+ };
1357
+ }
1358
+ };
1359
+ function toKebabCase(value) {
1360
+ return value.replace(/(?<lower>[a-z0-9])(?<upper>[A-Z])/g, "$<lower>-$<upper>").replace(/(?<first>[A-Z])(?<rest>[A-Z][a-z])/g, "$<first>-$<rest>").toLowerCase();
1361
+ }
1362
+
1363
+ // eslint/pasika/rules/support-folder-shape.ts
1364
+ import fs3 from "fs";
1365
+ import path13 from "path";
1366
+ var SUPPORT_FOLDERS3 = /* @__PURE__ */ new Set(["constants", "types", "schemas"]);
1367
+ var INDEX_NAMES = /* @__PURE__ */ new Set(["index.ts", "index.tsx", "index.mts", "index.cts"]);
1368
+ var supportFolderShapeRule = {
1369
+ meta: {
1370
+ schema: [],
1371
+ type: "problem",
1372
+ docs: {
1373
+ description: "Require support-folder exports to be defined in index.ts or named-re-exported by it."
1374
+ }
1375
+ },
1376
+ create(context) {
1377
+ const filename = path13.resolve(context.filename);
1378
+ const baseName = path13.basename(filename);
1379
+ if (!INDEX_NAMES.has(baseName)) return {};
1380
+ const folder = path13.basename(path13.dirname(filename));
1381
+ if (!SUPPORT_FOLDERS3.has(folder)) return {};
1382
+ const directory = path13.dirname(filename);
1383
+ let entries;
1384
+ try {
1385
+ entries = fs3.readdirSync(directory);
1386
+ } catch {
1387
+ return {};
1388
+ }
1389
+ const siblingModules = entries.filter((entry) => entry !== baseName && /\.(?:[cm]?tsx?|jsx?)$/.test(entry));
1390
+ if (siblingModules.length === 0) return {};
1391
+ return {
1392
+ Program(node) {
1393
+ const source = context.sourceCode.text;
1394
+ const hasDirectExport = /export\s+(?:const|let|var|function|class|type|interface|enum)\b/.test(source);
1395
+ if (hasDirectExport) return;
1396
+ const exportedFiles = /* @__PURE__ */ new Set();
1397
+ const exportPattern = /export\s+(?:\{[^}]*\}|\*[^;]*)\s+from\s+["'](?<specifier>\.[^"']+)["']/g;
1398
+ for (const match of source.matchAll(exportPattern)) {
1399
+ const specifier = match.groups?.specifier;
1400
+ if (specifier) exportedFiles.add(path13.basename(specifier));
1401
+ }
1402
+ const missing = siblingModules.filter((entry) => {
1403
+ const stem = entry.replace(/\.(?:[cm]?tsx?|jsx?)$/, "");
1404
+ return !exportedFiles.has(stem);
1405
+ });
1406
+ if (missing.length === 0) return;
1407
+ context.report({
1408
+ node,
1409
+ message: `${folder}/index.ts must named-re-export every support file: ${missing.join(", ")}. See docs/code-organization-guide/rules/${folder === "constants" ? "constants" : "types-and-schemas"}-rule.md`
1410
+ });
1411
+ }
1412
+ };
1413
+ }
1414
+ };
1415
+
1416
+ // eslint/pasika/rules/import-through-index.ts
1417
+ import path14 from "path";
1418
+ var importThroughIndexRule = {
1419
+ meta: {
1420
+ schema: [],
1421
+ type: "problem",
1422
+ docs: {
1423
+ description: "Require imports of support files to use their support-folder index."
1424
+ }
1425
+ },
1426
+ create(context) {
1427
+ const filename = path14.resolve(context.filename);
1428
+ const sourceRoot2 = sourceRootOf(filename);
1429
+ return {
1430
+ Program(node) {
1431
+ for (const specifier of importSpecifiers(context.sourceCode.text)) {
1432
+ const target = resolveSpecifier(filename, specifier, sourceRoot2);
1433
+ if (!target) continue;
1434
+ const targetSegments = segmentsOf(target, sourceRoot2);
1435
+ const supportFolderIndex = targetSegments.findIndex(
1436
+ (segment) => ["constants", "types", "schemas"].includes(segment)
1437
+ );
1438
+ const supportFolder = supportFolderIndex >= 0 ? targetSegments[supportFolderIndex] : void 0;
1439
+ if (!supportFolder || path14.basename(target).startsWith("index.")) continue;
1440
+ const folderIndex = targetSegments.slice(0, supportFolderIndex + 1);
1441
+ const expected = `@/${folderIndex.join("/")}`;
1442
+ context.report({
1443
+ node,
1444
+ message: `Import support files through ${expected} instead of "${specifier}". See the code-organization-guide exports and imports rules.`
1445
+ });
1446
+ }
1447
+ }
1448
+ };
1449
+ }
1450
+ };
1451
+ function importSpecifiers(source) {
1452
+ const specifiers = [];
1453
+ const pattern = /\bimport\s+(?:(?:type\s+)?[\s\S]*?\sfrom\s+)?["'](?<specifier>[^"']+)["']/g;
1454
+ for (const match of source.matchAll(pattern)) {
1455
+ const specifier = match.groups?.specifier;
1456
+ if (specifier) specifiers.push(specifier);
1457
+ }
1458
+ return specifiers;
1459
+ }
1460
+ function sourceRootOf(filename) {
1461
+ const marker = `${path14.sep}src${path14.sep}`;
1462
+ const srcIndex = filename.lastIndexOf(marker);
1463
+ return srcIndex >= 0 ? filename.slice(0, srcIndex + marker.length - 1) : path14.resolve("src");
1464
+ }
1465
+
1466
+ // eslint/pasika/rules/util-file-name.ts
1467
+ import path15 from "path";
1468
+ function toKebabCase2(value) {
1469
+ return value.replace(/(?<lower>[a-z0-9])(?<upper>[A-Z])/g, "$<lower>-$<upper>").replace(/(?<first>[A-Z])(?<rest>[A-Z][a-z])/g, "$<first>-$<rest>").toLowerCase();
1470
+ }
1471
+ var utilFileNameRule = {
1472
+ meta: {
1473
+ schema: [],
1474
+ type: "problem",
1475
+ docs: {
1476
+ description: "Require single-function utility files to use the function's kebab-case name."
1477
+ }
1478
+ },
1479
+ create(context) {
1480
+ const filename = path15.resolve(context.filename);
1481
+ const segments = filename.replace(/\\/g, "/").split("/");
1482
+ if (!segments.includes("utils")) return {};
1483
+ let module;
1484
+ try {
1485
+ module = parseModule(filename);
1486
+ } catch {
1487
+ return {};
1488
+ }
1489
+ const functions = module.exports.filter((entry) => entry.kind === "function");
1490
+ if (functions.length !== 1 || module.exports.length !== 1) return {};
1491
+ const functionName = functions[0]?.name;
1492
+ if (!functionName) return {};
1493
+ const expected = toKebabCase2(functionName);
1494
+ const actual = path15.basename(filename, path15.extname(filename));
1495
+ if (!expected || actual === expected) return {};
1496
+ return {
1497
+ Program(node) {
1498
+ context.report({
1499
+ node,
1500
+ message: `A utility file exporting ${functionName} must be named ${expected}.${path15.extname(filename).slice(1)}.`
1501
+ });
1502
+ }
1503
+ };
1504
+ }
1505
+ };
1506
+
1507
+ // eslint/pasika/rules/no-util-barrel.ts
1508
+ import path16 from "path";
1509
+ var noUtilBarrelRule = {
1510
+ meta: {
1511
+ schema: [],
1512
+ type: "problem",
1513
+ docs: {
1514
+ description: "Require utilities to be imported directly without a barrel."
1515
+ }
1516
+ },
1517
+ create(context) {
1518
+ const filename = path16.resolve(context.filename);
1519
+ const sourceRoot2 = sourceRootOf2(filename);
1520
+ return {
1521
+ Program(node) {
1522
+ for (const specifier of importSpecifiers2(context.sourceCode.text)) {
1523
+ const target = resolveSpecifier(filename, specifier, sourceRoot2);
1524
+ if (!target) continue;
1525
+ const segments = target.replace(/\\/g, "/").split("/");
1526
+ const utilsIndex = segments.lastIndexOf("utils");
1527
+ if (utilsIndex < 0 || !path16.basename(target).startsWith("index.")) continue;
1528
+ context.report({
1529
+ node,
1530
+ message: `Import utilities directly instead of through "${specifier}". See docs/code-organization-guide/rules/utilities-rule.md`
1531
+ });
1532
+ }
1533
+ }
1534
+ };
1535
+ }
1536
+ };
1537
+ function importSpecifiers2(source) {
1538
+ const specifiers = [];
1539
+ const pattern = /\bimport\s+(?:(?:type\s+)?[\s\S]*?\sfrom\s+)?["'](?<specifier>[^"']+)["']/g;
1540
+ for (const match of source.matchAll(pattern)) {
1541
+ const specifier = match.groups?.specifier;
1542
+ if (specifier) specifiers.push(specifier);
1543
+ }
1544
+ return specifiers;
1545
+ }
1546
+ function sourceRootOf2(filename) {
1547
+ const marker = `${path16.sep}src${path16.sep}`;
1548
+ const srcIndex = filename.lastIndexOf(marker);
1549
+ return srcIndex >= 0 ? filename.slice(0, srcIndex + marker.length - 1) : path16.resolve("src");
1550
+ }
1551
+
1552
+ // eslint/pasika/rules/jsx-hygiene.ts
1553
+ import ts3 from "typescript";
1554
+ var ARITHMETIC_OPERATORS = /* @__PURE__ */ new Set(["+", "-", "*", "/", "%"]);
1555
+ function isCallExpression(node) {
1556
+ return node.type === "CallExpression";
1557
+ }
1558
+ function isMemberExpression(node) {
1559
+ return ["MemberExpression", "OptionalMemberExpression"].includes(node.type);
1560
+ }
1561
+ function logicalOperatorCount(node) {
1562
+ if (node.type !== "LogicalExpression") return 0;
1563
+ return 1 + logicalOperatorCount(node.left) + logicalOperatorCount(node.right);
1564
+ }
1565
+ function containsConditionalIn(node, sourceText) {
1566
+ const start = node.range?.[0] ?? 0;
1567
+ const end = node.range?.[1] ?? sourceText.length;
1568
+ const sourceFile = ts3.createSourceFile(
1569
+ "fragment.tsx",
1570
+ sourceText.slice(start, end),
1571
+ ts3.ScriptTarget.Latest,
1572
+ true,
1573
+ ts3.ScriptKind.TSX
1574
+ );
1575
+ let found = false;
1576
+ const visit = (child) => {
1577
+ if (found) return;
1578
+ if (ts3.isConditionalExpression(child)) {
1579
+ found = true;
1580
+ return;
1581
+ }
1582
+ ts3.forEachChild(child, visit);
1583
+ };
1584
+ visit(sourceFile);
1585
+ return found;
1586
+ }
1587
+ function hasNestedTernary(node, sourceText) {
1588
+ if (node.type !== "ConditionalExpression") return false;
1589
+ return containsConditionalIn(node.consequent, sourceText) || containsConditionalIn(node.alternate, sourceText);
1590
+ }
1591
+ function hasChainedCall(node) {
1592
+ if (!isCallExpression(node) || !isMemberExpression(node.callee)) return false;
1593
+ return isCallExpression(node.callee.object) || isMemberExpression(node.callee.object);
1594
+ }
1595
+ function hasExternalCall(node) {
1596
+ if (!isCallExpression(node)) return false;
1597
+ const callee = node.callee;
1598
+ if (callee.type === "Identifier" && callee.name === "cn") return false;
1599
+ if (isMemberExpression(callee)) return false;
1600
+ return callee.type === "Identifier";
1601
+ }
1602
+ function findViolation(node, sourceText) {
1603
+ if (node.type === "BinaryExpression" && ARITHMETIC_OPERATORS.has(node.operator)) {
1604
+ return "arithmetic";
1605
+ }
1606
+ if (hasChainedCall(node)) return "chained built-in method calls";
1607
+ if (hasExternalCall(node)) return "calls to functions declared outside the component";
1608
+ if (hasNestedTernary(node, sourceText)) return "nested ternaries";
1609
+ if (logicalOperatorCount(node) >= 2) return "conditions containing two or more logical operators";
1610
+ return void 0;
1611
+ }
1612
+ var jsxHygieneRule = {
1613
+ meta: {
1614
+ schema: [],
1615
+ type: "problem",
1616
+ docs: {
1617
+ description: "Require complex JSX expressions to be extracted before return."
1618
+ }
1619
+ },
1620
+ create(context) {
1621
+ if (!context.filename.endsWith(".tsx") && !context.filename.endsWith(".jsx")) return {};
1622
+ const sourceText = context.sourceCode.text;
1623
+ function checkExpression(node) {
1624
+ const violation = findViolation(node, sourceText);
1625
+ if (!violation) return;
1626
+ context.report({
1627
+ node,
1628
+ message: `Extract ${violation} from JSX before return. See docs/code-organization-guide/rules/jsx-hygiene-rule.md`
1629
+ });
1630
+ }
1631
+ return {
1632
+ JSXExpressionContainer(node) {
1633
+ const expression = node.expression;
1634
+ if (!expression || expression.type === "JSXEmptyExpression") return;
1635
+ checkExpression(expression);
1636
+ }
1637
+ };
1638
+ }
1639
+ };
1640
+
1641
+ // eslint/pasika/rules/interactive-component.ts
1642
+ var INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
1643
+ "a",
1644
+ "button",
1645
+ "details",
1646
+ "embed",
1647
+ "iframe",
1648
+ "input",
1649
+ "label",
1650
+ "select",
1651
+ "textarea",
1652
+ "video"
1653
+ ]);
1654
+ function tagName(node) {
1655
+ const name = node.openingElement?.name;
1656
+ if (name?.type !== "JSXIdentifier") return void 0;
1657
+ return name.name;
1658
+ }
1659
+ function isInteractive(node) {
1660
+ const name = tagName(node);
1661
+ return name !== void 0 && INTERACTIVE_TAGS.has(name);
1662
+ }
1663
+ function isComponentElement(node) {
1664
+ const name = tagName(node);
1665
+ return name !== void 0 && /^[A-Z]/.test(name);
1666
+ }
1667
+ function isComponentReturn(node) {
1668
+ const parent = node.parent;
1669
+ if (parent.type === "ReturnStatement") return true;
1670
+ return parent.type === "ArrowFunctionExpression" && parent.body === node;
1671
+ }
1672
+ var interactiveComponentRule = {
1673
+ meta: {
1674
+ schema: [],
1675
+ type: "problem",
1676
+ docs: {
1677
+ description: "Require interactive HTML elements mixed with other content to be extracted into components."
1678
+ }
1679
+ },
1680
+ create(context) {
1681
+ if (!context.filename.endsWith(".tsx") && !context.filename.endsWith(".jsx")) return {};
1682
+ return {
1683
+ JSXElement(node) {
1684
+ if (!isInteractive(node)) return;
1685
+ if (isComponentElement(node.parent)) {
1686
+ return;
1687
+ }
1688
+ if (isInteractive(node.parent)) {
1689
+ return;
1690
+ }
1691
+ if (isComponentReturn(node)) {
1692
+ return;
1693
+ }
1694
+ const name = tagName(node);
1695
+ if (name === void 0) return;
1696
+ context.report({
1697
+ node,
1698
+ message: `Extract the interactive <${name}> into a descriptive component. See docs/code-organization-guide/rules/interactive-component-rule.md`
1699
+ });
1700
+ }
1701
+ };
1702
+ }
1703
+ };
1704
+
1705
+ // eslint/pasika/rules/ui-state.ts
1706
+ var STATE_PROPS = /* @__PURE__ */ new Set([
1707
+ "disabled",
1708
+ "loading",
1709
+ "selected",
1710
+ "expanded",
1711
+ "pressed",
1712
+ "checked",
1713
+ "busy",
1714
+ "invalid",
1715
+ "open"
1716
+ ]);
1717
+ var STATE_ATTRIBUTES = /* @__PURE__ */ new Set([
1718
+ "disabled",
1719
+ "aria-busy",
1720
+ "aria-selected",
1721
+ "aria-expanded",
1722
+ "aria-pressed",
1723
+ "aria-checked",
1724
+ "aria-invalid",
1725
+ "open"
1726
+ ]);
1727
+ var STATE_CLASS_RE = /^(?:disabled|aria-(?:busy|selected|expanded|pressed|checked|invalid)|data-\[state)/;
1728
+ function isComponentElement2(node) {
1729
+ const name = node.openingElement?.name;
1730
+ return name?.type === "JSXIdentifier" && /^[A-Z]/.test(name.name);
1731
+ }
1732
+ function expressionName(node) {
1733
+ if (node?.type !== "JSXExpressionContainer") return void 0;
1734
+ if (node.expression?.type === "Identifier") return node.expression.name;
1735
+ return void 0;
1736
+ }
1737
+ var uiStateRule = {
1738
+ meta: {
1739
+ schema: [],
1740
+ type: "problem",
1741
+ docs: {
1742
+ description: "Require semantic UI states and Tailwind state variants."
1743
+ }
1744
+ },
1745
+ create(context) {
1746
+ return {
1747
+ JSXElement(node) {
1748
+ if (!isComponentElement2(node)) return;
1749
+ const attributes = node.openingElement?.attributes ?? [];
1750
+ const names = /* @__PURE__ */ new Set();
1751
+ for (const attribute of attributes) {
1752
+ names.add(String(attribute.name?.name));
1753
+ }
1754
+ for (const attribute of attributes) {
1755
+ const name = String(attribute.name?.name);
1756
+ const stateName = expressionName(attribute.value);
1757
+ if (!stateName || !STATE_PROPS.has(stateName)) continue;
1758
+ const hasNativeState = [...STATE_ATTRIBUTES].some((nativeName) => names.has(nativeName));
1759
+ if (!hasNativeState) {
1760
+ context.report({
1761
+ node: attribute,
1762
+ message: `UI state prop "${name}" should be expressed with a native or ARIA state attribute. See docs/styling-guide/rules/component-ui-state-rule.md`
1763
+ });
1764
+ }
1765
+ }
1766
+ for (const attribute of attributes) {
1767
+ if (String(attribute.name?.name) !== "className") continue;
1768
+ const value = attribute.value;
1769
+ const text = value?.type === "Literal" ? String(value.value) : "";
1770
+ if (value?.type === "JSXExpressionContainer" && value.expression.type === "ConditionalExpression") {
1771
+ const consequent = value.expression.consequent;
1772
+ const alternate = value.expression.alternate;
1773
+ const classes = [consequent, alternate].filter(
1774
+ (branch) => branch.type === "Literal" && typeof branch.value === "string"
1775
+ ).flatMap((branch) => String(branch.value).split(/\s+/));
1776
+ if (classes.length > 0 && !classes.some((className) => STATE_CLASS_RE.test(className))) {
1777
+ context.report({
1778
+ node: attribute,
1779
+ message: "Use a Tailwind state variant for a UI state instead of conditional replacement classes. See docs/styling-guide/rules/component-ui-state-rule.md"
1780
+ });
1781
+ }
1782
+ }
1783
+ if (text.split(/\s+/).some((className) => className.includes("state-"))) {
1784
+ return;
1785
+ }
1786
+ }
1787
+ }
1788
+ };
1789
+ }
1790
+ };
1791
+
1792
+ // eslint/pasika/rules/cva-appearance-props.ts
1793
+ var VISUAL_OPTION_NAMES = /* @__PURE__ */ new Set([
1794
+ "size",
1795
+ "variant",
1796
+ "tone",
1797
+ "color",
1798
+ "appearance",
1799
+ "intent",
1800
+ "style",
1801
+ "scheme",
1802
+ "theme"
1803
+ ]);
1804
+ function hasCvaCall(body) {
1805
+ for (const stmt of body) {
1806
+ if (stmt.type !== "VariableDeclaration") continue;
1807
+ for (const decl of stmt.declarations) {
1808
+ const init = decl.init;
1809
+ if (init?.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "cva") {
1810
+ return true;
1811
+ }
1812
+ }
1813
+ }
1814
+ return false;
1815
+ }
1816
+ var cvaAppearancePropsRule = {
1817
+ meta: {
1818
+ schema: [],
1819
+ type: "problem",
1820
+ docs: {
1821
+ description: "Components with visual option props must define them using cva."
1822
+ }
1823
+ },
1824
+ create(context) {
1825
+ const program = context.sourceCode.ast;
1826
+ const hasCva = hasCvaCall(program.body);
1827
+ if (hasCva) return {};
1828
+ return {
1829
+ TSTypeAliasDeclaration(node) {
1830
+ if (node.id?.type !== "Identifier" || !node.id.name?.endsWith("Props")) return;
1831
+ if (node.typeAnnotation?.type !== "TSTypeLiteral") return;
1832
+ for (const member of node.typeAnnotation.members ?? []) {
1833
+ if (member.type !== "TSPropertySignature") continue;
1834
+ const keyName = member.key?.type === "Identifier" ? member.key.name : void 0;
1835
+ if (!keyName || !VISUAL_OPTION_NAMES.has(keyName)) continue;
1836
+ const typeAnnotation = member.typeAnnotation?.typeAnnotation ?? member.typeAnnotation;
1837
+ if (!typeAnnotation) continue;
1838
+ if (typeAnnotation.type === "TSUnionType" || typeAnnotation.type === "TSTypeLiteral") {
1839
+ context.report({
1840
+ node,
1841
+ message: `Visual option prop "${keyName}" must be defined through a cva() call rather than a manual union type. See docs/styling-guide/rules/component-variant-rule.md`
1842
+ });
1843
+ }
1844
+ }
1845
+ }
1846
+ };
1847
+ }
1848
+ };
1849
+
1850
+ // eslint/pasika/rules/cva-boolean-variants.ts
1851
+ function extractCvaInfo(node) {
1852
+ if (node.callee.type !== "Identifier" || node.callee.name !== "cva") {
1853
+ return void 0;
1854
+ }
1855
+ const variantNames = /* @__PURE__ */ new Set();
1856
+ const compoundVariants = /* @__PURE__ */ new Set();
1857
+ const options = node.arguments[1];
1858
+ if (options?.type !== "ObjectExpression") return { variantNames, compoundVariants };
1859
+ for (const prop of options.properties) {
1860
+ if (prop.type !== "Property") continue;
1861
+ const keyName = prop.key.type === "Identifier" ? prop.key.name : void 0;
1862
+ if (keyName === "variants" && prop.value.type === "ObjectExpression") {
1863
+ for (const vProp of prop.value.properties) {
1864
+ if (vProp.type !== "Property") continue;
1865
+ const vName = vProp.key.type === "Identifier" ? vProp.key.name : void 0;
1866
+ if (vName) variantNames.add(vName);
1867
+ }
1868
+ }
1869
+ if (keyName === "compoundVariants" && prop.value.type === "ArrayExpression") {
1870
+ for (const entry of prop.value.elements) {
1871
+ if (entry?.type !== "ObjectExpression") continue;
1872
+ for (const cvProp of entry.properties) {
1873
+ if (cvProp.type !== "Property") continue;
1874
+ const cvName = cvProp.key.type === "Identifier" ? cvProp.key.name : void 0;
1875
+ if (cvName && variantNames.has(cvName)) {
1876
+ compoundVariants.add(cvName);
1877
+ }
1878
+ }
1879
+ }
1880
+ }
1881
+ }
1882
+ return { variantNames, compoundVariants };
1883
+ }
1884
+ var cvaBooleanVariantsRule = {
1885
+ meta: {
1886
+ schema: [],
1887
+ type: "problem",
1888
+ docs: {
1889
+ description: "Enforce correct boolean variant placement between CVA and cn()."
1890
+ }
1891
+ },
1892
+ create(context) {
1893
+ const cvaInfos = [];
1894
+ return {
1895
+ // Collect all cva() calls in the file
1896
+ CallExpression(node) {
1897
+ const info = extractCvaInfo(node);
1898
+ if (info) cvaInfos.push(info);
1899
+ },
1900
+ // Detect boolean && "class" patterns anywhere inside a className attribute
1901
+ "JSXAttribute[name.name='className'] LogicalExpression[operator='&&']"(node) {
1902
+ if (cvaInfos.length === 0) return;
1903
+ if (node.type !== "LogicalExpression") return;
1904
+ const left = node.left;
1905
+ const right = node.right;
1906
+ if (right.type !== "Literal" || typeof right.value !== "string") {
1907
+ return;
1908
+ }
1909
+ if (left.type !== "Identifier") return;
1910
+ const propName = left.name;
1911
+ for (const info of cvaInfos) {
1912
+ if (info.variantNames.has(propName)) {
1913
+ context.report({
1914
+ node,
1915
+ message: `Boolean prop "${propName}" is used as a standalone conditional in cn() but is also defined as a CVA variant. Move it to one place: either CVA (with both true/false treatments) or cn() (standalone). See docs/styling-guide/rules/component-variant-rule.md`
1916
+ });
1917
+ }
1918
+ }
1919
+ }
1920
+ };
1921
+ }
1922
+ };
1923
+
1924
+ // eslint/pasika/rules/cross-feature-import.ts
1925
+ import path17 from "path";
1926
+ var FEATURES_SEGMENT = "features";
1927
+ function featureNameOf(resolvedPath, sourceRoot2) {
1928
+ const relative = path17.relative(sourceRoot2, resolvedPath);
1929
+ if (relative.startsWith("..")) return void 0;
1930
+ const segments = relative.split(path17.sep);
1931
+ if (segments[0] !== FEATURES_SEGMENT || segments.length < 2) return void 0;
1932
+ return segments[1];
1933
+ }
1934
+ var crossFeatureImportRule = {
1935
+ meta: {
1936
+ schema: [],
1937
+ type: "problem",
1938
+ docs: {
1939
+ description: "Require non-composition files that import from two or more feature folders to live in src/compositions/."
1940
+ }
1941
+ },
1942
+ create(context) {
1943
+ const filename = context.filename;
1944
+ if (!filename.endsWith(".tsx") && !filename.endsWith(".jsx")) return {};
1945
+ const sourceRoot2 = path17.resolve("src");
1946
+ const fileRelative = path17.relative(sourceRoot2, filename);
1947
+ if (fileRelative.startsWith("..")) return {};
1948
+ const fileSegments = fileRelative.split(path17.sep);
1949
+ const isInCompositions = fileSegments[0] === "compositions";
1950
+ const isInApp = fileSegments[0] === "app";
1951
+ const isConfig = fileSegments[0] === "config";
1952
+ if (isInCompositions || isInApp || isConfig) return {};
1953
+ const importedFeatures = /* @__PURE__ */ new Set();
1954
+ let alreadyReported = false;
1955
+ return {
1956
+ ImportDeclaration(node) {
1957
+ if (alreadyReported) return;
1958
+ const source = node.source;
1959
+ if (typeof source.value !== "string") return;
1960
+ let resolved;
1961
+ if (source.value.startsWith("@/")) {
1962
+ resolved = path17.resolve(sourceRoot2, source.value.slice(2));
1963
+ } else if (source.value.startsWith(".")) {
1964
+ resolved = path17.resolve(path17.dirname(filename), source.value);
1965
+ }
1966
+ if (!resolved) return;
1967
+ const feature = featureNameOf(resolved, sourceRoot2);
1968
+ if (feature) importedFeatures.add(feature);
1969
+ if (importedFeatures.size >= 2) {
1970
+ alreadyReported = true;
1971
+ const names = [...importedFeatures].sort().join(", ");
1972
+ context.report({
1973
+ node,
1974
+ message: `This component imports from two or more feature folders (${names}) and must live in src/compositions/. See docs/code-organization-guide/rules/component-placement-rule.md`
1975
+ });
1976
+ }
1977
+ }
1978
+ };
1979
+ }
1980
+ };
1981
+
1982
+ // eslint/pasika/rules/pure-function-extract.ts
1983
+ import path18 from "path";
1984
+ function isComponentLikeName(name) {
1985
+ return /^[A-Z]/.test(name);
1986
+ }
1987
+ function isHookName2(name) {
1988
+ return /^use[A-Z]/.test(name);
1989
+ }
1990
+ function hasHookUsage(body) {
1991
+ for (const stmt of body.body) {
1992
+ if (stmt.type !== "ExpressionStatement") continue;
1993
+ const expr = stmt.expression;
1994
+ if (expr.type === "CallExpression" && expr.callee.type === "Identifier" && isHookName2(expr.callee.name)) {
1995
+ return true;
1996
+ }
1997
+ }
1998
+ return false;
1999
+ }
2000
+ var pureFunctionExtractRule = {
2001
+ meta: {
2002
+ schema: [],
2003
+ type: "problem",
2004
+ docs: {
2005
+ description: "Require pure functions declared in component files to be extracted to utils/."
2006
+ }
2007
+ },
2008
+ create(context) {
2009
+ const filename = context.filename;
2010
+ if (!filename.endsWith(".tsx") && !filename.endsWith(".jsx")) return {};
2011
+ const sourceRoot2 = path18.resolve("src");
2012
+ const relative = path18.relative(sourceRoot2, filename);
2013
+ if (relative.startsWith("..")) return {};
2014
+ const segments = relative.split(path18.sep);
2015
+ if (segments[0] === "utils") return {};
2016
+ if (segments[0] === "app") return {};
2017
+ const supportFolders = /* @__PURE__ */ new Set(["hooks", "types", "schemas", "constants", "utils"]);
2018
+ if (segments.length >= 2 && supportFolders.has(segments[segments.length - 1] ?? "")) return {};
2019
+ function report3(node, name) {
2020
+ context.report({
2021
+ node,
2022
+ message: `Extract pure function "${name}" to utils/. See docs/code-organization-guide/rules/utilities-rule.md`
2023
+ });
2024
+ }
2025
+ return {
2026
+ FunctionDeclaration(node) {
2027
+ const exported = node.parent?.type === "ExportNamedDeclaration";
2028
+ if (!exported) return;
2029
+ const name = node.id?.name;
2030
+ if (!name) return;
2031
+ if (isComponentLikeName(name) || isHookName2(name)) return;
2032
+ if (!node.body || hasHookUsage(node.body)) return;
2033
+ report3(node, name);
2034
+ },
2035
+ VariableDeclarator(node) {
2036
+ if (node.id.type !== "Identifier") return;
2037
+ const name = node.id.name;
2038
+ if (!name) return;
2039
+ const exported = node.parent.parent?.type === "ExportNamedDeclaration";
2040
+ if (!exported) return;
2041
+ if (isComponentLikeName(name) || isHookName2(name)) return;
2042
+ const init = node.init;
2043
+ if (!init || init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression") {
2044
+ return;
2045
+ }
2046
+ if (init.body.type === "BlockStatement" && hasHookUsage(init.body)) return;
2047
+ report3(node, name);
2048
+ }
2049
+ };
2050
+ }
2051
+ };
2052
+
2053
+ // eslint/pasika/rules/hook-complexity.ts
2054
+ import path19 from "path";
2055
+ import ts4 from "typescript";
2056
+ var REACT_HOOKS = /* @__PURE__ */ new Set([
2057
+ "useState",
2058
+ "useEffect",
2059
+ "useContext",
2060
+ "useReducer",
2061
+ "useCallback",
2062
+ "useMemo",
2063
+ "useRef",
2064
+ "useLayoutEffect",
2065
+ "useImperativeHandle",
2066
+ "useDebugValue",
2067
+ "useDeferredValue",
2068
+ "useTransition",
2069
+ "useId",
2070
+ "useSyncExternalStore",
2071
+ "useInsertionEffect"
2072
+ ]);
2073
+ function isHookName3(name) {
2074
+ return /^use[A-Z]/.test(name);
2075
+ }
2076
+ function countImperativeCategories(body, sourceText) {
2077
+ const start = body.range?.[0] ?? 0;
2078
+ const end = body.range?.[1] ?? sourceText.length;
2079
+ const sourceFile = ts4.createSourceFile(
2080
+ "hook.ts",
2081
+ sourceText.slice(start, end),
2082
+ ts4.ScriptTarget.Latest,
2083
+ true,
2084
+ ts4.ScriptKind.TS
2085
+ );
2086
+ const categories = /* @__PURE__ */ new Set();
2087
+ const visit = (node) => {
2088
+ if (ts4.isCallExpression(node) && ts4.isIdentifier(node.expression) && REACT_HOOKS.has(node.expression.text)) {
2089
+ categories.add(node.expression.text);
2090
+ }
2091
+ ts4.forEachChild(node, visit);
2092
+ };
2093
+ visit(sourceFile);
2094
+ return categories.size;
2095
+ }
2096
+ var hookComplexityRule = {
2097
+ meta: {
2098
+ schema: [],
2099
+ type: "problem",
2100
+ docs: {
2101
+ description: "Require extraction of complex single-consumer hooks and inline of simple ones."
2102
+ }
2103
+ },
2104
+ create(context) {
2105
+ const filename = context.filename;
2106
+ const sourceRoot2 = path19.resolve("src");
2107
+ const relative = path19.relative(sourceRoot2, filename);
2108
+ if (relative.startsWith("..")) return {};
2109
+ const segments = relative.split(path19.sep);
2110
+ const sourceText = context.sourceCode.text;
2111
+ function checkHook(node, name, body, exported) {
2112
+ if (!exported) return;
2113
+ if (!name || !isHookName3(name)) return;
2114
+ if (!body) return;
2115
+ const imperativeCount = countImperativeCategories(body, sourceText);
2116
+ const parentFolder = segments.length >= 2 ? segments[segments.length - 2] : void 0;
2117
+ const inSupportFolder = parentFolder === "hooks";
2118
+ if (imperativeCount >= 2 && !inSupportFolder) {
2119
+ context.report({
2120
+ node,
2121
+ message: `Hook "${name}" has ${String(imperativeCount)} imperative categories and must be extracted to a hooks/ folder. See docs/code-organization-guide/rules/hook-extraction-rule.md`
2122
+ });
2123
+ } else if (imperativeCount < 2 && inSupportFolder) {
2124
+ context.report({
2125
+ node,
2126
+ message: `Hook "${name}" has fewer than two imperative categories and must stay inline in its consumer file. See docs/code-organization-guide/rules/hook-extraction-rule.md`
2127
+ });
2128
+ }
2129
+ }
2130
+ return {
2131
+ FunctionDeclaration(node) {
2132
+ const exported = node.parent?.type === "ExportNamedDeclaration";
2133
+ checkHook(node, node.id?.name, node.body, exported);
2134
+ },
2135
+ VariableDeclarator(node) {
2136
+ if (node.id.type !== "Identifier") return;
2137
+ const exported = node.parent.parent?.type === "ExportNamedDeclaration";
2138
+ if (!exported) return;
2139
+ const init = node.init;
2140
+ if (!init || init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression") {
2141
+ return;
2142
+ }
2143
+ if (init.body.type !== "BlockStatement") return;
2144
+ checkHook(node, node.id.name, init.body, true);
2145
+ }
2146
+ };
2147
+ }
2148
+ };
2149
+
2150
+ // eslint/pasika/rules/locale-dotted-path.ts
2151
+ import path20 from "path";
2152
+ function isInLocalesDir(filename) {
2153
+ const segments = path20.resolve(filename).split(path20.sep);
2154
+ const srcIdx = segments.lastIndexOf("src");
2155
+ return srcIdx !== -1 && segments[srcIdx + 1] === "locales";
2156
+ }
2157
+ function isLocalesAccess(node) {
2158
+ if (node.type === "Identifier" && node.name === "locales") {
2159
+ return true;
2160
+ }
2161
+ if (node.type === "MemberExpression" && node.object.type === "Identifier" && node.object.name === "locales" && node.property.type === "Identifier") {
2162
+ return true;
2163
+ }
2164
+ return false;
2165
+ }
2166
+ var localeDottedPathRule = {
2167
+ meta: {
2168
+ schema: [],
2169
+ type: "problem",
2170
+ docs: {
2171
+ description: "Require reading namespaced locales through their full dotted path."
2172
+ }
2173
+ },
2174
+ create(context) {
2175
+ if (isInLocalesDir(context.filename)) return {};
2176
+ return {
2177
+ VariableDeclarator(node) {
2178
+ if (node.id.type !== "ObjectPattern") return;
2179
+ const init = node.init;
2180
+ if (!init || !isLocalesAccess(init)) return;
2181
+ for (const prop of node.id.properties) {
2182
+ if (prop.type !== "Property") continue;
2183
+ let valueName;
2184
+ if (prop.value.type === "Identifier") {
2185
+ valueName = prop.value.name;
2186
+ } else if (prop.key.type === "Identifier") {
2187
+ valueName = prop.key.name;
2188
+ }
2189
+ if (valueName) {
2190
+ context.report({
2191
+ node,
2192
+ message: `Destructuring locales.${valueName} loses the namespace; read through the full dotted path instead.`
2193
+ });
2194
+ }
2195
+ }
2196
+ }
2197
+ };
2198
+ }
2199
+ };
2200
+
2201
+ // eslint/pasika/rules/locales-location.ts
2202
+ import path21 from "path";
2203
+ function isLocalesFile(filename) {
2204
+ const segments = path21.resolve(filename).split(path21.sep);
2205
+ const srcIdx = segments.lastIndexOf("src");
2206
+ return srcIdx !== -1 && segments[srcIdx + 1] === "locales";
2207
+ }
2208
+ var LOCALE_NAME_RE = /^[a-z][a-zA-Z0-9]*$/;
2209
+ function looksLikeLocaleKey(name) {
2210
+ return LOCALE_NAME_RE.test(name);
2211
+ }
2212
+ var localesLocationRule = {
2213
+ meta: {
2214
+ schema: [],
2215
+ type: "problem",
2216
+ docs: {
2217
+ description: "Require all locales to live in the named locales object."
2218
+ }
2219
+ },
2220
+ create(context) {
2221
+ if (isLocalesFile(context.filename)) return {};
2222
+ const filename = context.filename;
2223
+ const segments = path21.resolve(filename).split(path21.sep);
2224
+ const srcIdx = segments.lastIndexOf("src");
2225
+ if (srcIdx === -1) return {};
2226
+ const folder = segments[srcIdx + 1];
2227
+ if (folder === "app" || folder === "config") return {};
2228
+ return {
2229
+ VariableDeclarator(node) {
2230
+ if (node.id.type === "Identifier" && node.init?.type === "ObjectExpression" && node.init.properties.length > 0 && looksLikeLocaleKey(node.id.name)) {
2231
+ const hasStringValues = node.init.properties.some((p) => p.type === "Property" && p.value.type === "Literal");
2232
+ if (hasStringValues) {
2233
+ context.report({
2234
+ node,
2235
+ message: "User-facing strings must live in src/locales/, not inline in component files."
2236
+ });
2237
+ }
2238
+ }
2239
+ }
2240
+ };
2241
+ }
2242
+ };
2243
+
2244
+ // eslint/pasika/rules/hook-extraction.ts
2245
+ import path22 from "path";
2246
+ var hookExtractionRule = {
2247
+ meta: {
2248
+ schema: [],
2249
+ type: "problem",
2250
+ docs: {
2251
+ description: "Require a custom hook with two or more consumers to live in its own hooks/ file."
2252
+ }
2253
+ },
2254
+ create(context) {
2255
+ const sourceRoot2 = path22.resolve("src");
2256
+ const file = path22.resolve(context.filename);
2257
+ const segments = segmentsOf(file, sourceRoot2);
2258
+ if (segments.length === 0) return {};
2259
+ const index = getProjectIndex(sourceRoot2);
2260
+ if (!index) return {};
2261
+ const module = index.modules.get(file);
2262
+ if (!module) return {};
2263
+ const folder = folderSegmentsOf(file, sourceRoot2);
2264
+ const alreadyInHooksFolder = folder.length > 0 && folder[folder.length - 1] === "hooks";
2265
+ const reusedHooks = module.exports.filter((exp) => exp.kind === "hook" && !alreadyInHooksFolder).map((exp) => ({ exp, consumers: index.symbolConsumers.get(symbolKey(file, exp.name)) })).filter(({ consumers }) => (consumers?.size ?? 0) >= 2);
2266
+ if (reusedHooks.length === 0) return {};
2267
+ return {
2268
+ Program(node) {
2269
+ for (const { exp, consumers } of reusedHooks) {
2270
+ context.report({
2271
+ node,
2272
+ loc: { line: exp.line, column: 0 },
2273
+ message: `Hook "${exp.name}" has ${String(consumers?.size ?? 0)} consumers; extract it to its own file in a hooks/ folder. See docs/code-organization-guide/rules/hook-extraction-rule.md`
2274
+ });
2275
+ }
2276
+ }
2277
+ };
2278
+ }
2279
+ };
2280
+
2281
+ // eslint/pasika/rules/value-extraction.ts
2282
+ import path23 from "path";
2283
+ var valueExtractionRule = {
2284
+ meta: {
2285
+ schema: [],
2286
+ type: "problem",
2287
+ docs: {
2288
+ description: "Require a value imported independently to be extracted as a constant."
2289
+ }
2290
+ },
2291
+ create(context) {
2292
+ const sourceRoot2 = path23.resolve("src");
2293
+ const file = path23.resolve(context.filename);
2294
+ const segments = segmentsOf(file, sourceRoot2);
2295
+ if (segments.length === 0 || segments[0] !== "app") return {};
2296
+ const index = getProjectIndex(sourceRoot2);
2297
+ if (!index) return {};
2298
+ const consumers = [...index.consumers.get(file) ?? []];
2299
+ const outsideApp = consumers.filter((consumer) => segmentsOf(consumer, sourceRoot2)[0] !== "app");
2300
+ if (outsideApp.length === 0) return {};
2301
+ return {
2302
+ Program(node) {
2303
+ context.report({
2304
+ node,
2305
+ loc: { line: 1, column: 0 },
2306
+ message: `This file in src/app/ is imported by ${describeConsumers(outsideApp, sourceRoot2)} outside src/app/. Extract the value to a shared or feature folder so it can be imported independently. See docs/code-organization-guide/rules/constants-rule.md`
2307
+ });
2308
+ }
2309
+ };
2310
+ }
2311
+ };
2312
+
2313
+ // eslint/pasika/rules/config-extraction.ts
2314
+ import path24 from "path";
2315
+ var configExtractionRule = {
2316
+ meta: {
2317
+ schema: [],
2318
+ type: "problem",
2319
+ docs: {
2320
+ description: "Require config-only types and schemas to be extracted into the module's support folder."
2321
+ }
2322
+ },
2323
+ create(context) {
2324
+ const sourceRoot2 = path24.resolve("src");
2325
+ const file = path24.resolve(context.filename);
2326
+ const segments = segmentsOf(file, sourceRoot2);
2327
+ if (segments.length < 3 || segments[0] !== "config") return {};
2328
+ if (SUPPORT_FOLDERS2.has(segments[2] ?? "")) return {};
2329
+ const index = getProjectIndex(sourceRoot2);
2330
+ if (!index) return {};
2331
+ const module = index.modules.get(file);
2332
+ if (!module) return {};
2333
+ const suspects = module.exports.filter((exp) => exp.kind === "type" || exp.kind === "schema");
2334
+ if (suspects.length === 0) return {};
2335
+ const findings = [];
2336
+ for (const exp of suspects) {
2337
+ const consumers = [...index.symbolConsumers.get(symbolKey(file, exp.name)) ?? []];
2338
+ const outsideConfig = consumers.filter((consumer) => segmentsOf(consumer, sourceRoot2)[0] !== "config");
2339
+ if (outsideConfig.length > 0) {
2340
+ findings.push({
2341
+ line: exp.line,
2342
+ message: `${exp.kind} "${exp.name}" in a configuration module is imported by ${describeConsumers(outsideConfig, sourceRoot2)} outside src/config/. Move it to the matching root support folder. See docs/code-organization-guide/rules/configuration-rule.md`
2343
+ });
2344
+ } else if (consumers.length > 0) {
2345
+ findings.push({
2346
+ line: exp.line,
2347
+ message: `${exp.kind} "${exp.name}" is used only to implement this configuration module; extract it to the module's ${exp.kind === "schema" ? "schemas/" : "types/"} folder even with one consumer. See docs/code-organization-guide/rules/configuration-rule.md`
2348
+ });
2349
+ }
2350
+ }
2351
+ if (findings.length === 0) return {};
2352
+ return {
2353
+ Program(node) {
2354
+ for (const finding of findings) {
2355
+ context.report({ node, loc: { line: finding.line, column: 0 }, message: finding.message });
2356
+ }
2357
+ }
2358
+ };
2359
+ }
2360
+ };
2361
+
2362
+ // eslint/pasika/rules/component-nesting.ts
2363
+ import path25 from "path";
2364
+ var componentNestingRule = {
2365
+ meta: {
2366
+ schema: [],
2367
+ type: "problem",
2368
+ docs: {
2369
+ description: "Require a nested component to have exclusive child components, not only support files."
2370
+ }
2371
+ },
2372
+ create(context) {
2373
+ const sourceRoot2 = path25.resolve("src");
2374
+ const file = path25.resolve(context.filename);
2375
+ const segments = segmentsOf(file, sourceRoot2);
2376
+ if (segments.length !== 4 || segments[0] !== "features") return {};
2377
+ const index = getProjectIndex(sourceRoot2);
2378
+ if (!index) return {};
2379
+ const module = index.modules.get(file);
2380
+ if (!module?.exports.some((exp) => exp.kind === "component")) return {};
2381
+ const folder = segments.slice(0, 3);
2382
+ const hasChildComponent = [...index.modules.values()].some((candidate) => {
2383
+ if (candidate.file === file) return false;
2384
+ const candidateSegments = segmentsOf(candidate.file, sourceRoot2);
2385
+ if (candidateSegments.length !== 4) return false;
2386
+ if (candidateSegments[0] !== folder[0] || candidateSegments[1] !== folder[1] || candidateSegments[2] !== folder[2]) {
2387
+ return false;
2388
+ }
2389
+ if (candidateSegments[3]?.startsWith("index.")) return false;
2390
+ return candidate.exports.some((exp) => exp.kind === "component");
2391
+ });
2392
+ if (hasChildComponent) return {};
2393
+ const flatFolder = segments.slice(0, 2).join("/");
2394
+ return {
2395
+ Program(node) {
2396
+ context.report({
2397
+ node,
2398
+ loc: { line: 1, column: 0 },
2399
+ message: `This component is nested in src/${folder.join("/")}/ but its folder has no exclusive child components \u2014 only support files. Flatten it back into src/${flatFolder}/. See docs/code-organization-guide/rules/folder-nesting-rule.md`
2400
+ });
2401
+ }
2402
+ };
2403
+ }
2404
+ };
2405
+
2406
+ // eslint/pasika/rules/stay-flat.ts
2407
+ import path26 from "path";
2408
+ var stayFlatRule = {
2409
+ meta: {
2410
+ schema: [],
2411
+ type: "problem",
2412
+ docs: {
2413
+ description: "Require a component with exclusive children to be nested in a folder with the same name."
2414
+ }
2415
+ },
2416
+ create(context) {
2417
+ const sourceRoot2 = path26.resolve("src");
2418
+ const file = path26.resolve(context.filename);
2419
+ const segments = segmentsOf(file, sourceRoot2);
2420
+ if (segments.length !== 3 || segments[0] !== "features") return {};
2421
+ const index = getProjectIndex(sourceRoot2);
2422
+ if (!index) return {};
2423
+ const module = index.modules.get(file);
2424
+ if (!module?.exports.some((exp) => exp.kind === "component")) return {};
2425
+ const featureName = segments[1];
2426
+ if (!featureName) return {};
2427
+ const exclusiveChildren = [...index.modules.values()].filter((candidate) => {
2428
+ if (candidate.file === file) return false;
2429
+ const candidateSegments = segmentsOf(candidate.file, sourceRoot2);
2430
+ if (candidateSegments.length !== 3) return false;
2431
+ if (candidateSegments[0] !== "features" || candidateSegments[1] !== featureName) return false;
2432
+ if (candidateSegments[2]?.startsWith("index.")) return false;
2433
+ if (!candidate.exports.some((exp) => exp.kind === "component")) return false;
2434
+ const consumers = index.consumers.get(candidate.file) ?? /* @__PURE__ */ new Set();
2435
+ if (consumers.size === 0) return false;
2436
+ return [...consumers].every((consumer) => {
2437
+ if (consumer === file) return true;
2438
+ const consumerSegments = segmentsOf(consumer, sourceRoot2);
2439
+ if (consumerSegments[0] !== "features" || consumerSegments[1] !== featureName) return false;
2440
+ return !(index.modules.get(consumer)?.exports.some((exp) => exp.kind === "component") ?? false);
2441
+ });
2442
+ });
2443
+ if (exclusiveChildren.length === 0) return {};
2444
+ const childNames = exclusiveChildren.map((child) => child.exports.find((exp) => exp.kind === "component")?.name).filter((name) => name !== void 0);
2445
+ return {
2446
+ Program(node) {
2447
+ context.report({
2448
+ node,
2449
+ loc: { line: 1, column: 0 },
2450
+ message: `This component has exclusive child component(s) ${childNames.join(", ")}; nest it in a folder named after it inside src/features/${featureName}/. See docs/code-organization-guide/rules/folder-nesting-rule.md`
2451
+ });
2452
+ }
2453
+ };
2454
+ }
2455
+ };
2456
+
2457
+ // eslint/pasika/rules/type-extraction.ts
2458
+ import path27 from "path";
2459
+ var typeExtractionRule = {
2460
+ meta: {
2461
+ schema: [],
2462
+ type: "problem",
2463
+ docs: {
2464
+ description: "Require a type or schema imported without its defining code to be extracted."
2465
+ }
2466
+ },
2467
+ create(context) {
2468
+ const sourceRoot2 = path27.resolve("src");
2469
+ const file = path27.resolve(context.filename);
2470
+ const segments = segmentsOf(file, sourceRoot2);
2471
+ if (segments.length === 0) return {};
2472
+ const index = getProjectIndex(sourceRoot2);
2473
+ if (!index) return {};
2474
+ const module = index.modules.get(file);
2475
+ if (!module) return {};
2476
+ const componentExport = module.exports.find((exp) => exp.kind === "component");
2477
+ const codeExports = module.exports.filter((exp) => exp.kind !== "type" && exp.kind !== "other");
2478
+ const findings = [];
2479
+ for (const exp of module.exports) {
2480
+ if (exp.kind !== "type" && exp.kind !== "schema") continue;
2481
+ const consumers = [...index.symbolConsumers.get(symbolKey(file, exp.name)) ?? []];
2482
+ if (consumers.length === 0) continue;
2483
+ if (componentExport) {
2484
+ const componentConsumers = index.symbolConsumers.get(symbolKey(file, componentExport.name)) ?? /* @__PURE__ */ new Set();
2485
+ const independent2 = consumers.filter((consumer) => !componentConsumers.has(consumer));
2486
+ if (independent2.length === 0) continue;
2487
+ findings.push({
2488
+ line: exp.line,
2489
+ message: `${exp.kind} "${exp.name}" is imported by ${describeConsumers(independent2, sourceRoot2)} without the component "${componentExport.name}" that defines it. Extract it to a types/ or schemas/ folder. See docs/code-organization-guide/rules/types-and-schemas-rule.md`
2490
+ });
2491
+ continue;
2492
+ }
2493
+ if (codeExports.length === 0) continue;
2494
+ const independent = consumers.filter(
2495
+ (consumer) => !codeExports.some((other) => index.symbolConsumers.get(symbolKey(file, other.name))?.has(consumer))
2496
+ );
2497
+ if (independent.length === 0) continue;
2498
+ findings.push({
2499
+ line: exp.line,
2500
+ message: `${exp.kind} "${exp.name}" is imported by ${describeConsumers(independent, sourceRoot2)} without using the code in this file. Extract it to a types/ or schemas/ folder. See docs/code-organization-guide/rules/types-and-schemas-rule.md`
2501
+ });
2502
+ }
2503
+ if (findings.length === 0) return {};
2504
+ return {
2505
+ Program(node) {
2506
+ for (const finding of findings) {
2507
+ context.report({ node, loc: { line: finding.line, column: 0 }, message: finding.message });
2508
+ }
2509
+ }
2510
+ };
2511
+ }
2512
+ };
2513
+
2514
+ // eslint/pasika/rules/locale-placement.ts
2515
+ import path28 from "path";
2516
+ import { readFileSync as readFileSync2 } from "fs";
2517
+ import ts5 from "typescript";
2518
+ var LOCALE_ACCESS = /\blocales\.(?<key>[A-Za-z_$][\w$]*)/g;
2519
+ var camelCase = (name) => name.replace(/-[a-z]/g, (match) => match.slice(1).toUpperCase());
2520
+ var FORCED_TOP_LEVEL = /* @__PURE__ */ new Set(["app", "shared", "compositions", "config"]);
2521
+ function forcesTopLevel(segments) {
2522
+ return FORCED_TOP_LEVEL.has(segments[0] ?? "") || SUPPORT_FOLDERS2.has(segments[0] ?? "");
2523
+ }
2524
+ function localePlacement(text) {
2525
+ const sourceFile = ts5.createSourceFile("locales.ts", text, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
2526
+ for (const statement of sourceFile.statements) {
2527
+ if (!ts5.isVariableStatement(statement)) continue;
2528
+ const isExported2 = (ts5.getModifiers(statement) ?? []).some(
2529
+ (modifier) => modifier.kind === ts5.SyntaxKind.ExportKeyword
2530
+ );
2531
+ if (!isExported2) continue;
2532
+ for (const declaration of statement.declarationList.declarations) {
2533
+ if (!ts5.isIdentifier(declaration.name) || declaration.name.text !== "locales") continue;
2534
+ if (!declaration.initializer || !ts5.isObjectLiteralExpression(declaration.initializer)) continue;
2535
+ const placement = /* @__PURE__ */ new Map();
2536
+ for (const property of declaration.initializer.properties) {
2537
+ if (!ts5.isPropertyAssignment(property)) continue;
2538
+ let name;
2539
+ if (ts5.isIdentifier(property.name)) name = property.name.text;
2540
+ else if (ts5.isStringLiteral(property.name)) name = property.name.text;
2541
+ if (name === void 0) continue;
2542
+ const line = sourceFile.getLineAndCharacterOfPosition(property.getStart(sourceFile)).line + 1;
2543
+ placement.set(name, {
2544
+ kind: ts5.isObjectLiteralExpression(property.initializer) ? "nested" : "top",
2545
+ line
2546
+ });
2547
+ }
2548
+ return placement;
2549
+ }
2550
+ }
2551
+ return void 0;
2552
+ }
2553
+ var localePlacementRule = {
2554
+ meta: {
2555
+ schema: [],
2556
+ type: "problem",
2557
+ docs: {
2558
+ description: "Require shared locales at the top level and single-feature locales in a namespaced object."
2559
+ }
2560
+ },
2561
+ create(context) {
2562
+ const sourceRoot2 = path28.resolve("src");
2563
+ const file = path28.resolve(context.filename);
2564
+ const segments = segmentsOf(file, sourceRoot2);
2565
+ if (segments.length === 0) return {};
2566
+ const index = getProjectIndex(sourceRoot2);
2567
+ if (!index) return {};
2568
+ const localesFile = [...index.modules.keys()].find((candidate) => {
2569
+ const candidateSegments = segmentsOf(candidate, sourceRoot2);
2570
+ return candidateSegments.length === 2 && candidateSegments[0] === "locales" && candidateSegments[1]?.startsWith("index.");
2571
+ });
2572
+ if (!localesFile || file !== localesFile) return {};
2573
+ const placement = localePlacement(readFileSync2(localesFile, "utf8"));
2574
+ if (!placement) return {};
2575
+ const keyReaders = /* @__PURE__ */ new Map();
2576
+ const keyFeatures = /* @__PURE__ */ new Map();
2577
+ const keyForcedTopLevel = /* @__PURE__ */ new Set();
2578
+ for (const [candidateFile, module] of index.modules) {
2579
+ if (candidateFile === localesFile) continue;
2580
+ const importsLocales = module.imports.some(
2581
+ (moduleImport) => resolveSpecifier(candidateFile, moduleImport.specifier, sourceRoot2) === localesFile
2582
+ );
2583
+ if (!importsLocales) continue;
2584
+ const candidateSegments = segmentsOf(candidateFile, sourceRoot2);
2585
+ for (const match of readFileSync2(candidateFile, "utf8").matchAll(LOCALE_ACCESS)) {
2586
+ const key = match.groups?.key;
2587
+ if (key === void 0) continue;
2588
+ const readers = keyReaders.get(key) ?? /* @__PURE__ */ new Set();
2589
+ readers.add(candidateFile);
2590
+ keyReaders.set(key, readers);
2591
+ if (candidateSegments[0] === "features" && candidateSegments.length >= 2) {
2592
+ const features = keyFeatures.get(key) ?? /* @__PURE__ */ new Set();
2593
+ features.add(candidateSegments[1] ?? "");
2594
+ keyFeatures.set(key, features);
2595
+ } else if (forcesTopLevel(candidateSegments)) {
2596
+ keyForcedTopLevel.add(key);
2597
+ }
2598
+ }
2599
+ }
2600
+ const findings = [];
2601
+ for (const [key, readers] of keyReaders) {
2602
+ const features = keyFeatures.get(key) ?? /* @__PURE__ */ new Set();
2603
+ const shared = features.size > 1 || keyForcedTopLevel.has(key);
2604
+ const current = placement.get(key);
2605
+ if (shared) {
2606
+ if (current?.kind === "nested") {
2607
+ findings.push({
2608
+ line: current.line,
2609
+ message: `Locale "${key}" is read by ${describeConsumers([...readers], sourceRoot2)} and must live at the top level of locales. See docs/code-organization-guide/rules/locales-rule.md`
2610
+ });
2611
+ }
2612
+ continue;
2613
+ }
2614
+ if (features.size !== 1) continue;
2615
+ const featureName = [...features][0] ?? "";
2616
+ const expected = camelCase(featureName);
2617
+ if (current?.kind === "top") {
2618
+ findings.push({
2619
+ line: current.line,
2620
+ message: `Locale "${key}" is read only by the ${featureName} feature; it must live in an object named "${expected}". See docs/code-organization-guide/rules/locales-rule.md`
2621
+ });
2622
+ } else if (current?.kind === "nested" && key !== expected) {
2623
+ findings.push({
2624
+ line: current.line,
2625
+ message: `Locale "${key}" is read only by the ${featureName} feature; it must live in an object named "${expected}", not "${key}". See docs/code-organization-guide/rules/locales-rule.md`
2626
+ });
2627
+ }
2628
+ }
2629
+ if (findings.length === 0) return {};
2630
+ return {
2631
+ Program(node) {
2632
+ for (const finding of findings) {
2633
+ context.report({ node, loc: { line: finding.line, column: 0 }, message: finding.message });
2634
+ }
2635
+ }
2636
+ };
2637
+ }
2638
+ };
2639
+
2640
+ // eslint/pasika/rules/shared-style-dedup.ts
2641
+ import path29 from "path";
2642
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
2643
+ var CLASS_NAME = /className="(?<classes>[^"]+)"/g;
2644
+ var comboCache;
2645
+ function combosFor(index) {
2646
+ const files = [...index.modules.keys()].filter((file) => file.endsWith(".tsx") || file.endsWith(".jsx")).sort((left, right) => left.localeCompare(right));
2647
+ let fingerprint2 = "";
2648
+ for (const file of files) {
2649
+ try {
2650
+ fingerprint2 += String(statSync2(file).mtimeMs);
2651
+ } catch {
2652
+ fingerprint2 += "0";
2653
+ }
2654
+ }
2655
+ if (comboCache?.sourceRoot === index.sourceRoot && comboCache.fingerprint === fingerprint2) {
2656
+ return comboCache.combos;
2657
+ }
2658
+ const combos = /* @__PURE__ */ new Map();
2659
+ for (const file of files) {
2660
+ const text = readFileSync3(file, "utf8");
2661
+ for (const match of text.matchAll(CLASS_NAME)) {
2662
+ const classes = (match.groups?.classes ?? "").split(/\s+/).filter(Boolean);
2663
+ if (classes.length < 2) continue;
2664
+ const key = [...classes].sort().join(" ");
2665
+ const users = combos.get(key) ?? /* @__PURE__ */ new Set();
2666
+ users.add(file);
2667
+ combos.set(key, users);
2668
+ }
2669
+ }
2670
+ comboCache = { sourceRoot: index.sourceRoot, fingerprint: fingerprint2, combos };
2671
+ return combos;
2672
+ }
2673
+ var sharedStyleDedupRule = {
2674
+ meta: {
2675
+ schema: [],
2676
+ type: "problem",
2677
+ docs: {
2678
+ description: "Require a className combination used by two or more components to become a named custom utility."
2679
+ }
2680
+ },
2681
+ create(context) {
2682
+ const sourceRoot2 = path29.resolve("src");
2683
+ const file = path29.resolve(context.filename);
2684
+ const segments = segmentsOf(file, sourceRoot2);
2685
+ if (segments.length === 0) return {};
2686
+ const index = getProjectIndex(sourceRoot2);
2687
+ if (!index) return {};
2688
+ const combos = combosFor(index);
2689
+ const sharedHere = [...combos.entries()].filter(([, users]) => users.size >= 2 && users.has(file)).filter(([, users]) => [...users].sort((left, right) => left.localeCompare(right))[0] === file);
2690
+ if (sharedHere.length === 0) return {};
2691
+ return {
2692
+ Program(node) {
2693
+ for (const [combo, users] of sharedHere) {
2694
+ context.report({
2695
+ node,
2696
+ loc: { line: 1, column: 0 },
2697
+ message: `Class combination "${combo}" is used by ${String(users.size)} components and should change together; create a named custom Tailwind utility for it. See docs/styling-guide/rules/theme-and-utility-definition-rule.md`
2698
+ });
2699
+ }
2700
+ }
2701
+ };
2702
+ }
2703
+ };
2704
+
2705
+ // eslint/pasika/rules/no-eslint-disable.ts
2706
+ var noEslintDisableRule = {
2707
+ meta: {
2708
+ schema: [],
2709
+ type: "problem",
2710
+ docs: {
2711
+ description: "Forbid eslint-disable directives."
2712
+ }
2713
+ },
2714
+ create(context) {
2715
+ return {
2716
+ Program() {
2717
+ for (const comment of context.sourceCode.getAllComments()) {
2718
+ if (!comment.value.includes("eslint-disable")) continue;
2719
+ if (!comment.loc) continue;
2720
+ context.report({
2721
+ loc: comment.loc,
2722
+ message: "Do not use eslint-disable directives; fix the reported violation instead."
2723
+ });
2724
+ }
2725
+ }
2726
+ };
2727
+ }
2728
+ };
2729
+
2730
+ // eslint/pasika/rules/zod-schema-validation.ts
2731
+ var NON_SCHEMA_OBJECTS = /* @__PURE__ */ new Set(["JSON", "Date", "Number"]);
2732
+ var SCHEMA_METHODS = /* @__PURE__ */ new Set(["safeParse", "parse", "assert"]);
2733
+ function isRecord(value) {
2734
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2735
+ }
2736
+ function isTypePredicate(node) {
2737
+ return node.returnType?.typeAnnotation?.type === "TSTypePredicate";
2738
+ }
2739
+ function guardsUnknown(node) {
2740
+ return (node.params ?? []).some((param) => {
2741
+ if (!isRecord(param)) return false;
2742
+ const typeAnnotation = param.typeAnnotation;
2743
+ if (!isRecord(typeAnnotation)) return false;
2744
+ const annotation = typeAnnotation.typeAnnotation;
2745
+ if (!isRecord(annotation)) return false;
2746
+ return annotation.type === "TSUnknownKeyword" || annotation.type === "TSAnyKeyword";
2747
+ });
2748
+ }
2749
+ function delegatesToSchema(node) {
2750
+ if (Array.isArray(node)) return node.some(delegatesToSchema);
2751
+ if (!isRecord(node)) return false;
2752
+ if (node.type === "CallExpression") {
2753
+ const callee = node.callee;
2754
+ if (isRecord(callee) && callee.type === "MemberExpression") {
2755
+ const property = callee.property;
2756
+ const object = callee.object;
2757
+ if (isRecord(property) && property.type === "Identifier" && typeof property.name === "string" && SCHEMA_METHODS.has(property.name) && // A named object is only a schema if it is not a known non-schema
2758
+ // object (JSON.parse, Date.parse, ...); any other callee (a call, a
2759
+ // member chain) is a delegation to whatever returns it.
2760
+ !(isRecord(object) && object.type === "Identifier" && typeof object.name === "string" && NON_SCHEMA_OBJECTS.has(object.name))) {
2761
+ return true;
2762
+ }
2763
+ }
2764
+ }
2765
+ for (const key of Object.keys(node)) {
2766
+ if (key === "callee" || key === "parent") continue;
2767
+ if (delegatesToSchema(node[key])) return true;
2768
+ }
2769
+ return false;
2770
+ }
2771
+ var zodSchemaValidationRule = {
2772
+ meta: {
2773
+ schema: [],
2774
+ type: "problem",
2775
+ docs: {
2776
+ description: "Require Zod schemas for runtime validation instead of hand-written type guards."
2777
+ }
2778
+ },
2779
+ create(context) {
2780
+ function checkFunction(node) {
2781
+ if (!isTypePredicate(node)) return;
2782
+ if (!guardsUnknown(node)) return;
2783
+ if (delegatesToSchema(node.body)) return;
2784
+ context.report({
2785
+ loc: node.loc ?? { line: 1, column: 0 },
2786
+ message: "This hand-written type guard validates an unknown value ad hoc. Use a Zod schema instead: schema.safeParse(value) returns a discriminated result whose .success narrows the type. See docs/agent-policy.md"
2787
+ });
2788
+ }
2789
+ return {
2790
+ FunctionDeclaration(node) {
2791
+ checkFunction(node);
2792
+ },
2793
+ FunctionExpression(node) {
2794
+ checkFunction(node);
2795
+ },
2796
+ ArrowFunctionExpression(node) {
2797
+ checkFunction(node);
2798
+ }
2799
+ };
2800
+ }
2801
+ };
2802
+
2803
+ // eslint/pasika/rules/source-under-src.ts
2804
+ import path30 from "path";
2805
+ var NON_SOURCE_ROOT_DIRS = /* @__PURE__ */ new Set([
2806
+ ".agents",
2807
+ ".cache",
2808
+ ".github",
2809
+ ".husky",
2810
+ ".next",
2811
+ ".tmp",
2812
+ ".turbo",
2813
+ ".vercel",
2814
+ ".vulyk",
2815
+ "__tests__",
2816
+ "assets",
2817
+ "bin",
2818
+ "build",
2819
+ "cli",
2820
+ "coverage",
2821
+ "dist",
2822
+ "docs",
2823
+ "enforcement",
2824
+ "eslint",
2825
+ "node_modules",
2826
+ "public",
2827
+ "static",
2828
+ "test",
2829
+ "tests",
2830
+ "tmp",
2831
+ "vendor"
2832
+ ]);
2833
+ var MODULE_EXTENSION = /\.(?:cjs|cts|js|jsx|mjs|mts|ts|tsx)$/;
2834
+ var CONFIG_FILE = /^[\w.-]+\.config\.(?:cjs|cts|js|jsx|mjs|mts|ts|tsx)$/;
2835
+ var DECLARATION_FILE = /\.d\.(?:cjs|cts|mts|ts)$/;
2836
+ var sourceUnderSrcRule = {
2837
+ meta: {
2838
+ schema: [],
2839
+ type: "problem",
2840
+ docs: {
2841
+ description: "Report application source files that live outside src/."
2842
+ }
2843
+ },
2844
+ create(context) {
2845
+ const filename = path30.resolve(context.filename);
2846
+ if (!MODULE_EXTENSION.test(filename)) return {};
2847
+ const relative = path30.relative(process.cwd(), filename).replace(/\\/g, "/");
2848
+ if (relative === "src" || relative.startsWith("src/")) return {};
2849
+ const topLevel = relative.split("/")[0] ?? "";
2850
+ if (NON_SOURCE_ROOT_DIRS.has(topLevel)) return {};
2851
+ if (!relative.includes("/")) {
2852
+ const basename = path30.basename(filename);
2853
+ if (CONFIG_FILE.test(basename) || DECLARATION_FILE.test(basename) || basename.startsWith(".")) return {};
2854
+ }
2855
+ return {
2856
+ Program(node) {
2857
+ context.report({
2858
+ node,
2859
+ loc: { line: 1, column: 0 },
2860
+ message: `Application source must live under src/. Move "${relative}" under src/.`
2861
+ });
2862
+ }
2863
+ };
2864
+ }
2865
+ };
2866
+
2867
+ // eslint/pasika/rules/md/helpers.ts
2868
+ var RFC_PATTERN = /\b(?<keyword>MUST NOT|MUST|SHALL NOT|SHALL|SHOULD NOT|SHOULD|MAY|RECOMMENDED|NOT RECOMMENDED|OPTIONAL|REQUIRED)\b/;
2869
+ function containsRfcKeyword(text) {
2870
+ const match = RFC_PATTERN.exec(text);
2871
+ return match?.groups?.keyword ?? null;
2872
+ }
2873
+ function getFilename(context) {
2874
+ return context.filename;
2875
+ }
2876
+ function getTextContent(node) {
2877
+ if ("value" in node && typeof node.value === "string") return node.value;
2878
+ if ("children" in node) {
2879
+ return node.children.map(getTextContent).join("");
2880
+ }
2881
+ return "";
2882
+ }
2883
+ function getLine(node) {
2884
+ return node.position?.start.line ?? 0;
2885
+ }
2886
+
2887
+ // eslint/pasika/rules/md/doc-kind-suffix.ts
2888
+ var docKindSuffixRule = {
2889
+ meta: {
2890
+ type: "problem",
2891
+ docs: {
2892
+ description: "Documentation files must have a -guide, -rule, -reference, or -policy suffix.",
2893
+ recommended: true
2894
+ }
2895
+ },
2896
+ create(context) {
2897
+ return {
2898
+ root(node) {
2899
+ const filename = getFilename(context);
2900
+ if (!filename.endsWith(".md")) return;
2901
+ if (filename.endsWith("-guide.md") || filename.endsWith("-rule.md") || filename.endsWith("-reference.md") || filename.endsWith("-policy.md")) {
2902
+ return;
2903
+ }
2904
+ context.report({
2905
+ node,
2906
+ message: "file name carries no -guide, -rule, -reference, or -policy suffix"
2907
+ });
2908
+ }
2909
+ };
2910
+ }
2911
+ };
2912
+
2913
+ // eslint/pasika/rules/md/title-matches-file-name.ts
2914
+ import path31 from "path";
2915
+ function toExpectedFileName(title) {
2916
+ return `${title.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}.md`;
2917
+ }
2918
+ var titleMatchesFileNameRule = {
2919
+ meta: {
2920
+ type: "problem",
2921
+ docs: {
2922
+ description: "Document title must match the file name in kebab-case.",
2923
+ recommended: true
2924
+ }
2925
+ },
2926
+ create(context) {
2927
+ return {
2928
+ heading(node) {
2929
+ if (node.depth !== 1) return;
2930
+ const filename = getFilename(context);
2931
+ if (!filename.endsWith(".md")) return;
2932
+ const title = getTextContent(node).trim();
2933
+ const expectedFileName = toExpectedFileName(title);
2934
+ const actualFileName = path31.basename(filename);
2935
+ if (!title) {
2936
+ context.report({
2937
+ node,
2938
+ message: "document has no `# Title` heading"
2939
+ });
2940
+ } else if (expectedFileName !== actualFileName) {
2941
+ context.report({
2942
+ node,
2943
+ message: `title "${title}" expects file name ${expectedFileName}`
2944
+ });
2945
+ }
2946
+ }
2947
+ };
2948
+ }
2949
+ };
2950
+
2951
+ // eslint/pasika/rules/md/overview-present.ts
2952
+ var overviewPresentRule = {
2953
+ meta: {
2954
+ type: "problem",
2955
+ docs: {
2956
+ description: "Overview must follow the title in a documentation file.",
2957
+ recommended: true
2958
+ }
2959
+ },
2960
+ create(context) {
2961
+ return {
2962
+ root(node) {
2963
+ const filename = getFilename(context);
2964
+ if (!filename.endsWith(".md")) return;
2965
+ let titleLine = 0;
2966
+ for (const child of node.children) {
2967
+ if (child.type === "heading" && child.depth === 1) {
2968
+ titleLine = getLine(child);
2969
+ break;
2970
+ }
2971
+ }
2972
+ if (!titleLine) return;
2973
+ let hasOverview = false;
2974
+ for (const child of node.children) {
2975
+ if (child.type === "paragraph" && getLine(child) > titleLine && getTextContent(child).trim()) {
2976
+ hasOverview = true;
2977
+ break;
2978
+ }
2979
+ }
2980
+ if (!hasOverview) {
2981
+ context.report({
2982
+ node,
2983
+ message: "no overview follows the title"
2984
+ });
2985
+ }
2986
+ }
2987
+ };
2988
+ }
2989
+ };
2990
+
2991
+ // eslint/pasika/rules/md/overview-length.ts
2992
+ function countSentences(text) {
2993
+ return text.split(/[.!?](?:\s+|$)/).filter((part) => part.trim() !== "").length;
2994
+ }
2995
+ var overviewLengthRule = {
2996
+ meta: {
2997
+ type: "problem",
2998
+ docs: {
2999
+ description: "Overview must contain at most two sentences.",
3000
+ recommended: true
3001
+ }
3002
+ },
3003
+ create(context) {
3004
+ return {
3005
+ root(node) {
3006
+ const filename = getFilename(context);
3007
+ if (!filename.endsWith(".md")) return;
3008
+ let titleLine = 0;
3009
+ for (const child of node.children) {
3010
+ if (child.type === "heading" && child.depth === 1) {
3011
+ titleLine = getLine(child);
3012
+ break;
3013
+ }
3014
+ }
3015
+ if (!titleLine) return;
3016
+ for (const child of node.children) {
3017
+ if (child.type !== "paragraph" || getLine(child) <= titleLine) continue;
3018
+ const text = getTextContent(child).trim();
3019
+ if (!text || text.startsWith("#")) continue;
3020
+ const sentenceCount = countSentences(text);
3021
+ if (sentenceCount > 2) {
3022
+ context.report({
3023
+ node: child,
3024
+ message: `overview uses ${String(sentenceCount)} sentences, at most two are allowed`
3025
+ });
3026
+ }
3027
+ break;
3028
+ }
3029
+ }
3030
+ };
3031
+ }
3032
+ };
3033
+
3034
+ // eslint/pasika/rules/md/guide-overview-no-links.ts
3035
+ function containsLink(node) {
3036
+ if (node.type === "link") return true;
3037
+ if ("children" in node) {
3038
+ return node.children.some(containsLink);
3039
+ }
3040
+ return false;
3041
+ }
3042
+ var guideOverviewNoLinksRule = {
3043
+ meta: {
3044
+ type: "problem",
3045
+ docs: {
3046
+ description: "Guide overview must not link to other documents.",
3047
+ recommended: true
3048
+ }
3049
+ },
3050
+ create(context) {
3051
+ return {
3052
+ root(node) {
3053
+ const filename = getFilename(context);
3054
+ if (!filename.endsWith("-guide.md")) return;
3055
+ let titleLine = 0;
3056
+ for (const child of node.children) {
3057
+ if (child.type === "heading" && child.depth === 1) {
3058
+ titleLine = getLine(child);
3059
+ break;
3060
+ }
3061
+ }
3062
+ if (!titleLine) return;
3063
+ for (const child of node.children) {
3064
+ if (child.type !== "paragraph" || getLine(child) <= titleLine) continue;
3065
+ const text = getTextContent(child).trim();
3066
+ if (!text || text.startsWith("#")) continue;
3067
+ if (containsLink(child)) {
3068
+ context.report({
3069
+ node: child,
3070
+ message: "guide overview links another document"
3071
+ });
3072
+ }
3073
+ break;
3074
+ }
3075
+ }
3076
+ };
3077
+ }
3078
+ };
3079
+
3080
+ // eslint/pasika/rules/md/guide-step-single-sentence.ts
3081
+ function countSentences2(text) {
3082
+ return text.split(/[.!?](?:\s+|$)/).filter((part) => part.trim() !== "").length;
3083
+ }
3084
+ var guideStepSingleSentenceRule = {
3085
+ meta: {
3086
+ type: "problem",
3087
+ docs: {
3088
+ description: "Each guide step must be concise and use one sentence.",
3089
+ recommended: true
3090
+ }
3091
+ },
3092
+ create(context) {
3093
+ const listsOnPath = [];
3094
+ return {
3095
+ list(node) {
3096
+ listsOnPath.push(node);
3097
+ },
3098
+ "list:exit"() {
3099
+ listsOnPath.pop();
3100
+ },
3101
+ listItem(node) {
3102
+ const filename = getFilename(context);
3103
+ if (!filename.endsWith("-guide.md")) return;
3104
+ if (!listsOnPath.at(-1)?.ordered) return;
3105
+ const text = getTextContent(node).trim();
3106
+ const sentenceCount = countSentences2(text);
3107
+ if (sentenceCount > 1) {
3108
+ context.report({
3109
+ node,
3110
+ message: `step uses ${String(sentenceCount)} sentences`
3111
+ });
3112
+ }
3113
+ }
3114
+ };
3115
+ }
3116
+ };
3117
+
3118
+ // eslint/pasika/rules/md/guide-step-single-link.ts
3119
+ function countDocLinks(node) {
3120
+ if (node.type === "link" && node.url.endsWith(".md")) return 1;
3121
+ if ("children" in node) {
3122
+ return node.children.reduce((sum, child) => sum + countDocLinks(child), 0);
3123
+ }
3124
+ return 0;
3125
+ }
3126
+ function visitSteps(node, check) {
3127
+ if (node.type === "list" && node.ordered) {
3128
+ for (const child of node.children) check(child);
3129
+ }
3130
+ if ("children" in node) {
3131
+ for (const child of node.children) visitSteps(child, check);
3132
+ }
3133
+ }
3134
+ var guideStepSingleLinkRule = {
3135
+ meta: {
3136
+ type: "problem",
3137
+ docs: {
3138
+ description: "Each guide step must link at most one document total.",
3139
+ recommended: true
3140
+ }
3141
+ },
3142
+ create(context) {
3143
+ return {
3144
+ root(node) {
3145
+ const filename = getFilename(context);
3146
+ if (!filename.endsWith("-guide.md")) return;
3147
+ visitSteps(node, (item) => {
3148
+ const links = countDocLinks(item);
3149
+ if (links > 1) {
3150
+ context.report({
3151
+ node: item,
3152
+ message: `step links ${String(links)} documents`
3153
+ });
3154
+ }
3155
+ });
3156
+ }
3157
+ };
3158
+ }
3159
+ };
3160
+
3161
+ // eslint/pasika/rules/md/guide-states-no-requirement.ts
3162
+ var guideStatesNoRequirementRule = {
3163
+ meta: {
3164
+ type: "problem",
3165
+ docs: {
3166
+ description: "Guide must not state requirements with RFC 2119 vocabulary.",
3167
+ recommended: true
3168
+ }
3169
+ },
3170
+ create(context) {
3171
+ return {
3172
+ listItem(node) {
3173
+ const filename = getFilename(context);
3174
+ if (!filename.endsWith("-guide.md")) return;
3175
+ const text = getTextContent(node);
3176
+ if (containsRfcKeyword(text)) {
3177
+ context.report({
3178
+ node,
3179
+ message: "guide states a requirement with RFC 2119 vocabulary"
3180
+ });
3181
+ }
3182
+ }
3183
+ };
3184
+ }
3185
+ };
3186
+
3187
+ // eslint/pasika/rules/md/requirement-present.ts
3188
+ function hasRequirement(node) {
3189
+ if (node.type === "listItem") {
3190
+ if (containsRfcKeyword(getTextContent(node))) return true;
3191
+ }
3192
+ if ("children" in node) {
3193
+ for (const child of node.children) {
3194
+ if (hasRequirement(child)) return true;
3195
+ }
3196
+ }
3197
+ return false;
3198
+ }
3199
+ var requirementPresentRule = {
3200
+ meta: {
3201
+ type: "problem",
3202
+ docs: {
3203
+ description: "Rule and policy documents must state at least one requirement.",
3204
+ recommended: true
3205
+ }
3206
+ },
3207
+ create(context) {
3208
+ return {
3209
+ root(node) {
3210
+ const filename = getFilename(context);
3211
+ if (!filename.endsWith("-rule.md") && !filename.endsWith("-policy.md")) {
3212
+ return;
3213
+ }
3214
+ if (!hasRequirement(node)) {
3215
+ const kind = filename.endsWith("-rule.md") ? "rule" : "policy";
3216
+ context.report({
3217
+ node,
3218
+ message: `${kind} document states no requirement`
3219
+ });
3220
+ }
3221
+ }
3222
+ };
3223
+ }
3224
+ };
3225
+
3226
+ // eslint/pasika/rules/md/rule-paired-examples.ts
3227
+ var rulePairedExamplesRule = {
3228
+ meta: {
3229
+ type: "problem",
3230
+ docs: {
3231
+ description: "Rule must have paired Incorrect/Correct examples.",
3232
+ recommended: true
3233
+ }
3234
+ },
3235
+ create(context) {
3236
+ return {
3237
+ root(node) {
3238
+ const filename = getFilename(context);
3239
+ if (!filename.endsWith("-rule.md")) return;
3240
+ const exampleHeadings = [];
3241
+ for (const child of node.children) {
3242
+ if (child.type === "heading" && child.depth === 2) {
3243
+ const text = getTextContent(child).trim();
3244
+ if (/^(?:Incorrect|Correct)\b/.test(text)) {
3245
+ exampleHeadings.push({ line: getLine(child), text });
3246
+ }
3247
+ }
3248
+ }
3249
+ const incorrect = exampleHeadings.filter((h) => h.text.startsWith("Incorrect"));
3250
+ const correct = exampleHeadings.filter((h) => h.text.startsWith("Correct"));
3251
+ if (incorrect.length === 0 || incorrect.length !== correct.length) {
3252
+ context.report({
3253
+ node,
3254
+ message: `${String(incorrect.length)} Incorrect and ${String(correct.length)} Correct examples`
3255
+ });
3256
+ }
3257
+ }
3258
+ };
3259
+ }
3260
+ };
3261
+
3262
+ // eslint/pasika/rules/md/example-heading-description.ts
3263
+ var exampleHeadingDescriptionRule = {
3264
+ meta: {
3265
+ type: "problem",
3266
+ docs: {
3267
+ description: "Example headings must have an em-dash description.",
3268
+ recommended: true
3269
+ }
3270
+ },
3271
+ create(context) {
3272
+ return {
3273
+ heading(node) {
3274
+ const filename = getFilename(context);
3275
+ if (!filename.endsWith("-rule.md")) return;
3276
+ if (node.depth !== 2) return;
3277
+ const text = getTextContent(node).trim();
3278
+ if (!/^(?:Incorrect|Correct)\b/.test(text)) return;
3279
+ if (!/^(?:Incorrect|Correct) — .+/.test(text)) {
3280
+ context.report({
3281
+ node,
3282
+ message: `example heading has no em-dash description: ${text}`
3283
+ });
3284
+ }
3285
+ }
3286
+ };
3287
+ }
3288
+ };
3289
+
3290
+ // eslint/pasika/rules/md/policy-no-examples.ts
3291
+ var policyNoExamplesRule = {
3292
+ meta: {
3293
+ type: "problem",
3294
+ docs: {
3295
+ description: "Policy document must not contain Incorrect/Correct examples.",
3296
+ recommended: true
3297
+ }
3298
+ },
3299
+ create(context) {
3300
+ return {
3301
+ heading(node) {
3302
+ const filename = getFilename(context);
3303
+ if (!filename.endsWith("-policy.md")) return;
3304
+ if (node.depth !== 2) return;
3305
+ const text = getTextContent(node).trim();
3306
+ if (/^(?:Incorrect|Correct)\b/.test(text)) {
3307
+ context.report({
3308
+ node,
3309
+ message: `policy document contains an example: ${text}`
3310
+ });
3311
+ }
3312
+ }
3313
+ };
3314
+ }
3315
+ };
3316
+
3317
+ // eslint/pasika/rules/md/no-cross-document-link.ts
3318
+ function linkedKind(filename) {
3319
+ if (filename.endsWith("-rule.md")) return "rule";
3320
+ if (filename.endsWith("-reference.md")) return "reference";
3321
+ if (filename.endsWith("-policy.md")) return "policy";
3322
+ return void 0;
3323
+ }
3324
+ var noCrossDocumentLinkRule = {
3325
+ meta: {
3326
+ type: "problem",
3327
+ docs: {
3328
+ description: "Rules, references, and policy must not link to other documents.",
3329
+ recommended: true
3330
+ }
3331
+ },
3332
+ create(context) {
3333
+ return {
3334
+ link(node) {
3335
+ const filename = getFilename(context);
3336
+ const kind = linkedKind(filename);
3337
+ if (!kind) return;
3338
+ if (node.url.endsWith(".md")) {
3339
+ context.report({
3340
+ node,
3341
+ message: `${kind} links another document: ${node.url}`
3342
+ });
3343
+ }
3344
+ }
3345
+ };
3346
+ }
3347
+ };
3348
+
3349
+ // eslint/pasika/rules/md/reference-no-rfc-vocabulary.ts
3350
+ var referenceNoRfcVocabularyRule = {
3351
+ meta: {
3352
+ type: "problem",
3353
+ docs: {
3354
+ description: "Reference documents must not use RFC 2119 vocabulary (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY) in prose.",
3355
+ recommended: true
3356
+ }
3357
+ },
3358
+ create(context) {
3359
+ return {
3360
+ text(node) {
3361
+ const filename = getFilename(context);
3362
+ if (!filename.endsWith("-reference.md")) return;
3363
+ const keyword = containsRfcKeyword(node.value);
3364
+ if (keyword) {
3365
+ context.report({
3366
+ node,
3367
+ message: `reference uses RFC 2119 vocabulary: ${keyword}`
3368
+ });
3369
+ }
3370
+ }
3371
+ };
3372
+ }
3373
+ };
3374
+
3375
+ // eslint/pasika/rules/md/reference-block-headings.ts
3376
+ var referenceBlockHeadingsRule = {
3377
+ meta: {
3378
+ type: "problem",
3379
+ docs: {
3380
+ description: "Reference block headings rule.",
3381
+ recommended: true
3382
+ }
3383
+ },
3384
+ create(context) {
3385
+ return {
3386
+ root(node) {
3387
+ const filename = getFilename(context);
3388
+ if (!filename.endsWith("-reference.md")) return;
3389
+ const sectionHeadings = [];
3390
+ for (const child of node.children) {
3391
+ if (child.type === "heading" && child.depth >= 2) {
3392
+ sectionHeadings.push(child);
3393
+ }
3394
+ }
3395
+ if (sectionHeadings.length === 1) {
3396
+ context.report({
3397
+ node: node.children[0] ?? node,
3398
+ message: "reference has exactly one section heading, so either a single block is headed or a first block is not"
3399
+ });
3400
+ }
3401
+ }
3402
+ };
3403
+ }
3404
+ };
3405
+
3406
+ // eslint/pasika/rules/md/support-document-placement.ts
3407
+ import path32 from "path";
3408
+ var supportDocumentPlacementRule = {
3409
+ meta: {
3410
+ type: "problem",
3411
+ docs: {
3412
+ description: "Rules must live in rules/, references must live in references/.",
3413
+ recommended: true
3414
+ }
3415
+ },
3416
+ create(context) {
3417
+ return {
3418
+ root(node) {
3419
+ const filename = getFilename(context);
3420
+ if (!filename.endsWith(".md")) return;
3421
+ const parentFolder = path32.basename(path32.dirname(filename));
3422
+ if (filename.endsWith("-rule.md") && parentFolder !== "rules") {
3423
+ context.report({
3424
+ node,
3425
+ message: `rule lives in "${parentFolder}/" instead of "rules/"`
3426
+ });
3427
+ }
3428
+ if (filename.endsWith("-reference.md") && parentFolder !== "references") {
3429
+ context.report({
3430
+ node,
3431
+ message: `reference lives in "${parentFolder}/" instead of "references/"`
3432
+ });
3433
+ }
3434
+ }
3435
+ };
3436
+ }
3437
+ };
3438
+
3439
+ // eslint/pasika/rules/md/no-template-prompt.ts
3440
+ var TEMPLATE_PROMPT = /^\s*\[[A-Z0-9].*\]\s*$/;
3441
+ var noTemplatePromptRule = {
3442
+ meta: {
3443
+ type: "problem",
3444
+ docs: {
3445
+ description: "No leftover bracketed template prompts.",
3446
+ recommended: true
3447
+ }
3448
+ },
3449
+ create(context) {
3450
+ return {
3451
+ text(node) {
3452
+ if (TEMPLATE_PROMPT.test(node.value)) {
3453
+ context.report({
3454
+ node,
3455
+ message: "leftover bracketed template prompt"
3456
+ });
3457
+ }
3458
+ }
3459
+ };
3460
+ }
3461
+ };
3462
+
3463
+ // eslint/pasika/rules/md/guide-folder-entry-point.ts
3464
+ import path34 from "path";
3465
+
3466
+ // eslint/pasika/rules/md/project-index.ts
3467
+ import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
3468
+ import path33 from "path";
3469
+ var KIND_BY_SUFFIX = [
3470
+ ["-rule.md", "rule"],
3471
+ ["-guide.md", "guide"],
3472
+ ["-reference.md", "reference"],
3473
+ ["-policy.md", "policy"]
3474
+ ];
3475
+ function listMarkdownFiles(dir) {
3476
+ return readdirSync2(dir).flatMap((entry) => {
3477
+ const entryPath = path33.join(dir, entry);
3478
+ if (statSync3(entryPath).isDirectory()) {
3479
+ return entry.startsWith("_") ? [] : listMarkdownFiles(entryPath);
3480
+ }
3481
+ return entry.endsWith(".md") ? [entryPath] : [];
3482
+ });
3483
+ }
3484
+ function extractTitle(filePath) {
3485
+ const content = readFileSync4(filePath, "utf8");
3486
+ const match = /^# (?<title>.+)$/m.exec(content);
3487
+ return match?.groups?.title?.trim() ?? "";
3488
+ }
3489
+ var cache2 = /* @__PURE__ */ new Map();
3490
+ function getProjectDocs(docsRoot) {
3491
+ const cached = cache2.get(docsRoot);
3492
+ if (cached) return cached;
3493
+ const files = listMarkdownFiles(docsRoot);
3494
+ const docs = files.sort((a, b) => a.localeCompare(b)).map((filePath) => {
3495
+ const fileName = path33.basename(filePath);
3496
+ const kind = KIND_BY_SUFFIX.find(([suffix]) => fileName.endsWith(suffix))?.[1];
3497
+ return {
3498
+ filePath,
3499
+ doc: path33.relative(docsRoot, filePath).split(path33.sep).join("/"),
3500
+ fileName,
3501
+ kind,
3502
+ title: extractTitle(filePath)
3503
+ };
3504
+ });
3505
+ cache2.set(docsRoot, docs);
3506
+ return docs;
3507
+ }
3508
+ function findDocsRoot(filePath) {
3509
+ let dir = path33.dirname(filePath);
3510
+ for (; ; ) {
3511
+ if (path33.basename(dir) === "docs" && statSync3(dir).isDirectory()) {
3512
+ return dir;
3513
+ }
3514
+ const parent = path33.dirname(dir);
3515
+ if (parent === dir) return void 0;
3516
+ dir = parent;
3517
+ }
3518
+ }
3519
+
3520
+ // eslint/pasika/rules/md/guide-folder-entry-point.ts
3521
+ var guideFolderEntryPointRule = {
3522
+ meta: {
3523
+ type: "problem",
3524
+ docs: {
3525
+ description: "Guide folders must have an entry point.",
3526
+ recommended: true
3527
+ }
3528
+ },
3529
+ create(context) {
3530
+ return {
3531
+ root(node) {
3532
+ const filename = getFilename(context);
3533
+ if (!filename.endsWith(".md")) return;
3534
+ const docsRoot = findDocsRoot(filename);
3535
+ if (!docsRoot) return;
3536
+ const docs = getProjectDocs(docsRoot);
3537
+ const guideFolders = new Set(
3538
+ docs.filter((doc) => ["rules", "references"].includes(path34.basename(path34.dirname(doc.filePath)))).map((doc) => path34.dirname(path34.dirname(doc.filePath))).filter((folder) => path34.resolve(folder) !== path34.resolve(docsRoot))
3539
+ );
3540
+ const currentDir = path34.dirname(filename);
3541
+ if (guideFolders.has(currentDir)) {
3542
+ const expectedEntryPoint = `${path34.basename(currentDir)}.md`;
3543
+ const hasEntryPoint = docs.some(
3544
+ (doc) => doc.kind === "guide" && path34.dirname(doc.filePath) === currentDir && doc.fileName === expectedEntryPoint
3545
+ );
3546
+ if (!hasEntryPoint) {
3547
+ context.report({
3548
+ node,
3549
+ message: `folder holds support documents but has no ${expectedEntryPoint} entry point`
3550
+ });
3551
+ }
3552
+ }
3553
+ }
3554
+ };
3555
+ }
3556
+ };
3557
+
3558
+ // eslint/pasika/rules/md/rfc-only-in-bullets.ts
3559
+ function checkOutsideBullets(node, context, insideBullet) {
3560
+ if (node.type === "listItem") {
3561
+ for (const child of node.children) {
3562
+ checkOutsideBullets(child, context, true);
3563
+ }
3564
+ return;
3565
+ }
3566
+ if (node.type === "text") {
3567
+ const keyword = containsRfcKeyword(node.value);
3568
+ if (keyword && !insideBullet) {
3569
+ context.report({
3570
+ node,
3571
+ message: `RFC 2119 vocabulary "${keyword}" appears outside a bullet point`
3572
+ });
3573
+ }
3574
+ return;
3575
+ }
3576
+ if ("children" in node) {
3577
+ for (const child of node.children) {
3578
+ checkOutsideBullets(child, context, insideBullet);
3579
+ }
3580
+ }
3581
+ }
3582
+ var rfcOnlyInBulletsRule = {
3583
+ meta: {
3584
+ type: "problem",
3585
+ docs: {
3586
+ description: "RFC 2119 vocabulary must appear only in bullet points.",
3587
+ recommended: true
3588
+ }
3589
+ },
3590
+ create(context) {
3591
+ return {
3592
+ root(node) {
3593
+ const filename = getFilename(context);
3594
+ if (!filename.endsWith("-rule.md")) return;
3595
+ checkOutsideBullets(node, context, false);
3596
+ }
3597
+ };
3598
+ }
3599
+ };
3600
+
3601
+ // eslint/pasika/rules/md/policy-subject-headings.ts
3602
+ var policySubjectHeadingsRule = {
3603
+ meta: {
3604
+ type: "problem",
3605
+ docs: {
3606
+ description: "Policy document must group its bullets under a heading per subject.",
3607
+ recommended: true
3608
+ }
3609
+ },
3610
+ create(context) {
3611
+ return {
3612
+ root(node) {
3613
+ const filename = getFilename(context);
3614
+ if (!filename.endsWith("-policy.md")) return;
3615
+ let titleLine = 0;
3616
+ let firstSubjectLine = 0;
3617
+ let overviewLine = 0;
3618
+ let foundTitle = false;
3619
+ let foundOverview = false;
3620
+ for (const child of node.children) {
3621
+ const line = getLine(child);
3622
+ if (!foundTitle && child.type === "heading" && child.depth === 1) {
3623
+ titleLine = line;
3624
+ foundTitle = true;
3625
+ continue;
3626
+ }
3627
+ if (!foundTitle) continue;
3628
+ if (!foundOverview && child.type === "paragraph" && line > titleLine) {
3629
+ overviewLine = line;
3630
+ foundOverview = true;
3631
+ continue;
3632
+ }
3633
+ if (!firstSubjectLine && child.type === "heading" && child.depth === 2 && line > titleLine) {
3634
+ firstSubjectLine = line;
3635
+ }
3636
+ }
3637
+ if (!titleLine || !firstSubjectLine) return;
3638
+ const hasUngroupedBullet = node.children.some((child) => {
3639
+ if (child.type !== "list") return false;
3640
+ const line = getLine(child);
3641
+ return line > overviewLine && line < firstSubjectLine;
3642
+ });
3643
+ if (hasUngroupedBullet) {
3644
+ context.report({
3645
+ node,
3646
+ message: "policy bullet appears before the first subject heading"
3647
+ });
3648
+ }
3649
+ }
3650
+ };
3651
+ }
3652
+ };
3653
+
3654
+ // eslint/pasika/rules/md/guide-link-anchors.ts
3655
+ function visitSteps2(node, check) {
3656
+ if (node.type === "list" && node.ordered) {
3657
+ for (const child of node.children) check(child);
3658
+ }
3659
+ if ("children" in node) {
3660
+ for (const child of node.children) visitSteps2(child, check);
3661
+ }
3662
+ }
3663
+ function collectGuideLinks(node) {
3664
+ if (node.type === "link" && node.url.endsWith("-guide.md")) return [node];
3665
+ if ("children" in node) {
3666
+ return node.children.flatMap(collectGuideLinks);
3667
+ }
3668
+ return [];
3669
+ }
3670
+ var guideLinkAnchorsRule = {
3671
+ meta: {
3672
+ type: "problem",
3673
+ docs: {
3674
+ description: "A step that links another Guide must link directly to a How To section.",
3675
+ recommended: true
3676
+ }
3677
+ },
3678
+ create(context) {
3679
+ return {
3680
+ root(node) {
3681
+ const filename = getFilename(context);
3682
+ if (!filename.endsWith("-guide.md")) return;
3683
+ visitSteps2(node, (item) => {
3684
+ for (const link of collectGuideLinks(item)) {
3685
+ if (!link.url.includes("#")) {
3686
+ context.report({
3687
+ node: link,
3688
+ message: `guide link ${link.url} does not point to a specific section`
3689
+ });
3690
+ }
3691
+ }
3692
+ });
3693
+ }
3694
+ };
3695
+ }
3696
+ };
3697
+
3698
+ // eslint/pasika/rules/md/no-nested-how-to.ts
3699
+ var noNestedHowToRule = {
3700
+ meta: {
3701
+ type: "problem",
3702
+ docs: {
3703
+ description: "How To sections must not nest inside other How To sections.",
3704
+ recommended: true
3705
+ }
3706
+ },
3707
+ create(context) {
3708
+ return {
3709
+ root(node) {
3710
+ const filename = getFilename(context);
3711
+ if (!filename.endsWith("-guide.md")) return;
3712
+ const howToHeadings = [];
3713
+ for (const child of node.children) {
3714
+ if (child.type !== "heading") continue;
3715
+ const text = getTextContent(child).trim();
3716
+ if (!/^How To\b/.test(text)) continue;
3717
+ howToHeadings.push({ line: getLine(child), depth: child.depth, text });
3718
+ }
3719
+ for (let i = 1; i < howToHeadings.length; i++) {
3720
+ const prev = howToHeadings[i - 1];
3721
+ const current = howToHeadings[i];
3722
+ if (prev && current && current.depth > prev.depth) {
3723
+ context.report({
3724
+ node,
3725
+ message: `How To section "${current.text}" nests inside another How To section`
3726
+ });
3727
+ }
3728
+ }
3729
+ }
3730
+ };
3731
+ }
3732
+ };
3733
+
3734
+ // eslint/pasika/rules/md/glossary-term-linking.ts
3735
+ import { readFileSync as readFileSync5 } from "fs";
3736
+ import path35 from "path";
3737
+ function extractGlossaryTerms(filePath) {
3738
+ const content = readFileSync5(filePath, "utf8");
3739
+ const terms = [];
3740
+ const headingPattern = /^## (?<term>.+)$/gm;
3741
+ let match;
3742
+ while ((match = headingPattern.exec(content)) !== null) {
3743
+ const term = match.groups?.term?.trim();
3744
+ if (term) terms.push(term);
3745
+ }
3746
+ return terms;
3747
+ }
3748
+ function collectSteps(node) {
3749
+ const texts = [];
3750
+ const firstStepLinks = [];
3751
+ if (node.type === "list" && node.ordered) {
3752
+ for (const item of node.children) {
3753
+ texts.push(getTextContent(item));
3754
+ if (firstStepLinks.length === 0) {
3755
+ collectDocLinks(item, firstStepLinks);
3756
+ }
3757
+ }
3758
+ }
3759
+ if ("children" in node) {
3760
+ for (const child of node.children) {
3761
+ const nested = collectSteps(child);
3762
+ texts.push(...nested.texts);
3763
+ if (firstStepLinks.length === 0) firstStepLinks.push(...nested.firstStepLinks);
3764
+ }
3765
+ }
3766
+ return { texts, firstStepLinks };
3767
+ }
3768
+ function collectDocLinks(node, out) {
3769
+ if (node.type === "link" && node.url.endsWith(".md")) out.push(node.url);
3770
+ if ("children" in node) {
3771
+ for (const child of node.children) collectDocLinks(child, out);
3772
+ }
3773
+ }
3774
+ var glossaryTermLinkingRule = {
3775
+ meta: {
3776
+ type: "problem",
3777
+ docs: {
3778
+ description: "A Guide whose steps use glossary terms must link that Reference from its first step.",
3779
+ recommended: true
3780
+ }
3781
+ },
3782
+ create(context) {
3783
+ return {
3784
+ root(node) {
3785
+ const filename = getFilename(context);
3786
+ if (!filename.endsWith("-guide.md")) return;
3787
+ const docsRoot = findDocsRoot(filename);
3788
+ if (!docsRoot) return;
3789
+ const docs = getProjectDocs(docsRoot);
3790
+ const guideDir = path35.dirname(filename);
3791
+ const guideReferences = docs.filter(
3792
+ (doc) => doc.kind === "reference" && path35.dirname(doc.filePath) === guideDir
3793
+ );
3794
+ if (guideReferences.length === 0) return;
3795
+ const glossaryTerms = [];
3796
+ for (const ref of guideReferences) {
3797
+ glossaryTerms.push(...extractGlossaryTerms(ref.filePath));
3798
+ }
3799
+ if (glossaryTerms.length === 0) return;
3800
+ const { texts: stepTexts, firstStepLinks } = collectSteps(node);
3801
+ const usedTerms = glossaryTerms.filter((term) => stepTexts.some((text) => text.includes(term)));
3802
+ if (usedTerms.length === 0) return;
3803
+ const hasRefLink = firstStepLinks.some((link) => guideReferences.some((ref) => link.includes(ref.fileName)));
3804
+ if (!hasRefLink) {
3805
+ context.report({
3806
+ node,
3807
+ message: `guide uses glossary terms (${usedTerms.join(", ")}) but first step does not link the reference`
3808
+ });
3809
+ }
3810
+ }
3811
+ };
3812
+ }
3813
+ };
3814
+
3815
+ // eslint/pasika/rules/md/index.ts
3816
+ var mdRules = {
3817
+ "doc-kind-suffix": docKindSuffixRule,
3818
+ "title-matches-file-name": titleMatchesFileNameRule,
3819
+ "overview-present": overviewPresentRule,
3820
+ "overview-length": overviewLengthRule,
3821
+ "guide-overview-no-links": guideOverviewNoLinksRule,
3822
+ "guide-step-single-sentence": guideStepSingleSentenceRule,
3823
+ "guide-step-single-link": guideStepSingleLinkRule,
3824
+ "guide-states-no-requirement": guideStatesNoRequirementRule,
3825
+ "requirement-present": requirementPresentRule,
3826
+ "rule-paired-examples": rulePairedExamplesRule,
3827
+ "example-heading-description": exampleHeadingDescriptionRule,
3828
+ "policy-no-examples": policyNoExamplesRule,
3829
+ "no-cross-document-link": noCrossDocumentLinkRule,
3830
+ "reference-no-rfc-vocabulary": referenceNoRfcVocabularyRule,
3831
+ "reference-block-headings": referenceBlockHeadingsRule,
3832
+ "support-document-placement": supportDocumentPlacementRule,
3833
+ "no-template-prompt": noTemplatePromptRule,
3834
+ "guide-folder-entry-point": guideFolderEntryPointRule,
3835
+ "rfc-only-in-bullets": rfcOnlyInBulletsRule,
3836
+ "policy-subject-headings": policySubjectHeadingsRule,
3837
+ "guide-link-anchors": guideLinkAnchorsRule,
3838
+ "no-nested-how-to": noNestedHowToRule,
3839
+ "glossary-term-linking": glossaryTermLinkingRule
3840
+ };
3841
+
3842
+ // eslint/pasika/rules/css/helpers.ts
3843
+ function walkNodes(node, visit) {
3844
+ visit(node);
3845
+ if ("children" in node && node.children) {
3846
+ for (const child of node.children) walkNodes(child, visit);
3847
+ }
3848
+ if ("prelude" in node && node.prelude) {
3849
+ walkNodes(node.prelude, visit);
3850
+ }
3851
+ if ("block" in node && node.block) {
3852
+ walkNodes(node.block, visit);
3853
+ }
3854
+ }
3855
+ function atrules(node) {
3856
+ const result = [];
3857
+ walkNodes(node, (current) => {
3858
+ if (current.type === "Atrule") result.push(current);
3859
+ });
3860
+ return result;
3861
+ }
3862
+ function atrulesNamed(node, name) {
3863
+ return atrules(node).filter((candidate) => candidate.type === "Atrule" && candidate.name === name);
3864
+ }
3865
+ function blockChildren(node) {
3866
+ if (!node) return [];
3867
+ if (!("block" in node) || !node.block) return [];
3868
+ return node.block.children;
3869
+ }
3870
+ function preludeIdentifiers(node) {
3871
+ if (!node) return [];
3872
+ if (!("prelude" in node) || !node.prelude) return [];
3873
+ const names = [];
3874
+ walkNodes(node.prelude, (current) => {
3875
+ if (current.type === "Identifier") names.push(current.name);
3876
+ if (current.type === "Layer") names.push(current.name);
3877
+ });
3878
+ return names;
3879
+ }
3880
+ function rules(node) {
3881
+ const result = [];
3882
+ walkNodes(node, (current) => {
3883
+ if (current.type === "Rule") result.push(current);
3884
+ });
3885
+ return result;
3886
+ }
3887
+ function selectorNames(node) {
3888
+ if (!node) return [];
3889
+ if (!("prelude" in node) || !node.prelude) return [];
3890
+ const names = [];
3891
+ walkNodes(node.prelude, (current) => {
3892
+ if (current.type === "PseudoClassSelector" || current.type === "TypeSelector" || current.type === "ClassSelector") {
3893
+ names.push(current.name);
3894
+ }
3895
+ });
3896
+ return names;
3897
+ }
3898
+
3899
+ // eslint/pasika/rules/css/theme-reset.ts
3900
+ var themeResetRule = {
3901
+ meta: {
3902
+ schema: [],
3903
+ type: "problem",
3904
+ docs: {
3905
+ description: "Require Tailwind's default theme reset --*: initial."
3906
+ }
3907
+ },
3908
+ create(context) {
3909
+ return {
3910
+ "StyleSheet:exit"(node) {
3911
+ const resetFound = atrulesNamed(node, "theme").some(
3912
+ (theme) => blockChildren(theme).some((child) => {
3913
+ if (child.type === "Declaration") return child.property.startsWith("--*");
3914
+ if (child.type === "Raw") return child.value.includes("--*: initial");
3915
+ return false;
3916
+ })
3917
+ );
3918
+ if (!resetFound) {
3919
+ context.report({
3920
+ node,
3921
+ message: "The global stylesheet must reset Tailwind's default theme with --*: initial inside an @theme block."
3922
+ });
3923
+ }
3924
+ }
3925
+ };
3926
+ }
3927
+ };
3928
+
3929
+ // eslint/pasika/rules/css/root-variables.ts
3930
+ var rootVariablesRule = {
3931
+ meta: {
3932
+ schema: [],
3933
+ type: "problem",
3934
+ docs: {
3935
+ description: "Require styling values to be CSS variables in :root referenced through @theme inline."
3936
+ }
3937
+ },
3938
+ create(context) {
3939
+ return {
3940
+ "StyleSheet:exit"(node) {
3941
+ const hasRootVars = rules(node).some((rule) => {
3942
+ if (!selectorNames(rule).includes("root")) return false;
3943
+ return blockChildren(rule).some((child) => child.type === "Declaration");
3944
+ });
3945
+ if (!hasRootVars) {
3946
+ context.report({
3947
+ node,
3948
+ message: "Styling values must be defined as CSS variables in a :root block."
3949
+ });
3950
+ }
3951
+ const referencingThemes = atrulesNamed(node, "theme").filter(
3952
+ (theme) => blockChildren(theme).some((child) => {
3953
+ if (child.type === "Raw") return child.value.includes("var(");
3954
+ if (child.type === "Declaration") return JSON.stringify(child.value).includes("var(");
3955
+ return false;
3956
+ })
3957
+ );
3958
+ const inlineThemes = atrulesNamed(node, "theme").filter(
3959
+ (theme) => preludeIdentifiers(theme).includes("inline")
3960
+ );
3961
+ if (referencingThemes.length > 0 && inlineThemes.length === 0) {
3962
+ context.report({
3963
+ node,
3964
+ message: "Tailwind theme variables that reference CSS variables must use @theme inline."
3965
+ });
3966
+ }
3967
+ }
3968
+ };
3969
+ }
3970
+ };
3971
+
3972
+ // eslint/pasika/rules/css/apply-usage.ts
3973
+ var applyUsageRule = {
3974
+ meta: {
3975
+ schema: [],
3976
+ type: "problem",
3977
+ docs: {
3978
+ description: "Require @apply for project style declarations inside global selectors."
3979
+ }
3980
+ },
3981
+ create(context) {
3982
+ return {
3983
+ "StyleSheet:exit"(node) {
3984
+ const layers = node.children.filter((child) => child.type === "Atrule" && child.name === "layer");
3985
+ for (const layer of layers) {
3986
+ for (const child of blockChildren(layer)) {
3987
+ if (child.type !== "Rule") continue;
3988
+ for (const declaration of blockChildren(child)) {
3989
+ if (declaration.type !== "Declaration") continue;
3990
+ context.report({
3991
+ node: declaration,
3992
+ message: `Style declaration "${declaration.property}" inside a global selector must use @apply.`
3993
+ });
3994
+ }
3995
+ }
3996
+ }
3997
+ }
3998
+ };
3999
+ }
4000
+ };
4001
+
4002
+ // eslint/pasika/rules/css/base-layer-pair.ts
4003
+ var baseLayerPairRule = {
4004
+ meta: {
4005
+ schema: [],
4006
+ type: "problem",
4007
+ docs: {
4008
+ description: "Require the base layer to apply base-canvas and base-ink to the body."
4009
+ }
4010
+ },
4011
+ create(context) {
4012
+ return {
4013
+ "StyleSheet:exit"(node) {
4014
+ const baseLayers = atrulesNamed(node, "layer").filter((layer) => preludeIdentifiers(layer).includes("base"));
4015
+ if (baseLayers.length === 0) return;
4016
+ const applied = [];
4017
+ for (const layer of baseLayers) {
4018
+ for (const child of blockChildren(layer)) {
4019
+ if (child.type !== "Rule") continue;
4020
+ if (!selectorNames(child).includes("body")) continue;
4021
+ for (const declaration of blockChildren(child)) {
4022
+ if (declaration.type !== "Atrule" || declaration.name !== "apply") continue;
4023
+ applied.push(...preludeIdentifiers(declaration));
4024
+ }
4025
+ }
4026
+ }
4027
+ const hasCanvas = applied.some((name) => name.includes("canvas"));
4028
+ const hasInk = applied.some((name) => name.includes("ink"));
4029
+ if (!hasCanvas || !hasInk) {
4030
+ context.report({
4031
+ node,
4032
+ message: "The global base layer must apply a canvas and an ink utility to the document body as the default page pair."
4033
+ });
4034
+ }
4035
+ }
4036
+ };
4037
+ }
4038
+ };
4039
+
4040
+ // eslint/pasika/rules/css/stylesheet-ordering.ts
4041
+ var SECTION_ORDER = ["imports", "custom-variant", "root", "theme", "utility", "base styles", "keyframes"];
4042
+ var stylesheetOrderingRule = {
4043
+ meta: {
4044
+ schema: [],
4045
+ type: "problem",
4046
+ docs: {
4047
+ description: "Require the global stylesheet sections in the documented order."
4048
+ }
4049
+ },
4050
+ create(context) {
4051
+ return {
4052
+ "StyleSheet:exit"(node) {
4053
+ const sections = [];
4054
+ let index = 0;
4055
+ for (const child of node.children) {
4056
+ if (child.type === "Atrule" && child.name === "import") {
4057
+ sections.push({ name: "imports", at: index });
4058
+ } else if (child.type === "Atrule" && child.name === "custom-variant") {
4059
+ sections.push({ name: "custom-variant", at: index });
4060
+ } else if (child.type === "Atrule" && child.name === "theme") {
4061
+ sections.push({ name: "theme", at: index });
4062
+ } else if (child.type === "Atrule" && child.name === "utility") {
4063
+ sections.push({ name: "utility", at: index });
4064
+ } else if (child.type === "Atrule" && child.name === "keyframes") {
4065
+ sections.push({ name: "keyframes", at: index });
4066
+ } else if (child.type === "Atrule" && child.name === "layer") {
4067
+ if (preludeIdentifiers(child).includes("base")) {
4068
+ sections.push({ name: "base styles", at: index });
4069
+ }
4070
+ } else if (child.type === "Rule" && selectorNames(child).includes("root")) {
4071
+ sections.push({ name: "root", at: index });
4072
+ }
4073
+ index += 1;
4074
+ }
4075
+ const firstAt = /* @__PURE__ */ new Map();
4076
+ for (const section of sections) {
4077
+ if (!firstAt.has(section.name)) firstAt.set(section.name, section.at ?? 0);
4078
+ }
4079
+ let lastSeen = -1;
4080
+ for (const name of SECTION_ORDER) {
4081
+ const at = firstAt.get(name);
4082
+ if (at === void 0) continue;
4083
+ if (at < lastSeen) {
4084
+ context.report({
4085
+ node,
4086
+ message: `Global stylesheet sections must be ordered: imports, @custom-variant, :root, @theme, custom utilities, base styles, keyframes. Found "${name}" before an earlier section.`
4087
+ });
4088
+ return;
4089
+ }
4090
+ lastSeen = at;
4091
+ }
4092
+ }
4093
+ };
4094
+ }
4095
+ };
4096
+
4097
+ // eslint/pasika/rules/css/css-variable-naming.ts
4098
+ var cssVariableNamingRule = {
4099
+ meta: {
4100
+ schema: [],
4101
+ type: "problem",
4102
+ docs: {
4103
+ description: "Require background variables to end in -canvas and text variables to end in -ink."
4104
+ }
4105
+ },
4106
+ create(context) {
4107
+ return {
4108
+ Declaration(node) {
4109
+ const property = node.property;
4110
+ if (!property.startsWith("--")) return;
4111
+ const lower = property.toLowerCase();
4112
+ if ((lower.includes("bg") || lower.includes("background")) && !lower.endsWith("-canvas")) {
4113
+ context.report({
4114
+ node,
4115
+ message: `CSS variable "${property}" looks like a background token; name it --<role>-canvas instead.`
4116
+ });
4117
+ }
4118
+ if (lower.includes("text") && !lower.endsWith("-ink")) {
4119
+ context.report({
4120
+ node,
4121
+ message: `CSS variable "${property}" looks like a text token; name it --<role>-ink instead.`
4122
+ });
4123
+ }
4124
+ }
4125
+ };
4126
+ }
4127
+ };
4128
+
4129
+ // eslint/pasika/rules/css/custom-utility-apply.ts
4130
+ function rawProperty(raw) {
4131
+ const match = /^\s*(?<property>[a-z][a-z-]*)\s*:/.exec(raw);
4132
+ return match?.groups?.property;
4133
+ }
4134
+ var customUtilityApplyRule = {
4135
+ meta: {
4136
+ schema: [],
4137
+ type: "problem",
4138
+ docs: {
4139
+ description: "Require @utility blocks to style through @apply."
4140
+ }
4141
+ },
4142
+ create(context) {
4143
+ return {
4144
+ "StyleSheet:exit"(node) {
4145
+ for (const utility of atrulesNamed(node, "utility")) {
4146
+ for (const child of blockChildren(utility)) {
4147
+ if (child.type === "Atrule" && child.name === "apply") continue;
4148
+ if (child.type === "Declaration") {
4149
+ context.report({
4150
+ node: child,
4151
+ message: `Custom utility must use @apply for styling declaration "${child.property}".`
4152
+ });
4153
+ continue;
4154
+ }
4155
+ if (child.type === "Raw") {
4156
+ const property = rawProperty(child.value);
4157
+ if (property !== void 0) {
4158
+ context.report({
4159
+ node: child,
4160
+ message: `Custom utility must use @apply for styling declaration "${property}".`
4161
+ });
4162
+ }
4163
+ }
4164
+ }
4165
+ }
4166
+ }
4167
+ };
4168
+ }
4169
+ };
4170
+
4171
+ // eslint/pasika/rules/css/surface-utility.ts
4172
+ var surfaceUtilityRule = {
4173
+ meta: {
4174
+ schema: [],
4175
+ type: "problem",
4176
+ docs: {
4177
+ description: "Require repeated canvas+ink combinations to become a *-surface utility."
4178
+ }
4179
+ },
4180
+ create(context) {
4181
+ return {
4182
+ "StyleSheet:exit"(node) {
4183
+ const combinations = /* @__PURE__ */ new Map();
4184
+ for (const apply of atrulesNamed(node, "apply")) {
4185
+ const classes = preludeIdentifiers(apply);
4186
+ if (!classes.some((name) => name.includes("canvas")) || !classes.some((name) => name.includes("ink"))) {
4187
+ continue;
4188
+ }
4189
+ const key = [...classes].sort().join(" ");
4190
+ combinations.set(key, (combinations.get(key) ?? 0) + 1);
4191
+ }
4192
+ for (const [combo, count] of combinations) {
4193
+ if (count < 2) continue;
4194
+ context.report({
4195
+ node,
4196
+ message: `Combination "${combo}" appears ${String(count)} times. Create a *-surface custom Tailwind utility for it.`
4197
+ });
4198
+ }
4199
+ }
4200
+ };
4201
+ }
4202
+ };
4203
+
4204
+ // eslint/pasika/rules/css/theme-variable-namespace.ts
4205
+ var themeVariableNamespaceRule = {
4206
+ meta: {
4207
+ schema: [],
4208
+ type: "problem",
4209
+ docs: {
4210
+ description: "Require utility classes from the same theme namespace to share a prefix."
4211
+ }
4212
+ },
4213
+ create(context) {
4214
+ return {
4215
+ "StyleSheet:exit"(node) {
4216
+ const namespaces = /* @__PURE__ */ new Set();
4217
+ for (const theme of atrulesNamed(node, "theme")) {
4218
+ for (const child of blockChildren(theme)) {
4219
+ if (child.type !== "Declaration") continue;
4220
+ const segments = child.property.replace(/^--/, "").split("-");
4221
+ if (segments[0]) namespaces.add(segments[0]);
4222
+ }
4223
+ }
4224
+ for (const apply of atrulesNamed(node, "apply")) {
4225
+ const classes = preludeIdentifiers(apply);
4226
+ for (const ns of namespaces) {
4227
+ const matching = classes.filter((name) => name.startsWith(`${ns}-`) || name === ns);
4228
+ if (matching.length < 2) continue;
4229
+ const prefixes = matching.map((name) => name.split("-").slice(0, 2).join("-"));
4230
+ if (new Set(prefixes).size > 1) {
4231
+ context.report({
4232
+ node: apply,
4233
+ message: `Utility classes ${matching.join(", ")} use namespace "${ns}" but lack a shared prefix; use the theme-variable namespace prefix.`
4234
+ });
4235
+ }
4236
+ }
4237
+ }
4238
+ }
4239
+ };
4240
+ }
4241
+ };
4242
+
4243
+ // eslint/pasika/rules/css/global-css-location.ts
4244
+ var globalCssLocationRule = {
4245
+ meta: {
4246
+ schema: [],
4247
+ type: "problem",
4248
+ docs: {
4249
+ description: "Require project global CSS to live only in the entry point stylesheet."
4250
+ }
4251
+ },
4252
+ create(context) {
4253
+ return {
4254
+ "StyleSheet:exit"(node) {
4255
+ const registersTailwind = node.children.some(
4256
+ (child) => child.type === "Atrule" && child.name === "import" && JSON.stringify(child.prelude).includes("tailwindcss")
4257
+ );
4258
+ if (registersTailwind) return;
4259
+ const hasProjectCss = node.children.some((child) => {
4260
+ if (child.type === "Atrule" && child.name === "import") return false;
4261
+ if (child.type === "Comment") return false;
4262
+ return true;
4263
+ });
4264
+ if (!hasProjectCss) return;
4265
+ context.report({
4266
+ node,
4267
+ message: "Global CSS must live in the global stylesheet entry point that registers Tailwind, not in this file."
4268
+ });
4269
+ }
4270
+ };
4271
+ }
4272
+ };
4273
+
4274
+ // eslint/pasika/rules/css/index.ts
4275
+ var cssRules = {
4276
+ "theme-reset": themeResetRule,
4277
+ "root-variables": rootVariablesRule,
4278
+ "apply-usage": applyUsageRule,
4279
+ "base-layer-pair": baseLayerPairRule,
4280
+ "stylesheet-ordering": stylesheetOrderingRule,
4281
+ "css-variable-naming": cssVariableNamingRule,
4282
+ "custom-utility-apply": customUtilityApplyRule,
4283
+ "surface-utility": surfaceUtilityRule,
4284
+ "theme-variable-namespace": themeVariableNamespaceRule,
4285
+ "global-css-location": globalCssLocationRule
4286
+ };
4287
+
4288
+ // eslint/pasika/rules/json/no-cache-flag.ts
4289
+ function memberName(member) {
4290
+ return member.name.type === "String" ? member.name.value : member.name.name;
4291
+ }
4292
+ var noCacheFlagRule = {
4293
+ meta: {
4294
+ schema: [],
4295
+ type: "problem",
4296
+ docs: {
4297
+ description: "Require lint scripts to not pass ESLint's --cache flag."
4298
+ }
4299
+ },
4300
+ create(context) {
4301
+ return {
4302
+ Document(node) {
4303
+ const root = node.body;
4304
+ if (root.type !== "Object") return;
4305
+ const scripts = root.members.find((member) => memberName(member) === "scripts");
4306
+ if (scripts?.value.type !== "Object") return;
4307
+ for (const member of scripts.value.members) {
4308
+ const name = memberName(member);
4309
+ if (name !== "lint" && name !== "fix") continue;
4310
+ if (member.value.type !== "String") continue;
4311
+ if (member.value.value.includes("--cache")) {
4312
+ context.report({
4313
+ node: member,
4314
+ message: "Lint scripts must not pass ESLint's --cache flag because cross-file rules require every file in the run."
4315
+ });
4316
+ }
4317
+ }
4318
+ }
4319
+ };
4320
+ }
4321
+ };
4322
+
4323
+ // eslint/pasika/rules/json/no-vulyk-dependency.ts
4324
+ function memberName2(member) {
4325
+ return member.name.type === "String" ? member.name.value : member.name.name;
4326
+ }
4327
+ var noVulykDependencyRule = {
4328
+ meta: {
4329
+ schema: [],
4330
+ type: "problem",
4331
+ docs: {
4332
+ description: "Require vulyk to run as an ephemeral command, not a package.json dependency."
4333
+ }
4334
+ },
4335
+ create(context) {
4336
+ return {
4337
+ Document(node) {
4338
+ const root = node.body;
4339
+ if (root.type !== "Object") return;
4340
+ for (const key of ["dependencies", "devDependencies"]) {
4341
+ const section = root.members.find((member) => memberName2(member) === key);
4342
+ if (section?.value.type !== "Object") continue;
4343
+ const vulyk = section.value.members.find((member) => memberName2(member) === "vulyk");
4344
+ if (vulyk) {
4345
+ context.report({
4346
+ node: vulyk,
4347
+ message: 'Vulyk must not be a package.json dependency. Run it as "npx vulyk@latest" instead.'
4348
+ });
4349
+ }
4350
+ }
4351
+ }
4352
+ };
4353
+ }
4354
+ };
4355
+
4356
+ // eslint/pasika/rules/json/index.ts
4357
+ var jsonRules = {
4358
+ "no-cache-flag": noCacheFlagRule,
4359
+ "no-vulyk-dependency": noVulykDependencyRule
4360
+ };
4361
+
4362
+ // eslint/pasika/index.ts
4363
+ var pasikaRules = {
4364
+ "component-placement": componentPlacementRule,
4365
+ "support-file-placement": supportFilePlacementRule,
4366
+ "application-structure": applicationStructureRule,
4367
+ "named-exports": namedExportsRule,
4368
+ "data-testid-case": dataTestIdCaseRule,
4369
+ "support-folder-shape": supportFolderShapeRule,
4370
+ "import-through-index": importThroughIndexRule,
4371
+ "util-file-name": utilFileNameRule,
4372
+ "no-util-barrel": noUtilBarrelRule,
4373
+ "jsx-hygiene": jsxHygieneRule,
4374
+ "interactive-component": interactiveComponentRule,
4375
+ "ui-state": uiStateRule,
4376
+ "filename-case": filenameCaseRule,
4377
+ "import-boundaries": importBoundariesRule,
4378
+ "no-mixed-concerns": noMixedConcernsRule,
4379
+ "no-arbitrary-tailwind": noArbitraryTailwindRule,
4380
+ "enforce-cn-merge": enforceCnMergeRule,
4381
+ "enforce-cva-variant-props": enforceCvaVariantPropsRule,
4382
+ "cva-appearance-props": cvaAppearancePropsRule,
4383
+ "cva-boolean-variants": cvaBooleanVariantsRule,
4384
+ "enforce-barrel-exports": enforceBarrelExportsRule,
4385
+ "cross-feature-import": crossFeatureImportRule,
4386
+ "pure-function-extract": pureFunctionExtractRule,
4387
+ "hook-complexity": hookComplexityRule,
4388
+ "locale-dotted-path": localeDottedPathRule,
4389
+ "locales-location": localesLocationRule,
4390
+ "hook-extraction": hookExtractionRule,
4391
+ "value-extraction": valueExtractionRule,
4392
+ "config-extraction": configExtractionRule,
4393
+ "component-nesting": componentNestingRule,
4394
+ "stay-flat": stayFlatRule,
4395
+ "type-extraction": typeExtractionRule,
4396
+ "locale-placement": localePlacementRule,
4397
+ "shared-style-dedup": sharedStyleDedupRule,
4398
+ "no-eslint-disable": noEslintDisableRule,
4399
+ "zod-schema-validation": zodSchemaValidationRule,
4400
+ "source-under-src": sourceUnderSrcRule
4401
+ };
4402
+ var pasikaRuleIds = Object.keys(pasikaRules).map((name) => `pasika/${name}`);
4403
+ var pasikaMdRuleIds = Object.keys(mdRules).map((name) => `pasika/${name}`);
4404
+ var pasikaCssRuleIds = Object.keys(cssRules).map((name) => `pasika/${name}`);
4405
+ var pasikaJsonRuleIds = Object.keys(jsonRules).map((name) => `pasika/${name}`);
4406
+ var allPasikaRuleIds = [...pasikaRuleIds, ...pasikaMdRuleIds, ...pasikaCssRuleIds, ...pasikaJsonRuleIds];
4407
+ var pasikaConfig = {
4408
+ files: ["src/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}"],
4409
+ plugins: {
4410
+ pasika: { rules: pasikaRules }
4411
+ },
4412
+ rules: Object.fromEntries(pasikaRuleIds.map((id) => [id, "error"]))
4413
+ };
4414
+ export {
4415
+ allPasikaRuleIds,
4416
+ cssRules,
4417
+ jsonRules,
4418
+ mdRules,
4419
+ pasikaConfig,
4420
+ pasikaCssRuleIds,
4421
+ pasikaJsonRuleIds,
4422
+ pasikaMdRuleIds,
4423
+ pasikaRuleIds,
4424
+ pasikaRules
90
4425
  };