@cascivo/eslint-plugin 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <!-- generated by scripts/readme/generate.ts — edit readme.body.md, not this file -->
2
2
 
3
3
  <div align="center">
4
- <a href="https://cascivo.com"><img src="https://cascivo.com/favicon.svg" width="72" height="72" alt="cascivo logo"></a>
4
+ <a href="https://cascivo.com"><img src="https://cascivo.com/logo-mark-img-accent.svg" width="72" height="72" alt="cascivo logo"></a>
5
5
  <h1>@cascivo/eslint-plugin</h1>
6
6
  <p><strong>ESLint rule that turns cascivo's near-miss prop names into an actionable message — the wrong guess, the prop that exists, and why</strong></p>
7
7
 
@@ -16,9 +16,12 @@
16
16
 
17
17
  ---
18
18
 
19
- One ESLint rule: **`cascivo/prop-vocabulary`**. It reports the prop names an adopter is likely to guess wrong, and answers with the prop that exists and why.
19
+ Two ESLint rules:
20
20
 
21
- You probably do not need to install this directly [`@cascivo/eslint-config`](../eslint-config) depends on it and enables the rule at `warn`.
21
+ - **`cascivo/prop-vocabulary`** reports the prop names an adopter is likely to guess wrong, and answers with the prop that exists and why.
22
+ - **`cascivo/token-values`** — reports a `--cascivo-*` custom property that does not exist, and names the one that does.
23
+
24
+ You probably do not need to install this directly — [`@cascivo/eslint-config`](../eslint-config) depends on it and enables both rules at `warn`.
22
25
 
23
26
  ## Why it exists
24
27
 
@@ -50,6 +53,30 @@ Property 'tone' does not exist on type 'TextProps'.
50
53
  | `import { Dialog } from '@cascivo/react'` | `Dialog` is `Modal` | ✅ (unaliased imports only) |
51
54
  | `<DataTable items={…}>` | it takes `rows` — the one component that does | — |
52
55
 
56
+ ## `cascivo/token-values` — the name that does not exist
57
+
58
+ `prop-vocabulary` exists because a correct TypeScript error teaches nothing. This rule exists because there is **no error at all**.
59
+
60
+ CSS silently drops an unknown custom property. `--cascivo-color-acent: red` does not warn, does not fail the build, does not appear in DevTools, and does not throw. It has no effect, and the hunt for the cause starts in the component. React makes it worse: `CSSProperties` has no index signature for `--*` keys, so every custom property in a `style` prop reaches the DOM through a cast you wrote — and a cast launders a typo by definition.
61
+
62
+ ```
63
+ `--cascivo-color-acent` is not a cascivo token — CSS drops an unknown custom property
64
+ silently, so this has no effect. Did you mean `--cascivo-color-accent`?
65
+ ```
66
+
67
+ It also finds names written with the words in the wrong order, which plain edit distance never does:
68
+
69
+ | You wrote | It says |
70
+ | -------------------------------------------------------------- | ---------------------------------------------------------------------------- |
71
+ | `--cascivo-color-acent` | did you mean `--cascivo-color-accent` |
72
+ | `--cascivo-text-color` | did you mean `--cascivo-color-text` (`--cascivo-text-*` is the _size_ scale) |
73
+ | `color: 'var(--cascivo-color-txt)'` | same check, inside a `var()` reference |
74
+ | `background: '#3b82f6'` where exactly one token has that value | use `var(--cascivo-…)` so it follows the theme |
75
+
76
+ The name set is generated by `scripts/style-contract/generate.ts` from the same catalog the docs site and `cascivo audit --ai` read, so the three cannot disagree. `cascivo audit --ai` reports the same class at **error** — it also scans your CSS, which a lint rule cannot.
77
+
78
+ Your own custom properties are none of its business: only the `--cascivo-` namespace is checked.
79
+
53
80
  ## Install
54
81
 
