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,27 +0,0 @@
1
- /** What an exported name is, as far as the placement rules need to care. */
2
- export type ExportKind = "component" | "hook" | "type" | "schema" | "constant" | "function" | "other";
3
- export interface ModuleExport {
4
- name: string;
5
- kind: ExportKind;
6
- line: number;
7
- }
8
- export interface ModuleImport {
9
- /** The specifier exactly as written. */
10
- specifier: string;
11
- /** Names taken from the module; empty for a side-effect or namespace import. */
12
- names: string[];
13
- line: number;
14
- }
15
- export interface ParsedModule {
16
- file: string;
17
- imports: ModuleImport[];
18
- exports: ModuleExport[];
19
- }
20
- /**
21
- * Parses one module for the names it exports and the modules it imports.
22
- *
23
- * Uses the TypeScript parser without a program or typechecker: the placement
24
- * rules only need the shape of the import and export statements, and parsing a
25
- * file in isolation keeps this fast enough to run over a whole tree.
26
- */
27
- export declare function parseModule(file: string): ParsedModule;
@@ -1,128 +0,0 @@
1
- import path from "node:path";
2
- import { readFileSync } from "node:fs";
3
- import ts from "typescript";
4
- const isPascalCase = (name) => /^[A-Z][A-Za-z0-9]*$/.test(name);
5
- const isHookName = (name) => /^use[A-Z]/.test(name);
6
- const isSchemaName = (name) => /[Ss]chema$/.test(name);
7
- function lineOf(sourceFile, node) {
8
- return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
9
- }
10
- function returnsJsx(node) {
11
- let found = false;
12
- const visit = (child) => {
13
- if (found)
14
- return;
15
- if (ts.isJsxElement(child) ||
16
- ts.isJsxSelfClosingElement(child) ||
17
- ts.isJsxFragment(child) ||
18
- ts.isJsxOpeningFragment(child)) {
19
- found = true;
20
- return;
21
- }
22
- ts.forEachChild(child, visit);
23
- };
24
- ts.forEachChild(node, visit);
25
- return found;
26
- }
27
- function classifyFunction(name, isTsx, hasJsx) {
28
- if (isHookName(name))
29
- return "hook";
30
- if (isTsx && isPascalCase(name) && hasJsx)
31
- return "component";
32
- return "function";
33
- }
34
- function classifyValue(name, initializer, isTsx) {
35
- const isFunctionLike = initializer !== undefined &&
36
- (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer) || ts.isFunctionDeclaration(initializer));
37
- if (isHookName(name))
38
- return "hook";
39
- if (isSchemaName(name))
40
- return "schema";
41
- if (isTsx && isPascalCase(name) && (initializer === undefined || returnsJsx(initializer) || isFunctionLike)) {
42
- return "component";
43
- }
44
- if (isFunctionLike)
45
- return "function";
46
- return "constant";
47
- }
48
- /**
49
- * Parses one module for the names it exports and the modules it imports.
50
- *
51
- * Uses the TypeScript parser without a program or typechecker: the placement
52
- * rules only need the shape of the import and export statements, and parsing a
53
- * file in isolation keeps this fast enough to run over a whole tree.
54
- */
55
- export function parseModule(file) {
56
- const text = readFileSync(file, "utf8");
57
- const isTsx = file.endsWith(".tsx") || file.endsWith(".jsx");
58
- const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
59
- const imports = [];
60
- const exports = [];
61
- const addImport = (specifierNode, names, node) => {
62
- if (!ts.isStringLiteral(specifierNode))
63
- return;
64
- imports.push({ specifier: specifierNode.text, names, line: lineOf(sourceFile, node) });
65
- };
66
- for (const statement of sourceFile.statements) {
67
- if (ts.isImportDeclaration(statement)) {
68
- const names = [];
69
- const bindings = statement.importClause?.namedBindings;
70
- if (statement.importClause?.name)
71
- names.push(statement.importClause.name.text);
72
- if (bindings && ts.isNamedImports(bindings)) {
73
- for (const element of bindings.elements)
74
- names.push(element.propertyName?.text ?? element.name.text);
75
- }
76
- addImport(statement.moduleSpecifier, names, statement);
77
- continue;
78
- }
79
- if (ts.isExportDeclaration(statement)) {
80
- // `export { x } from "./y"` both imports and re-exports.
81
- if (statement.moduleSpecifier) {
82
- const names = [];
83
- if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
84
- for (const element of statement.exportClause.elements) {
85
- names.push(element.propertyName?.text ?? element.name.text);
86
- }
87
- }
88
- addImport(statement.moduleSpecifier, names, statement);
89
- }
90
- if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
91
- for (const element of statement.exportClause.elements) {
92
- exports.push({ name: element.name.text, kind: "other", line: lineOf(sourceFile, element) });
93
- }
94
- }
95
- continue;
96
- }
97
- const isExported = ts.canHaveModifiers(statement)
98
- ? (ts.getModifiers(statement) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
99
- : false;
100
- if (!isExported)
101
- continue;
102
- if (ts.isFunctionDeclaration(statement) && statement.name) {
103
- const name = statement.name.text;
104
- exports.push({
105
- name,
106
- kind: classifyFunction(name, isTsx, returnsJsx(statement)),
107
- line: lineOf(sourceFile, statement),
108
- });
109
- continue;
110
- }
111
- if (ts.isVariableStatement(statement)) {
112
- for (const declaration of statement.declarationList.declarations) {
113
- if (!ts.isIdentifier(declaration.name))
114
- continue;
115
- exports.push({
116
- name: declaration.name.text,
117
- kind: classifyValue(declaration.name.text, declaration.initializer, isTsx),
118
- line: lineOf(sourceFile, declaration),
119
- });
120
- }
121
- continue;
122
- }
123
- if (ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement)) {
124
- exports.push({ name: statement.name.text, kind: "type", line: lineOf(sourceFile, statement) });
125
- }
126
- }
127
- return { file: path.resolve(file), imports, exports };
128
- }
@@ -1,2 +0,0 @@
1
- import type { Rule } from "eslint";
2
- export declare const applicationStructureRule: Rule.RuleModule;
@@ -1,152 +0,0 @@
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
- };
@@ -1,16 +0,0 @@
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
- };
@@ -1,145 +0,0 @@
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
- }
@@ -1,12 +0,0 @@
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;
@@ -1,69 +0,0 @@
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 path from "node:path";
12
- import { getProjectIndex } from "../project/index.js";
13
- import { segmentsOf } from "../project/ccf.js";
14
- export const componentNestingRule = {
15
- meta: {
16
- schema: [],
17
- type: "problem",
18
- docs: {
19
- description: "Require a nested component to have exclusive child components, not only support files.",
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 subfolder:
27
- // src/features/<feature>/<folder>/<file>
28
- if (segments.length !== 4 || segments[0] !== "features")
29
- return {};
30
- const index = getProjectIndex(sourceRoot);
31
- if (!index)
32
- return {};
33
- const module = index.modules.get(file);
34
- if (!module?.exports.some((exp) => exp.kind === "component"))
35
- return {};
36
- const folder = segments.slice(0, 3);
37
- // Another component file in the same folder is an exclusive child and makes
38
- // the nesting real; an index barrel does not count.
39
- const hasChildComponent = [...index.modules.values()].some((candidate) => {
40
- if (candidate.file === file)
41
- return false;
42
- const candidateSegments = segmentsOf(candidate.file, sourceRoot);
43
- if (candidateSegments.length !== 4)
44
- return false;
45
- if (candidateSegments[0] !== folder[0] ||
46
- candidateSegments[1] !== folder[1] ||
47
- candidateSegments[2] !== folder[2]) {
48
- return false;
49
- }
50
- if (candidateSegments[3]?.startsWith("index."))
51
- return false;
52
- return candidate.exports.some((exp) => exp.kind === "component");
53
- });
54
- if (hasChildComponent)
55
- return {};
56
- const flatFolder = segments.slice(0, 2).join("/");
57
- return {
58
- Program(node) {
59
- context.report({
60
- node,
61
- loc: { line: 1, column: 0 },
62
- message: `This component is nested in src/${folder.join("/")}/ but its folder has no exclusive ` +
63
- `child components — only support files. Flatten it back into src/${flatFolder}/. ` +
64
- "See docs/code-organization-guide/rules/folder-nesting-rule.md",
65
- });
66
- },
67
- };
68
- },
69
- };
@@ -1,11 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/component-placement
3
- *
4
- * Enforces the placement requirements from the "Component Placement Rule" by
5
- * reading the whole source tree, because where a component belongs depends on
6
- * which files import it — something a single-file pass cannot see.
7
- *
8
- * @see docs/code-organization-guide/rules/component-placement-rule.md
9
- */
10
- import type { Rule } from "eslint";
11
- export declare const componentPlacementRule: Rule.RuleModule;
@@ -1,75 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/component-placement
3
- *
4
- * Enforces the placement requirements from the "Component Placement Rule" by
5
- * reading the whole source tree, because where a component belongs depends on
6
- * which files import it — something a single-file pass cannot see.
7
- *
8
- * @see docs/code-organization-guide/rules/component-placement-rule.md
9
- */
10
- import path from "node:path";
11
- import { getProjectIndex } from "../project/index.js";
12
- import { describeConsumers, folderSegmentsOf, formatFolder, isConfigModule, isUnderApp, resolveComponentPlacement, segmentsOf, } from "../project/ccf.js";
13
- const REASON_TEXT = {
14
- ccf: "that is the closest folder its consumers share",
15
- "across-features": "its consumers span more than one feature, and no feature may import from another",
16
- "across-layers": "its consumers span more than one layer, so no feature can own it",
17
- };
18
- const sameFolder = (left, right) => left.length === right.length && left.every((segment, depth) => segment === right[depth]);
19
- export const componentPlacementRule = {
20
- meta: {
21
- schema: [],
22
- type: "problem",
23
- docs: {
24
- description: "Enforce that a component lives in the folder its consumers imply.",
25
- },
26
- },
27
- create(context) {
28
- const filename = context.filename;
29
- if (!filename.endsWith(".tsx") && !filename.endsWith(".jsx"))
30
- return {};
31
- const sourceRoot = path.resolve("src");
32
- const index = getProjectIndex(sourceRoot);
33
- if (!index)
34
- return {};
35
- const componentFile = path.resolve(filename);
36
- const segments = segmentsOf(componentFile, sourceRoot);
37
- if (segments.length === 0)
38
- return {};
39
- // Routing files and configuration modules are not placed by their consumers.
40
- if (isUnderApp(segments) || isConfigModule(segments))
41
- return {};
42
- const module = index.modules.get(componentFile);
43
- if (!module?.exports.some((moduleExport) => moduleExport.kind === "component"))
44
- return {};
45
- const currentFolder = folderSegmentsOf(componentFile, sourceRoot);
46
- const placement = resolveComponentPlacement(componentFile, index);
47
- return {
48
- Program(node) {
49
- if (!placement) {
50
- // No consumer counts, so the component belongs to the feature it represents.
51
- if (segments[0] !== "features" || segments.length < 3) {
52
- context.report({
53
- node,
54
- loc: { line: 1, column: 0 },
55
- message: `This component has no consumer outside src/app/ or a configuration module, ` +
56
- `so it belongs in the feature folder it represents, not in ${formatFolder(currentFolder)}. ` +
57
- "See docs/code-organization-guide/rules/component-placement-rule.md",
58
- });
59
- }
60
- return;
61
- }
62
- if (sameFolder(currentFolder, placement.expectedFolder))
63
- return;
64
- const explanation = REASON_TEXT[placement.reason] ?? "that is where its consumers place it";
65
- context.report({
66
- node,
67
- loc: { line: 1, column: 0 },
68
- message: `Move this component to ${formatFolder(placement.expectedFolder)} — ${explanation}. ` +
69
- `Imported by ${describeConsumers(placement.countedConsumers, sourceRoot)}. ` +
70
- "See docs/code-organization-guide/rules/component-placement-rule.md",
71
- });
72
- },
73
- };
74
- },
75
- };
@@ -1,14 +0,0 @@
1
- /**
2
- * ESLint rule: pasika/config-extraction
3
- *
4
- * A type, schema, or utility used only to implement one configuration module
5
- * MUST be extracted even with one consumer. A configuration module is a
6
- * src/config/<name>/ folder with index.ts as its entry point, so any type or
7
- * schema still declared in a module file that is not a support file belongs in
8
- * the module's types/ or schemas/ folder — or, once a consumer outside the
9
- * module imports it, in the matching root support folder.
10
- *
11
- * @see docs/code-organization-guide/rules/configuration-rule.md
12
- */
13
- import type { Rule } from "eslint";
14
- export declare const configExtractionRule: Rule.RuleModule;