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,78 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/enforce-barrel-exports
3
- *
4
- * Enforces the "Folder Nesting Rule" — index.ts only re-exports parent component.
5
- *
6
- * @see docs/code-organization-guide/rules/folder-nesting-rule.md
7
- */
8
- import path from "node:path";
9
- import fs from "node:fs";
10
- function isPascalCase(str) {
11
- return /^[A-Z][A-Za-z0-9]*$/.test(str);
12
- }
13
- function isKebabCase(str) {
14
- return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(str);
15
- }
16
- const SUPPORT_FOLDERS = new Set(["types", "schemas", "hooks", "constants", "utils", "config", "locales"]);
17
- export const enforceBarrelExportsRule = {
18
- meta: {
19
- schema: [],
20
- type: "problem",
21
- docs: {
22
- description: "Enforce index.ts barrels only re-export the parent component.",
23
- },
24
- },
25
- create(context) {
26
- const filename = context.filename;
27
- if (!filename)
28
- return {};
29
- const baseName = path.basename(filename);
30
- if (baseName !== "index.ts" && baseName !== "index.cts" && baseName !== "index.mts")
31
- return {};
32
- const dirPath = path.dirname(filename);
33
- const folderName = path.basename(dirPath);
34
- const parentFolderName = path.basename(path.dirname(dirPath));
35
- if (SUPPORT_FOLDERS.has(folderName))
36
- return {};
37
- if (!isPascalCase(folderName) && !isKebabCase(folderName))
38
- return {};
39
- const matchingTsx = fs.existsSync(path.join(dirPath, `${folderName}.tsx`)) ? folderName : null;
40
- if (!matchingTsx)
41
- return {};
42
- if (!isPascalCase(parentFolderName) && !isKebabCase(parentFolderName))
43
- return {};
44
- const reExportedNames = new Set();
45
- return {
46
- ExportNamedDeclaration(node) {
47
- if (!node.source)
48
- return;
49
- for (const spec of node.specifiers) {
50
- if (spec.exported.type === "Identifier") {
51
- reExportedNames.add(spec.exported.name);
52
- }
53
- }
54
- },
55
- "Program:exit"() {
56
- if (reExportedNames.size === 0)
57
- return;
58
- if (!reExportedNames.has(matchingTsx)) {
59
- context.report({
60
- loc: { line: 1, column: 0 },
61
- message: `index.ts in "${folderName}/" must re-export "${matchingTsx}". ` +
62
- "See docs/code-organization-guide/rules/folder-nesting-rule.md",
63
- });
64
- return;
65
- }
66
- const nonParentExports = [...reExportedNames].filter((n) => n !== matchingTsx);
67
- if (nonParentExports.length > 0) {
68
- context.report({
69
- loc: { line: 1, column: 0 },
70
- message: `index.ts must not re-export exclusive children: ${nonParentExports.join(", ")}. ` +
71
- `Only "${matchingTsx}" may be re-exported. ` +
72
- "See docs/code-organization-guide/rules/folder-nesting-rule.md",
73
- });
74
- }
75
- },
76
- };
77
- },
78
- };
@@ -1,21 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/enforce-cn-merge
3
- *
4
- * Enforces the "Class Composition Rule".
5
- *
6
- * @see docs/styling-guide/rules/class-composition-rule.md
7
- */
8
- import type { Rule } from "eslint";
9
- import type { JsxAttributeNode } from "../ast-types.js";
10
- export declare const enforceCnMergeRule: {
11
- meta: {
12
- schema: never[];
13
- type: "problem";
14
- docs: {
15
- description: string;
16
- };
17
- };
18
- create(context: Rule.RuleContext): {
19
- JSXAttribute(node: JsxAttributeNode): void;
20
- };
21
- };
@@ -1,86 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/enforce-cn-merge
3
- *
4
- * Enforces the "Class Composition Rule".
5
- *
6
- * @see docs/styling-guide/rules/class-composition-rule.md
7
- */
8
- function classCount(str) {
9
- return str.split(/\s+/).filter(Boolean).length;
10
- }
11
- function isComponentName(name) {
12
- return name?.type === "JSXIdentifier" && /^[A-Z]/.test(name.name);
13
- }
14
- function isOuterLayoutClass(className) {
15
- const utility = className.split(":").pop() ?? className;
16
- 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(utility);
17
- }
18
- function stringArguments(node) {
19
- if (node?.type === "Literal" && typeof node.value === "string")
20
- return [node.value];
21
- if (node?.type === "TemplateLiteral")
22
- return node.quasis.map((quasi) => quasi.value.raw);
23
- if (node?.type === "ConditionalExpression")
24
- return [...stringArguments(node.consequent), ...stringArguments(node.alternate)];
25
- if (node?.type === "LogicalExpression")
26
- return [...stringArguments(node.left), ...stringArguments(node.right)];
27
- if (node?.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "cn") {
28
- return node.arguments.flatMap((argument) => stringArguments(argument));
29
- }
30
- return [];
31
- }
32
- export const enforceCnMergeRule = {
33
- meta: { schema: [], type: "problem", docs: { description: "Enforce cn() for conditional class merging." } },
34
- create(context) {
35
- return {
36
- JSXAttribute(node) {
37
- const attributeName = node.name?.name ?? "";
38
- const element = node.parent.parent;
39
- const isComponentProp = isComponentName(element.openingElement?.name);
40
- if (isComponentProp && attributeName !== "className" && attributeName.endsWith("ClassName")) {
41
- 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" });
42
- return;
43
- }
44
- if (attributeName !== "className" && attributeName !== "class")
45
- return;
46
- const valueNode = node.value;
47
- if (!valueNode)
48
- return;
49
- if (isComponentProp && attributeName === "className") {
50
- const expression = valueNode.type === "JSXExpressionContainer" ? valueNode.expression : valueNode;
51
- const invalidClass = stringArguments(expression).flatMap((value) => value.split(/\s+/).filter(Boolean)).find((className) => !isOuterLayoutClass(className));
52
- if (invalidClass)
53
- 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` });
54
- return;
55
- }
56
- if (valueNode.type === "Literal" && typeof valueNode.value === "string") {
57
- if (classCount(valueNode.value) > 5)
58
- 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" });
59
- return;
60
- }
61
- if (valueNode.type !== "JSXExpressionContainer")
62
- return;
63
- const expr = valueNode.expression;
64
- if (expr.type === "BinaryExpression" && expr.operator === "+") {
65
- context.report({ node, message: "Use cn() instead of + operator for className. See docs/styling-guide/rules/class-composition-rule.md" });
66
- return;
67
- }
68
- if (expr.type === "TemplateLiteral" && expr.expressions.length > 0) {
69
- context.report({ node, message: "Use cn() instead of template literals with conditionals for className. See docs/styling-guide/rules/class-composition-rule.md" });
70
- return;
71
- }
72
- if (expr.type === "Literal" && typeof expr.value === "string" && classCount(expr.value) > 5) {
73
- 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" });
74
- return;
75
- }
76
- if (expr.type === "CallExpression" && expr.callee.type === "Identifier" && expr.callee.name === "cn") {
77
- for (const arg of expr.arguments) {
78
- if (arg.type === "Literal" && typeof arg.value === "string" && classCount(arg.value) > 5) {
79
- 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" });
80
- }
81
- }
82
- }
83
- },
84
- };
85
- },
86
- };
@@ -1,9 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/enforce-cva-variant-props
3
- *
4
- * Enforces the "Component Variant Rule" — VariantProps from CVA.
5
- *
6
- * @see docs/styling-guide/rules/component-variant-rule.md
7
- */
8
- import type { Rule } from "eslint";
9
- export declare const enforceCvaVariantPropsRule: Rule.RuleModule;
@@ -1,86 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/enforce-cva-variant-props
3
- *
4
- * Enforces the "Component Variant Rule" — VariantProps from CVA.
5
- *
6
- * @see docs/styling-guide/rules/component-variant-rule.md
7
- */
8
- export const enforceCvaVariantPropsRule = {
9
- meta: {
10
- schema: [],
11
- type: "problem",
12
- docs: {
13
- description: "Enforce VariantProps from CVA instead of manual variant union types.",
14
- },
15
- },
16
- create(context) {
17
- const cvaDefinitions = new Map();
18
- return {
19
- VariableDeclarator(node) {
20
- const init = node.init;
21
- if (init?.type !== "CallExpression" ||
22
- init.callee.type !== "Identifier" ||
23
- init.callee.name !== "cva" ||
24
- node.id.type !== "Identifier") {
25
- return;
26
- }
27
- const args = init.arguments;
28
- if (args.length < 2)
29
- return;
30
- const options = args[1];
31
- if (options?.type !== "ObjectExpression")
32
- return;
33
- for (const prop of options.properties) {
34
- if (prop.type !== "Property" ||
35
- prop.key.type !== "Identifier" ||
36
- prop.key.name !== "variants" ||
37
- prop.value.type !== "ObjectExpression") {
38
- continue;
39
- }
40
- const variantNames = prop.value.properties
41
- .filter((p) => p.type === "Property" && p.key.type === "Identifier")
42
- .map((p) => p.key.name);
43
- if (variantNames.length > 0) {
44
- cvaDefinitions.set(node.id.name, variantNames);
45
- }
46
- }
47
- },
48
- TSTypeAliasDeclaration(node) {
49
- const aliasName = node.id?.name;
50
- if (!aliasName?.endsWith("Props"))
51
- return;
52
- if (node.typeAnnotation?.type !== "TSTypeLiteral")
53
- return;
54
- for (const member of node.typeAnnotation.members ?? []) {
55
- if (member.type !== "TSPropertySignature")
56
- continue;
57
- const keyName = member.key?.type === "Identifier" ? member.key.name : undefined;
58
- if (!keyName)
59
- continue;
60
- const typeAnn = member.typeAnnotation?.typeAnnotation;
61
- if (typeAnn?.type !== "TSUnionType")
62
- continue;
63
- // typescript-eslint emits ESTree `Literal` nodes; other TypeScript
64
- // parsers emit `StringLiteral`. Accept both so the rule works
65
- // whichever parser the consuming config installs.
66
- const allStringLiterals = (typeAnn.types ?? []).every((t) => t.type === "TSLiteralType" &&
67
- (t.literal?.type === "StringLiteral" ||
68
- (t.literal?.type === "Literal" && typeof t.literal.value === "string")));
69
- if (allStringLiterals && (typeAnn.types?.length ?? 0) >= 2) {
70
- for (const variantNames of cvaDefinitions.values()) {
71
- if (variantNames.includes(keyName)) {
72
- context.report({
73
- node,
74
- message: `Variant prop "${keyName}" duplicates CVA variant values. ` +
75
- "Use VariantProps<typeof variants> instead of manually writing union types. " +
76
- "See docs/styling-guide/rules/component-variant-rule.md",
77
- });
78
- return;
79
- }
80
- }
81
- }
82
- }
83
- },
84
- };
85
- },
86
- };
@@ -1,2 +0,0 @@
1
- import type { Rule } from "eslint";
2
- export declare const filenameCaseRule: Rule.RuleModule;
@@ -1,123 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/filename-case
3
- *
4
- * Enforces the naming conventions from the "Smart vs Dumb Component Rule":
5
- * - Smart components: PascalCase.tsx
6
- * - Dumb components: kebab-case.tsx
7
- * - Non-component files: kebab-case
8
- *
9
- * @see docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md
10
- */
11
- import path from "node:path";
12
- import { parseComponentInfo } from "./component-conventions.js";
13
- /**
14
- * Next.js App Router routing files that are exempt from filename-case checks.
15
- * https://nextjs.org/docs/app/getting-started/project-structure#routing-files
16
- */
17
- const NEXT_ROUTING_FILES = new Set([
18
- "page",
19
- "layout",
20
- "loading",
21
- "error",
22
- "not-found",
23
- "route",
24
- "template",
25
- "default",
26
- "middleware",
27
- "instrumentation",
28
- ]);
29
- /** Suffixes that form a compound extension with the real extension (e.g. .example.tsx, .test.ts). */
30
- const COMPOUND_SUFFIXES = new Set(["example", "test", "spec", "stories"]);
31
- /**
32
- * Checks whether a string is in kebab-case.
33
- * Allows lowercase letters, digits, and hyphens (but not leading/trailing/double hyphens).
34
- */
35
- function isKebabCase(str) {
36
- return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(str);
37
- }
38
- /**
39
- * Checks whether a string is in PascalCase.
40
- * Starts with an uppercase letter, followed by alphanumeric characters.
41
- */
42
- function isPascalCase(str) {
43
- return /^[A-Z][A-Za-z0-9]*$/.test(str);
44
- }
45
- export const filenameCaseRule = {
46
- meta: {
47
- schema: [],
48
- type: "problem",
49
- docs: {
50
- description: "Enforce pasika filename conventions: kebab-case by default, PascalCase for smart .tsx components.",
51
- },
52
- },
53
- create(context) {
54
- const filename = context.filename;
55
- if (!filename) {
56
- return {};
57
- }
58
- // Only check files under src/
59
- const normalized = filename.replace(/\\/g, "/");
60
- if (!normalized.includes("/src/")) {
61
- return {};
62
- }
63
- const ext = path.extname(filename);
64
- let base = path.basename(filename, ext);
65
- // Strip compound suffix (e.g. ".example", ".test", ".stories", ".spec") to get the real base name
66
- const baseExt = path.extname(base);
67
- if (baseExt && COMPOUND_SUFFIXES.has(baseExt.slice(1))) {
68
- base = path.basename(base, baseExt);
69
- }
70
- // Next.js routing files are exempt
71
- if (NEXT_ROUTING_FILES.has(base)) {
72
- return {};
73
- }
74
- // .tsx components use PascalCase when smart and kebab-case when dumb.
75
- if (ext === ".tsx") {
76
- return {
77
- Program(node) {
78
- const components = parseComponentInfo(context.sourceCode.text, filename);
79
- const component = components[0];
80
- if (component?.smart && !isPascalCase(base)) {
81
- context.report({
82
- node,
83
- message: `Smart component files must use PascalCase.tsx. Filename "${path.basename(filename)}" is not PascalCase.`,
84
- });
85
- return;
86
- }
87
- if (component && !component.smart && !isKebabCase(base)) {
88
- context.report({
89
- node,
90
- message: `Dumb component files must use kebab-case.tsx. Filename "${path.basename(filename)}" is not kebab-case.`,
91
- });
92
- return;
93
- }
94
- if (!component && !(isPascalCase(base) || isKebabCase(base))) {
95
- context.report({
96
- node,
97
- message: `Filename "${path.basename(filename)}" does not match pasika conventions. Component files must be PascalCase.tsx (smart) or kebab-case.tsx (dumb).`,
98
- });
99
- }
100
- },
101
- };
102
- }
103
- // Everything else must be kebab-case
104
- if (!isKebabCase(base)) {
105
- return report(context, filename, ext);
106
- }
107
- return {};
108
- },
109
- };
110
- function report(context, filename, ext) {
111
- return {
112
- Program(node) {
113
- const convention = ext === ".tsx"
114
- ? "Component files must be PascalCase.tsx (smart) or kebab-case.tsx (dumb)."
115
- : "Non-component files must use kebab-case.";
116
- context.report({
117
- node,
118
- loc: { line: 1, column: 0 },
119
- message: `Filename "${path.basename(filename)}" does not match pasika conventions. ${convention}`,
120
- });
121
- },
122
- };
123
- }
@@ -1,12 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/hook-complexity
3
- *
4
- * - A custom hook with exactly one consumer MUST be extracted when it contains
5
- * two or more imperative categories.
6
- * - A custom hook with one consumer that contains fewer than two imperative
7
- * categories MUST stay inline in its consumer file.
8
- *
9
- * @see docs/code-organization-guide/rules/hook-extraction-rule.md
10
- */
11
- import type { Rule } from "eslint";
12
- export declare const hookComplexityRule: Rule.RuleModule;
@@ -1,114 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/hook-complexity
3
- *
4
- * - A custom hook with exactly one consumer MUST be extracted when it contains
5
- * two or more imperative categories.
6
- * - A custom hook with one consumer that contains fewer than two imperative
7
- * categories MUST stay inline in its consumer file.
8
- *
9
- * @see docs/code-organization-guide/rules/hook-extraction-rule.md
10
- */
11
- import path from "node:path";
12
- import ts from "typescript";
13
- const REACT_HOOKS = new Set([
14
- "useState",
15
- "useEffect",
16
- "useContext",
17
- "useReducer",
18
- "useCallback",
19
- "useMemo",
20
- "useRef",
21
- "useLayoutEffect",
22
- "useImperativeHandle",
23
- "useDebugValue",
24
- "useDeferredValue",
25
- "useTransition",
26
- "useId",
27
- "useSyncExternalStore",
28
- "useInsertionEffect",
29
- ]);
30
- function isHookName(name) {
31
- return /^use[A-Z]/.test(name);
32
- }
33
- /**
34
- * Counts the distinct React hook categories called anywhere inside a hook body.
35
- * Re-parses just the body's source slice with the TypeScript compiler so the
36
- * walk stays fully typed instead of unrolling ESTree unions by hand.
37
- */
38
- function countImperativeCategories(body, sourceText) {
39
- const start = body.range?.[0] ?? 0;
40
- const end = body.range?.[1] ?? sourceText.length;
41
- const sourceFile = ts.createSourceFile("hook.ts", sourceText.slice(start, end), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
42
- const categories = new Set();
43
- const visit = (node) => {
44
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && REACT_HOOKS.has(node.expression.text)) {
45
- categories.add(node.expression.text);
46
- }
47
- ts.forEachChild(node, visit);
48
- };
49
- visit(sourceFile);
50
- return categories.size;
51
- }
52
- export const hookComplexityRule = {
53
- meta: {
54
- schema: [],
55
- type: "problem",
56
- docs: {
57
- description: "Require extraction of complex single-consumer hooks and inline of simple ones.",
58
- },
59
- },
60
- create(context) {
61
- const filename = context.filename;
62
- const sourceRoot = path.resolve("src");
63
- const relative = path.relative(sourceRoot, filename);
64
- if (relative.startsWith(".."))
65
- return {};
66
- const segments = relative.split(path.sep);
67
- const sourceText = context.sourceCode.text;
68
- function checkHook(node, name, body, exported) {
69
- if (!exported)
70
- return;
71
- if (!name || !isHookName(name))
72
- return;
73
- if (!body)
74
- return;
75
- const imperativeCount = countImperativeCategories(body, sourceText);
76
- const parentFolder = segments.length >= 2 ? segments[segments.length - 2] : undefined;
77
- const inSupportFolder = parentFolder === "hooks";
78
- if (imperativeCount >= 2 && !inSupportFolder) {
79
- context.report({
80
- node,
81
- message: `Hook "${name}" has ${String(imperativeCount)} imperative categories and must be extracted to a hooks/ folder. ` +
82
- "See docs/code-organization-guide/rules/hook-extraction-rule.md",
83
- });
84
- }
85
- else if (imperativeCount < 2 && inSupportFolder) {
86
- context.report({
87
- node,
88
- message: `Hook "${name}" has fewer than two imperative categories and must stay inline in its consumer file. ` +
89
- "See docs/code-organization-guide/rules/hook-extraction-rule.md",
90
- });
91
- }
92
- }
93
- return {
94
- FunctionDeclaration(node) {
95
- const exported = node.parent?.type === "ExportNamedDeclaration";
96
- checkHook(node, node.id?.name, node.body, exported);
97
- },
98
- VariableDeclarator(node) {
99
- if (node.id.type !== "Identifier")
100
- return;
101
- const exported = node.parent.parent?.type === "ExportNamedDeclaration";
102
- if (!exported)
103
- return;
104
- const init = node.init;
105
- if (!init || (init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression")) {
106
- return;
107
- }
108
- if (init.body.type !== "BlockStatement")
109
- return;
110
- checkHook(node, node.id.name, init.body, true);
111
- },
112
- };
113
- },
114
- };
@@ -1,11 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/hook-extraction
3
- *
4
- * A custom hook MUST be extracted to its own file when two or more consumers
5
- * use it. The file must be read across the tree, because how many files import
6
- * a hook is not visible from one file.
7
- *
8
- * @see docs/code-organization-guide/rules/hook-extraction-rule.md
9
- */
10
- import type { Rule } from "eslint";
11
- export declare const hookExtractionRule: Rule.RuleModule;
@@ -1,55 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/hook-extraction
3
- *
4
- * A custom hook MUST be extracted to its own file when two or more consumers
5
- * use it. The file must be read across the tree, because how many files import
6
- * a hook is not visible from one file.
7
- *
8
- * @see docs/code-organization-guide/rules/hook-extraction-rule.md
9
- */
10
- import path from "node:path";
11
- import { getProjectIndex, symbolKey } from "../project/index.js";
12
- import { folderSegmentsOf, segmentsOf } from "../project/ccf.js";
13
- export const hookExtractionRule = {
14
- meta: {
15
- schema: [],
16
- type: "problem",
17
- docs: {
18
- description: "Require a custom hook with two or more consumers to live in its own hooks/ file.",
19
- },
20
- },
21
- create(context) {
22
- const sourceRoot = path.resolve("src");
23
- const file = path.resolve(context.filename);
24
- const segments = segmentsOf(file, sourceRoot);
25
- if (segments.length === 0)
26
- return {};
27
- const index = getProjectIndex(sourceRoot);
28
- if (!index)
29
- return {};
30
- const module = index.modules.get(file);
31
- if (!module)
32
- return {};
33
- const folder = folderSegmentsOf(file, sourceRoot);
34
- const alreadyInHooksFolder = folder.length > 0 && folder[folder.length - 1] === "hooks";
35
- const reusedHooks = module.exports
36
- .filter((exp) => exp.kind === "hook" && !alreadyInHooksFolder)
37
- .map((exp) => ({ exp, consumers: index.symbolConsumers.get(symbolKey(file, exp.name)) }))
38
- .filter(({ consumers }) => (consumers?.size ?? 0) >= 2);
39
- if (reusedHooks.length === 0)
40
- return {};
41
- return {
42
- Program(node) {
43
- for (const { exp, consumers } of reusedHooks) {
44
- context.report({
45
- node,
46
- loc: { line: exp.line, column: 0 },
47
- message: `Hook "${exp.name}" has ${String(consumers?.size ?? 0)} consumers; ` +
48
- "extract it to its own file in a hooks/ folder. " +
49
- "See docs/code-organization-guide/rules/hook-extraction-rule.md",
50
- });
51
- }
52
- },
53
- };
54
- },
55
- };
@@ -1,2 +0,0 @@
1
- import type { Rule } from "eslint";
2
- export declare const importBoundariesRule: Rule.RuleModule;