55
82
  ```sh
@@ -96,4 +123,4 @@ pnpm add @cascivo/eslint-plugin
96
123
 
97
124
  [cascivo.com](https://cascivo.com) · [Docs](https://cascivo.com/docs) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/cascivo/cascivo) · AI agents: read [`llms.txt`](https://cascivo.com/llms.txt) (install steps + component index, plain text) or use [`@cascivo/mcp`](https://github.com/cascivo/cascivo/tree/main/packages/mcp) and [`registry.json`](https://github.com/cascivo/cascivo/blob/main/registry.json) · MIT
98
125
 
99
- <div align="center"><a href="https://cascivo.com"><img src="https://cascivo.com/favicon.svg" width="28" height="28" alt="cascivo"></a></div>
126
+ <div align="center"><a href="https://cascivo.com"><img src="https://cascivo.com/logo-mark-img.svg" width="28" height="28" alt="cascivo"></a></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cascivo/eslint-plugin",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "ESLint rule that turns cascivo's near-miss prop names into an actionable message — the wrong guess, the prop that exists, and why",
6
6
  "keywords": [
@@ -38,6 +38,7 @@
38
38
  "provenance": true
39
39
  },
40
40
  "devDependencies": {
41
+ "@typescript-eslint/parser": "8.66.0",
41
42
  "eslint": "10.8.0"
42
43
  },
43
44
  "peerDependencies": {
package/readme.body.md CHANGED
@@ -1,6 +1,9 @@
1
- One ESLint rule: **`cascivo/prop-vocabulary`**. It reports the prop names an adopter is likely to guess wrong, and answers with the prop that exists and why.
1
+ Two ESLint rules:
2
2
 
3
- You probably do not need to install this directly [`@cascivo/eslint-config`](../eslint-config) depends on it and enables the rule at `warn`.
3
+ - **`cascivo/prop-vocabulary`** reports the prop names an adopter is likely to guess wrong, and answers with the prop that exists and why.
4
+ - **`cascivo/token-values`** — reports a `--cascivo-*` custom property that does not exist, and names the one that does.
5
+
6
+ You probably do not need to install this directly — [`@cascivo/eslint-config`](../eslint-config) depends on it and enables both rules at `warn`.
4
7
 
5
8
  ## Why it exists
6
9
 
@@ -32,6 +35,30 @@ Property 'tone' does not exist on type 'TextProps'.
32
35
  | `import { Dialog } from '@cascivo/react'` | `Dialog` is `Modal` | ✅ (unaliased imports only) |
33
36
  | `<DataTable items={…}>` | it takes `rows` — the one component that does | — |
34
37
 
38
+ ## `cascivo/token-values` — the name that does not exist
39
+
40
+ `prop-vocabulary` exists because a correct TypeScript error teaches nothing. This rule exists because there is **no error at all**.
41
+
42
+ CSS silently drops an unknown custom property. `--cascivo-color-acent: red` does not warn, does not fail the build, does not appear in DevTools, and does not throw. It has no effect, and the hunt for the cause starts in the component. React makes it worse: `CSSProperties` has no index signature for `--*` keys, so every custom property in a `style` prop reaches the DOM through a cast you wrote — and a cast launders a typo by definition.
43
+
44
+ ```
45
+ `--cascivo-color-acent` is not a cascivo token — CSS drops an unknown custom property
46
+ silently, so this has no effect. Did you mean `--cascivo-color-accent`?
47
+ ```
48
+
49
+ It also finds names written with the words in the wrong order, which plain edit distance never does:
50
+
51
+ | You wrote | It says |
52
+ | -------------------------------------------------------------- | ---------------------------------------------------------------------------- |
53
+ | `--cascivo-color-acent` | did you mean `--cascivo-color-accent` |
54
+ | `--cascivo-text-color` | did you mean `--cascivo-color-text` (`--cascivo-text-*` is the _size_ scale) |
55
+ | `color: 'var(--cascivo-color-txt)'` | same check, inside a `var()` reference |
56
+ | `background: '#3b82f6'` where exactly one token has that value | use `var(--cascivo-…)` so it follows the theme |
57
+
58
+ The name set is generated by `scripts/style-contract/generate.ts` from the same catalog the docs site and `cascivo audit --ai` read, so the three cannot disagree. `cascivo audit --ai` reports the same class at **error** — it also scans your CSS, which a lint rule cannot.
59
+
60
+ Your own custom properties are none of its business: only the `--cascivo-` namespace is checked.
61
+
35
62
  ## Install
36
63
 
37
64
  ```sh
