@starci/eslint-canon-fe 9.2.0 → 9.4.0

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 (178) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +5 -1
  3. package/dist/brand-values.d.ts +28 -0
  4. package/dist/brand-values.js +172 -0
  5. package/dist/button-intent.d.ts +11 -0
  6. package/dist/button-intent.js +361 -0
  7. package/dist/callback-shape.d.ts +7 -0
  8. package/dist/callback-shape.js +396 -0
  9. package/dist/class-names.d.ts +19 -0
  10. package/dist/class-names.js +170 -0
  11. package/dist/client-boundary.d.ts +42 -0
  12. package/dist/client-boundary.js +404 -0
  13. package/dist/comments.d.ts +47 -0
  14. package/dist/comments.js +156 -0
  15. package/dist/control-height.d.ts +7 -0
  16. package/dist/control-height.js +83 -0
  17. package/dist/drawing.d.ts +31 -0
  18. package/dist/drawing.js +376 -0
  19. package/dist/env-owner.d.ts +22 -0
  20. package/dist/env-owner.js +291 -0
  21. package/dist/fe-stack.d.ts +10 -0
  22. package/dist/fe-stack.js +73 -0
  23. package/dist/file-layout.d.ts +71 -0
  24. package/dist/file-layout.js +465 -0
  25. package/dist/formatting.d.ts +25 -0
  26. package/dist/formatting.js +104 -0
  27. package/dist/function-budget.d.ts +9 -0
  28. package/dist/function-budget.js +48 -0
  29. package/dist/grammar-shim.d.ts +5 -0
  30. package/dist/grammar-shim.js +126 -0
  31. package/dist/hooks-folder.d.ts +34 -0
  32. package/dist/hooks-folder.js +320 -0
  33. package/dist/hygiene.d.ts +39 -0
  34. package/dist/hygiene.js +540 -0
  35. package/dist/index.d.ts +105 -0
  36. package/dist/index.js +310 -0
  37. package/{landmark.mjs → dist/landmark.d.ts} +4 -6
  38. package/dist/landmark.js +6 -0
  39. package/dist/lib/ast.d.ts +36 -0
  40. package/dist/lib/ast.js +94 -0
  41. package/dist/lib/bindings.d.ts +84 -0
  42. package/dist/lib/bindings.js +168 -0
  43. package/dist/lib/class-text.d.ts +3 -0
  44. package/dist/lib/class-text.js +30 -0
  45. package/dist/lib/config.d.ts +81 -0
  46. package/dist/lib/config.js +141 -0
  47. package/dist/lib/drawing-scope.d.ts +5 -0
  48. package/{lib/drawing-scope.mjs → dist/lib/drawing-scope.js} +6 -11
  49. package/dist/lib/eslint-boundary.d.ts +23 -0
  50. package/dist/lib/eslint-boundary.js +6 -0
  51. package/dist/lib/fe-stack.d.ts +49 -0
  52. package/dist/lib/fe-stack.js +98 -0
  53. package/dist/lib/hfs-view.d.ts +74 -0
  54. package/dist/lib/hfs-view.js +6 -0
  55. package/dist/lib/hfs.d.ts +34 -0
  56. package/dist/lib/hfs.js +72 -0
  57. package/dist/lib/next.d.ts +4 -0
  58. package/dist/lib/next.js +10 -0
  59. package/dist/lib/own-hook-origin.d.ts +17 -0
  60. package/dist/lib/own-hook-origin.js +52 -0
  61. package/dist/lib/package-root.d.ts +4 -0
  62. package/dist/lib/package-root.js +10 -0
  63. package/dist/lib/path.d.ts +2 -0
  64. package/{lib/path.mjs → dist/lib/path.js} +1 -1
  65. package/dist/lib/platform.d.ts +26 -0
  66. package/{lib/platform.mjs → dist/lib/platform.js} +38 -35
  67. package/dist/lib/program-for.d.ts +5 -0
  68. package/dist/lib/program-for.js +23 -0
  69. package/dist/lib/pure-owner-helper.d.ts +10 -0
  70. package/dist/lib/pure-owner-helper.js +156 -0
  71. package/dist/lib/react.d.ts +16 -0
  72. package/dist/lib/react.js +22 -0
  73. package/dist/lib/redux-hook-origin.d.ts +4 -0
  74. package/dist/lib/redux-hook-origin.js +50 -0
  75. package/dist/lib/rule.d.ts +28 -0
  76. package/dist/lib/rule.js +5 -0
  77. package/dist/lib/scope.d.ts +88 -0
  78. package/{lib/scope.mjs → dist/lib/scope.js} +83 -93
  79. package/dist/lib/typed-class-values.d.ts +13 -0
  80. package/dist/lib/typed-class-values.js +92 -0
  81. package/dist/lib/types.d.ts +44 -0
  82. package/dist/lib/types.js +64 -0
  83. package/dist/lib/why.d.ts +350 -0
  84. package/dist/lib/why.js +350 -0
  85. package/dist/lint-escape-hatch.d.ts +24 -0
  86. package/{lint-escape-hatch.mjs → dist/lint-escape-hatch.js} +38 -45
  87. package/dist/lists.d.ts +31 -0
  88. package/dist/lists.js +202 -0
  89. package/dist/loading.d.ts +34 -0
  90. package/dist/loading.js +157 -0
  91. package/dist/naming.d.ts +33 -0
  92. package/dist/naming.js +176 -0
  93. package/dist/native-controls.d.ts +24 -0
  94. package/dist/native-controls.js +105 -0
  95. package/dist/next-conventions.d.ts +48 -0
  96. package/dist/next-conventions.js +452 -0
  97. package/dist/project-graph.d.ts +5 -0
  98. package/{project-graph.mjs → dist/project-graph.js} +8 -12
  99. package/dist/props-and-slots.d.ts +19 -0
  100. package/dist/props-and-slots.js +320 -0
  101. package/dist/served-locale.d.ts +38 -0
  102. package/dist/served-locale.js +263 -0
  103. package/dist/shape-slot.d.ts +18 -0
  104. package/dist/shape-slot.js +324 -0
  105. package/dist/size-and-state-budget.d.ts +46 -0
  106. package/dist/size-and-state-budget.js +424 -0
  107. package/dist/size-growth.d.ts +24 -0
  108. package/dist/size-growth.js +64 -0
  109. package/dist/sonar-parity.d.ts +5 -0
  110. package/{sonar-parity.mjs → dist/sonar-parity.js} +11 -7
  111. package/dist/surface-rules.d.ts +18 -0
  112. package/dist/surface-rules.js +491 -0
  113. package/dist/swr-family.d.ts +15 -0
  114. package/dist/swr-family.js +419 -0
  115. package/dist/the-split.d.ts +27 -0
  116. package/dist/the-split.js +157 -0
  117. package/dist/tokens.d.ts +9 -0
  118. package/dist/tokens.js +35 -0
  119. package/dist/translation.d.ts +51 -0
  120. package/dist/translation.js +403 -0
  121. package/dist/transport.d.ts +73 -0
  122. package/dist/transport.js +885 -0
  123. package/dist/type-safety.d.ts +43 -0
  124. package/dist/type-safety.js +139 -0
  125. package/dist/ui-rulings.d.ts +11 -0
  126. package/dist/ui-rulings.js +121 -0
  127. package/package.json +31 -9
  128. package/runtime/knowledge/hfs/canon-pins.yaml +22 -14
  129. package/runtime/scripts/lib/package-at.mjs +5 -5
  130. package/brand-values.mjs +0 -172
  131. package/button-intent.mjs +0 -320
  132. package/callback-shape.mjs +0 -332
  133. package/class-names.mjs +0 -149
  134. package/client-boundary.mjs +0 -400
  135. package/comments.mjs +0 -159
  136. package/control-height.mjs +0 -77
  137. package/drawing.mjs +0 -315
  138. package/env-owner.mjs +0 -244
  139. package/fe-stack.mjs +0 -86
  140. package/file-layout.mjs +0 -472
  141. package/formatting.mjs +0 -110
  142. package/function-budget.mjs +0 -49
  143. package/grammar-shim.mjs +0 -123
  144. package/hooks-folder.mjs +0 -300
  145. package/hygiene.mjs +0 -513
  146. package/index.mjs +0 -317
  147. package/lib/ast.mjs +0 -97
  148. package/lib/bindings.mjs +0 -159
  149. package/lib/class-text.mjs +0 -29
  150. package/lib/config.mjs +0 -142
  151. package/lib/fe-stack.mjs +0 -77
  152. package/lib/hfs.mjs +0 -49
  153. package/lib/next.mjs +0 -6
  154. package/lib/own-hook-origin.mjs +0 -53
  155. package/lib/program-for.mjs +0 -22
  156. package/lib/pure-owner-helper.mjs +0 -148
  157. package/lib/react.mjs +0 -28
  158. package/lib/redux-hook-origin.mjs +0 -44
  159. package/lib/typed-class-values.mjs +0 -81
  160. package/lib/types.mjs +0 -63
  161. package/lib/why.mjs +0 -352
  162. package/lists.mjs +0 -193
  163. package/loading.mjs +0 -156
  164. package/naming.mjs +0 -185
  165. package/native-controls.mjs +0 -108
  166. package/next-conventions.mjs +0 -447
  167. package/props-and-slots.mjs +0 -276
  168. package/served-locale.mjs +0 -243
  169. package/size-and-state-budget.mjs +0 -376
  170. package/size-growth.mjs +0 -62
  171. package/surface-rules.mjs +0 -443
  172. package/swr-family.mjs +0 -326
  173. package/the-split.mjs +0 -155
  174. package/tokens.mjs +0 -39
  175. package/translation.mjs +0 -392
  176. package/transport.mjs +0 -830
  177. package/type-safety.mjs +0 -147
  178. package/ui-rulings.mjs +0 -134
