@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/translation.mjs DELETED
@@ -1,392 +0,0 @@
1
- /**
2
- * The rules that hold `translation.md` (HFS R58, `FE_I18N_LITERAL`, and R286 feedback copy).
3
- *
4
- * The original pair forbids a translation call below a block - easy and mostly redundant, since
5
- * the split rule already stops the drawing half reaching for the runtime - and holds the law:
6
- * NO LITERAL COPY AT ANY TIER, IN ANY LANGUAGE, AND NO PRAGMA.
7
- *
8
- * One escape is enough to turn a rule into a comment convention, so there is no tier
9
- * exemption and no pragma: a word a reader can see or hear comes from `t()` over a `next-intl`
10
- * catalogue, and the catalogue (`messages/<locale>.json`) is the only place a second language is
11
- * content.
12
- *
13
- * THE ATTRIBUTES MATTER MORE THAN THE MARKUP. Copy hides in `aria-label`, `placeholder`, `title`
14
- * and `alt` precisely because none of them reads as a sentence when you scan the file - and an
15
- * `aria-label` is not a small case: a screen reader treats it as the primary text, so an English
16
- * one on a translated surface is the loudest defect on the page for the reader least able to work
17
- * around it. It also hides in object copy (`{ title: "..." }`), which is how a page's metadata
18
- * ends up in one language regardless of the URL.
19
- *
20
- * TWO STRENGTHS OF TEST, ON PURPOSE. JSX text and the attributes a reader hears (`label`, `title`,
21
- * `alt`, `placeholder`, the `aria-*` set) are copy whatever they say, so ANY word reports: `Save` is
22
- * copy, `logo` is copy. An object property or a generic prop (`description`, `hint`) may carry a
23
- * token (`{ title: "sm" }`), so those need to look like prose - a space, a non-ASCII letter or a
24
- * leading capital. A crude test that fires on real copy
25
- * and spares tokens is worth more than a clever one nobody trusts.
26
- */
27
-
28
- import { isContentFile } from "./comments.mjs"
29
- import { variableOf } from "./lib/bindings.mjs"
30
- import { globalReferences, isComponentFile, kindOfFile } from "./lib/scope.mjs"
31
- import { why } from "./lib/why.mjs"
32
-
33
- /** True when a file sits in a component layer that must not resolve copy: every layer but `blocks` receives every word it renders, so it can know no domain and no sentence. */
34
- const isVocabularyFile = (context) => isComponentFile(context) && kindOfFile(context) !== "blocks"
35
-
36
- /** Attributes a reader sees or hears: any word in them is copy. */
37
- const STRICT_ATTRS = new Set([
38
- "label",
39
- "aria-label",
40
- "aria-description",
41
- "aria-roledescription",
42
- "aria-valuetext",
43
- "aria-placeholder",
44
- "placeholder",
45
- "title",
46
- "alt",
47
- ])
48
-
49
- /** Props that usually carry a sentence but may carry a token (a variant, an id). */
50
- const PROSE_ATTRS = new Set([
51
- "description",
52
- "helperText",
53
- "hint",
54
- "tooltip",
55
- "caption",
56
- "heading",
57
- "subtitle",
58
- "errorMessage",
59
- "emptyText",
60
- "loadingText",
61
- "confirmLabel",
62
- "cancelLabel",
63
- ])
64
-
65
- /** Object keys whose value is shown to a reader. */
66
- const COPY_KEYS = new Set([
67
- "title",
68
- "label",
69
- "description",
70
- "placeholder",
71
- "message",
72
- "heading",
73
- "subtitle",
74
- "caption",
75
- "tooltip",
76
- "hint",
77
- "cta",
78
- "text",
79
- "alt",
80
- "ariaLabel",
81
- "helperText",
82
- "emptyText",
83
- "errorMessage",
84
- "successMessage",
85
- ])
86
-
87
- /** Calls that resolve a word at render time. */
88
- const RESOLVES_COPY = /^(?:useTranslations|useLocale|useFormatter|getTranslations)$/
89
-
90
- /**
91
- * True when the text contains a word: two or more letters in any script.
92
- *
93
- * TWO LETTERS, NOT ONE, AND THE SAME AS THE REPOSITORY GATE. A lone letter is a unit, a separator or a
94
- * key cap (`x`, `k`, `A`); a word is where copy starts. The repository's i18n catalog check
95
- * draws the line at two letters for JSX text and copy attributes, and this rule draws it in the same
96
- * place so the lint and the gate never disagree about what a literal is. Neither has a suppression
97
- * marker.
98
- */
99
- const hasLetter = (text) => typeof text === "string" && /\p{L}{2,}/u.test(text)
100
-
101
- /** Prose rather than a token: it has a space, a non-ASCII letter, or it starts like a sentence. */
102
- const looksLikeProse = (text) =>
103
- hasLetter(text) && (/\s/.test(text) || /[^\x00-\x7F]/.test(text) || /^[A-Z]/.test(text))
104
-
105
- /** Static string carried by a literal or an expression-free template, else null. */
106
- const staticString = (node) => {
107
- if (!node) return null
108
- if (node.type === "Literal" && typeof node.value === "string") return node.value
109
- if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
110
- return node.quasis.map((quasi) => quasi.value.cooked ?? "").join("")
111
- }
112
- return null
113
- }
114
-
115
- /**
116
- * The static text of a template literal that has substitutions, each substitution standing as `#`
117
- * (a template of "the count, then installed" reads "# installed"); null for anything that is not such a template.
118
- */
119
- const templateText = (node) => {
120
- if (!node || node.type !== "TemplateLiteral" || node.expressions.length === 0) return null
121
- return node.quasis.map((quasi) => quasi.value.cooked ?? "").join("#")
122
- }
123
-
124
- /** True when the static parts of a template with substitutions contain a word (a count then "installed", not two ids joined by a dash). */
125
- const templateHasWord = (node) => {
126
- const text = templateText(node)
127
- return text !== null && hasLetter(text.replaceAll("#", " "))
128
- }
129
-
130
- /** Punctuation at either edge of a word, outside its letters and hyphens. */
131
- const WORD_START = new Set(['"', "'", "("])
132
- const WORD_END = new Set([".", ",", "!", "?", ":", ";", '"', ")"])
133
-
134
- /** Remove the edge punctuation in a single pass, preserving an apostrophe used as the core. */
135
- const wordCore = (token) => {
136
- let end = token.length
137
- while (end > 0 && WORD_END.has(token[end - 1])) end -= 1
138
- let start = 0
139
- while (start < end && WORD_START.has(token[start])) start += 1
140
- if (start > 0 && token[start - 1] === "'") start -= 1
141
- return token.slice(start, end)
142
- }
143
-
144
- /** A sentence word allows hyphens between letters; an all-capitals chain (YYYY-MM-DD) is a format. */
145
- const isSentenceWord = (token) =>
146
- /^[\p{L}\p{N}#'\u2019]+(?:-[\p{L}\p{N}#]+)*$/u.test(wordCore(token)) && !/^\p{Lu}{2,}(?:-\p{Lu}{2,})+$/u.test(token)
147
-
148
- /**
149
- * A sentence rather than a token, for a property whose key does not say it is copy: two or more words made of
150
- * letters and sentence punctuation only (so a class list, a URL, a media type or an id never qualifies), and it starts
151
- * like a sentence, ends like one, or carries a non-ASCII letter.
152
- */
153
- const looksLikeSentence = (text) => {
154
- const tokens = text.trim().split(/\s+/)
155
- if (tokens.length < 2) return false
156
- if (!tokens.every(isSentenceWord)) return false
157
- if (tokens.filter((token) => /\p{L}{2,}/u.test(token)).length < 2) return false
158
- return /^\p{Lu}/u.test(text.trim()) || /[^\x00-\x7F]/.test(text) || /[.!?]$/.test(text.trim())
159
- }
160
-
161
- /** Static string carried by a JSX attribute value, else null. */
162
- const attributeText = (node) => {
163
- const value = node && node.value
164
- if (!value) return null
165
- if (value.type === "JSXExpressionContainer") return staticString(value.expression)
166
- return staticString(value)
167
- }
168
-
169
- /** The value node behind a JSX attribute, for de-duplication against the second-language walk. */
170
- const attributeValueNode = (node) =>
171
- node.value && node.value.type === "JSXExpressionContainer" ? node.value.expression : node.value
172
-
173
- /** The name of an object property key, else null. */
174
- const keyName = (property) => {
175
- if (property.computed) return null
176
- if (property.key.type === "Identifier") return property.key.name
177
- if (property.key.type === "Literal" && typeof property.key.value === "string") return property.key.value
178
- return null
179
- }
180
-
181
- // -- COPY-1 ----------------------------------------------------------------------------------------
182
-
183
- /** A component below a block never resolves a word. */
184
- export const noCopyResolutionBelowBlock = {
185
- meta: {
186
- type: "problem",
187
- docs: { description: "The vocabulary tiers receive resolved strings; they never resolve one." },
188
- schema: [],
189
- messages: {
190
- resolves:
191
- "`{{name}}(...)` resolves a word in a tier that receives every word it renders. This component would then need the translation runtime to be rendered from a fixture, and it would have to know which situation the reader is in to pick the right sentence - which is the connected half's job, one file away.",
192
- },
193
- },
194
- create(context) {
195
- if (!isVocabularyFile(context)) return {}
196
- return {
197
- CallExpression(node) {
198
- const callee = node.callee
199
- if (!callee || callee.type !== "Identifier" || !RESOLVES_COPY.test(callee.name)) return
200
- context.report({ node, messageId: "resolves", data: { name: callee.name } })
201
- },
202
- }
203
- },
204
- }
205
-
206
- // -- COPY-2 ----------------------------------------------------------------------------------------
207
-
208
- /** No literal copy at any tier, in any language, and no pragma to allow one. */
209
- export const noHardcodedCopy = {
210
- meta: {
211
- type: "problem",
212
- docs: { description: "User-facing text comes from a `next-intl` catalogue through `t()`, at every tier." },
213
- schema: [],
214
- messages: {
215
- text:
216
- "`{{text}}` is copy written into source. A reader in another language sees it exactly as written, and there is no tier or comment that makes that acceptable: take the sentence from the catalogue with `t(\"key\")`.",
217
- attribute:
218
- "`{{attr}}=\"{{text}}\"` is copy, hardcoded. One reader in another language sees it verbatim - and `{{attr}}` is where copy hides, because it does not read as a sentence when scanning the file. Use `t(\"key\")`.",
219
- property:
220
- "`{{key}}: \"{{text}}\"` is copy in an object, so whatever displays this object (a page title, an empty state, a toast) shows it in one language. Use `t(\"key\")`, or build the object where `t` is in scope.",
221
- },
222
- },
223
- create(context) {
224
- if (isContentFile(context)) return {}
225
- const source = context.sourceCode || context.getSourceCode()
226
- return {
227
- JSXText(node) {
228
- const text = String(node.value || "").trim()
229
- if (hasLetter(text)) context.report({ node, messageId: "text", data: { text } })
230
- },
231
- JSXExpressionContainer(node) {
232
- // `<p>{"Hello"}</p>`: a literal in child position is JSX text with braces around it.
233
- const parent = node.parent
234
- if (!parent || (parent.type !== "JSXElement" && parent.type !== "JSXFragment")) return
235
- if (templateHasWord(node.expression)) {
236
- return context.report({ node, messageId: "text", data: { text: source.getText(node.expression) } })
237
- }
238
- const text = staticString(node.expression)
239
- if (text === null || !hasLetter(text.trim())) return
240
- context.report({ node, messageId: "text", data: { text: text.trim() } })
241
- },
242
- JSXAttribute(node) {
243
- const attr = node.name && node.name.type === "JSXIdentifier" ? node.name.name : null
244
- if (!attr) return
245
- const strict = STRICT_ATTRS.has(attr)
246
- if (!strict && !PROSE_ATTRS.has(attr)) return
247
- const value = attributeValueNode(node)
248
- const template = templateText(value)
249
- const text = template ?? attributeText(node)
250
- if (text === null) return
251
- if (strict ? !hasLetter(text.replaceAll("#", " ")) : !looksLikeProse(text)) return
252
- context.report({ node, messageId: "attribute", data: { attr, text: template === null ? text : source.getText(value) } })
253
- },
254
- Property(node) {
255
- const key = keyName(node)
256
- if (!key || node.parent.type !== "ObjectExpression") return
257
- const template = templateText(node.value)
258
- const text = template ?? staticString(node.value)
259
- if (text === null) return
260
- // a copy key needs prose; any other key needs a whole sentence: { ready: "Your course is ready" } in a hook is copy
261
- if (!(COPY_KEYS.has(key) ? looksLikeProse(text) : looksLikeSentence(text))) return
262
- context.report({ node, messageId: "property", data: { key, text: template === null ? text : source.getText(node.value) } })
263
- },
264
- }
265
- },
266
- }
267
-
268
- /** The static name of a member, including a bracketed string name. */
269
- const memberName = (node) => {
270
- if (node?.type !== "MemberExpression") return null
271
- return node.computed ? staticString(node.property) : node.property.name ?? null
272
- }
273
-
274
- /** The identifier at the root of a fluent call or property chain. */
275
- const callRoot = (node) => {
276
- if (!node) return null
277
- if (node.type === "Identifier") return node
278
- if (node.type === "CallExpression") return callRoot(node.callee)
279
- if (node.type === "MemberExpression") return callRoot(node.object)
280
- if (["ChainExpression", "TSAsExpression", "TSNonNullExpression"].includes(node.type)) return callRoot(node.expression)
281
- return null
282
- }
283
-
284
- /** Methods whose first argument is a Zod validation message rather than a value constraint. */
285
- const ZOD_MESSAGE_FIRST = new Set([
286
- "email", "url", "uuid", "cuid", "cuid2", "ulid", "emoji", "base64", "base64url", "ip", "ipv4", "ipv6",
287
- "datetime", "date", "time", "duration", "jwt", "nanoid", "nonempty", "finite", "int", "safe",
288
- "positive", "negative", "nonnegative", "nonpositive",
289
- ])
290
-
291
- /** Methods whose constraint or predicate precedes the validation message. */
292
- const ZOD_MESSAGE_SECOND = new Set([
293
- "min", "max", "length", "regex", "includes", "startsWith", "endsWith", "gt", "gte", "lt", "lte", "multipleOf", "refine",
294
- ])
295
-
296
- /** Toast methods that receive visible feedback as their first argument; dismiss receives an id. */
297
- const TOAST_MESSAGE_METHODS = new Set(["success", "error", "info", "warning", "warn", "loading", "message"])
298
-
299
- /** Literal feedback and validation messages are catalogue copy in both editions. */
300
- export const noCopyInCall = {
301
- meta: {
302
- type: "problem",
303
- docs: { description: "FE_COPY_IN_CALL: toast and Zod validation messages come from next-intl." },
304
- schema: [],
305
- messages: {
306
- copy: "FE_COPY_IN_CALL: `{{text}}` is literal feedback or validation copy. Resolve the message from the catalogue with t().",
307
- },
308
- },
309
- create(context) {
310
- const source = context.sourceCode ?? context.getSourceCode()
311
- return {
312
- CallExpression(node) {
313
- const root = callRoot(node.callee)
314
- if (!root) return
315
- const binding = variableOf(context, root)?.defs[0] ?? null
316
- const imported = binding?.type === "ImportBinding" ? binding.parent.source.value : null
317
- const method = memberName(node.callee)
318
- const zod = imported === "zod" || imported?.startsWith("zod/") || (!binding && root.name === "z")
319
- const toast = root.name === "toast" || (imported && /(?:^|\/)toast(?:\/|$)/.test(imported) &&
320
- (binding.node.type === "ImportDefaultSpecifier" || binding.node.imported?.name === "toast"))
321
- let message
322
- if (toast && (node.callee.type === "Identifier" || TOAST_MESSAGE_METHODS.has(method))) message = node.arguments[0]
323
- else if (zod && ZOD_MESSAGE_FIRST.has(method)) message = node.arguments[0]
324
- else if (zod && ZOD_MESSAGE_SECOND.has(method)) message = node.arguments[1]
325
- if (!message) return
326
- const text = staticString(message)
327
- if (text !== null ? hasLetter(text) : templateHasWord(message)) {
328
- context.report({ node: message, messageId: "copy", data: { text: text ?? source.getText(message) } })
329
- }
330
- },
331
- }
332
- },
333
- }
334
-
335
- /** Browser alerts bypass the application's translated toast feedback. */
336
- export const noAlert = {
337
- meta: {
338
- type: "problem",
339
- docs: { description: "FE_NO_ALERT: feedback uses the toast module rather than browser alert()." },
340
- schema: [],
341
- messages: {
342
- alert: "FE_NO_ALERT: browser alert() bypasses the application feedback surface. Use the toast module with a translated message.",
343
- },
344
- },
345
- create(context) {
346
- const calls = []
347
- return {
348
- CallExpression(node) { calls.push(node) },
349
- "Program:exit"() {
350
- const globals = new Set(globalReferences(context, new Set(["alert", "window", "globalThis", "self"])))
351
- for (const node of calls) {
352
- const callee = node.callee
353
- const direct = callee.type === "Identifier" && callee.name === "alert" && globals.has(callee)
354
- const member = memberName(callee) === "alert" && callee.object.type === "Identifier" && globals.has(callee.object)
355
- if (direct || member) context.report({ node, messageId: "alert" })
356
- }
357
- },
358
- }
359
- },
360
- }
361
-
362
- // Subcodes remain available to catalog validation when this module is imported independently.
363
- Object.assign(why, {
364
- "no-copy-in-call": {
365
- code: "FE_COPY_IN_CALL",
366
- en: "`<file>` passes literal copy to a toast or a Zod validation message.",
367
- fix: "Resolve feedback and validation messages from the next-intl catalogue with t().",
368
- },
369
- "no-alert": {
370
- code: "FE_NO_ALERT",
371
- en: "`<file>` calls browser alert(), bypassing the application's feedback surface.",
372
- fix: "Use the toast module with a translated message.",
373
- },
374
- })
375
-
376
- /** The rules this law contributes to the plugin. */
377
- export const rules = {
378
- "no-copy-resolution-below-block": noCopyResolutionBelowBlock,
379
- "no-hardcoded-copy": noHardcodedCopy,
380
- "no-copy-in-call": noCopyInCall,
381
- "no-alert": noAlert,
382
- }
383
-
384
- /**
385
- * The level this law asks for, as the plugin's own opinion.
386
- *
387
- * `no-hardcoded-copy` is the one a repository with history should expect a large count from, and
388
- * every report is a real move: the string has to be lifted into a catalogue key and read through
389
- * `t()`, which is work rather than a deletion. That is the reason there is no `warn` rollout and no
390
- * pragma - a repository burns the count down before it adopts, it does not live beside it.
391
- */
392
- export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))