pasika 0.3.3 → 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 (188) hide show
  1. package/README.md +13 -17
  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 +3 -3
  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 -55
  10. package/dist/enforcement/coverage.js +0 -239
  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 -98
  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
@@ -1,97 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/no-arbitrary-tailwind
3
- *
4
- * Enforces the "Arbitrary Value Rule" — no arbitrary-value Tailwind classes.
5
- *
6
- * @see docs/styling-guide/rules/arbitrary-value-rule.md
7
- */
8
- /**
9
- * Matches Tailwind arbitrary-value utility classes like rounded-[13px], text-[#fff].
10
- *
11
- * Does NOT match variant prefixes like min-[400px]:flex-row or md:flex.
12
- * The negative lookahead (?![-\w]*:) excludes anything followed by a colon + more.
13
- *
14
- * Note: Tailwind breakpoint variants (sm:, md:, lg:, etc.) don't use brackets,
15
- * so they're never matched. Only bracket-based variants like min-[400px]: need
16
- * the lookahead exclusion.
17
- */
18
- const ARBITRARY_VALUE_RE = /(?:^|(?<=\s))(?:[a-z]+(?:-[a-z]+)*)-\[[^\]]+\](?![-\w]*:)/g;
19
- const CLASS_HELPERS = new Set(["cn", "clsx", "twMerge", "twJoin"]);
20
- export const noArbitraryTailwindRule = {
21
- meta: {
22
- schema: [],
23
- type: "problem",
24
- docs: {
25
- description: "Disallow Tailwind arbitrary-value classes like rounded-[13px].",
26
- },
27
- },
28
- create(context) {
29
- function reportClassString(node, value) {
30
- ARBITRARY_VALUE_RE.lastIndex = 0;
31
- const match = ARBITRARY_VALUE_RE.exec(value);
32
- if (!match)
33
- return;
34
- context.report({
35
- node,
36
- message: `Tailwind arbitrary-value class "${match[0]}" is not allowed. ` +
37
- "Use a named token or custom utility. " +
38
- "See docs/styling-guide/rules/arbitrary-value-rule.md",
39
- });
40
- }
41
- /** Walks every expression a class name can hide in: conditionals, arrays, and object keys. */
42
- function checkExpression(node, expression) {
43
- if (!expression)
44
- return;
45
- if (expression.type === "Literal") {
46
- if (typeof expression.value === "string")
47
- reportClassString(node, expression.value);
48
- return;
49
- }
50
- if (expression.type === "TemplateLiteral") {
51
- for (const quasi of expression.quasis)
52
- reportClassString(node, quasi.value.raw);
53
- return;
54
- }
55
- if (expression.type === "LogicalExpression") {
56
- checkExpression(node, expression.left);
57
- checkExpression(node, expression.right);
58
- return;
59
- }
60
- if (expression.type === "ConditionalExpression") {
61
- checkExpression(node, expression.consequent);
62
- checkExpression(node, expression.alternate);
63
- return;
64
- }
65
- if (expression.type === "ArrayExpression") {
66
- for (const element of expression.elements)
67
- checkExpression(node, element);
68
- return;
69
- }
70
- if (expression.type === "ObjectExpression") {
71
- // clsx and cn accept `{ "px-[3px]": isActive }`, so the keys carry classes.
72
- for (const property of expression.properties) {
73
- if (property.type === "Property")
74
- checkExpression(node, property.key);
75
- }
76
- }
77
- }
78
- return {
79
- JSXAttribute(node) {
80
- const attributeName = node.name?.name;
81
- if (attributeName !== "className" && attributeName !== "class")
82
- return;
83
- const value = node.value;
84
- if (!value)
85
- return;
86
- checkExpression(node, value.type === "JSXExpressionContainer" ? value.expression : value);
87
- },
88
- CallExpression(node) {
89
- if (node.callee.type !== "Identifier" || !CLASS_HELPERS.has(node.callee.name))
90
- return;
91
- for (const argument of node.arguments) {
92
- checkExpression(node, argument);
93
- }
94
- },
95
- };
96
- },
97
- };
@@ -1,10 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/no-eslint-disable
3
- *
4
- * Code MUST NOT use eslint-disable directives, and a reported violation MUST
5
- * be fixed instead.
6
- *
7
- * @see docs/agent-policy.md
8
- */
9
- import type { Rule } from "eslint";
10
- export declare const noEslintDisableRule: Rule.RuleModule;
@@ -1,33 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/no-eslint-disable
3
- *
4
- * Code MUST NOT use eslint-disable directives, and a reported violation MUST
5
- * be fixed instead.
6
- *
7
- * @see docs/agent-policy.md
8
- */
9
- export const noEslintDisableRule = {
10
- meta: {
11
- schema: [],
12
- type: "problem",
13
- docs: {
14
- description: "Forbid eslint-disable directives.",
15
- },
16
- },
17
- create(context) {
18
- return {
19
- Program() {
20
- for (const comment of context.sourceCode.getAllComments()) {
21
- if (!comment.value.includes("eslint-disable"))
22
- continue;
23
- if (!comment.loc)
24
- continue;
25
- context.report({
26
- loc: comment.loc,
27
- message: "Do not use eslint-disable directives; fix the reported violation instead.",
28
- });
29
- }
30
- },
31
- };
32
- },
33
- };
@@ -1,9 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/no-mixed-concerns
3
- *
4
- * Enforces the "No Mixed Concerns Rule" — one React component per .tsx file.
5
- *
6
- * @see docs/code-organization-guide/rules/no-mixed-concerns-rule.md
7
- */
8
- import type { Rule } from "eslint";
9
- export declare const noMixedConcernsRule: Rule.RuleModule;
@@ -1,71 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/no-mixed-concerns
3
- *
4
- * Enforces the "No Mixed Concerns Rule" — one React component per .tsx file.
5
- *
6
- * @see docs/code-organization-guide/rules/no-mixed-concerns-rule.md
7
- */
8
- function getExportName(declaration) {
9
- if (declaration?.type === "FunctionDeclaration" && declaration.id) {
10
- return declaration.id.name ?? null;
11
- }
12
- if (declaration?.type === "VariableDeclaration") {
13
- const declarator = declaration.declarations?.[0];
14
- if (declarator?.id?.type === "Identifier" &&
15
- (declarator.init?.type === "ArrowFunctionExpression" || declarator.init?.type === "FunctionExpression")) {
16
- return declarator.id.name ?? null;
17
- }
18
- }
19
- if (declaration?.type === "ArrowFunctionExpression") {
20
- return "default";
21
- }
22
- if (declaration?.type === "FunctionExpression" && declaration.id) {
23
- return declaration.id.name ?? null;
24
- }
25
- return null;
26
- }
27
- export const noMixedConcernsRule = {
28
- meta: {
29
- schema: [],
30
- type: "problem",
31
- docs: {
32
- description: "Enforce one exported React component per .tsx file.",
33
- },
34
- },
35
- create(context) {
36
- if (!context.filename.endsWith(".tsx"))
37
- return {};
38
- let exportedComponentCount = 0;
39
- const extraExports = [];
40
- function registerExport(name) {
41
- exportedComponentCount++;
42
- if (exportedComponentCount > 1) {
43
- extraExports.push({ name });
44
- }
45
- }
46
- return {
47
- ExportNamedDeclaration(node) {
48
- const name = getExportName(node.declaration);
49
- if (name)
50
- registerExport(name);
51
- },
52
- ExportDefaultDeclaration(node) {
53
- const name = getExportName(node.declaration);
54
- if (name)
55
- registerExport(name);
56
- },
57
- "Program:exit"() {
58
- if (exportedComponentCount > 1) {
59
- for (const extra of extraExports) {
60
- context.report({
61
- loc: { line: 1, column: 0 },
62
- message: `File exports multiple components. "${extra.name}" is an extra component export. ` +
63
- "Move it to its own file. Each .tsx file MUST contain exactly one component. " +
64
- "See docs/code-organization-guide/rules/no-mixed-concerns-rule.md",
65
- });
66
- }
67
- }
68
- },
69
- };
70
- },
71
- };
@@ -1,2 +0,0 @@
1
- import type { Rule } from "eslint";
2
- export declare const noUtilBarrelRule: Rule.RuleModule;
@@ -1,48 +0,0 @@
1
- import path from "node:path";
2
- import { resolveSpecifier } from "../project/index.js";
3
- export const noUtilBarrelRule = {
4
- meta: {
5
- schema: [],
6
- type: "problem",
7
- docs: {
8
- description: "Require utilities to be imported directly without a barrel.",
9
- },
10
- },
11
- create(context) {
12
- const filename = path.resolve(context.filename);
13
- const sourceRoot = sourceRootOf(filename);
14
- return {
15
- Program(node) {
16
- for (const specifier of importSpecifiers(context.sourceCode.text)) {
17
- const target = resolveSpecifier(filename, specifier, sourceRoot);
18
- if (!target)
19
- continue;
20
- const segments = target.replace(/\\/g, "/").split("/");
21
- const utilsIndex = segments.lastIndexOf("utils");
22
- if (utilsIndex < 0 || !path.basename(target).startsWith("index."))
23
- continue;
24
- context.report({
25
- node,
26
- message: `Import utilities directly instead of through "${specifier}". ` +
27
- "See docs/code-organization-guide/rules/utilities-rule.md",
28
- });
29
- }
30
- },
31
- };
32
- },
33
- };
34
- function importSpecifiers(source) {
35
- const specifiers = [];
36
- const pattern = /\bimport\s+(?:(?:type\s+)?[\s\S]*?\sfrom\s+)?["'](?<specifier>[^"']+)["']/g;
37
- for (const match of source.matchAll(pattern)) {
38
- const specifier = match.groups?.specifier;
39
- if (specifier)
40
- specifiers.push(specifier);
41
- }
42
- return specifiers;
43
- }
44
- function sourceRootOf(filename) {
45
- const marker = `${path.sep}src${path.sep}`;
46
- const srcIndex = filename.lastIndexOf(marker);
47
- return srcIndex >= 0 ? filename.slice(0, srcIndex + marker.length - 1) : path.resolve("src");
48
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/pure-function-extract
3
- *
4
- * A pure function MUST be extracted to utils/, even when it has one consumer.
5
- *
6
- * @see docs/code-organization-guide/rules/utilities-rule.md
7
- */
8
- import type { Rule } from "eslint";
9
- export declare const pureFunctionExtractRule: Rule.RuleModule;
@@ -1,92 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/pure-function-extract
3
- *
4
- * A pure function MUST be extracted to utils/, even when it has one consumer.
5
- *
6
- * @see docs/code-organization-guide/rules/utilities-rule.md
7
- */
8
- import path from "node:path";
9
- function isComponentLikeName(name) {
10
- return /^[A-Z]/.test(name);
11
- }
12
- function isHookName(name) {
13
- return /^use[A-Z]/.test(name);
14
- }
15
- function hasHookUsage(body) {
16
- for (const stmt of body.body) {
17
- if (stmt.type !== "ExpressionStatement")
18
- continue;
19
- const expr = stmt.expression;
20
- if (expr.type === "CallExpression" && expr.callee.type === "Identifier" && isHookName(expr.callee.name)) {
21
- return true;
22
- }
23
- }
24
- return false;
25
- }
26
- export const pureFunctionExtractRule = {
27
- meta: {
28
- schema: [],
29
- type: "problem",
30
- docs: {
31
- description: "Require pure functions declared in component files to be extracted to utils/.",
32
- },
33
- },
34
- create(context) {
35
- const filename = context.filename;
36
- if (!filename.endsWith(".tsx") && !filename.endsWith(".jsx"))
37
- return {};
38
- const sourceRoot = path.resolve("src");
39
- const relative = path.relative(sourceRoot, filename);
40
- if (relative.startsWith(".."))
41
- return {};
42
- const segments = relative.split(path.sep);
43
- // Already in utils/ or a support folder — fine
44
- if (segments[0] === "utils")
45
- return {};
46
- if (segments[0] === "app")
47
- return {};
48
- const supportFolders = new Set(["hooks", "types", "schemas", "constants", "utils"]);
49
- if (segments.length >= 2 && supportFolders.has(segments[segments.length - 1] ?? ""))
50
- return {};
51
- function report(node, name) {
52
- context.report({
53
- node,
54
- message: `Extract pure function "${name}" to utils/. See docs/code-organization-guide/rules/utilities-rule.md`,
55
- });
56
- }
57
- return {
58
- FunctionDeclaration(node) {
59
- const exported = node.parent?.type === "ExportNamedDeclaration";
60
- if (!exported)
61
- return;
62
- const name = node.id?.name;
63
- if (!name)
64
- return;
65
- if (isComponentLikeName(name) || isHookName(name))
66
- return;
67
- if (!node.body || hasHookUsage(node.body))
68
- return;
69
- report(node, name);
70
- },
71
- VariableDeclarator(node) {
72
- if (node.id.type !== "Identifier")
73
- return;
74
- const name = node.id.name;
75
- if (!name)
76
- return;
77
- const exported = node.parent.parent?.type === "ExportNamedDeclaration";
78
- if (!exported)
79
- return;
80
- if (isComponentLikeName(name) || isHookName(name))
81
- return;
82
- const init = node.init;
83
- if (!init || (init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression")) {
84
- return;
85
- }
86
- if (init.body.type === "BlockStatement" && hasHookUsage(init.body))
87
- return;
88
- report(node, name);
89
- },
90
- };
91
- },
92
- };
@@ -1,13 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/shared-style-dedup
3
- *
4
- * A style or group of styles that two or more components use together and
5
- * should change together MUST become one named custom utility. Styling used by
6
- * only one component MUST stay in that component file as static Tailwind
7
- * utility classes. The rule compares className lists across the whole tree,
8
- * because a single file cannot see what other components use.
9
- *
10
- * @see docs/styling-guide/rules/theme-and-utility-definition-rule.md
11
- */
12
- import type { Rule } from "eslint";
13
- export declare const sharedStyleDedupRule: Rule.RuleModule;
@@ -1,91 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/shared-style-dedup
3
- *
4
- * A style or group of styles that two or more components use together and
5
- * should change together MUST become one named custom utility. Styling used by
6
- * only one component MUST stay in that component file as static Tailwind
7
- * utility classes. The rule compares className lists across the whole tree,
8
- * because a single file cannot see what other components use.
9
- *
10
- * @see docs/styling-guide/rules/theme-and-utility-definition-rule.md
11
- */
12
- import path from "node:path";
13
- import { readFileSync, statSync } from "node:fs";
14
- import { getProjectIndex } from "../project/index.js";
15
- import { segmentsOf } from "../project/ccf.js";
16
- /** Static className string literals; dynamic class lists are not comparable across files. */
17
- const CLASS_NAME = /className="(?<classes>[^"]+)"/g;
18
- let comboCache;
19
- /** className combinations used by each component file, memoized per tree. */
20
- function combosFor(index) {
21
- const files = [...index.modules.keys()]
22
- .filter((file) => file.endsWith(".tsx") || file.endsWith(".jsx"))
23
- .sort((left, right) => left.localeCompare(right));
24
- let fingerprint = "";
25
- for (const file of files) {
26
- try {
27
- fingerprint += String(statSync(file).mtimeMs);
28
- }
29
- catch {
30
- fingerprint += "0";
31
- }
32
- }
33
- if (comboCache?.sourceRoot === index.sourceRoot && comboCache.fingerprint === fingerprint) {
34
- return comboCache.combos;
35
- }
36
- const combos = new Map();
37
- for (const file of files) {
38
- const text = readFileSync(file, "utf8");
39
- for (const match of text.matchAll(CLASS_NAME)) {
40
- const classes = (match.groups?.classes ?? "").split(/\s+/).filter(Boolean);
41
- if (classes.length < 2)
42
- continue;
43
- const key = [...classes].sort().join(" ");
44
- const users = combos.get(key) ?? new Set();
45
- users.add(file);
46
- combos.set(key, users);
47
- }
48
- }
49
- comboCache = { sourceRoot: index.sourceRoot, fingerprint, combos };
50
- return combos;
51
- }
52
- export const sharedStyleDedupRule = {
53
- meta: {
54
- schema: [],
55
- type: "problem",
56
- docs: {
57
- description: "Require a className combination used by two or more components to become a named custom utility.",
58
- },
59
- },
60
- create(context) {
61
- const sourceRoot = path.resolve("src");
62
- const file = path.resolve(context.filename);
63
- const segments = segmentsOf(file, sourceRoot);
64
- if (segments.length === 0)
65
- return {};
66
- const index = getProjectIndex(sourceRoot);
67
- if (!index)
68
- return {};
69
- // Report each shared combination once, on the alphabetically first file
70
- // that uses it, so the whole tree produces one finding per combination.
71
- const combos = combosFor(index);
72
- const sharedHere = [...combos.entries()]
73
- .filter(([, users]) => users.size >= 2 && users.has(file))
74
- .filter(([, users]) => [...users].sort((left, right) => left.localeCompare(right))[0] === file);
75
- if (sharedHere.length === 0)
76
- return {};
77
- return {
78
- Program(node) {
79
- for (const [combo, users] of sharedHere) {
80
- context.report({
81
- node,
82
- loc: { line: 1, column: 0 },
83
- message: `Class combination "${combo}" is used by ${String(users.size)} components and should change ` +
84
- "together; create a named custom Tailwind utility for it. " +
85
- "See docs/styling-guide/rules/theme-and-utility-definition-rule.md",
86
- });
87
- }
88
- },
89
- };
90
- },
91
- };
@@ -1,12 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/stay-flat
3
- *
4
- * A component MUST stay flat until it has one or more exclusive child
5
- * components, then MUST be nested in a folder with the same name. An exclusive
6
- * child is a sibling component that only this component uses, so a flat
7
- * component that owns such children must move into a folder named after it.
8
- *
9
- * @see docs/code-organization-guide/rules/folder-nesting-rule.md
10
- */
11
- import type { Rule } from "eslint";
12
- export declare const stayFlatRule: Rule.RuleModule;
@@ -1,81 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/stay-flat
3
- *
4
- * A component MUST stay flat until it has one or more exclusive child
5
- * components, then MUST be nested in a folder with the same name. An exclusive
6
- * child is a sibling component that only this component uses, so a flat
7
- * component that owns such children must move into a folder named after it.
8
- *
9
- * @see docs/code-organization-guide/rules/folder-nesting-rule.md
10
- */
11
- import path from "node:path";
12
- import { getProjectIndex } from "../project/index.js";
13
- import { segmentsOf } from "../project/ccf.js";
14
- export const stayFlatRule = {
15
- meta: {
16
- schema: [],
17
- type: "problem",
18
- docs: {
19
- description: "Require a component with exclusive children to be nested in a folder with the same name.",
20
- },
21
- },
22
- create(context) {
23
- const sourceRoot = path.resolve("src");
24
- const file = path.resolve(context.filename);
25
- const segments = segmentsOf(file, sourceRoot);
26
- // A component file directly inside a feature folder: src/features/<feature>/<file>
27
- if (segments.length !== 3 || segments[0] !== "features")
28
- return {};
29
- const index = getProjectIndex(sourceRoot);
30
- if (!index)
31
- return {};
32
- const module = index.modules.get(file);
33
- if (!module?.exports.some((exp) => exp.kind === "component"))
34
- return {};
35
- const featureName = segments[1];
36
- if (!featureName)
37
- return {};
38
- // A sibling component file is an exclusive child when every file that
39
- // imports it is this component or a support file in the same feature folder.
40
- const exclusiveChildren = [...index.modules.values()].filter((candidate) => {
41
- if (candidate.file === file)
42
- return false;
43
- const candidateSegments = segmentsOf(candidate.file, sourceRoot);
44
- if (candidateSegments.length !== 3)
45
- return false;
46
- if (candidateSegments[0] !== "features" || candidateSegments[1] !== featureName)
47
- return false;
48
- if (candidateSegments[2]?.startsWith("index."))
49
- return false;
50
- if (!candidate.exports.some((exp) => exp.kind === "component"))
51
- return false;
52
- const consumers = index.consumers.get(candidate.file) ?? new Set();
53
- if (consumers.size === 0)
54
- return false;
55
- return [...consumers].every((consumer) => {
56
- if (consumer === file)
57
- return true;
58
- const consumerSegments = segmentsOf(consumer, sourceRoot);
59
- if (consumerSegments[0] !== "features" || consumerSegments[1] !== featureName)
60
- return false;
61
- return !(index.modules.get(consumer)?.exports.some((exp) => exp.kind === "component") ?? false);
62
- });
63
- });
64
- if (exclusiveChildren.length === 0)
65
- return {};
66
- const childNames = exclusiveChildren
67
- .map((child) => child.exports.find((exp) => exp.kind === "component")?.name)
68
- .filter((name) => name !== undefined);
69
- return {
70
- Program(node) {
71
- context.report({
72
- node,
73
- loc: { line: 1, column: 0 },
74
- message: `This component has exclusive child component(s) ${childNames.join(", ")}; ` +
75
- `nest it in a folder named after it inside src/features/${featureName}/. ` +
76
- "See docs/code-organization-guide/rules/folder-nesting-rule.md",
77
- });
78
- },
79
- };
80
- },
81
- };
@@ -1,15 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/support-file-placement
3
- *
4
- * Enforces where a hook, type, schema, constant, or utility lives, which depends
5
- * on the files that import it and so cannot be decided from one file.
6
- *
7
- * @see docs/code-organization-guide/rules/types-and-schemas-rule.md
8
- * @see docs/code-organization-guide/rules/constants-rule.md
9
- * @see docs/code-organization-guide/rules/utilities-rule.md
10
- * @see docs/code-organization-guide/rules/hook-extraction-rule.md
11
- * @see docs/code-organization-guide/rules/configuration-rule.md
12
- * @see docs/code-organization-guide/rules/folder-nesting-rule.md
13
- */
14
- import type { Rule } from "eslint";
15
- export declare const supportFilePlacementRule: Rule.RuleModule;
@@ -1,70 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/support-file-placement
3
- *
4
- * Enforces where a hook, type, schema, constant, or utility lives, which depends
5
- * on the files that import it and so cannot be decided from one file.
6
- *
7
- * @see docs/code-organization-guide/rules/types-and-schemas-rule.md
8
- * @see docs/code-organization-guide/rules/constants-rule.md
9
- * @see docs/code-organization-guide/rules/utilities-rule.md
10
- * @see docs/code-organization-guide/rules/hook-extraction-rule.md
11
- * @see docs/code-organization-guide/rules/configuration-rule.md
12
- * @see docs/code-organization-guide/rules/folder-nesting-rule.md
13
- */
14
- import path from "node:path";
15
- import { getProjectIndex } from "../project/index.js";
16
- import { describeConsumers, folderSegmentsOf, formatFolder, isConfigModule, resolveSupportPlacement, segmentsOf, SUPPORT_FOLDERS, } from "../project/ccf.js";
17
- /**
18
- * Support folders a configuration module keeps regardless of who imports them: a
19
- * constant or type whose meaning is derived from the configuration stays beside
20
- * it even when consumers exist outside the module.
21
- */
22
- const CONFIG_OWNED_FOLDERS = new Set(["types", "constants"]);
23
- const REASON_TEXT = {
24
- "app-consumer": "a file under src/app/ imports it, so it belongs to the app-wide support folder",
25
- "config-module": "every file that imports it belongs to that configuration module",
26
- ccf: "that is the closest folder its consumers share",
27
- "across-features": "its consumers span more than one feature, so no feature can own it",
28
- "across-layers": "its consumers span more than one layer, so no layer can own it",
29
- };
30
- const sameFolder = (left, right) => left.length === right.length && left.every((segment, depth) => segment === right[depth]);
31
- export const supportFilePlacementRule = {
32
- meta: {
33
- schema: [],
34
- type: "problem",
35
- docs: {
36
- description: "Enforce that a support file lives in the folder its consumers imply.",
37
- },
38
- },
39
- create(context) {
40
- const sourceRoot = path.resolve("src");
41
- const supportFile = path.resolve(context.filename);
42
- const currentFolder = folderSegmentsOf(supportFile, sourceRoot);
43
- const supportFolder = currentFolder[currentFolder.length - 1];
44
- // Only a file already inside a support folder is placed by this rule; a
45
- // declaration still sitting beside its consumer is an extraction question.
46
- if (supportFolder === undefined || !SUPPORT_FOLDERS.has(supportFolder))
47
- return {};
48
- // A type or constant whose meaning comes from a configuration may stay with
49
- // it however widely it is used, so its consumers cannot place it.
50
- if (isConfigModule(segmentsOf(supportFile, sourceRoot)) && CONFIG_OWNED_FOLDERS.has(supportFolder))
51
- return {};
52
- const index = getProjectIndex(sourceRoot);
53
- if (!index)
54
- return {};
55
- const placement = resolveSupportPlacement(supportFile, supportFolder, index);
56
- if (!placement || sameFolder(currentFolder, placement.expectedFolder))
57
- return {};
58
- return {
59
- Program(node) {
60
- context.report({
61
- node,
62
- loc: { line: 1, column: 0 },
63
- message: `Move this file to ${formatFolder(placement.expectedFolder)} — ` +
64
- `${REASON_TEXT[placement.reason] ?? "that is where its consumers place it"}. ` +
65
- `Imported by ${describeConsumers(placement.countedConsumers, sourceRoot)}.`,
66
- });
67
- },
68
- };
69
- },
70
- };
@@ -1,2 +0,0 @@
1
- import type { Rule } from "eslint";
2
- export declare const supportFolderShapeRule: Rule.RuleModule;