@cascivo/eslint-plugin 0.1.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/LICENSE +21 -0
- package/README.md +99 -0
- package/package.json +50 -0
- package/readme.body.md +69 -0
- package/src/index.d.ts +17 -0
- package/src/index.js +14 -0
- package/src/prop-vocabulary.js +176 -0
- package/src/prop-vocabulary.json +101 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) cascivo contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<!-- generated by scripts/readme/generate.ts — edit readme.body.md, not this file -->
|
|
2
|
+
|
|
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>
|
|
5
|
+
<h1>@cascivo/eslint-plugin</h1>
|
|
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
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/@cascivo/eslint-plugin)
|
|
9
|
+
[](https://www.npmjs.com/package/@cascivo/eslint-plugin)
|
|
10
|
+
[](https://github.com/cascivo/cascivo/blob/main/LICENSE)
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
[npm](https://www.npmjs.com/package/@cascivo/eslint-plugin) · [cascivo.com](https://cascivo.com) · [Docs](https://cascivo.com/docs) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/cascivo/cascivo)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
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.
|
|
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`.
|
|
22
|
+
|
|
23
|
+
## Why it exists
|
|
24
|
+
|
|
25
|
+
A 2026-08-21 adopter report put it precisely:
|
|
26
|
+
|
|
27
|
+
> A fresh adopter's success is currently load-bearing on the docs staying this good — the API itself still has the sharp edges. **The docs are doing work the API should eventually do itself.**
|
|
28
|
+
|
|
29
|
+
Most of that work moved into the type system: `label` and `ariaLabel` both compile now, `Switch` is exported, `Field` takes `hint`. What is left is the class TypeScript structurally cannot help with. `<Text tone="subtle">` is a correct type error whose message —
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
Property 'tone' does not exist on type 'TextProps'.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
— names the mistake and teaches nothing, so you go looking for the docs anyway. That is the dependency the report flagged. TypeScript has no mechanism for a custom message on an unknown prop; a runtime warning cannot tell a typo from a legitimate DOM passthrough and arrives after the build you are fixing. A lint rule is the one layer that can carry the sentence:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
`Text` has no `tone` prop — it is `muted`. `tone` is the catalog's SEVERITY vocabulary
|
|
39
|
+
(Status, Badge, Timeline, SideNav). Text emphasis is the boolean `muted`.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## What it catches
|
|
43
|
+
|
|
44
|
+
| You wrote | It says | Autofix |
|
|
45
|
+
| ---------------------------------------------- | ---------------------------------------------------------- | --------------------------- |
|
|
46
|
+
| `<Text tone="subtle">` | the prop is `muted`; `tone` is the severity vocabulary | — |
|
|
47
|
+
| `<Flex gap="4">` | `gap` takes a number | ✅ → `gap={4}` |
|
|
48
|
+
| `<Flex justify="between">` with no `direction` | `Flex` is vertical by default, unlike CSS/Chakra/MUI/Radix | — |
|
|
49
|
+
| `const { theme } = useTheme()` | it returns a tuple | — |
|
|
50
|
+
| `import { Dialog } from '@cascivo/react'` | `Dialog` is `Modal` | ✅ (unaliased imports only) |
|
|
51
|
+
| `<DataTable items={…}>` | it takes `rows` — the one component that does | — |
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
pnpm add -D @cascivo/eslint-config # brings this plugin with it
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
// eslint.config.js
|
|
61
|
+
import cascivo from '@cascivo/eslint-config'
|
|
62
|
+
|
|
63
|
+
export default [...yourConfig, ...cascivo] // spread LAST — flat config is last-wins
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or wire the rule up yourself:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
import cascivoPlugin from '@cascivo/eslint-plugin'
|
|
70
|
+
|
|
71
|
+
export default [
|
|
72
|
+
{
|
|
73
|
+
plugins: { cascivo: cascivoPlugin },
|
|
74
|
+
rules: { 'cascivo/prop-vocabulary': 'warn' },
|
|
75
|
+
},
|
|
76
|
+
]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## `warn`, not `error`
|
|
80
|
+
|
|
81
|
+
Deliberate. A lint rule that fails your build over a naming opinion gets the whole config deleted — and that takes `react-hooks/immutability` with it, which is what `@cascivo/eslint-config` exists for. Raise it to `error` yourself if you want it enforced.
|
|
82
|
+
|
|
83
|
+
## Adding a case
|
|
84
|
+
|
|
85
|
+
Every case lives in `near-misses.json`, from which `scripts/eslint-vocabulary/generate.ts` emits the data the rule reads. Each row is validated against `registry.json` at generation time: the prop it recommends must exist on that component, and the prop it warns about must not. A rename on either side fails `pnpm regen` rather than shipping a rule that teaches the wrong thing with the authority of a fix list.
|
|
86
|
+
|
|
87
|
+
So the next friction report costs a row in a JSON file, not a paragraph in a guide.
|
|
88
|
+
|
|
89
|
+
## Install
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
pnpm add @cascivo/eslint-plugin
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
[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
|
+
|
|
99
|
+
<div align="center"><a href="https://cascivo.com"><img src="https://cascivo.com/favicon.svg" width="28" height="28" alt="cascivo"></a></div>
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cascivo/eslint-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
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
|
+
"keywords": [
|
|
7
|
+
"cascivo",
|
|
8
|
+
"design-system",
|
|
9
|
+
"eslint",
|
|
10
|
+
"eslint-plugin",
|
|
11
|
+
"react"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/cascivo/cascivo/tree/main/packages/eslint-plugin#readme",
|
|
14
|
+
"bugs": "https://github.com/cascivo/cascivo/issues",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "urbanisierung",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/cascivo/cascivo.git",
|
|
20
|
+
"directory": "packages/eslint-plugin"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src/**/*.js",
|
|
24
|
+
"src/**/*.json",
|
|
25
|
+
"src/**/*.d.ts",
|
|
26
|
+
"!src/**/*.test.js"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"exports": {
|
|
30
|
+
"./package.json": "./package.json",
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./src/index.d.ts",
|
|
33
|
+
"default": "./src/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"provenance": true
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"eslint": "10.8.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"eslint": ">=9.0.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "echo 'eslint-plugin: plain ESM, no build'",
|
|
48
|
+
"test": "node --test src/*.test.js"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/readme.body.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
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.
|
|
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`.
|
|
4
|
+
|
|
5
|
+
## Why it exists
|
|
6
|
+
|
|
7
|
+
A 2026-08-21 adopter report put it precisely:
|
|
8
|
+
|
|
9
|
+
> A fresh adopter's success is currently load-bearing on the docs staying this good — the API itself still has the sharp edges. **The docs are doing work the API should eventually do itself.**
|
|
10
|
+
|
|
11
|
+
Most of that work moved into the type system: `label` and `ariaLabel` both compile now, `Switch` is exported, `Field` takes `hint`. What is left is the class TypeScript structurally cannot help with. `<Text tone="subtle">` is a correct type error whose message —
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Property 'tone' does not exist on type 'TextProps'.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
— names the mistake and teaches nothing, so you go looking for the docs anyway. That is the dependency the report flagged. TypeScript has no mechanism for a custom message on an unknown prop; a runtime warning cannot tell a typo from a legitimate DOM passthrough and arrives after the build you are fixing. A lint rule is the one layer that can carry the sentence:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
`Text` has no `tone` prop — it is `muted`. `tone` is the catalog's SEVERITY vocabulary
|
|
21
|
+
(Status, Badge, Timeline, SideNav). Text emphasis is the boolean `muted`.
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What it catches
|
|
25
|
+
|
|
26
|
+
| You wrote | It says | Autofix |
|
|
27
|
+
| ---------------------------------------------- | ---------------------------------------------------------- | --------------------------- |
|
|
28
|
+
| `<Text tone="subtle">` | the prop is `muted`; `tone` is the severity vocabulary | — |
|
|
29
|
+
| `<Flex gap="4">` | `gap` takes a number | ✅ → `gap={4}` |
|
|
30
|
+
| `<Flex justify="between">` with no `direction` | `Flex` is vertical by default, unlike CSS/Chakra/MUI/Radix | — |
|
|
31
|
+
| `const { theme } = useTheme()` | it returns a tuple | — |
|
|
32
|
+
| `import { Dialog } from '@cascivo/react'` | `Dialog` is `Modal` | ✅ (unaliased imports only) |
|
|
33
|
+
| `<DataTable items={…}>` | it takes `rows` — the one component that does | — |
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
pnpm add -D @cascivo/eslint-config # brings this plugin with it
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
// eslint.config.js
|
|
43
|
+
import cascivo from '@cascivo/eslint-config'
|
|
44
|
+
|
|
45
|
+
export default [...yourConfig, ...cascivo] // spread LAST — flat config is last-wins
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or wire the rule up yourself:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import cascivoPlugin from '@cascivo/eslint-plugin'
|
|
52
|
+
|
|
53
|
+
export default [
|
|
54
|
+
{
|
|
55
|
+
plugins: { cascivo: cascivoPlugin },
|
|
56
|
+
rules: { 'cascivo/prop-vocabulary': 'warn' },
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## `warn`, not `error`
|
|
62
|
+
|
|
63
|
+
Deliberate. A lint rule that fails your build over a naming opinion gets the whole config deleted — and that takes `react-hooks/immutability` with it, which is what `@cascivo/eslint-config` exists for. Raise it to `error` yourself if you want it enforced.
|
|
64
|
+
|
|
65
|
+
## Adding a case
|
|
66
|
+
|
|
67
|
+
Every case lives in `near-misses.json`, from which `scripts/eslint-vocabulary/generate.ts` emits the data the rule reads. Each row is validated against `registry.json` at generation time: the prop it recommends must exist on that component, and the prop it warns about must not. A rename on either side fails `pnpm regen` rather than shipping a rule that teaches the wrong thing with the authority of a fix list.
|
|
68
|
+
|
|
69
|
+
So the next friction report costs a row in a JSON file, not a paragraph in a guide.
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cascivo/eslint-plugin` — one rule, `cascivo/prop-vocabulary`.
|
|
3
|
+
*
|
|
4
|
+
* Typed loosely on purpose, like `@cascivo/eslint-config`: the plugin ships plain data and a
|
|
5
|
+
* visitor, and deliberately does not depend on `eslint`, so it stays installable next to any
|
|
6
|
+
* ESLint 9+ without a version handshake.
|
|
7
|
+
*/
|
|
8
|
+
export interface CascadeRule {
|
|
9
|
+
meta: Record<string, unknown>
|
|
10
|
+
create: (context: unknown) => Record<string, unknown>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
declare const plugin: {
|
|
14
|
+
meta: { name: string; version: string }
|
|
15
|
+
rules: { 'prop-vocabulary': CascadeRule }
|
|
16
|
+
}
|
|
17
|
+
export default plugin
|
package/src/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cascivo/eslint-plugin` — see `prop-vocabulary.js` for why this exists.
|
|
3
|
+
*
|
|
4
|
+
* Shipped as a plugin rather than folded into `@cascivo/eslint-config` because that package
|
|
5
|
+
* deliberately contains no rules and no `eslint` dependency; it is pure flat-config data.
|
|
6
|
+
* `@cascivo/eslint-config` depends on this one and enables the rule at `warn`, so an adopter
|
|
7
|
+
* who already installed the config gets the messages without changing anything.
|
|
8
|
+
*/
|
|
9
|
+
import propVocabulary from './prop-vocabulary.js'
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
meta: { name: '@cascivo/eslint-plugin', version: '0.1.0' },
|
|
13
|
+
rules: { 'prop-vocabulary': propVocabulary },
|
|
14
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cascivo/prop-vocabulary` — say what the prop actually is.
|
|
3
|
+
*
|
|
4
|
+
* ## Why a lint rule and not more documentation
|
|
5
|
+
*
|
|
6
|
+
* The 2026-08-21 adopter report's sharpest finding was not any of its eleven defects. It was
|
|
7
|
+
* this: "a fresh adopter's success is currently load-bearing on the docs staying this good —
|
|
8
|
+
* the API itself still has the sharp edges. **The docs are doing work the API should
|
|
9
|
+
* eventually do itself.**"
|
|
10
|
+
*
|
|
11
|
+
* Most of that work moved into the type system: `label` and `ariaLabel` both compile now,
|
|
12
|
+
* `Switch` is exported, `Field` takes `hint`. What is left is the class TypeScript
|
|
13
|
+
* structurally cannot help with. `<Text tone="subtle">` is a correct type error whose message
|
|
14
|
+
* — "Property 'tone' does not exist on type 'TextProps'" — names the mistake and teaches
|
|
15
|
+
* nothing, so the adopter goes looking for the docs anyway, which is exactly the dependency
|
|
16
|
+
* the report flagged. TypeScript has no mechanism for a custom message on an unknown prop;
|
|
17
|
+
* a runtime warning cannot tell a typo from a legitimate DOM passthrough and fires after the
|
|
18
|
+
* build you are trying to fix. A lint rule is the one layer that can carry the sentence.
|
|
19
|
+
*
|
|
20
|
+
* ## Why `warn`, not `error`
|
|
21
|
+
*
|
|
22
|
+
* `@cascivo/eslint-config` enables this at `warn`. A lint rule that fails somebody's build
|
|
23
|
+
* over a naming opinion gets the whole config deleted, and then the adopter loses
|
|
24
|
+
* `react-hooks/immutability` too — which is the thing that config actually exists for.
|
|
25
|
+
*
|
|
26
|
+
* ## Why the data is generated
|
|
27
|
+
*
|
|
28
|
+
* Every case lives in `prop-vocabulary.json`, generated from
|
|
29
|
+
* `packages/eslint-plugin/near-misses.json` and validated against `registry.json` at build
|
|
30
|
+
* time (see `scripts/eslint-vocabulary/generate.ts`). The next friction report costs a row in
|
|
31
|
+
* a JSON file rather than a paragraph in a guide, and a row that stops being true fails
|
|
32
|
+
* `pnpm regen` instead of shipping a rule that lies.
|
|
33
|
+
*/
|
|
34
|
+
import { createRequire } from 'node:module'
|
|
35
|
+
|
|
36
|
+
const require = createRequire(import.meta.url)
|
|
37
|
+
/** @type {import('./types.js').Vocabulary} */
|
|
38
|
+
const vocabulary = require('./prop-vocabulary.json')
|
|
39
|
+
|
|
40
|
+
/** The element name as written, e.g. `Text` in `<Text>` and `Card.Header` in `<Card.Header>`. */
|
|
41
|
+
function elementName(node) {
|
|
42
|
+
const name = node.name
|
|
43
|
+
if (!name) return null
|
|
44
|
+
if (name.type === 'JSXIdentifier') return name.name
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Names of the plain attributes on this element (spreads are unknowable, and skipped). */
|
|
49
|
+
function attributeNames(node) {
|
|
50
|
+
return new Set(
|
|
51
|
+
node.attributes
|
|
52
|
+
.filter((a) => a.type === 'JSXAttribute' && a.name?.type === 'JSXIdentifier')
|
|
53
|
+
.map((a) => a.name.name),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const rule = {
|
|
58
|
+
meta: {
|
|
59
|
+
type: 'problem',
|
|
60
|
+
docs: {
|
|
61
|
+
description:
|
|
62
|
+
'Report cascivo props an adopter is likely to guess wrong, naming the prop that exists',
|
|
63
|
+
url: 'https://github.com/cascivo/cascivo/tree/main/packages/eslint-plugin#readme',
|
|
64
|
+
},
|
|
65
|
+
fixable: 'code',
|
|
66
|
+
schema: [],
|
|
67
|
+
messages: {
|
|
68
|
+
wrongProp: '`{{component}}` has no `{{wrote}}` prop — it is `{{is}}`. {{note}}',
|
|
69
|
+
stringSpaceStep:
|
|
70
|
+
'`{{prop}}` takes a NUMBER, not a string: write `{{prop}}={{{value}}}`. The space scale is a numeric SpaceStep, so `{{prop}}="7"` cannot type-check into a token that does not exist.',
|
|
71
|
+
foreignImport:
|
|
72
|
+
'`{{wrote}}` is `{{is}}` in cascivo. Import `{{is}}` — same component, the name this system uses.',
|
|
73
|
+
tupleHook:
|
|
74
|
+
'`{{hook}}()` returns a TUPLE — write `const [value, setValue] = {{hook}}()`. {{note}}',
|
|
75
|
+
defaultDirection: '`{{component}}` has no `direction`, so it is VERTICAL. {{note}}',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
create(context) {
|
|
80
|
+
const source = context.sourceCode ?? context.getSourceCode?.()
|
|
81
|
+
const numeric = new Set(vocabulary.numericProps)
|
|
82
|
+
const byComponent = new Map()
|
|
83
|
+
for (const row of vocabulary.props) {
|
|
84
|
+
if (!byComponent.has(row.component)) byComponent.set(row.component, [])
|
|
85
|
+
byComponent.get(row.component).push(row)
|
|
86
|
+
}
|
|
87
|
+
const defaultDirection = new Map(vocabulary.defaultDirection.map((row) => [row.component, row]))
|
|
88
|
+
const tupleHooks = new Map(vocabulary.tupleHooks.map((row) => [row.hook, row]))
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
JSXOpeningElement(node) {
|
|
92
|
+
const component = elementName(node)
|
|
93
|
+
if (!component) return
|
|
94
|
+
const present = attributeNames(node)
|
|
95
|
+
|
|
96
|
+
for (const row of byComponent.get(component) ?? []) {
|
|
97
|
+
if (!present.has(row.wrote)) continue
|
|
98
|
+
const attribute = node.attributes.find(
|
|
99
|
+
(a) => a.type === 'JSXAttribute' && a.name?.name === row.wrote,
|
|
100
|
+
)
|
|
101
|
+
context.report({
|
|
102
|
+
node: attribute ?? node,
|
|
103
|
+
messageId: 'wrongProp',
|
|
104
|
+
data: { component, wrote: row.wrote, is: row.is, note: row.note },
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// `<Flex justify="between">` with no `direction` is a centred column, not a row.
|
|
109
|
+
const dir = defaultDirection.get(component)
|
|
110
|
+
if (
|
|
111
|
+
dir &&
|
|
112
|
+
!present.has('direction') &&
|
|
113
|
+
(present.has('justify') || present.has('align')) &&
|
|
114
|
+
// A spread may carry `direction`; do not guess about what we cannot see.
|
|
115
|
+
!node.attributes.some((a) => a.type === 'JSXSpreadAttribute')
|
|
116
|
+
) {
|
|
117
|
+
context.report({
|
|
118
|
+
node,
|
|
119
|
+
messageId: 'defaultDirection',
|
|
120
|
+
data: { component, note: dir.note },
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
JSXAttribute(node) {
|
|
126
|
+
if (node.name?.type !== 'JSXIdentifier' || !numeric.has(node.name.name)) return
|
|
127
|
+
const value = node.value
|
|
128
|
+
if (value?.type !== 'Literal' || typeof value.value !== 'string') return
|
|
129
|
+
if (!/^\d+$/.test(value.value)) return
|
|
130
|
+
const prop = node.name.name
|
|
131
|
+
context.report({
|
|
132
|
+
node,
|
|
133
|
+
messageId: 'stringSpaceStep',
|
|
134
|
+
data: { prop, value: value.value },
|
|
135
|
+
fix: (fixer) => fixer.replaceText(value, `{${value.value}}`),
|
|
136
|
+
})
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
ImportDeclaration(node) {
|
|
140
|
+
const from = node.source?.value
|
|
141
|
+
if (from !== '@cascivo/react' && from !== '@cascivo/charts') return
|
|
142
|
+
for (const spec of node.specifiers) {
|
|
143
|
+
if (spec.type !== 'ImportSpecifier' || spec.imported?.type !== 'Identifier') continue
|
|
144
|
+
const real = vocabulary.imports[spec.imported.name]
|
|
145
|
+
if (!real) continue
|
|
146
|
+
context.report({
|
|
147
|
+
node: spec,
|
|
148
|
+
messageId: 'foreignImport',
|
|
149
|
+
data: { wrote: spec.imported.name, is: real },
|
|
150
|
+
// Only safe when the local name is not aliased — `{ Dialog as D }` would need
|
|
151
|
+
// every use site rewritten, which a fixer must not attempt.
|
|
152
|
+
fix:
|
|
153
|
+
spec.local?.name === spec.imported.name && source
|
|
154
|
+
? (fixer) => fixer.replaceText(spec, real)
|
|
155
|
+
: null,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
VariableDeclarator(node) {
|
|
161
|
+
if (node.id?.type !== 'ObjectPattern') return
|
|
162
|
+
const init = node.init
|
|
163
|
+
if (init?.type !== 'CallExpression' || init.callee?.type !== 'Identifier') return
|
|
164
|
+
const row = tupleHooks.get(init.callee.name)
|
|
165
|
+
if (!row) return
|
|
166
|
+
context.report({
|
|
167
|
+
node: node.id,
|
|
168
|
+
messageId: 'tupleHook',
|
|
169
|
+
data: { hook: row.hook, note: row.note },
|
|
170
|
+
})
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export default rule
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "GENERATED by scripts/eslint-vocabulary/generate.ts — edit packages/eslint-plugin/near-misses.json, then `pnpm regen`.",
|
|
3
|
+
"props": [
|
|
4
|
+
{
|
|
5
|
+
"component": "Text",
|
|
6
|
+
"wrote": "tone",
|
|
7
|
+
"is": "muted",
|
|
8
|
+
"note": "`tone` is the catalog's SEVERITY vocabulary (Status, Badge, Timeline, SideNav). Text emphasis is the boolean `muted`. For coloured text that means something, use Status or Badge.",
|
|
9
|
+
"report": "2026-08-21 item 2"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"component": "DataTable",
|
|
13
|
+
"wrote": "items",
|
|
14
|
+
"is": "rows",
|
|
15
|
+
"note": "DataTable renders a <table>, where \"rows\" is the domain word. Every other collection prop in the catalog is `items`.",
|
|
16
|
+
"report": "2026-08-08"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"component": "DataList",
|
|
20
|
+
"wrote": "rows",
|
|
21
|
+
"is": "items",
|
|
22
|
+
"note": "`rows` is DataTable's word and only DataTable's.",
|
|
23
|
+
"report": "2026-08-08"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"component": "Badge",
|
|
27
|
+
"wrote": "shape",
|
|
28
|
+
"is": "variant",
|
|
29
|
+
"note": "A visual style enum is always `variant` — never shape/kind/type/appearance.",
|
|
30
|
+
"report": "2026-08-08"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"numericProps": ["gap", "columnGap", "rowGap"],
|
|
34
|
+
"tupleHooks": [
|
|
35
|
+
{
|
|
36
|
+
"hook": "useTheme",
|
|
37
|
+
"note": "`useTheme()` returns a tuple `[name, setTheme]`, like useState. The object shape `{ theme, setTheme }` is next-themes'.",
|
|
38
|
+
"report": "2026-08-21"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"defaultDirection": [
|
|
42
|
+
{
|
|
43
|
+
"component": "Flex",
|
|
44
|
+
"note": "`Flex` defaults to `direction=\"vertical\"` — unlike CSS, and unlike Chakra/MUI/Radix. With no `direction`, `justify`/`align` act on the block axis, so a row comes out as a centred column.",
|
|
45
|
+
"report": "2026-08-21"
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"imports": {
|
|
49
|
+
"ToggleSwitch": "Toggle",
|
|
50
|
+
"Dialog": "Modal",
|
|
51
|
+
"DialogRoot": "Modal",
|
|
52
|
+
"Chip": "Tag",
|
|
53
|
+
"Pill": "Tag",
|
|
54
|
+
"DropdownMenu": "Dropdown",
|
|
55
|
+
"Placeholder": "Skeleton",
|
|
56
|
+
"Shimmer": "Skeleton",
|
|
57
|
+
"Popper": "Popover",
|
|
58
|
+
"SegmentedButton": "SegmentedControl",
|
|
59
|
+
"Disclosure": "Collapsible",
|
|
60
|
+
"SideSheet": "Sheet",
|
|
61
|
+
"SlideOver": "Sheet",
|
|
62
|
+
"NavigationDrawer": "Drawer",
|
|
63
|
+
"ConfirmDialog": "AlertDialog",
|
|
64
|
+
"Confirm": "AlertDialog",
|
|
65
|
+
"CircularProgress": "Spinner",
|
|
66
|
+
"Loader": "Spinner",
|
|
67
|
+
"LinearProgress": "InlineLoading",
|
|
68
|
+
"LoadingIndicator": "InlineLoading",
|
|
69
|
+
"SelectNative": "NativeSelect",
|
|
70
|
+
"Table": "DataTable",
|
|
71
|
+
"DataGrid": "DataTable",
|
|
72
|
+
"Layout": "AppShell",
|
|
73
|
+
"Shell": "AppShell",
|
|
74
|
+
"AppLayout": "AppShell",
|
|
75
|
+
"Sidebar": "SideNav",
|
|
76
|
+
"NavigationRail": "SideNav",
|
|
77
|
+
"SideBar": "SideNav",
|
|
78
|
+
"CommandPalette": "CommandMenu",
|
|
79
|
+
"Cmdk": "CommandMenu",
|
|
80
|
+
"Spotlight": "CommandMenu",
|
|
81
|
+
"EmptyView": "EmptyState",
|
|
82
|
+
"NoData": "EmptyState",
|
|
83
|
+
"ZeroState": "EmptyState",
|
|
84
|
+
"MoreMenu": "OverflowMenu",
|
|
85
|
+
"KebabMenu": "OverflowMenu",
|
|
86
|
+
"ActionMenu": "OverflowMenu",
|
|
87
|
+
"AvatarStack": "AvatarGroup",
|
|
88
|
+
"AvatarPile": "AvatarGroup",
|
|
89
|
+
"NumberField": "NumberInput",
|
|
90
|
+
"SpinButton": "NumberInput",
|
|
91
|
+
"Stepper": "NumberInput",
|
|
92
|
+
"PinInput": "OtpInput",
|
|
93
|
+
"VerificationCode": "OtpInput",
|
|
94
|
+
"ChipInput": "TagsInput",
|
|
95
|
+
"TokenInput": "TagsInput",
|
|
96
|
+
"Rating": "RatingGroup",
|
|
97
|
+
"StarRating": "RatingGroup",
|
|
98
|
+
"ToggleButtonGroup": "ToggleGroup",
|
|
99
|
+
"ButtonToggleGroup": "ToggleGroup"
|
|
100
|
+
}
|
|
101
|
+
}
|