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,139 +0,0 @@
1
- import { readdirSync, statSync } from "node:fs";
2
- import path from "node:path";
3
- import { parseModule } from "./parse-module.js";
4
- const MODULE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
5
- const INDEX_BASENAMES = ["index.ts", "index.tsx", "index.mts", "index.cts", "index.js", "index.jsx"];
6
- /**
7
- * How long a built index is trusted before its inputs are re-checked. A long
8
- * lived ESLint server keeps this module in memory across edits, so the index has
9
- * to notice a changed tree without re-reading it on every single file.
10
- */
11
- const REVALIDATE_AFTER_MS = 2000;
12
- export const symbolKey = (file, name) => `${file}\u0000${name}`;
13
- function listSourceFiles(dir) {
14
- let entries;
15
- try {
16
- entries = readdirSync(dir);
17
- }
18
- catch {
19
- return [];
20
- }
21
- return entries.flatMap((entry) => {
22
- if (entry.startsWith(".") || entry === "node_modules")
23
- return [];
24
- const entryPath = path.join(dir, entry);
25
- let stats;
26
- try {
27
- stats = statSync(entryPath);
28
- }
29
- catch {
30
- return [];
31
- }
32
- if (stats.isDirectory())
33
- return listSourceFiles(entryPath);
34
- return MODULE_EXTENSIONS.includes(path.extname(entry)) ? [entryPath] : [];
35
- });
36
- }
37
- /** Cheap fingerprint of the tree, so an unchanged tree is never re-parsed. */
38
- function fingerprint(files) {
39
- let total = 0;
40
- for (const file of files) {
41
- try {
42
- total += statSync(file).mtimeMs;
43
- }
44
- catch {
45
- /* A file removed between listing and stat just drops out of the fingerprint. */
46
- }
47
- }
48
- return `${String(files.length)}:${String(total)}`;
49
- }
50
- /** Resolves an import specifier to a file inside the source tree, if it points at one. */
51
- export function resolveSpecifier(fromFile, specifier, sourceRoot) {
52
- let base;
53
- if (specifier.startsWith("@/")) {
54
- base = path.resolve(sourceRoot, specifier.slice(2));
55
- }
56
- else if (specifier.startsWith(".")) {
57
- base = path.resolve(path.dirname(fromFile), specifier);
58
- }
59
- else {
60
- return undefined;
61
- }
62
- const candidates = [
63
- base,
64
- ...MODULE_EXTENSIONS.map((extension) => `${base}${extension}`),
65
- ...INDEX_BASENAMES.map((name) => path.join(base, name)),
66
- ];
67
- for (const candidate of candidates) {
68
- try {
69
- if (statSync(candidate).isFile())
70
- return candidate;
71
- }
72
- catch {
73
- /* Try the next candidate. */
74
- }
75
- }
76
- return undefined;
77
- }
78
- function build(sourceRoot, files) {
79
- const modules = new Map();
80
- const consumers = new Map();
81
- const symbolConsumers = new Map();
82
- for (const file of files) {
83
- try {
84
- modules.set(file, parseModule(file));
85
- }
86
- catch {
87
- /* A file that cannot be read or parsed contributes nothing to the graph. */
88
- }
89
- }
90
- for (const [file, module] of modules) {
91
- for (const moduleImport of module.imports) {
92
- const target = resolveSpecifier(file, moduleImport.specifier, sourceRoot);
93
- if (!target || !modules.has(target))
94
- continue;
95
- const fileConsumers = consumers.get(target) ?? new Set();
96
- fileConsumers.add(file);
97
- consumers.set(target, fileConsumers);
98
- for (const name of moduleImport.names) {
99
- const key = symbolKey(target, name);
100
- const nameConsumers = symbolConsumers.get(key) ?? new Set();
101
- nameConsumers.add(file);
102
- symbolConsumers.set(key, nameConsumers);
103
- }
104
- }
105
- }
106
- return { sourceRoot, modules, consumers, symbolConsumers };
107
- }
108
- let cache;
109
- /**
110
- * The project index for a source tree, rebuilt only when the tree changed.
111
- * Returns undefined when the tree does not exist, which is how a repository
112
- * without a `src/` folder opts out of every cross-file rule.
113
- */
114
- export function getProjectIndex(sourceRoot) {
115
- const now = Date.now();
116
- if (cache?.index.sourceRoot === sourceRoot && now - cache.checkedAt < REVALIDATE_AFTER_MS) {
117
- return cache.index;
118
- }
119
- try {
120
- if (!statSync(sourceRoot).isDirectory())
121
- return undefined;
122
- }
123
- catch {
124
- return undefined;
125
- }
126
- const files = listSourceFiles(sourceRoot).sort((left, right) => left.localeCompare(right));
127
- const currentFingerprint = fingerprint(files);
128
- if (cache?.index.sourceRoot === sourceRoot && cache.fingerprint === currentFingerprint) {
129
- cache.checkedAt = now;
130
- return cache.index;
131
- }
132
- const index = build(sourceRoot, files);
133
- cache = { index, checkedAt: now, fingerprint: currentFingerprint };
134
- return index;
135
- }
136
- /** Drops the memoized index. Used by tests that write a fresh tree per case. */
137
- export function clearProjectIndex() {
138
- cache = undefined;
139
- }
@@ -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;