package/src/index.js CHANGED
@@ -7,8 +7,9 @@
7
7
  * who already installed the config gets the messages without changing anything.
8
8
  */
9
9
  import propVocabulary from './prop-vocabulary.js'
10
+ import tokenValues from './token-values.js'
10
11
 
11
12
  export default {
12
13
  meta: { name: '@cascivo/eslint-plugin', version: '0.1.0' },
13
- rules: { 'prop-vocabulary': propVocabulary },
14
+ rules: { 'prop-vocabulary': propVocabulary, 'token-values': tokenValues },
14
15
  }
@@ -0,0 +1,356 @@
1
+ {
2
+ "_comment": "GENERATED by scripts/style-contract/generate.ts — run `pnpm regen`. Do not edit by hand. Names omit the shared `--cascivo-` prefix; the rule restores it.",
3
+ "tokens": [
4
+ "aspect-ratio",
5
+ "blue-100",
6
+ "blue-200",
7
+ "blue-300",
8
+ "blue-400",
9
+ "blue-50",
10
+ "blue-500",
11
+ "blue-600",
12
+ "blue-700",
13
+ "blue-800",
14
+ "blue-900",
15
+ "blue-950",
16
+ "border-default",
17
+ "border-strong",
18
+ "border-subtle",
19
+ "brand-accent",
20
+ "brand-gradient-end",
21
+ "brand-gradient-start",
22
+ "brand-ink",
23
+ "brand-paper",
24
+ "brand-primary",
25
+ "button-destructive-bg",
26
+ "button-destructive-bg-hover",
27
+ "button-ghost-bg",
28
+ "button-ghost-bg-hover",
29
+ "button-primary-bg",
30
+ "button-primary-bg-active",
31
+ "button-primary-bg-hover",
32
+ "button-radius",
33
+ "button-secondary-bg",
34
+ "button-secondary-bg-hover",
35
+ "calendar-bg",
36
+ "calendar-cell-size",
37
+ "calendar-day-selected-bg",
38
+ "calendar-day-selected-fg",
39
+ "calendar-day-today-color",
40
+ "calendar-radius",
41
+ "calendar-range-bg",
42
+ "chart-1",
43
+ "chart-2",
44
+ "chart-3",
45
+ "chart-4",
46
+ "chart-5",
47
+ "chart-6",
48
+ "chart-7",
49
+ "chart-8",
50
+ "chart-anim-dur",
51
+ "chart-anim-ease",
52
+ "chart-anim-props",
53
+ "chart-axis",
54
+ "chart-fill-opacity",
55
+ "chart-fill-opacity-overlap",
56
+ "chart-grid",
57
+ "color-accent",
58
+ "color-accent-active",
59
+ "color-accent-content",
60
+ "color-accent-foreground",
61
+ "color-accent-hover",
62
+ "color-accent-muted",
63
+ "color-accent-subtle",
64
+ "color-accent-text",
65
+ "color-accent-text-hover",
66
+ "color-active-bg",
67
+ "color-background",
68
+ "color-bg",
69
+ "color-bg-subtle",
70
+ "color-border",
71
+ "color-border-strong",
72
+ "color-destructive",
73
+ "color-destructive-content",
74
+ "color-destructive-foreground",
75
+ "color-destructive-hover",
76
+ "color-destructive-subtle",
77
+ "color-error",
78
+ "color-error-content",
79
+ "color-focus-ring",
80
+ "color-foreground",
81
+ "color-foreground-muted",
82
+ "color-info",
83
+ "color-info-content",
84
+ "color-info-foreground",
85
+ "color-info-subtle",
86
+ "color-picker-alpha-to",
87
+ "color-picker-area-size",
88
+ "color-primary",
89
+ "color-primary-active",
90
+ "color-primary-content",
91
+ "color-primary-fg",
92
+ "color-primary-hover",
93
+ "color-scrim",
94
+ "color-secondary",
95
+ "color-secondary-content",
96
+ "color-secondary-hover",
97
+ "color-secondary-subtle",
98
+ "color-success",
99
+ "color-success-content",
100
+ "color-success-foreground",
101
+ "color-success-subtle",
102
+ "color-surface",
103
+ "color-surface-2",
104
+ "color-surface-overlay",
105
+ "color-surface-raised",
106
+ "color-text",
107
+ "color-text-muted",
108
+ "color-text-on-accent",
109
+ "color-text-on-destructive",
110
+ "color-text-subtle",
111
+ "color-warning",
112
+ "color-warning-content",
113
+ "color-warning-foreground",
114
+ "color-warning-subtle",
115
+ "context-x",
116
+ "context-y",
117
+ "control-height-lg",
118
+ "control-height-md",
119
+ "control-height-sm",
120
+ "data-table-cell-gap",
121
+ "data-table-height",
122
+ "data-table-max-height",
123
+ "date-picker-day-today-color",
124
+ "dialog-body-gap",
125
+ "disabled-opacity",
126
+ "dropdown-anchor",
127
+ "duration-100",
128
+ "duration-150",
129
+ "duration-200",
130
+ "duration-300",
131
+ "duration-500",
132
+ "duration-75",
133
+ "duration-loop",
134
+ "duration-loop-fast",
135
+ "duration-loop-slow",
136
+ "ease-decel",
137
+ "ease-emphasized",
138
+ "ease-in",
139
+ "ease-in-out",
140
+ "ease-out",
141
+ "ease-spring",
142
+ "editor-bg",
143
+ "editor-border",
144
+ "editor-bracket",
145
+ "editor-caret-line",
146
+ "editor-current-line",
147
+ "editor-fg",
148
+ "editor-gutter-active",
149
+ "editor-gutter-bg",
150
+ "editor-gutter-fg",
151
+ "editor-match",
152
+ "editor-match-current",
153
+ "editor-selection",
154
+ "editor-syntax-attr",
155
+ "editor-syntax-boolean",
156
+ "editor-syntax-comment",
157
+ "editor-syntax-function",
158
+ "editor-syntax-keyword",
159
+ "editor-syntax-number",
160
+ "editor-syntax-operator",
161
+ "editor-syntax-property",
162
+ "editor-syntax-punctuation",
163
+ "editor-syntax-regexp",
164
+ "editor-syntax-string",
165
+ "editor-syntax-tag",
166
+ "editor-syntax-type",
167
+ "editor-syntax-variable",
168
+ "editor-tab-size",
169
+ "flash-tint",
170
+ "focus-ring",
171
+ "font-bold",
172
+ "font-display",
173
+ "font-medium",
174
+ "font-mono",
175
+ "font-normal",
176
+ "font-sans",
177
+ "font-semibold",
178
+ "font-size-2xl",
179
+ "font-size-3xl",
180
+ "font-size-4xl",
181
+ "font-size-base",
182
+ "font-size-lg",
183
+ "font-size-sm",
184
+ "font-size-xl",
185
+ "font-size-xs",
186
+ "gray-0",
187
+ "gray-100",
188
+ "gray-200",
189
+ "gray-300",
190
+ "gray-400",
191
+ "gray-50",
192
+ "gray-500",
193
+ "gray-600",
194
+ "gray-700",
195
+ "gray-800",
196
+ "gray-900",
197
+ "gray-950",
198
+ "green-100",
199
+ "green-200",
200
+ "green-400",
201
+ "green-50",
202
+ "green-500",
203
+ "green-600",
204
+ "green-700",
205
+ "green-900",
206
+ "header-bg",
207
+ "hover-opacity",
208
+ "kpi-padding",
209
+ "leading-none",
210
+ "leading-normal",
211
+ "leading-relaxed",
212
+ "leading-snug",
213
+ "leading-tight",
214
+ "link-color",
215
+ "logo-size",
216
+ "motion-attention",
217
+ "motion-drawer",
218
+ "motion-emphasis",
219
+ "motion-enter",
220
+ "motion-exit",
221
+ "orange-100",
222
+ "orange-400",
223
+ "orange-50",
224
+ "orange-500",
225
+ "orange-600",
226
+ "popover-anchor",
227
+ "progress-color",
228
+ "progress-value",
229
+ "radial-color",
230
+ "radial-progress",
231
+ "radial-size",
232
+ "radial-thickness",
233
+ "radius-2xl",
234
+ "radius-badge",
235
+ "radius-base",
236
+ "radius-button",
237
+ "radius-card",
238
+ "radius-component",
239
+ "radius-control",
240
+ "radius-field",
241
+ "radius-full",
242
+ "radius-indicator",
243
+ "radius-input",
244
+ "radius-item",
245
+ "radius-lg",
246
+ "radius-md",
247
+ "radius-modal",
248
+ "radius-none",
249
+ "radius-overlay",
250
+ "radius-pill",
251
+ "radius-sm",
252
+ "radius-surface",
253
+ "radius-xl",
254
+ "red-100",
255
+ "red-200",
256
+ "red-400",
257
+ "red-50",
258
+ "red-500",
259
+ "red-600",
260
+ "red-700",
261
+ "red-900",
262
+ "resizable-ratio",
263
+ "ring-color",
264
+ "ring-offset",
265
+ "ring-width",
266
+ "screen-lg",
267
+ "screen-md",
268
+ "screen-sm",
269
+ "screen-xl",
270
+ "scroll-area-height",
271
+ "scroll-area-width",
272
+ "search-width",
273
+ "shadow-lg",
274
+ "shadow-md",
275
+ "shadow-overlay",
276
+ "shadow-sm",
277
+ "shadow-xl",
278
+ "shadow-xs",
279
+ "shell-aside-inline-size",
280
+ "shell-header-block-size",
281
+ "shell-panel-inline-size",
282
+ "sidenav-bg",
283
+ "sidenav-inline-size",
284
+ "sidenav-rail-inline-size",
285
+ "skeleton-height",
286
+ "skeleton-width",
287
+ "space-0",
288
+ "space-1",
289
+ "space-10",
290
+ "space-12",
291
+ "space-16",
292
+ "space-2",
293
+ "space-20",
294
+ "space-24",
295
+ "space-3",
296
+ "space-4",
297
+ "space-5",
298
+ "space-6",
299
+ "space-8",
300
+ "stack-index",
301
+ "stack-offset",
302
+ "table-zebra-bg",
303
+ "target-min",
304
+ "target-min-coarse",
305
+ "text-2xl",
306
+ "text-2xl-fluid",
307
+ "text-3xl",
308
+ "text-3xl-fluid",
309
+ "text-4xl",
310
+ "text-4xl-fluid",
311
+ "text-base",
312
+ "text-body",
313
+ "text-body-sm",
314
+ "text-caption",
315
+ "text-code",
316
+ "text-display",
317
+ "text-heading-lg",
318
+ "text-heading-md",
319
+ "text-heading-sm",
320
+ "text-label",
321
+ "text-lg",
322
+ "text-sm",
323
+ "text-ui",
324
+ "text-xl",
325
+ "text-xs",
326
+ "textarea-max-block-size",
327
+ "tooltip-anchor",
328
+ "tracking-normal",
329
+ "tracking-tight",
330
+ "tracking-wide",
331
+ "tree-indent",
332
+ "tree-level",
333
+ "warm-100",
334
+ "warm-200",
335
+ "warm-300",
336
+ "warm-400",
337
+ "warm-50",
338
+ "warm-500",
339
+ "warm-600",
340
+ "warm-700",
341
+ "warm-800",
342
+ "warm-900",
343
+ "yellow-100",
344
+ "yellow-400",
345
+ "yellow-50",
346
+ "yellow-500",
347
+ "z-base",
348
+ "z-dock",
349
+ "z-dropdown",
350
+ "z-modal",
351
+ "z-overlay",
352
+ "z-raised",
353
+ "z-toast",
354
+ "z-tooltip"
355
+ ]
356
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * `cascivo/token-values` — a `--cascivo-*` name that does not exist, reported where it is
3
+ * written.
4
+ *
5
+ * ## Why a lint rule, when there is already a token type and a token catalog
6
+ *
7
+ * There were three token surfaces before this rule and none of them could reject anything.
8
+ * `CascivoToken` was a generated union with no internal call site — an autocomplete aid an
9
+ * adopter had to opt into. `tokens.catalog.json` was data for the docs page. `cascivo audit`
10
+ * knew the token values but only ever looked for *literals that should be tokens*, never for
11
+ * *tokens that do not exist*.
12
+ *
13
+ * That left the single most expensive class of styling mistake completely unreported.
14
+ * **CSS silently drops an unknown custom property.** `style={{ '--cascivo-color-acent':
15
+ * 'red' }}` does not warn, does not fail the build, does not appear in DevTools, and does
16
+ * not throw — it has no effect, and the search for the cause starts in the component. Our
17
+ * own token docs say this in as many words about the `--cascivo-text-*` / `--cascivo-font-*`
18
+ * split, which is precisely the pair an agent gets wrong because it types from memory.
19
+ *
20
+ * TypeScript cannot cover this on its own: React's `CSSProperties` has no index signature
21
+ * for `--*` keys, so every custom property in a `style` prop reaches the DOM through a cast
22
+ * the adopter wrote, and a cast launders a typo by definition. `CascivoTokenStyle` +
23
+ * `satisfies` closes it for people who adopt the pattern; this rule closes it for everyone
24
+ * else, in the editor, before the build.
25
+ *
26
+ * ## Why `warn`, not `error`
27
+ *
28
+ * Same reasoning as `prop-vocabulary`, and it has not changed: a rule that fails somebody's
29
+ * build gets the whole config deleted, and that takes `react-hooks/immutability` with it.
30
+ * `cascivo audit --ai` reports the same finding at **error** level, because there it is an
31
+ * explicit gate somebody chose to run rather than something that appeared in their editor.
32
+ *
33
+ * ## Why the data is generated
34
+ *
35
+ * `token-catalog.json` comes from `scripts/style-contract/generate.ts`, which reads the same
36
+ * catalog the docs site and the CLI audit read. A token renamed in CSS updates this rule on
37
+ * the next `pnpm regen`; a hand-maintained list would eventually report a name that is
38
+ * correct, which is worse than reporting nothing.
39
+ *
40
+ * ## Why this rule does only one thing
41
+ *
42
+ * It does not also report "`#3b82f6` should be `var(--cascivo-blue-500)`". That is
43
+ * `cascivo audit --ai`'s `hardcoded-value`, which scopes by CSS property and reads CSS
44
+ * files — neither of which a lint rule looking at a custom-property key can do. Carrying a
45
+ * second, weaker copy here bought overlap and a false-positive surface, not coverage.
46
+ */
47
+ import { createRequire } from 'node:module'
48
+
49
+ const require = createRequire(import.meta.url)
50
+ /**
51
+ * Names are stored without the shared `--cascivo-` prefix — 339 copies of a constant is most
52
+ * of this package's 8 KB bundle budget. Restored here, once.
53
+ *
54
+ * @type {{ tokens: string[] }}
55
+ */
56
+ const catalog = require('./token-catalog.json')
57
+
58
+ const PREFIX = '--cascivo-'
59
+ const TOKENS = new Set(catalog.tokens.map((name) => PREFIX + name))
60
+
61
+ /** Levenshtein distance, capped — only used to pick a suggestion, never in a hot path. */
62
+ function distance(a, b) {
63
+ const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)])
64
+ for (let j = 0; j <= b.length; j++) rows[0][j] = j
65
+ for (let i = 1; i <= a.length; i++) {
66
+ for (let j = 1; j <= b.length; j++) {
67
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1
68
+ rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost)
69
+ }
70
+ }
71
+ return rows[a.length][b.length]
72
+ }
73
+
74
+ /**
75
+ * Tokens keyed by their segments in sorted order, so a name written with the words in the
76
+ * wrong order finds its token.
77
+ *
78
+ * This is not a hypothetical shape. `docs/TOKENS.md` documents `--cascivo-text-color` vs
79
+ * `--cascivo-color-text` as a standing trap, and word-order swaps are exactly what an agent
80
+ * produces when it types a token from memory — but they are four or five edits apart, so
81
+ * plain edit distance never finds them.
82
+ */
83
+ const BY_SORTED_SEGMENTS = new Map()
84
+ for (const name of catalog.tokens) {
85
+ const key = name.split('-').sort().join('-')
86
+ if (!BY_SORTED_SEGMENTS.has(key)) BY_SORTED_SEGMENTS.set(key, PREFIX + name)
87
+ }
88
+
89
+ /**
90
+ * The shipped token closest to what was written, or null when nothing is close enough.
91
+ *
92
+ * The threshold scales with name length so `--cascivo-color-acent` finds its token while a
93
+ * genuinely bespoke `--cascivo-my-app-thing` is left alone: guessing at a name the adopter
94
+ * invented on purpose is noise, and noise is how a rule gets switched off.
95
+ */
96
+ function nearest(name) {
97
+ const reordered = BY_SORTED_SEGMENTS.get(name.replace(PREFIX, '').split('-').sort().join('-'))
98
+ if (reordered) return reordered
99
+
100
+ const budget = Math.min(4, Math.max(2, Math.floor(name.length / 8)))
101
+ let best = null
102
+ let bestDistance = Number.POSITIVE_INFINITY
103
+ for (const token of TOKENS) {
104
+ const d = distance(name, token)
105
+ if (d < bestDistance) {
106
+ bestDistance = d
107
+ best = token
108
+ }
109
+ }
110
+ return bestDistance <= budget ? best : null
111
+ }
112
+
113
+ /** Every `--cascivo-*` name referenced by a `var()` inside a string value. */
114
+ function varReferences(value) {
115
+ return [...value.matchAll(/var\(\s*(--cascivo-[a-z0-9-]+)/gi)].map((m) => m[1])
116
+ }
117
+
118
+ /** The object literal a `style={…}` attribute carries, looking through a cast. */
119
+ function styleObject(attribute) {
120
+ if (attribute.value?.type !== 'JSXExpressionContainer') return null
121
+ let expression = attribute.value.expression
122
+ // `style={{ … } as CSSProperties}` — the cast every adopter writes, because
123
+ // `CSSProperties` has no index signature for custom properties.
124
+ while (expression?.type === 'TSAsExpression' || expression?.type === 'TSSatisfiesExpression') {
125
+ expression = expression.expression
126
+ }
127
+ return expression?.type === 'ObjectExpression' ? expression : null
128
+ }
129
+
130
+ /** The literal text of a property key, whether written quoted or computed-with-a-string. */
131
+ function keyName(property) {
132
+ const key = property.key
133
+ if (!key) return null
134
+ if (key.type === 'Literal' && typeof key.value === 'string') return key.value
135
+ if (property.computed && key.type === 'Literal' && typeof key.value === 'string') return key.value
136
+ return null
137
+ }
138
+
139
+ const rule = {
140
+ meta: {
141
+ type: 'problem',
142
+ docs: {
143
+ description: 'Report a --cascivo-* custom property that does not exist',
144
+ url: 'https://github.com/cascivo/cascivo/tree/main/packages/eslint-plugin#readme',
145
+ },
146
+ schema: [],
147
+ messages: {
148
+ unknownToken:
149
+ '`{{wrote}}` is not a cascivo token — CSS drops an unknown custom property silently, so this has no effect. Did you mean `{{suggestion}}`?',
150
+ unknownTokenNoGuess:
151
+ '`{{wrote}}` is not a cascivo token — CSS drops an unknown custom property silently, so this has no effect. The shipped names are in `@cascivo/tokens/tokens.json`.',
152
+ },
153
+ },
154
+
155
+ create(context) {
156
+ /** @param {import('estree').Node} node @param {string} name */
157
+ function checkTokenName(node, name) {
158
+ if (TOKENS.has(name)) return
159
+ const suggestion = nearest(name)
160
+ context.report({
161
+ node,
162
+ messageId: suggestion ? 'unknownToken' : 'unknownTokenNoGuess',
163
+ data: { wrote: name, suggestion: suggestion ?? '' },
164
+ })
165
+ }
166
+
167
+ return {
168
+ JSXAttribute(node) {
169
+ if (node.name?.type !== 'JSXIdentifier' || node.name.name !== 'style') return
170
+ const object = styleObject(node)
171
+ if (!object) return
172
+
173
+ for (const property of object.properties) {
174
+ if (property.type !== 'Property') continue
175
+
176
+ const key = keyName(property)
177
+ if (key?.startsWith('--cascivo-')) checkTokenName(property.key, key)
178
+
179
+ const value = property.value
180
+ if (value?.type !== 'Literal' || typeof value.value !== 'string') continue
181
+
182
+ for (const referenced of varReferences(value.value)) {
183
+ checkTokenName(value, referenced)
184
+ }
185
+ }
186
+ },
187
+ }
188
+ },
189
+ }
190
+
191
+ export default rule