pasika 0.3.1 → 0.3.2

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 (166) hide show
  1. package/README.md +7 -5
  2. package/dist/cli/index.js +32 -38
  3. package/dist/cli/output.d.ts +3 -0
  4. package/dist/cli/output.js +11 -0
  5. package/dist/enforcement/coverage.js +25 -29
  6. package/dist/enforcement/doctor.d.ts +16 -0
  7. package/dist/enforcement/doctor.js +249 -0
  8. package/dist/enforcement/types.d.ts +0 -3
  9. package/dist/enforcement/types.js +4 -13
  10. package/dist/eslint/pasika/ast-types.d.ts +122 -0
  11. package/dist/eslint/pasika/ast-types.js +1 -0
  12. package/dist/eslint/pasika/index.d.ts +58 -3
  13. package/dist/eslint/pasika/index.js +61 -1
  14. package/dist/eslint/pasika/rules/application-structure.d.ts +2 -0
  15. package/dist/eslint/pasika/rules/application-structure.js +152 -0
  16. package/dist/eslint/pasika/rules/component-conventions.d.ts +16 -0
  17. package/dist/eslint/pasika/rules/component-conventions.js +145 -0
  18. package/dist/eslint/pasika/rules/component-nesting.d.ts +12 -0
  19. package/dist/eslint/pasika/rules/component-nesting.js +69 -0
  20. package/dist/eslint/pasika/rules/config-extraction.d.ts +14 -0
  21. package/dist/eslint/pasika/rules/config-extraction.js +74 -0
  22. package/dist/eslint/pasika/rules/cross-feature-import.d.ts +2 -0
  23. package/dist/eslint/pasika/rules/cross-feature-import.js +75 -0
  24. package/dist/eslint/pasika/rules/css/apply-usage.d.ts +10 -0
  25. package/dist/eslint/pasika/rules/css/apply-usage.js +45 -0
  26. package/dist/eslint/pasika/rules/css/base-layer-pair.d.ts +10 -0
  27. package/dist/eslint/pasika/rules/css/base-layer-pair.js +49 -0
  28. package/dist/eslint/pasika/rules/css/css-variable-naming.d.ts +10 -0
  29. package/dist/eslint/pasika/rules/css/css-variable-naming.js +39 -0
  30. package/dist/eslint/pasika/rules/css/custom-utility-apply.d.ts +11 -0
  31. package/dist/eslint/pasika/rules/css/custom-utility-apply.js +52 -0
  32. package/dist/eslint/pasika/rules/css/global-css-location.d.ts +10 -0
  33. package/dist/eslint/pasika/rules/css/global-css-location.js +42 -0
  34. package/dist/eslint/pasika/rules/css/helpers.d.ts +29 -0
  35. package/dist/eslint/pasika/rules/css/helpers.js +93 -0
  36. package/dist/eslint/pasika/rules/css/index.d.ts +13 -0
  37. package/dist/eslint/pasika/rules/css/index.js +25 -0
  38. package/dist/eslint/pasika/rules/css/root-variables.d.ts +11 -0
  39. package/dist/eslint/pasika/rules/css/root-variables.js +53 -0
  40. package/dist/eslint/pasika/rules/css/rule-tester.d.ts +11 -0
  41. package/dist/eslint/pasika/rules/css/rule-tester.js +22 -0
  42. package/dist/eslint/pasika/rules/css/stylesheet-ordering.d.ts +11 -0
  43. package/dist/eslint/pasika/rules/css/stylesheet-ordering.js +76 -0
  44. package/dist/eslint/pasika/rules/css/surface-utility.d.ts +10 -0
  45. package/dist/eslint/pasika/rules/css/surface-utility.js +41 -0
  46. package/dist/eslint/pasika/rules/css/theme-reset.d.ts +9 -0
  47. package/dist/eslint/pasika/rules/css/theme-reset.js +36 -0
  48. package/dist/eslint/pasika/rules/css/theme-variable-namespace.d.ts +11 -0
  49. package/dist/eslint/pasika/rules/css/theme-variable-namespace.js +53 -0
  50. package/dist/eslint/pasika/rules/cva-appearance-props.d.ts +10 -0
  51. package/dist/eslint/pasika/rules/cva-appearance-props.js +77 -0
  52. package/dist/eslint/pasika/rules/cva-boolean-variants.d.ts +11 -0
  53. package/dist/eslint/pasika/rules/cva-boolean-variants.js +97 -0
  54. package/dist/eslint/pasika/rules/data-testid-case.d.ts +2 -0
  55. package/dist/eslint/pasika/rules/data-testid-case.js +76 -0
  56. package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +13 -1
  57. package/dist/eslint/pasika/rules/enforce-cn-merge.js +46 -44
  58. package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +53 -48
  59. package/dist/eslint/pasika/rules/filename-case.js +28 -5
  60. package/dist/eslint/pasika/rules/hook-complexity.d.ts +12 -0
  61. package/dist/eslint/pasika/rules/hook-complexity.js +114 -0
  62. package/dist/eslint/pasika/rules/hook-extraction.d.ts +11 -0
  63. package/dist/eslint/pasika/rules/hook-extraction.js +55 -0
  64. package/dist/eslint/pasika/rules/import-through-index.d.ts +2 -0
  65. package/dist/eslint/pasika/rules/import-through-index.js +52 -0
  66. package/dist/eslint/pasika/rules/interactive-component.d.ts +10 -0
  67. package/dist/eslint/pasika/rules/interactive-component.js +76 -0
  68. package/dist/eslint/pasika/rules/json/index.d.ts +5 -0
  69. package/dist/eslint/pasika/rules/json/index.js +9 -0
  70. package/dist/eslint/pasika/rules/json/no-cache-flag.d.ts +10 -0
  71. package/dist/eslint/pasika/rules/json/no-cache-flag.js +45 -0
  72. package/dist/eslint/pasika/rules/json/no-vulyk-dependency.d.ts +10 -0
  73. package/dist/eslint/pasika/rules/json/no-vulyk-dependency.js +41 -0
  74. package/dist/eslint/pasika/rules/json/rule-tester.d.ts +11 -0
  75. package/dist/eslint/pasika/rules/json/rule-tester.js +21 -0
  76. package/dist/eslint/pasika/rules/jsx-hygiene.d.ts +9 -0
  77. package/dist/eslint/pasika/rules/jsx-hygiene.js +108 -0
  78. package/dist/eslint/pasika/rules/locale-dotted-path.d.ts +10 -0
  79. package/dist/eslint/pasika/rules/locale-dotted-path.js +65 -0
  80. package/dist/eslint/pasika/rules/locale-placement.d.ts +13 -0
  81. package/dist/eslint/pasika/rules/locale-placement.js +163 -0
  82. package/dist/eslint/pasika/rules/locales-location.d.ts +11 -0
  83. package/dist/eslint/pasika/rules/locales-location.js +56 -0
  84. package/dist/eslint/pasika/rules/md/doc-kind-suffix.d.ts +5 -0
  85. package/dist/eslint/pasika/rules/md/doc-kind-suffix.js +29 -0
  86. package/dist/eslint/pasika/rules/md/example-heading-description.d.ts +5 -0
  87. package/dist/eslint/pasika/rules/md/example-heading-description.js +30 -0
  88. package/dist/eslint/pasika/rules/md/glossary-term-linking.d.ts +2 -0
  89. package/dist/eslint/pasika/rules/md/glossary-term-linking.js +99 -0
  90. package/dist/eslint/pasika/rules/md/guide-folder-entry-point.d.ts +2 -0
  91. package/dist/eslint/pasika/rules/md/guide-folder-entry-point.js +46 -0
  92. package/dist/eslint/pasika/rules/md/guide-link-anchors.d.ts +5 -0
  93. package/dist/eslint/pasika/rules/md/guide-link-anchors.js +50 -0
  94. package/dist/eslint/pasika/rules/md/guide-overview-no-links.d.ts +5 -0
  95. package/dist/eslint/pasika/rules/md/guide-overview-no-links.js +51 -0
  96. package/dist/eslint/pasika/rules/md/guide-states-no-requirement.d.ts +5 -0
  97. package/dist/eslint/pasika/rules/md/guide-states-no-requirement.js +26 -0
  98. package/dist/eslint/pasika/rules/md/guide-step-single-link.d.ts +5 -0
  99. package/dist/eslint/pasika/rules/md/guide-step-single-link.js +48 -0
  100. package/dist/eslint/pasika/rules/md/guide-step-single-sentence.d.ts +5 -0
  101. package/dist/eslint/pasika/rules/md/guide-step-single-sentence.js +42 -0
  102. package/dist/eslint/pasika/rules/md/helpers.d.ts +25 -0
  103. package/dist/eslint/pasika/rules/md/helpers.js +37 -0
  104. package/dist/eslint/pasika/rules/md/index.d.ts +27 -0
  105. package/dist/eslint/pasika/rules/md/index.js +53 -0
  106. package/dist/eslint/pasika/rules/md/no-cross-document-link.d.ts +5 -0
  107. package/dist/eslint/pasika/rules/md/no-cross-document-link.js +36 -0
  108. package/dist/eslint/pasika/rules/md/no-nested-how-to.d.ts +5 -0
  109. package/dist/eslint/pasika/rules/md/no-nested-how-to.js +39 -0
  110. package/dist/eslint/pasika/rules/md/no-template-prompt.d.ts +5 -0
  111. package/dist/eslint/pasika/rules/md/no-template-prompt.js +22 -0
  112. package/dist/eslint/pasika/rules/md/overview-length.d.ts +5 -0
  113. package/dist/eslint/pasika/rules/md/overview-length.js +47 -0
  114. package/dist/eslint/pasika/rules/md/overview-present.d.ts +5 -0
  115. package/dist/eslint/pasika/rules/md/overview-present.js +41 -0
  116. package/dist/eslint/pasika/rules/md/policy-no-examples.d.ts +5 -0
  117. package/dist/eslint/pasika/rules/md/policy-no-examples.js +28 -0
  118. package/dist/eslint/pasika/rules/md/policy-single-document.d.ts +5 -0
  119. package/dist/eslint/pasika/rules/md/policy-single-document.js +34 -0
  120. package/dist/eslint/pasika/rules/md/policy-subject-headings.d.ts +5 -0
  121. package/dist/eslint/pasika/rules/md/policy-subject-headings.js +58 -0
  122. package/dist/eslint/pasika/rules/md/project-index.d.ts +17 -0
  123. package/dist/eslint/pasika/rules/md/project-index.js +67 -0
  124. package/dist/eslint/pasika/rules/md/reference-block-headings.d.ts +6 -0
  125. package/dist/eslint/pasika/rules/md/reference-block-headings.js +31 -0
  126. package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.d.ts +6 -0
  127. package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.js +26 -0
  128. package/dist/eslint/pasika/rules/md/requirement-present.d.ts +5 -0
  129. package/dist/eslint/pasika/rules/md/requirement-present.js +41 -0
  130. package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.d.ts +5 -0
  131. package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.js +46 -0
  132. package/dist/eslint/pasika/rules/md/rule-paired-examples.d.ts +5 -0
  133. package/dist/eslint/pasika/rules/md/rule-paired-examples.js +36 -0
  134. package/dist/eslint/pasika/rules/md/rule-tester.d.ts +11 -0
  135. package/dist/eslint/pasika/rules/md/rule-tester.js +21 -0
  136. package/dist/eslint/pasika/rules/md/support-document-placement.d.ts +2 -0
  137. package/dist/eslint/pasika/rules/md/support-document-placement.js +36 -0
  138. package/dist/eslint/pasika/rules/md/title-matches-file-name.d.ts +2 -0
  139. package/dist/eslint/pasika/rules/md/title-matches-file-name.js +46 -0
  140. package/dist/eslint/pasika/rules/named-exports.d.ts +2 -0
  141. package/dist/eslint/pasika/rules/named-exports.js +49 -0
  142. package/dist/eslint/pasika/rules/no-eslint-disable.d.ts +10 -0
  143. package/dist/eslint/pasika/rules/no-eslint-disable.js +33 -0
  144. package/dist/eslint/pasika/rules/no-mixed-concerns.js +9 -22
  145. package/dist/eslint/pasika/rules/no-util-barrel.d.ts +2 -0
  146. package/dist/eslint/pasika/rules/no-util-barrel.js +48 -0
  147. package/dist/eslint/pasika/rules/pure-function-extract.d.ts +9 -0
  148. package/dist/eslint/pasika/rules/pure-function-extract.js +92 -0
  149. package/dist/eslint/pasika/rules/shared-style-dedup.d.ts +13 -0
  150. package/dist/eslint/pasika/rules/shared-style-dedup.js +91 -0
  151. package/dist/eslint/pasika/rules/stay-flat.d.ts +12 -0
  152. package/dist/eslint/pasika/rules/stay-flat.js +81 -0
  153. package/dist/eslint/pasika/rules/support-folder-shape.d.ts +2 -0
  154. package/dist/eslint/pasika/rules/support-folder-shape.js +59 -0
  155. package/dist/eslint/pasika/rules/type-extraction.d.ts +13 -0
  156. package/dist/eslint/pasika/rules/type-extraction.js +85 -0
  157. package/dist/eslint/pasika/rules/ui-state.d.ts +9 -0
  158. package/dist/eslint/pasika/rules/ui-state.js +99 -0
  159. package/dist/eslint/pasika/rules/util-file-name.d.ts +2 -0
  160. package/dist/eslint/pasika/rules/util-file-name.js +48 -0
  161. package/dist/eslint/pasika/rules/value-extraction.d.ts +13 -0
  162. package/dist/eslint/pasika/rules/value-extraction.js +48 -0
  163. package/enforcement/registry.json +196 -190
  164. package/package.json +13 -5
  165. package/dist/enforcement/docs-check.d.ts +0 -17
  166. package/dist/enforcement/docs-check.js +0 -162
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Local AST node types for the parser-specific shapes ESLint's ESTree types
3
+ * do not carry: JSX nodes and TypeScript type nodes.
4
+ *
5
+ * Every extra field is optional so that any `Rule.Node` is assignable to these
6
+ * types — the handler parameters of the rules must accept whatever the parser
7
+ * hands them. Narrowing happens with `type`-discriminant checks inside each
8
+ * rule, exactly as in `no-arbitrary-tailwind.ts`.
9
+ */
10
+ import type { Rule } from "eslint";
11
+ import type * as ESTree from "estree";
12
+ /** JSXIdentifier — the parser-specific name node of a JSX element or attribute. */
13
+ export interface JsxIdentifier {
14
+ type: "JSXIdentifier";
15
+ name: string;
16
+ }
17
+ /** JSXMemberExpression — `<Foo.Bar>`, used by element and attribute names. */
18
+ export interface JsxMemberExpression {
19
+ type: "JSXMemberExpression";
20
+ object: JsxIdentifier | JsxMemberExpression;
21
+ property: JsxIdentifier;
22
+ }
23
+ /** JSXExpressionContainer — the `{expression}` part of a JSX attribute value. */
24
+ export interface JsxExpressionContainer {
25
+ type: "JSXExpressionContainer";
26
+ expression: ESTree.Expression;
27
+ }
28
+ /** JSXAttribute — `name="value"` or `name={expression}`. */
29
+ export type JsxAttribute = Omit<Rule.Node, "type"> & {
30
+ type: string;
31
+ name: JsxIdentifier;
32
+ value: ESTree.Literal | JsxExpressionContainer | null;
33
+ parent?: JsxOpeningElementNode;
34
+ };
35
+ /** JSXSpreadAttribute — `{...props}`. */
36
+ export interface JsxSpreadAttribute {
37
+ type: "JSXSpreadAttribute";
38
+ name?: never;
39
+ value?: never;
40
+ argument: ESTree.Expression;
41
+ }
42
+ /** Anything that can appear inside `attributes` of a JSX opening element. */
43
+ export type JsxAttributeLike = JsxAttribute | JsxSpreadAttribute;
44
+ /** JSXOpeningElement — the part before the closing `>` of an element. */
45
+ export interface JsxOpeningElement {
46
+ type: "JSXOpeningElement";
47
+ name: JsxIdentifier | JsxMemberExpression;
48
+ attributes: JsxAttributeLike[];
49
+ }
50
+ /** A node that may be a JSX element — used for parents and helper parameters. */
51
+ export type MaybeJsxElement = Rule.Node & {
52
+ openingElement?: JsxOpeningElement;
53
+ };
54
+ /** JSXElement as received by a `JSXElement` visitor. */
55
+ export type JsxElementNode = MaybeJsxElement & {
56
+ parent?: MaybeJsxElement;
57
+ };
58
+ /** JSXAttribute as received by a `JSXAttribute` visitor, with its parent chain. */
59
+ export type JsxAttributeNode = Rule.Node & {
60
+ name?: JsxIdentifier;
61
+ value?: ESTree.Literal | JsxExpressionContainer | null;
62
+ parent?: JsxOpeningElementNode;
63
+ };
64
+ /** JSXOpeningElement reached through a parent chain. */
65
+ export type JsxOpeningElementNode = Rule.Node & {
66
+ name?: JsxIdentifier;
67
+ attributes?: JsxAttributeLike[];
68
+ parent?: JsxElementNode;
69
+ };
70
+ /**
71
+ * FunctionDeclaration as seen by a rule visitor. ESLint's ESTree typing says
72
+ * `id` is always present, but an anonymous `export default function () {}`
73
+ * reaches the visitor with `id: null`, so the field is kept nullable.
74
+ */
75
+ export type FunctionDeclarationNode = Rule.Node & {
76
+ id?: {
77
+ name?: string;
78
+ } | null;
79
+ body?: ESTree.BlockStatement;
80
+ };
81
+ /**
82
+ * TSTypeAliasDeclaration — `type X = ...`, from the typescript-eslint parser.
83
+ *
84
+ * Kept as a plain `Rule.Node` intersection so it can be passed to
85
+ * `context.report`, which requires an ESTree node.
86
+ */
87
+ export type TsTypeAliasDeclarationNode = Rule.Node & {
88
+ id?: {
89
+ type?: string;
90
+ name?: string;
91
+ };
92
+ typeAnnotation?: TsTypeNode;
93
+ };
94
+ /**
95
+ * A member of a type literal (TSPropertySignature, TSIndexSignature, ...).
96
+ *
97
+ * The `type` field is widened (via Omit) so it can be compared against the
98
+ * parser-specific "TSPropertySignature" string that ESTree's union lacks.
99
+ */
100
+ export type TsTypeElementNode = Omit<Rule.Node, "type"> & {
101
+ type: string;
102
+ key?: {
103
+ type?: string;
104
+ name?: string;
105
+ };
106
+ typeAnnotation?: TsTypeAnnotationNode;
107
+ };
108
+ /** TSTypeAnnotation — the `: T` wrapper around a type node. */
109
+ export type TsTypeAnnotationNode = Omit<Rule.Node, "type"> & {
110
+ type: string;
111
+ typeAnnotation?: TsTypeNode;
112
+ };
113
+ /** Any TypeScript type node the rules read (TSTypeLiteral, TSUnionType, ...). */
114
+ export type TsTypeNode = Omit<Rule.Node, "type"> & {
115
+ type: string;
116
+ members?: TsTypeElementNode[];
117
+ types?: TsTypeNode[];
118
+ literal?: {
119
+ type?: string;
120
+ value?: unknown;
121
+ };
122
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -6,9 +6,64 @@
6
6
  * documented requirement each rule covers is recorded in
7
7
  * `enforcement/registry.json` and verified by `pasika coverage`.
8
8
  */
9
- import type { Linter, Rule } from "eslint";
10
- /** Every rule the plugin provides, keyed by its unprefixed name. */
11
- export declare const pasikaRules: Record<string, Rule.RuleModule>;
9
+ import type { Linter } from "eslint";
10
+ import { mdRules } from "./rules/md/index.js";
11
+ import { cssRules } from "./rules/css/index.js";
12
+ import { jsonRules } from "./rules/json/index.js";
13
+ export declare const pasikaRules: {
14
+ "component-placement": import("eslint").Rule.RuleModule;
15
+ "support-file-placement": import("eslint").Rule.RuleModule;
16
+ "application-structure": import("eslint").Rule.RuleModule;
17
+ "named-exports": import("eslint").Rule.RuleModule;
18
+ "data-testid-case": import("eslint").Rule.RuleModule;
19
+ "support-folder-shape": import("eslint").Rule.RuleModule;
20
+ "import-through-index": import("eslint").Rule.RuleModule;
21
+ "util-file-name": import("eslint").Rule.RuleModule;
22
+ "no-util-barrel": import("eslint").Rule.RuleModule;
23
+ "jsx-hygiene": import("eslint").Rule.RuleModule;
24
+ "interactive-component": import("eslint").Rule.RuleModule;
25
+ "ui-state": import("eslint").Rule.RuleModule;
26
+ "filename-case": import("eslint").Rule.RuleModule;
27
+ "import-boundaries": import("eslint").Rule.RuleModule;
28
+ "no-mixed-concerns": import("eslint").Rule.RuleModule;
29
+ "no-arbitrary-tailwind": import("eslint").Rule.RuleModule;
30
+ "enforce-cn-merge": {
31
+ meta: {
32
+ schema: never[];
33
+ type: "problem";
34
+ docs: {
35
+ description: string;
36
+ };
37
+ };
38
+ create(context: import("eslint").Rule.RuleContext): {
39
+ JSXAttribute(node: import("./ast-types.js").JsxAttributeNode): void;
40
+ };
41
+ };
42
+ "enforce-cva-variant-props": import("eslint").Rule.RuleModule;
43
+ "cva-appearance-props": import("eslint").Rule.RuleModule;
44
+ "cva-boolean-variants": import("eslint").Rule.RuleModule;
45
+ "enforce-barrel-exports": import("eslint").Rule.RuleModule;
46
+ "cross-feature-import": import("eslint").Rule.RuleModule;
47
+ "pure-function-extract": import("eslint").Rule.RuleModule;
48
+ "hook-complexity": import("eslint").Rule.RuleModule;
49
+ "locale-dotted-path": import("eslint").Rule.RuleModule;
50
+ "locales-location": import("eslint").Rule.RuleModule;
51
+ "hook-extraction": import("eslint").Rule.RuleModule;
52
+ "value-extraction": import("eslint").Rule.RuleModule;
53
+ "config-extraction": import("eslint").Rule.RuleModule;
54
+ "component-nesting": import("eslint").Rule.RuleModule;
55
+ "stay-flat": import("eslint").Rule.RuleModule;
56
+ "type-extraction": import("eslint").Rule.RuleModule;
57
+ "locale-placement": import("eslint").Rule.RuleModule;
58
+ "shared-style-dedup": import("eslint").Rule.RuleModule;
59
+ "no-eslint-disable": import("eslint").Rule.RuleModule;
60
+ };
61
+ export { mdRules, cssRules, jsonRules };
12
62
  /** Rule ids as they appear in configuration and in lint output. */
13
63
  export declare const pasikaRuleIds: string[];
64
+ export declare const pasikaMdRuleIds: string[];
65
+ export declare const pasikaCssRuleIds: string[];
66
+ export declare const pasikaJsonRuleIds: string[];
67
+ /** Every rule id, as they appear in configuration and in lint output. */
68
+ export declare const allPasikaRuleIds: string[];
14
69
  export declare const pasikaConfig: Linter.Config;
@@ -7,20 +7,80 @@ import { enforceCvaVariantPropsRule } from "./rules/enforce-cva-variant-props.js
7
7
  import { enforceBarrelExportsRule } from "./rules/enforce-barrel-exports.js";
8
8
  import { componentPlacementRule } from "./rules/component-placement.js";
9
9
  import { supportFilePlacementRule } from "./rules/support-file-placement.js";
10
- /** Every rule the plugin provides, keyed by its unprefixed name. */
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";
11
39
  export const pasikaRules = {
12
40
  "component-placement": componentPlacementRule,
13
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,
14
52
  "filename-case": filenameCaseRule,
15
53
  "import-boundaries": importBoundariesRule,
16
54
  "no-mixed-concerns": noMixedConcernsRule,
17
55
  "no-arbitrary-tailwind": noArbitraryTailwindRule,
18
56
  "enforce-cn-merge": enforceCnMergeRule,
19
57
  "enforce-cva-variant-props": enforceCvaVariantPropsRule,
58
+ "cva-appearance-props": cvaAppearancePropsRule,
59
+ "cva-boolean-variants": cvaBooleanVariantsRule,
20
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,
21
75
  };
76
+ export { mdRules, cssRules, jsonRules };
22
77
  /** Rule ids as they appear in configuration and in lint output. */
23
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];
24
84
  export const pasikaConfig = {
25
85
  files: ["src/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}"],
26
86
  plugins: {
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "eslint";
2
+ export declare const applicationStructureRule: Rule.RuleModule;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * ESLint rule: pasika/application-structure
3
+ *
4
+ * Enforces the path-based parts of the Application Structure, Configuration,
5
+ * and Component Placement rules. Questions about ownership or extraction stay
6
+ * with the cross-file rules because they need the project graph.
7
+ *
8
+ * @see docs/code-organization-guide/rules/application-structure-rule.md
9
+ * @see docs/code-organization-guide/rules/configuration-rule.md
10
+ * @see docs/code-organization-guide/rules/component-placement-rule.md
11
+ */
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { parseModule } from "../project/parse-module.js";
15
+ import { segmentsOf, SUPPORT_FOLDERS } from "../project/ccf.js";
16
+ const MODULE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
17
+ const ROUTING_FILES = new Set([
18
+ "default",
19
+ "error",
20
+ "instrumentation",
21
+ "layout",
22
+ "loading",
23
+ "middleware",
24
+ "not-found",
25
+ "page",
26
+ "route",
27
+ "template",
28
+ ]);
29
+ const EXPECTED_SUPPORT_FOLDER = {
30
+ hook: "hooks",
31
+ type: "types",
32
+ schema: "schemas",
33
+ constant: "constants",
34
+ function: "utils",
35
+ };
36
+ function report(context, message) {
37
+ return {
38
+ Program(node) {
39
+ context.report({ node, loc: { line: 1, column: 0 }, message });
40
+ },
41
+ };
42
+ }
43
+ function isCodeFile(filename) {
44
+ return MODULE_EXTENSIONS.has(path.extname(filename));
45
+ }
46
+ function isRootSupportFolder(folder) {
47
+ return SUPPORT_FOLDERS.has(folder);
48
+ }
49
+ function exportedKinds(filename) {
50
+ try {
51
+ return new Set(parseModule(filename).exports.map((moduleExport) => moduleExport.kind));
52
+ }
53
+ catch {
54
+ return new Set();
55
+ }
56
+ }
57
+ function expectedSupportFolder(kinds) {
58
+ for (const kind of ["component", "hook", "type", "schema", "constant", "function"]) {
59
+ if (kinds.has(kind))
60
+ return EXPECTED_SUPPORT_FOLDER[kind];
61
+ }
62
+ return undefined;
63
+ }
64
+ function configModuleRoot(filename, sourceRoot) {
65
+ const segments = segmentsOf(filename, sourceRoot);
66
+ if (segments[0] !== "config" || segments.length < 3)
67
+ return undefined;
68
+ return path.join(sourceRoot, "config", segments[1] ?? "");
69
+ }
70
+ export const applicationStructureRule = {
71
+ meta: {
72
+ schema: [],
73
+ type: "problem",
74
+ docs: {
75
+ description: "Enforce the repository's application, configuration, and support-folder structure.",
76
+ },
77
+ },
78
+ create(context) {
79
+ const filename = path.resolve(context.filename);
80
+ const sourceRoot = path.resolve("src");
81
+ const segments = segmentsOf(filename, sourceRoot);
82
+ if (segments.length === 0)
83
+ return {};
84
+ const [topLevel, secondLevel] = segments;
85
+ if (topLevel === undefined)
86
+ return {};
87
+ if (segments.length > 1 &&
88
+ topLevel !== "app" &&
89
+ topLevel !== "compositions" &&
90
+ topLevel !== "features" &&
91
+ topLevel !== "shared" &&
92
+ topLevel !== "config" &&
93
+ topLevel !== "locales" &&
94
+ !isRootSupportFolder(topLevel)) {
95
+ return report(context, `Move this source file under an allowed src/ folder; "src/${topLevel}/" is not part of the application structure.`);
96
+ }
97
+ if (topLevel === "features" && segments.length >= 2 && secondLevel !== undefined) {
98
+ if (SUPPORT_FOLDERS.has(secondLevel)) {
99
+ return report(context, `Move this support folder inside a feature folder; "src/features/${secondLevel}/" does not belong to a feature.`);
100
+ }
101
+ if (segments.length === 2) {
102
+ return report(context, "src/features/ must contain only feature folders, not source files.");
103
+ }
104
+ }
105
+ if (topLevel === "config") {
106
+ if (segments.length === 2) {
107
+ return report(context, "A configuration module must be a src/config/<config-name>/ folder with index.ts as its entry point.");
108
+ }
109
+ const moduleRoot = configModuleRoot(filename, sourceRoot);
110
+ if (moduleRoot && !fs.existsSync(path.join(moduleRoot, "index.ts"))) {
111
+ return report(context, `Add src/config/${path.basename(moduleRoot)}/index.ts as the configuration module entry point.`);
112
+ }
113
+ if (moduleRoot && segments.length === 3 && path.basename(filename) !== "index.ts") {
114
+ const kinds = exportedKinds(filename);
115
+ const expected = expectedSupportFolder(kinds);
116
+ if (expected !== undefined) {
117
+ return report(context, `Move this configuration support file into src/config/${path.basename(moduleRoot)}/${expected}/.`);
118
+ }
119
+ }
120
+ }
121
+ if (topLevel === "app" && isCodeFile(filename)) {
122
+ const basename = path.basename(filename, path.extname(filename));
123
+ const currentFolder = path.basename(path.dirname(filename));
124
+ if (SUPPORT_FOLDERS.has(currentFolder)) {
125
+ return report(context, "src/app/ may contain routing files and framework assets, but ordinary components and support files must live outside src/app/.");
126
+ }
127
+ if (!ROUTING_FILES.has(basename) && path.extname(filename) !== ".css") {
128
+ return report(context, "src/app/ may contain routing files and framework assets, but ordinary components and support files must live outside src/app/.");
129
+ }
130
+ }
131
+ const currentFolder = path.basename(path.dirname(filename));
132
+ const kinds = exportedKinds(filename);
133
+ const isConfigModuleRoot = topLevel === "config" && segments.length === 3 && path.basename(filename) === "index.ts";
134
+ if (isConfigModuleRoot)
135
+ return {};
136
+ if (!SUPPORT_FOLDERS.has(currentFolder)) {
137
+ const expected = expectedSupportFolder(kinds);
138
+ if (expected !== undefined && segments.length > 2) {
139
+ return report(context, `Move this file to a ${expected}/ folder; ${currentFolder}/ is not a recognized support folder.`);
140
+ }
141
+ return {};
142
+ }
143
+ if (kinds.has("component")) {
144
+ return report(context, `A support folder must not contain a component; move ${path.basename(filename)} beside ${currentFolder}/.`);
145
+ }
146
+ const expected = expectedSupportFolder(kinds);
147
+ if (expected !== undefined && expected !== currentFolder) {
148
+ return report(context, `Move this file to a ${expected}/ folder; ${currentFolder}/ is reserved for ${expected === "utils" ? "utilities" : expected}.`);
149
+ }
150
+ return {};
151
+ },
152
+ };
@@ -0,0 +1,16 @@
1
+ import ts from "typescript";
2
+ export interface ComponentInfo {
3
+ name: string;
4
+ declaration: ts.FunctionDeclaration | ts.VariableDeclaration;
5
+ smart: boolean;
6
+ }
7
+ export declare function parseComponentInfo(text: string, filename: string): ComponentInfo[];
8
+ export interface SimpleRoot {
9
+ tagName: string;
10
+ attributes: ts.JsxAttributeLike[];
11
+ }
12
+ export declare function findSimpleRoot(component: ComponentInfo, _text: string, _filename: string): SimpleRoot | undefined;
13
+ export declare function getTestId(root: SimpleRoot): {
14
+ value?: string;
15
+ attribute?: ts.JsxAttribute;
16
+ };
@@ -0,0 +1,145 @@
1
+ import path from "node:path";
2
+ import ts from "typescript";
3
+ const isPascalCase = (name) => /^[A-Z][A-Za-z0-9]*$/.test(name);
4
+ const isHookCallName = (name) => /^use[A-Z]/.test(name);
5
+ function isExported(node) {
6
+ if (!ts.canHaveModifiers(node))
7
+ return false;
8
+ return (ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
9
+ }
10
+ function containsJsx(node) {
11
+ let found = false;
12
+ const visit = (child) => {
13
+ if (found)
14
+ return;
15
+ if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {
16
+ found = true;
17
+ return;
18
+ }
19
+ ts.forEachChild(child, visit);
20
+ };
21
+ ts.forEachChild(node, visit);
22
+ return found;
23
+ }
24
+ function containsSmartCall(node) {
25
+ let found = false;
26
+ const visit = (child) => {
27
+ if (found)
28
+ return;
29
+ if (ts.isCallExpression(child)) {
30
+ const expression = child.expression;
31
+ let name = "";
32
+ if (ts.isIdentifier(expression)) {
33
+ name = expression.text;
34
+ }
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))
51
+ return undefined;
52
+ return { name, declaration: node, smart: containsSmartCall(node) };
53
+ }
54
+ function componentFromVariable(node) {
55
+ if (!ts.isIdentifier(node.name) || !isPascalCase(node.name.text))
56
+ return undefined;
57
+ const initializer = node.initializer;
58
+ if (!initializer || (!ts.isArrowFunction(initializer) && !ts.isFunctionExpression(initializer)))
59
+ return undefined;
60
+ if (!containsJsx(initializer))
61
+ return undefined;
62
+ return {
63
+ name: node.name.text,
64
+ declaration: node,
65
+ smart: containsSmartCall(initializer),
66
+ };
67
+ }
68
+ export function parseComponentInfo(text, filename) {
69
+ const sourceFile = ts.createSourceFile(path.resolve(filename), text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
70
+ const components = [];
71
+ for (const statement of sourceFile.statements) {
72
+ if (!isExported(statement))
73
+ continue;
74
+ if (ts.isFunctionDeclaration(statement)) {
75
+ const component = componentFromFunction(statement);
76
+ if (component)
77
+ components.push(component);
78
+ continue;
79
+ }
80
+ if (ts.isVariableStatement(statement)) {
81
+ for (const declaration of statement.declarationList.declarations) {
82
+ const component = componentFromVariable(declaration);
83
+ if (component)
84
+ components.push(component);
85
+ }
86
+ }
87
+ }
88
+ return components;
89
+ }
90
+ function jsxTagName(element) {
91
+ const tagName = ts.isJsxElement(element) ? element.openingElement.tagName : element.tagName;
92
+ return ts.isIdentifier(tagName) ? tagName.text : undefined;
93
+ }
94
+ function rootFromExpression(expression) {
95
+ if (ts.isJsxSelfClosingElement(expression)) {
96
+ const tagName = jsxTagName(expression);
97
+ if (!tagName?.startsWith(tagName[0]?.toLowerCase() ?? ""))
98
+ return undefined;
99
+ return { tagName, attributes: [...expression.attributes.properties] };
100
+ }
101
+ if (ts.isJsxElement(expression)) {
102
+ const tagName = jsxTagName(expression);
103
+ if (!tagName?.startsWith(tagName[0]?.toLowerCase() ?? ""))
104
+ return undefined;
105
+ return { tagName, attributes: [...expression.openingElement.attributes.properties] };
106
+ }
107
+ return undefined;
108
+ }
109
+ export function findSimpleRoot(component, _text, _filename) {
110
+ const declaration = component.declaration;
111
+ let body;
112
+ if (ts.isFunctionDeclaration(declaration))
113
+ body = declaration.body;
114
+ else if (declaration.initializer && ts.isArrowFunction(declaration.initializer))
115
+ body = declaration.initializer.body;
116
+ else if (declaration.initializer && ts.isFunctionExpression(declaration.initializer)) {
117
+ body = declaration.initializer.body;
118
+ }
119
+ if (!body)
120
+ return undefined;
121
+ if (!ts.isBlock(body))
122
+ return rootFromExpression(body);
123
+ const returns = [];
124
+ const visit = (node) => {
125
+ if (node !== body && (ts.isFunctionLike(node) || ts.isClassLike(node)))
126
+ return;
127
+ if (ts.isReturnStatement(node))
128
+ returns.push(node);
129
+ ts.forEachChild(node, visit);
130
+ };
131
+ visit(body);
132
+ if (returns.length !== 1)
133
+ return undefined;
134
+ const expression = returns[0]?.expression;
135
+ return expression && ts.isExpression(expression) ? rootFromExpression(expression) : undefined;
136
+ }
137
+ export function getTestId(root) {
138
+ const attribute = root.attributes.find((candidate) => ts.isJsxAttribute(candidate) && ts.isIdentifier(candidate.name) && candidate.name.text === "data-testid");
139
+ if (!attribute)
140
+ return {};
141
+ const value = attribute.initializer;
142
+ if (!value || !ts.isStringLiteral(value))
143
+ return { attribute };
144
+ return { value: value.text, attribute };
145
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * ESLint rule: pasika/component-nesting
3
+ *
4
+ * A component MUST NOT be nested only because it has support files. Nesting is
5
+ * justified by exclusive child components, so a nested component folder whose
6
+ * only contents are the component, its index, and support folders must be
7
+ * flattened.
8
+ *
9
+ * @see docs/code-organization-guide/rules/folder-nesting-rule.md
10
+ */
11
+ import type { Rule } from "eslint";
12
+ export declare const componentNestingRule: Rule.RuleModule;