package/index.mjs DELETED
@@ -1,317 +0,0 @@
1
- /**
2
- * The universal frontend plugin: every applicable law's rules, gathered once.
3
- *
4
- * The public factory gathers every universal rule at error. A managed repository config composes
5
- * it with the selected Grammar-family factory instead of registering individual rules. Family
6
- * packages consume the public generic infrastructure exports; each rule implementation has one owner.
7
- *
8
- * Published IDs identify their owning law. Namespace migrations are explicit in the removed-vocabulary
9
- * catalog; compatibility aliases cannot create a second owner for a rule.
10
- */
11
- import { createRequire } from "node:module"
12
- import { join } from "node:path"
13
- import { pathToFileURL } from "node:url"
14
- import { recommended as brandValuesRecommended, rules as brandValuesRules } from "./brand-values.mjs"
15
- import { recommended as buttonIntentRecommended, rules as buttonIntentRules } from "./button-intent.mjs"
16
- import { recommended as controlHeightRecommended, rules as controlHeightRules } from "./control-height.mjs"
17
- import { recommended as clientBoundaryRecommended, rules as clientBoundaryRules } from "./client-boundary.mjs"
18
- import { recommended as commentsRecommended, rules as commentsRules } from "./comments.mjs"
19
- import { recommended as classNamesRecommended, rules as classNamesRules } from "./class-names.mjs"
20
- import { recommended as envOwnerRecommended, rules as envOwnerRules } from "./env-owner.mjs"
21
- import { recommended as feStackRecommended, rules as feStackRules } from "./fe-stack.mjs"
22
- import { recommended as fileLayoutRecommended, rules as fileLayoutRules } from "./file-layout.mjs"
23
- import { recommended as formattingRecommended, rules as formattingRules } from "./formatting.mjs"
24
- import { recommended as grammarShimRecommended, rules as grammarShimRules } from "./grammar-shim.mjs"
25
- import { recommended as hooksFolderRecommended, rules as hooksFolderRules } from "./hooks-folder.mjs"
26
- import { recommended as hygieneRecommended, rules as hygieneRules } from "./hygiene.mjs"
27
- import { recommended as landmarkRecommended, rules as landmarkRules } from "./landmark.mjs"
28
- import { recommended as listsRecommended, rules as listsRules } from "./lists.mjs"
29
- import { recommended as loadingRecommended, rules as loadingRules } from "./loading.mjs"
30
- import { recommended as lintEscapeRecommended, rules as lintEscapeRules } from "./lint-escape-hatch.mjs"
31
- import { recommended as namingRecommended, rules as namingRules } from "./naming.mjs"
32
- import { recommended as nativeControlsRecommended, rules as nativeControlsRules } from "./native-controls.mjs"
33
- import { recommended as nextConventionsRecommended, rules as nextConventionsRules } from "./next-conventions.mjs"
34
- import { recommended as propsRecommended, rules as propsRules } from "./props-and-slots.mjs"
35
- import { recommended as servedLocaleRecommended, rules as servedLocaleRules } from "./served-locale.mjs"
36
- import { recommended as drawingRecommended, rules as drawingRules } from "./drawing.mjs"
37
- import { recommended as sizeBudgetRecommended, rules as sizeBudgetRules } from "./size-and-state-budget.mjs"
38
- import { recommended as sizeGrowthRecommended, rules as sizeGrowthRules } from "./size-growth.mjs"
39
- import { recommended as surfaceRulesRecommended, rules as surfaceRules } from "./surface-rules.mjs"
40
- import { recommended as splitRecommended, rules as splitRules } from "./the-split.mjs"
41
- import { recommended as tokensRecommended, rules as tokensRules } from "./tokens.mjs"
42
- import { recommended as transportRecommended, rules as transportRules } from "./transport.mjs"
43
- import { recommended as translationRecommended, rules as translationRules } from "./translation.mjs"
44
- import { recommended as typeSafetyRecommended, rules as typeSafetyRules } from "./type-safety.mjs"
45
- import { recommended as projectGraphRecommended, rules as projectGraphRules } from "./project-graph.mjs"
46
- import { recommended as sonarParityRecommended, rules as sonarParityRules } from "./sonar-parity.mjs"
47
- import { buildFeConfig, buildPlainSourceConfig } from "./lib/config.mjs"
48
- import { why } from "./lib/why.mjs"
49
- import { recommended as uiRulingsRecommended, rules as uiRulingsRules } from "./ui-rulings.mjs"
50
-
51
- /** Each law's contribution, kept separate so a duplicate name is detectable rather than silent. */
52
- const CONTRIBUTIONS = [
53
- { law: "ui-rulings", rules: uiRulingsRules, recommended: uiRulingsRecommended },
54
- { law: "project-graph", rules: projectGraphRules, recommended: projectGraphRecommended },
55
- { law: "brand-values", rules: brandValuesRules, recommended: brandValuesRecommended },
56
- { law: "button-intent", rules: buttonIntentRules, recommended: buttonIntentRecommended },
57
- { law: "control-height", rules: controlHeightRules, recommended: controlHeightRecommended },
58
- { law: "client-boundary", rules: clientBoundaryRules, recommended: clientBoundaryRecommended },
59
- { law: "comments", rules: commentsRules, recommended: commentsRecommended },
60
- {
61
- law: "class-names",
62
- rules: classNamesRules,
63
- recommended: classNamesRecommended,
64
- },
65
- { law: "env-owner", rules: envOwnerRules, recommended: envOwnerRecommended },
66
- { law: "fe-stack", rules: feStackRules, recommended: feStackRecommended },
67
- { law: "file-layout", rules: fileLayoutRules, recommended: fileLayoutRecommended },
68
- { law: "formatting", rules: formattingRules, recommended: formattingRecommended },
69
- { law: "grammar-shim", rules: grammarShimRules, recommended: grammarShimRecommended },
70
- { law: "hooks-folder", rules: hooksFolderRules, recommended: hooksFolderRecommended },
71
- { law: "hygiene", rules: hygieneRules, recommended: hygieneRecommended },
72
- { law: "landmark", rules: landmarkRules, recommended: landmarkRecommended },
73
- { law: "lists", rules: listsRules, recommended: listsRecommended },
74
- { law: "loading", rules: loadingRules, recommended: loadingRecommended },
75
- { law: "lint-escape-hatch", rules: lintEscapeRules, recommended: lintEscapeRecommended },
76
- { law: "naming", rules: namingRules, recommended: namingRecommended },
77
- { law: "native-controls", rules: nativeControlsRules, recommended: nativeControlsRecommended },
78
- { law: "next-conventions", rules: nextConventionsRules, recommended: nextConventionsRecommended },
79
- { law: "props-and-slots", rules: propsRules, recommended: propsRecommended },
80
- { law: "served-locale", rules: servedLocaleRules, recommended: servedLocaleRecommended },
81
- { law: "drawing", rules: drawingRules, recommended: drawingRecommended },
82
- { law: "size-and-state-budget", rules: sizeBudgetRules, recommended: sizeBudgetRecommended },
83
- { law: "size-growth", rules: sizeGrowthRules, recommended: sizeGrowthRecommended },
84
- { law: "surface-rules", rules: surfaceRules, recommended: surfaceRulesRecommended },
85
- { law: "the-split", rules: splitRules, recommended: splitRecommended },
86
- { law: "tokens", rules: tokensRules, recommended: tokensRecommended },
87
- { law: "translation", rules: translationRules, recommended: translationRecommended },
88
- { law: "transport", rules: transportRules, recommended: transportRecommended },
89
- { law: "type-safety", rules: typeSafetyRules, recommended: typeSafetyRecommended },
90
- { law: "sonar-parity", rules: sonarParityRules, recommended: sonarParityRecommended },
91
- ]
92
-
93
- /** Every gathered law. */
94
- export const lawOwners = CONTRIBUTIONS.map((entry) => entry.law)
95
-
96
- /**
97
- * Which law declares each rule.
98
- *
99
- * Exported because a failing rule is a question about a LAW, and the shortest path from a build log
100
- * to the document that explains it is this map. It is also what the twin test walks to prove no two
101
- * laws claim one name.
102
- */
103
- export const ruleOwners = Object.fromEntries(
104
- CONTRIBUTIONS.flatMap((entry) => Object.keys(entry.rules).map((name) => [name, entry.law])),
105
- )
106
-
107
- /**
108
- * Every (law, rule name) pair as DECLARED, before any collapsing.
109
- *
110
- * `ruleOwners` above and `rules` below are both built with `Object.fromEntries`, which silently
111
- * keeps the last writer when two laws declare one name. A guard that walks either of them cannot
112
- * see a collision - the duplicate is gone before it looks, so every name appears exactly once BY
113
- * CONSTRUCTION and the check passes forever while being blind.
114
- *
115
- * That is not hypothetical. The back-end twin had this exact shape, three rules were declared by two
116
- * laws each, one copy was discarded on import, and its guard reported green throughout. This axis
117
- * has no collision today; publishing the raw declarations is what keeps that a FACT rather than an
118
- * assumption nobody can test.
119
- */
120
- export const ruleDeclarations = CONTRIBUTIONS.flatMap((entry) =>
121
- Object.keys(entry.rules).map((name) => ({ law: entry.law, name })),
122
- )
123
-
124
- /** Every rule this canon publishes, keyed by its published name. */
125
- export const rules = Object.fromEntries(
126
- CONTRIBUTIONS.flatMap((entry) => Object.entries(entry.rules)),
127
- )
128
-
129
- /**
130
- * The levels this canon asks for, as the plugin's own opinion.
131
- *
132
- * Every FE canon rule is an error. Existing debt is fixed before adoption; lowering architecture
133
- * to warn teaches every later author that the boundary is optional and is not a supported rollout.
134
- */
135
- export const recommended = Object.fromEntries(
136
- CONTRIBUTIONS.flatMap((entry) => Object.entries(entry.recommended)),
137
- )
138
-
139
- /** Flat-config options that make inline directives ineffective rather than merely forbidden. */
140
- export { linterOptions } from "./lib/config.mjs"
141
- export { loadHfs } from "./lib/hfs.mjs"
142
-
143
- /**
144
- * The Vietnamese why of each rule that carries a catalogue code: `{ code, vi, fixVi }`.
145
- *
146
- * Read by the harness, which quotes `vi` to the owner and files the finding under `code`.
147
- */
148
- export { why }
149
-
150
- /** The plugin object, shaped the way a flat config expects it. */
151
- const plugin = {
152
- meta: { name: "eslint-plugin-starci-fe" },
153
- rules,
154
- }
155
-
156
- export default plugin
157
-
158
- /**
159
- * The React Hooks plugin, from wherever the consuming repository installed it.
160
- *
161
- * A peer dependency is resolved from the package that asks for it, and a package linked by path
162
- * (`file:`) is resolved from its REAL location - which has no `node_modules` of the repository that
163
- * linked it. So the plugin is looked up from this package first and from the working directory
164
- * second; a repository that installed `eslint-plugin-react-hooks` beside its `eslint.config.mjs`
165
- * is found either way, and one that did not gets a message naming the install rather than a
166
- * resolver stack trace.
167
- */
168
- const loadReactHooks = () => {
169
- for (const base of [import.meta.url, pathToFileURL(join(process.cwd(), "noop.js")).href]) {
170
- try {
171
- return createRequire(base)("eslint-plugin-react-hooks")
172
- } catch {
173
- // try the next place
174
- }
175
- }
176
- throw new Error("starciFeConfig needs eslint-plugin-react-hooks 7 or newer: npm i -D eslint-plugin-react-hooks")
177
- }
178
-
179
- /** The typescript-eslint plugin whose type-aware rules the factory borrows (Sonar S1874, S6551 and S7503); a dependency of this package. */
180
- const loadTsPlugin = () => createRequire(import.meta.url)("@typescript-eslint/eslint-plugin")
181
-
182
- /** The Sonar way plugins (scripts/lib/sonar-way.mjs); dependencies of this package, pinned exactly. */
183
- const loadSonarPlugins = () => {
184
- const require = createRequire(import.meta.url)
185
- const unicorn = require("eslint-plugin-unicorn")
186
- return { sonarjs: require("eslint-plugin-sonarjs"), unicorn: unicorn.default ?? unicorn }
187
- }
188
-
189
- /**
190
- * The React Hooks rules of a plugin, all at error.
191
- *
192
- * The plugin's own recommended set is the definition of "the hooks rules" - it carries the classic
193
- * pair and the compiler-derived ones (`set-state-in-effect`, `refs`, `purity`, `immutability`) - and
194
- * it ships several of them at `warn`. A warning is the level a team learns to scroll past, so the
195
- * canon states every one as `error`. Four rules are named on purpose: a plugin too old to carry them
196
- * would otherwise be adopted silently with the rules that catch effect-driven state and ref reads
197
- * during render missing.
198
- *
199
- * @param {object} reactHooks - The plugin object.
200
- * @returns {Record<string, "error">} Every recommended hooks rule at error.
201
- */
202
- export const reactHooksRules = (reactHooks) => {
203
- const configured = reactHooks.configs?.flat?.recommended?.rules ?? {}
204
- for (const required of [
205
- "react-hooks/set-state-in-effect",
206
- "react-hooks/refs",
207
- "react-hooks/rules-of-hooks",
208
- "react-hooks/exhaustive-deps",
209
- ]) {
210
- if (!(required in configured)) {
211
- throw new Error(`eslint-plugin-react-hooks does not carry ${required}; install version 7 or newer`)
212
- }
213
- }
214
- return Object.fromEntries(Object.keys(configured).map((name) => [name, "error"]))
215
- }
216
-
217
- /**
218
- * The universal portion of a frontend's flat config, composed with its selected Grammar-family factory.
219
- *
220
- * It owns nothing the repository could choose: which files are linted comes from the HFS profile (every app's and every
221
- * workspace package's `src/`), not which rules are on, not their level, not whether an inline comment may
222
- * switch one off. NO RULE IS OFF - the factory throws rather than emit a block in which a published rule is missing or
223
- * below error, so a rule added to this package reaches every repository or the build of this package fails.
224
- *
225
- * @param {{ hfs: object }} input - The HFS view of the repository (`loadHfs(import.meta.url)`).
226
- * @returns {object[]} The flat config: ignores and the typed source block.
227
- */
228
- export const starciFeConfig = ({ hfs } = {}) => {
229
- const published = Object.keys(rules).map((name) => `starci-fe/${name}`)
230
- const notRequested = published.filter((name) => recommended[name] !== "error")
231
- if (notRequested.length > 0) {
232
- throw new Error(`starciFeConfig: these published rules are not requested at error: ${notRequested.join(", ")}`)
233
- }
234
- const reactHooks = loadReactHooks()
235
- return buildFeConfig({
236
- hfs,
237
- plugin,
238
- reactHooks,
239
- tsPlugin: loadTsPlugin(),
240
- sonar: loadSonarPlugins(),
241
- source: { ...recommended, ...reactHooksRules(reactHooks) },
242
- })
243
- }
244
-
245
- /**
246
- * The flat config of a TypeScript front-end tree that is no HFS app (a runtime package, the harness UI):
247
- * `export default starciPlainSourceConfig({ root: import.meta.dirname })`. It lints the tree's `src/` with types, with the inline-directive fence, the
248
- * React Hooks rules, the type-aware rules and the Sonar way rules, and with every canon rule that needs no slot view, all at `error`.
249
- *
250
- * @param {{ root: string }} input - The absolute directory holding the tree's `tsconfig.json` and `src/`.
251
- * @returns {object[]} The flat config: ignores, the typed source block and the Sonar way block.
252
- */
253
- export const starciPlainSourceConfig = ({ root } = {}) => {
254
- const reactHooks = loadReactHooks()
255
- return buildPlainSourceConfig({ root, plugin, hooks: reactHooksRules(reactHooks), reactHooks, tsPlugin: loadTsPlugin(), sonar: loadSonarPlugins() })
256
- }
257
-
258
- /**
259
- * The flat config of a TypeScript front-end tree that adopts the typed half of the canon first: the inline-directive fence, typed linting, the
260
- * React Hooks rules, the type-aware typescript-eslint rules and the Sonar way rules over the tree's `src/`, all at `error`. It is
261
- * `starciPlainSourceConfig` without the canon's own product-structure rules (`SLOT_FREE_RULES`: jsdoc on exports, arrow exports, English-only
262
- * source, grammar controls, ...), for a tree that has not been brought to them yet; a tree that has uses `starciPlainSourceConfig`.
263
- *
264
- * @param {{ root: string }} input - The absolute directory holding the tree's `tsconfig.json` and `src/`.
265
- * @returns {object[]} The flat config: ignores, the typed source block and the Sonar way block.
266
- */
267
- export const starciTypedSourceConfig = ({ root } = {}) => {
268
- const reactHooks = loadReactHooks()
269
- return buildPlainSourceConfig({ root, plugin, hooks: reactHooksRules(reactHooks), reactHooks, tsPlugin: loadTsPlugin(), sonar: loadSonarPlugins(), canon: false })
270
- }
271
-
272
- // A21 append-only registration: keep the already exported plugin and recommendation live.
273
- import { recommended as functionBudgetRecommended, rules as functionBudgetRules } from "./function-budget.mjs"
274
- CONTRIBUTIONS.push({ law: "function-budget", rules: functionBudgetRules, recommended: functionBudgetRecommended })
275
- lawOwners.push("function-budget")
276
- ruleDeclarations.push(...Object.keys(functionBudgetRules).map((name) => ({ law: "function-budget", name })))
277
- Object.assign(ruleOwners, Object.fromEntries(Object.keys(functionBudgetRules).map((name) => [name, "function-budget"])))
278
- Object.assign(rules, functionBudgetRules)
279
- Object.assign(recommended, functionBudgetRecommended)
280
- // A14 modules are also forwarded by the existing hooks-folder and transport contributions so
281
- // source-only catalog discovery can find them. The appended registration gives each module
282
- // ownership of those same rule instances without declaring either rule twice.
283
- import { recommended as swrFamilyRecommended, rules as swrFamilyRules } from "./swr-family.mjs"
284
- import { recommended as callbackShapeRecommended, rules as callbackShapeRules } from "./callback-shape.mjs"
285
-
286
- const A14_CONTRIBUTIONS = [
287
- { law: "swr-family", rules: swrFamilyRules, recommended: swrFamilyRecommended },
288
- { law: "callback-shape", rules: callbackShapeRules, recommended: callbackShapeRecommended },
289
- ]
290
-
291
- for (const addition of A14_CONTRIBUTIONS) {
292
- const names = new Set(Object.keys(addition.rules))
293
- for (const contribution of CONTRIBUTIONS) {
294
- for (const name of names) {
295
- if (name in contribution.rules && contribution.rules[name] !== addition.rules[name]) {
296
- throw new Error("A14 registration: two laws publish different rules named " + name)
297
- }
298
- }
299
- // Replace contribution views only; the law module's exported maps retain their forwarding
300
- // entries for the source-only catalog reader.
301
- contribution.rules = Object.fromEntries(Object.entries(contribution.rules).filter(([name]) => !names.has(name)))
302
- contribution.recommended = Object.fromEntries(
303
- Object.entries(contribution.recommended).filter(([name]) => !names.has(name.replace(/^starci-fe\//, ""))),
304
- )
305
- }
306
- CONTRIBUTIONS.push(addition)
307
- lawOwners.push(addition.law)
308
- for (const name of names) {
309
- const declarations = ruleDeclarations.filter((entry) => entry.name === name)
310
- if (declarations.length > 1) throw new Error("A14 registration: duplicate declarations for " + name)
311
- if (declarations.length === 1) declarations[0].law = addition.law
312
- else ruleDeclarations.push({ law: addition.law, name })
313
- ruleOwners[name] = addition.law
314
- }
315
- Object.assign(rules, addition.rules)
316
- Object.assign(recommended, addition.recommended)
317
- }
package/lib/ast.mjs DELETED
@@ -1,97 +0,0 @@
1
- /**
2
- * Small AST readers shared by the laws that look at JSX attributes, calls and effect callbacks.
3
- *
4
- * ONE PLACE, because each reader answers "what does this node say" and two copies answer it
5
- * slightly differently: a rule that treats `` `/a` `` as a string and a sibling that does not
6
- * disagree about which hrefs are hardcoded.
7
- */
8
-
9
- /** The JSX attribute called `name` on an opening element, or null. */
10
- export const attribute = (opening, name) =>
11
- opening.attributes.find(
12
- (entry) => entry.type === "JSXAttribute" && entry.name.type === "JSXIdentifier" && entry.name.name === name,
13
- ) ?? null
14
-
15
- /** The expression an attribute carries: a literal, or the inside of `{...}`; null for a bare attribute. */
16
- export const attributeValue = (entry) => {
17
- if (!entry || !entry.value) return null
18
- return entry.value.type === "JSXExpressionContainer" ? entry.value.expression : entry.value
19
- }
20
-
21
- /** The text of a string literal or of a template with no substitution; null for anything else. */
22
- export const stringOf = (node) => {
23
- if (!node) return null
24
- if (node.type === "Literal" && typeof node.value === "string") return node.value
25
- if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0]?.value.cooked ?? ""
26
- return null
27
- }
28
-
29
- /** The leading text of a string literal or template (up to the first substitution); null for anything else. */
30
- export const leadingTextOf = (node) => {
31
- if (!node) return null
32
- if (node.type === "TemplateLiteral") return node.quasis[0]?.value.cooked ?? ""
33
- return stringOf(node)
34
- }
35
-
36
- /** `a.b.c` for a chain of plain member accesses ending in an identifier; null for anything computed. */
37
- export const calleeName = (node) => {
38
- if (!node) return null
39
- if (node.type === "Identifier") return node.name
40
- if (node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier") {
41
- const head = calleeName(node.object)
42
- return head === null ? null : `${head}.${node.property.name}`
43
- }
44
- return null
45
- }
46
-
47
- /** The name of a JSX element for a plain tag or a component; null for a member or namespaced name. */
48
- export const tagName = (opening) => (opening.name.type === "JSXIdentifier" ? opening.name.name : null)
49
-
50
- /** True for a `className` / `class` JSX attribute. */
51
- export const isClassAttribute = (node) =>
52
- node.type === "JSXAttribute" && node.name && (node.name.name === "className" || node.name.name === "class")
53
-
54
- /** Static string carried by a JSX attribute or by a module constant holding a class string. */
55
- export const staticText = (value) => {
56
- if (!value) return null
57
- if (value.type === "Literal" && typeof value.value === "string") return value.value
58
- if (value.type === "TemplateLiteral" && value.expressions.length === 0) {
59
- return value.quasis.map((quasi) => quasi.value.cooked).join(" ")
60
- }
61
- if (value.type === "JSXExpressionContainer") return staticText(value.expression)
62
- return null
63
- }
64
-
65
- /** True for a function expression or an arrow function. */
66
- export const isFunction = (node) => node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression"
67
-
68
- /**
69
- * Every node under `root`, depth first, through the visitor keys of the parser in use.
70
- * `enter` may return `false` to skip a node's children.
71
- */
72
- export const walk = (sourceCode, root, enter) => {
73
- const keys = sourceCode.visitorKeys
74
- const visit = (node) => {
75
- if (enter(node) === false) return
76
- for (const key of keys[node.type] ?? []) {
77
- const child = node[key]
78
- if (Array.isArray(child)) {
79
- for (const item of child) if (item && typeof item.type === "string") visit(item)
80
- } else if (child && typeof child.type === "string") {
81
- visit(child)
82
- }
83
- }
84
- }
85
- visit(root)
86
- }
87
-
88
- /** True when an identifier called `name` appears anywhere under `root`. */
89
- export const mentions = (sourceCode, root, name) => {
90
- let found = false
91
- walk(sourceCode, root, (node) => {
92
- if (found) return false
93
- if (node.type === "Identifier" && node.name === name) found = true
94
- return undefined
95
- })
96
- return found
97
- }
package/lib/bindings.mjs DELETED
@@ -1,159 +0,0 @@
1
- /**
2
- * Bindings of the front-end canon: what an identifier resolves to through the scope manager, which module an import
3
- * comes from, which function a name stands for, and a stable KEY for an expression that names a place (a variable, a
4
- * member path, `this.x`). A rule that must decide "is this the same handle / target / listener" compares keys, never
5
- * spellings: two `id` variables in different scopes have different keys, `timerRef.current` has one key wherever it is
6
- * written.
7
- */
8
-
9
- import { stringOf } from "./ast.mjs"
10
-
11
- /** The globals that all name the browser's global object. */
12
- const GLOBAL_OBJECT = new Set(["window", "globalThis", "self"])
13
-
14
- /** Wrappers that change a node's type, not its identity. */
15
- const TRANSPARENT = new Set(["TSAsExpression", "TSNonNullExpression", "TSSatisfiesExpression", "TSTypeAssertion", "ChainExpression", "ParenthesizedExpression"])
16
-
17
- /**
18
- * The node without the wrappers that only change its type (`x as T`, `x!`, `x?.y`).
19
- *
20
- * @param {object} node - An ESTree node.
21
- * @returns {object} The inner node.
22
- */
23
- export const unwrap = (node) => {
24
- let current = node
25
- while (current && TRANSPARENT.has(current.type)) current = current.expression
26
- return current
27
- }
28
-
29
- /**
30
- * The node's parent with the type-only wrappers skipped upward.
31
- *
32
- * @param {object} node - An ESTree node.
33
- * @returns {object|null} The first parent that is not a type-only wrapper.
34
- */
35
- export const parentOf = (node) => {
36
- let current = node.parent
37
- while (current && TRANSPARENT.has(current.type)) current = current.parent
38
- return current ?? null
39
- }
40
-
41
- /** True for a function expression, an arrow function or a function declaration. */
42
- export const isFn = (node) =>
43
- Boolean(node) && (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration")
44
-
45
- /**
46
- * The scope-manager variable an identifier resolves to, or null for a global.
47
- *
48
- * @param {object} context - The ESLint rule context.
49
- * @param {object} identifier - An ESTree identifier.
50
- * @returns {object|null} The variable.
51
- */
52
- export const variableOf = (context, identifier) => {
53
- for (let scope = context.sourceCode.getScope(identifier); scope; scope = scope.upper) {
54
- const variable = scope.set.get(identifier.name)
55
- if (variable) return variable
56
- }
57
- return null
58
- }
59
-
60
- /**
61
- * Where an identifier was imported from.
62
- *
63
- * @param {object} context - The ESLint rule context.
64
- * @param {object} identifier - An ESTree identifier.
65
- * @returns {{ source: string, imported: string|null }|null} The module specifier and the imported name (`null` for a default or namespace import), or null when the identifier is not an import.
66
- */
67
- export const importOf = (context, identifier) => {
68
- const variable = variableOf(context, identifier)
69
- const definition = variable?.defs.find((entry) => entry.type === "ImportBinding")
70
- if (!definition) return null
71
- const specifier = definition.node
72
- const source = String(definition.parent.source.value)
73
- if (specifier.type === "ImportSpecifier") return { source, imported: specifier.imported.name ?? String(specifier.imported.value) }
74
- return { source, imported: null }
75
- }
76
-
77
- /** The property path a destructured binding selects from its initializer; null for an unselected or rest binding. */
78
- export const patternPath = (pattern, name) => {
79
- if (pattern.type === "Identifier") return pattern.name === name ? [] : null
80
- if (pattern.type === "AssignmentPattern") return patternPath(pattern.left, name)
81
- if (pattern.type !== "ObjectPattern") return null
82
- for (const property of pattern.properties) {
83
- if (property.type !== "Property") continue
84
- const nested = patternPath(property.value, name)
85
- if (nested !== null) return [property.computed ? stringOf(property.key) : property.key.name ?? property.key.value, ...nested]
86
- }
87
- return null
88
- }
89
-
90
- /**
91
- * The function an expression stands for: the function itself, or the function a same-file variable or declaration holds.
92
- *
93
- * @param {object} context - The ESLint rule context.
94
- * @param {object} node - An ESTree expression.
95
- * @returns {object|null} The function node, or null when it cannot be resolved in this file.
96
- */
97
- export const functionOf = (context, node) => {
98
- const inner = unwrap(node)
99
- if (!inner) return null
100
- if (isFn(inner)) return inner
101
- if (inner.type !== "Identifier") return null
102
- const variable = variableOf(context, inner)
103
- for (const definition of variable?.defs ?? []) {
104
- if (definition.type === "FunctionName" && definition.node.type === "FunctionDeclaration") return definition.node
105
- if (definition.type === "Variable" && definition.node.id.type === "Identifier") {
106
- const init = unwrap(definition.node.init)
107
- if (isFn(init)) return init
108
- }
109
- }
110
- return null
111
- }
112
-
113
- /** A stable numeric id per scope-manager variable, so keys compare by binding and not by spelling. */
114
- const VARIABLE_IDS = new WeakMap()
115
- let nextVariableId = 1
116
- const idOf = (variable) => {
117
- if (!VARIABLE_IDS.has(variable)) VARIABLE_IDS.set(variable, nextVariableId++)
118
- return VARIABLE_IDS.get(variable)
119
- }
120
-
121
- /**
122
- * The key of an expression that names a place: `v12` for a variable, `v12.current` for a member path, `this.timer` for
123
- * a member of `this`, `g:window` for the browser's global object under any of its names. Anything else has no key.
124
- *
125
- * @param {object} context - The ESLint rule context.
126
- * @param {object} node - An ESTree expression.
127
- * @returns {string|null} The key.
128
- */
129
- export const keyOf = (context, node) => {
130
- const inner = unwrap(node)
131
- if (!inner) return null
132
- if (inner.type === "Identifier") {
133
- const variable = variableOf(context, inner)
134
- if (variable && variable.defs.length > 0) return `v${idOf(variable)}`
135
- return GLOBAL_OBJECT.has(inner.name) ? "g:window" : `g:${inner.name}`
136
- }
137
- if (inner.type === "ThisExpression") return "this"
138
- if (inner.type === "MemberExpression") {
139
- const head = keyOf(context, inner.object)
140
- if (head === null) return null
141
- if (!inner.computed && inner.property.type === "Identifier") return `${head}.${inner.property.name}`
142
- if (inner.computed && inner.property.type === "Literal") return `${head}[${String(inner.property.value)}]`
143
- return null
144
- }
145
- return null
146
- }
147
-
148
- /**
149
- * The receiver and the member name of a call written as `receiver.name(...)`, or the bare name of `name(...)`.
150
- *
151
- * @param {object} call - An ESTree call expression.
152
- * @returns {{ receiver: object|null, name: string }|null} The parts, or null for a computed or exotic callee.
153
- */
154
- export const calleeParts = (call) => {
155
- const callee = unwrap(call.callee)
156
- if (callee.type === "Identifier") return { receiver: null, name: callee.name }
157
- if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier") return { receiver: callee.object, name: callee.property.name }
158
- return null
159
- }
@@ -1,29 +0,0 @@
1
- /** Shared static class-string readers for token discipline and Grammar scale checks. */
2
- import { isClassAttribute, staticText } from "./ast.mjs"
3
- import { isProductSource } from "./scope.mjs"
4
-
5
- /** Static strings in JSX, constants and arrays, preserving the authored class order. */
6
- const classText = (value) => {
7
- if (value?.type === "JSXExpressionContainer") return classText(value.expression)
8
- if (value?.type === "ArrayExpression") {
9
- const members = value.elements.map((element) => classText(element)).filter(Boolean)
10
- return members.length > 0 ? members.join(" ") : null
11
- }
12
- return staticText(value)
13
- }
14
-
15
- /** Visit class strings in product JSX, declarations and named class arrays. */
16
- export const classTextVisitors = (context, report) => {
17
- if (!isProductSource(context)) return {}
18
- return {
19
- JSXAttribute(node) {
20
- if (isClassAttribute(node)) report(node, classText(node.value))
21
- },
22
- VariableDeclarator(node) {
23
- report(node, classText(node.init))
24
- },
25
- Property(node) {
26
- if (!node.computed && node.key.type === "Identifier" && node.key.name === "classes") report(node, classText(node.value))
27
- },
28
- }
29
- }