@tenphi/eslint-plugin-tasty 1.18.0 → 1.19.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 +64 -45
- package/docs/rules/consistent-token-usage.md +72 -0
- package/docs/rules/known-property.md +55 -0
- package/docs/rules/no-classname-prop.md +60 -0
- package/docs/rules/no-ignored-pseudo-element-state.md +97 -0
- package/docs/rules/no-important.md +25 -0
- package/docs/rules/no-nested-selector.md +39 -0
- package/docs/rules/no-nested-state-map.md +33 -0
- package/docs/rules/no-own-at-root.md +57 -0
- package/docs/rules/no-protected-restyle.md +192 -0
- package/docs/rules/no-raw-color-values.md +49 -0
- package/docs/rules/no-raw-motion-duration.md +50 -0
- package/docs/rules/no-raw-transition-duration.md +56 -0
- package/docs/rules/no-runtime-styles-mutation.md +58 -0
- package/docs/rules/no-state-in-selector.md +142 -0
- package/docs/rules/no-style-prop.md +127 -0
- package/docs/rules/no-style-spread.md +53 -0
- package/docs/rules/no-styles-prop.md +57 -0
- package/docs/rules/no-unknown-state-alias.md +45 -0
- package/docs/rules/prefer-auto-calc.md +30 -0
- package/docs/rules/prefer-custom-property-syntax.md +56 -0
- package/docs/rules/prefer-directional-shorthand.md +46 -0
- package/docs/rules/prefer-element-selector.md +77 -0
- package/docs/rules/prefer-fill-mix.md +119 -0
- package/docs/rules/prefer-hide.md +31 -0
- package/docs/rules/prefer-longhand-property.md +25 -0
- package/docs/rules/prefer-shorthand-property.md +194 -0
- package/docs/rules/prefer-state-negation.md +44 -0
- package/docs/rules/require-default-state.md +50 -0
- package/docs/rules/static-no-dynamic-values.md +32 -0
- package/docs/rules/static-valid-selector.md +36 -0
- package/docs/rules/valid-boolean-property.md +39 -0
- package/docs/rules/valid-color-token.md +64 -0
- package/docs/rules/valid-custom-property.md +36 -0
- package/docs/rules/valid-custom-unit.md +34 -0
- package/docs/rules/valid-default-state-order.md +44 -0
- package/docs/rules/valid-directional-modifier.md +46 -0
- package/docs/rules/valid-preset.md +35 -0
- package/docs/rules/valid-radius-shape.md +31 -0
- package/docs/rules/valid-recipe.md +35 -0
- package/docs/rules/valid-state-definition.md +46 -0
- package/docs/rules/valid-state-key.md +62 -0
- package/docs/rules/valid-styles-structure.md +48 -0
- package/docs/rules/valid-sub-element.md +100 -0
- package/docs/rules/valid-transition.md +46 -0
- package/docs/rules/valid-value.md +65 -0
- package/harness/dev.harnesspm/hooks.json +12 -0
- package/harness/dev.harnesspm/rules/usage.md +11 -0
- package/harness/dev.harnesspm/scripts/check-edit.mjs +127 -0
- package/harness/dev.harnesspm/scripts/check.mjs +159 -0
- package/harness/dev.harnesspm/scripts/lint.mjs +236 -0
- package/harness/plugin.json +23 -0
- package/package.json +11 -7
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# tasty/valid-radius-shape
|
|
2
|
+
|
|
3
|
+
Reports a `radius` shape keyword Tasty does not know, such as `'circle'`.
|
|
4
|
+
|
|
5
|
+
**Presets:** error in recommended and strict. **Fix:** editor suggestions.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
`radius` takes four shape keywords besides lengths. Any other word renders no radius.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// ✅ Valid
|
|
15
|
+
radius: 'round'
|
|
16
|
+
radius: 'ellipse'
|
|
17
|
+
|
|
18
|
+
// ❌ Unknown radius shape 'circle'. Valid shapes: round, ellipse, leaf, backleaf.
|
|
19
|
+
radius: 'circle'
|
|
20
|
+
|
|
21
|
+
// ❌ Unknown radius shape 'roud'. Did you mean 'round'? Suggestion: 'round'
|
|
22
|
+
radius: 'roud'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Checks
|
|
26
|
+
|
|
27
|
+
Valid shapes: `round`, `ellipse`, `leaf`, `backleaf`. A word close to one of them gets an editor suggestion to replace it.
|
|
28
|
+
|
|
29
|
+
## Options
|
|
30
|
+
|
|
31
|
+
None.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# tasty/valid-recipe
|
|
2
|
+
|
|
3
|
+
Reports a recipe name in `recipe` that your config does not list.
|
|
4
|
+
|
|
5
|
+
**Presets:** error in recommended and strict. **Fix:** none.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Tasty drops a recipe it does not know, so the styles it should bring never apply.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// ✅ Valid, when tasty.config lists 'card' and 'elevated' in recipes
|
|
15
|
+
recipe: 'card elevated'
|
|
16
|
+
|
|
17
|
+
// ❌ Unknown recipe 'cards'
|
|
18
|
+
recipe: 'cards'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Checks
|
|
22
|
+
|
|
23
|
+
A recipe value is a space-separated list of names. A `/` separates the recipes applied after the component's own styles. `none` means no base recipes.
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
recipe: 'card elevated' → ['card', 'elevated']
|
|
27
|
+
recipe: 'reset input / autofill' → before: ['reset', 'input'], after: ['autofill']
|
|
28
|
+
recipe: 'none / autofill' → before: [], after: ['autofill']
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Each name must be in `recipes` in `tasty.config.*`. Without `recipes` in the config, the rule does nothing.
|
|
32
|
+
|
|
33
|
+
## Options
|
|
34
|
+
|
|
35
|
+
None.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# tasty/valid-state-definition
|
|
2
|
+
|
|
3
|
+
Reports an invalid state alias definition in `configure({ states })`: a key without `@`, or a value that is not a valid state expression.
|
|
4
|
+
|
|
5
|
+
**Presets:** warning in strict. **Fix:** none.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Every style that uses an alias gets its definition. A broken definition breaks each of those states at once.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// ✅ Valid
|
|
15
|
+
configure({
|
|
16
|
+
states: {
|
|
17
|
+
'@mobile': '@media(w < 768px)',
|
|
18
|
+
'@dark': '@root(theme=dark)',
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ❌ Empty @media() query.
|
|
23
|
+
configure({
|
|
24
|
+
states: {
|
|
25
|
+
'@mobile': '@media()',
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ❌ State alias 'mobile' must start with '@'.
|
|
30
|
+
configure({
|
|
31
|
+
states: {
|
|
32
|
+
mobile: '@media(w < 768px)',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Checks
|
|
38
|
+
|
|
39
|
+
1. An alias key starts with `@`.
|
|
40
|
+
2. Its value parses as a state expression, with the same parser as [`tasty/valid-state-key`](valid-state-key.md).
|
|
41
|
+
|
|
42
|
+
The rule reads `configure()` calls imported from Tasty.
|
|
43
|
+
|
|
44
|
+
## Options
|
|
45
|
+
|
|
46
|
+
None.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# tasty/valid-state-key
|
|
2
|
+
|
|
3
|
+
Reports a state key that does not parse, such as `'hovered &'` or `':hovr'`, a misused `_` fallback floor, and an `@alias` your config does not list.
|
|
4
|
+
|
|
5
|
+
**Presets:** error in recommended and strict. **Fix:** none.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Tasty skips a state key it cannot parse, so the value under it never applies. Nothing reports it at runtime.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// ✅ Valid
|
|
15
|
+
fill: { '': '#white', hovered: '#gray.05' }
|
|
16
|
+
fill: { '': '#white', ':hover': '#gray.05' }
|
|
17
|
+
fill: { '': '#white', '@mobile': '#gray' }
|
|
18
|
+
|
|
19
|
+
// ✅ A standalone '_' fallback floor
|
|
20
|
+
inset: { _: 'auto', '@(scroll-state(stuck: top))': '0 auto auto auto' }
|
|
21
|
+
|
|
22
|
+
// ✅ The '_' floor and the '' default together
|
|
23
|
+
fill: { _: '#white', '': '#gray', hovered: '#blue' }
|
|
24
|
+
|
|
25
|
+
// ❌ Expected expression after '&' operator.
|
|
26
|
+
fill: { '': '#white', 'hovered &': '#gray' }
|
|
27
|
+
|
|
28
|
+
// ❌ Unknown pseudo-class ':hovr'.
|
|
29
|
+
fill: { '': '#white', ':hovr': '#gray' }
|
|
30
|
+
|
|
31
|
+
// ❌ Unknown state alias '@mobil'. Configured aliases: @mobile.
|
|
32
|
+
fill: { '': '#white', '@mobil': '#gray' }
|
|
33
|
+
|
|
34
|
+
// ❌ The fallback floor "_" cannot be combined with other state logic.
|
|
35
|
+
fill: { '': '#white', '_ & hovered': '#blue' }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Valid state keys
|
|
39
|
+
|
|
40
|
+
- `''`: the default, the lowest priority among states.
|
|
41
|
+
- `_`: the fallback floor, standalone only.
|
|
42
|
+
- A modifier: `hovered`, `pressed`, `disabled`.
|
|
43
|
+
- A value modifier: `theme=danger`, `size=large`, `size^=sm`, with `=`, `^=`, `$=` or `*=`.
|
|
44
|
+
- A pseudo-class: any standard CSS pseudo-class, such as `:hover`, `:focus-visible`, `:popover-open`, `:nth-child(2n+1)` or `:dir(rtl)`.
|
|
45
|
+
- A class or an attribute: `.active`, `[aria-expanded="true"]`, `[data-mode]`.
|
|
46
|
+
- Combinations: `hovered & .active`, `!disabled`, `(loading | processing) & !readonly`, with `&`, `|`, `^` and `!`.
|
|
47
|
+
- Advanced states: `@media(...)`, `@root(...)`, `@parent(...)`, `@(...)` container queries, `@own(...)`, `@starting`, `@supports(...)`.
|
|
48
|
+
- Aliases: `@mobile`, `@dark` and others from `states` in `tasty.config.*` or local `@name` keys.
|
|
49
|
+
|
|
50
|
+
## Checks
|
|
51
|
+
|
|
52
|
+
1. **Syntax.** Operators, balanced parentheses, known pseudo-classes, and the dimension shorthands (`w`, `h`, `is`, `bs`) inside `@media(...)` and `@(...)`.
|
|
53
|
+
2. **Aliases.** When `states` in `tasty.config.*` lists aliases, every `@name` must be one of them or a local alias. [`tasty/no-unknown-state-alias`](no-unknown-state-alias.md) does the same with more context, in the strict preset.
|
|
54
|
+
3. **Fallback floor.** `_` stands alone. Combined with other state logic (`_ & hovered`, `_ | loading`), Tasty ignores the key.
|
|
55
|
+
|
|
56
|
+
## The `_` fallback floor
|
|
57
|
+
|
|
58
|
+
A standalone `_` key is a fallback for the whole map. Its value always applies, and states with higher priority layer over it through the cascade. It covers the case where a negated `@supports(...)` or container-query default would otherwise never apply, which leaves no rule active. `_` can sit next to `''`: one is the always-on floor, the other the negated default.
|
|
59
|
+
|
|
60
|
+
## Options
|
|
61
|
+
|
|
62
|
+
None.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# tasty/valid-styles-structure
|
|
2
|
+
|
|
3
|
+
Reports a styles object with the wrong shape: state keys at the top level, at-rule keys whose value is not the object they need, a `recipe` that is not a string, and the at-rule key spellings of Tasty v2.
|
|
4
|
+
|
|
5
|
+
**Presets:** error in recommended and strict. **Fix:** autofix (renamed at-rule keys).
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
State maps go inside property values. A state key at the top of a styles object, such as `':hover': { fill: '#gray' }`, is read as a property name and renders nothing.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// ❌ State key '' at top level is not valid. State maps belong inside property values.
|
|
15
|
+
tastyStatic({
|
|
16
|
+
'': { fill: '#white' },
|
|
17
|
+
_: { fill: '#white' }, // the '_' floor goes inside a property too
|
|
18
|
+
':hover': { fill: '#gray' },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// ✅ A state map inside the property
|
|
22
|
+
tastyStatic({
|
|
23
|
+
fill: {
|
|
24
|
+
'': '#white',
|
|
25
|
+
':hover': '#gray',
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ❌ '@properties' was renamed to '@property' in Tasty v3. Autofix: '@property'
|
|
30
|
+
tasty({ styles: { '@properties': { angle: { syntax: '<angle>', inherits: false, initialValue: '0deg' } } } });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Checks
|
|
34
|
+
|
|
35
|
+
1. Top-level keys are properties, sub-elements or special keys, not state keys.
|
|
36
|
+
2. `@keyframes` is an object of `{ name: { step: styles } }`.
|
|
37
|
+
3. `@property` is an object of `{ name: { syntax, inherits, initialValue } }`.
|
|
38
|
+
4. `@font-face` is an object of `{ familyName: descriptors | descriptors[] }`.
|
|
39
|
+
5. `@counter-style` is an object of `{ name: descriptors }`.
|
|
40
|
+
6. `@function` is an object of `{ '$$name': { args, result } }`.
|
|
41
|
+
7. `recipe` is a string.
|
|
42
|
+
8. The Tasty v2 spellings `@properties`, `@fontFace` and `@counterStyle` are renamed to `@property`, `@font-face` and `@counter-style`. This is the only autofix, so `eslint --fix` does most of a v2 upgrade.
|
|
43
|
+
|
|
44
|
+
An at-rule value is reported only when it is surely not an object: a literal, a template or an array. A variable such as `'@font-face': fontFaceMap` may hold the object and is left alone.
|
|
45
|
+
|
|
46
|
+
## Options
|
|
47
|
+
|
|
48
|
+
None.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# tasty/valid-sub-element
|
|
2
|
+
|
|
3
|
+
Sub-element values must be style objects, or `false` or `null` to disable the
|
|
4
|
+
sub-element. In a later `mergeStyles` layer or a `styles` prop, either one
|
|
5
|
+
removes the sub-element styles below it.
|
|
6
|
+
Their `$` selectors must select inner elements or pseudo-elements, rather than
|
|
7
|
+
reuse the sub-element mechanism to style the component root.
|
|
8
|
+
|
|
9
|
+
**Presets:** error in recommended and strict. **Fix:** none.
|
|
10
|
+
|
|
11
|
+
For a nested sub-element, `&` attaches to the containing sub-element's selector
|
|
12
|
+
rather than the component root. A nested `Heading: { $: '&:is(h1)' }` inside
|
|
13
|
+
`Outer` therefore styles `Outer` itself. Put that condition in `Outer`'s property
|
|
14
|
+
state maps using `@own(:is(h1))`, or select a descendant heading instead.
|
|
15
|
+
|
|
16
|
+
## Reported
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
const Heading = tasty({
|
|
20
|
+
styles: {
|
|
21
|
+
Level1: { $: '&:is(h1)', preset: 'h1' },
|
|
22
|
+
Level2: { $: '&:is(h2)', preset: 'h2' },
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`&:is(h1)` attaches directly to the root selector. It selects a root `h1`,
|
|
28
|
+
rather than an element named `Level1`. The same issue applies to root compounds
|
|
29
|
+
such as `&.active` or `&[data-level="1"]`. Each `$` value gets one error, even
|
|
30
|
+
when several branches select the root. A root-targeting branch is also reported
|
|
31
|
+
in a list that contains valid pseudo-element or descendant selectors.
|
|
32
|
+
|
|
33
|
+
## Style the root with a property state map
|
|
34
|
+
|
|
35
|
+
If the component itself can be a heading, put the condition in the root's
|
|
36
|
+
property state map. Preset names below assume corresponding configured presets.
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
const Heading = tasty({
|
|
40
|
+
styles: {
|
|
41
|
+
preset: {
|
|
42
|
+
'': 'body',
|
|
43
|
+
':is(h1)': 'h1',
|
|
44
|
+
':is(h2)': 'h2',
|
|
45
|
+
':is(h3)': 'h3',
|
|
46
|
+
':is(h4)': 'h4',
|
|
47
|
+
':is(h5)': 'h5',
|
|
48
|
+
':is(h6)': 'h6',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Choose the default or fallback preset according to the component's contract.
|
|
55
|
+
|
|
56
|
+
## Select descendant headings with an affix
|
|
57
|
+
|
|
58
|
+
If the headings are inside the component, use a descendant selector:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
const Content = tasty({
|
|
62
|
+
styles: {
|
|
63
|
+
Level1: { $: 'h1', preset: 'h1' },
|
|
64
|
+
Level2: { $: 'h2', preset: 'h2' },
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The `h1` affix selects descendant `h1` tags. Omitting `$` selects
|
|
70
|
+
`[data-element="Level1"]` instead. Combinators and `@` placeholders remain
|
|
71
|
+
supported, for example `$: '>h1'` and `$: '>@'`.
|
|
72
|
+
|
|
73
|
+
Root pseudo-elements remain valid:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
Before: { $: '&::before', content: '""' }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The legacy pseudo-element spellings `:before`, `:after`, `:first-line`, and
|
|
80
|
+
`:first-letter` are also supported. The separate `no-state-in-selector` rule
|
|
81
|
+
may warn about conditions such as `&:hover::before`; those conditions belong
|
|
82
|
+
in property state maps.
|
|
83
|
+
|
|
84
|
+
## Scope and limits
|
|
85
|
+
|
|
86
|
+
The selector check applies to literal strings and templates without interpolation
|
|
87
|
+
inside owned sub-elements, including nested sub-elements, typed/shared styles,
|
|
88
|
+
JSX/Storybook styles, and configured style helpers. TypeScript wrappers and
|
|
89
|
+
statically computed sub-element and `$` keys are supported.
|
|
90
|
+
|
|
91
|
+
Root `$` values, selector arguments to `tastyStatic()` or `useGlobalStyles()`,
|
|
92
|
+
unrelated objects, and dynamic selectors are outside the selector check.
|
|
93
|
+
Malformed selectors that cannot be parsed are left to syntax validation.
|
|
94
|
+
|
|
95
|
+
There is no autofix or editor suggestion: choosing between a root state map and
|
|
96
|
+
a descendant selector changes which elements receive the styles.
|
|
97
|
+
|
|
98
|
+
## Options
|
|
99
|
+
|
|
100
|
+
None.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# tasty/valid-transition
|
|
2
|
+
|
|
3
|
+
Reports a `transition` name that is neither a Tasty semantic transition nor a CSS property, a camelCase CSS name, and a CSS name that has a semantic equivalent.
|
|
4
|
+
|
|
5
|
+
**Presets:** warning in recommended and strict. **Fix:** editor suggestions.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Tasty expands semantic names (`fill`, `theme`, `radius`, …) into the CSS properties they cover. Any other name is lowercased and emitted as is, so a typo or a camelCase name animates nothing.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// ✅ Valid
|
|
15
|
+
transition: 'fill 0.2s'
|
|
16
|
+
transition: 'theme 0.3s ease'
|
|
17
|
+
transition: '$$my-prop 0.2s'
|
|
18
|
+
|
|
19
|
+
// ❌ Unknown transition name 'colour'
|
|
20
|
+
transition: 'colour 0.2s'
|
|
21
|
+
|
|
22
|
+
// ❌ 'gridTemplateColumns' compiles to 'gridtemplatecolumns', which is not a CSS property.
|
|
23
|
+
// Suggestion: 'grid-template-columns'
|
|
24
|
+
transition: 'gridTemplateColumns 0.2s'
|
|
25
|
+
|
|
26
|
+
// ❌ 'background-color' has a Tasty semantic equivalent. Suggestion: 'fill'
|
|
27
|
+
transition: 'background-color 0.2s'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Checks
|
|
31
|
+
|
|
32
|
+
A transition value is a comma-separated list. Each entry is `name [duration] [easing] [delay]`.
|
|
33
|
+
|
|
34
|
+
1. The name must be a semantic transition, a CSS property, or a `$$name` custom property reference.
|
|
35
|
+
2. A CSS property must be kebab-case. `zIndex` is the exception: it is a semantic name that compiles to `z-index`.
|
|
36
|
+
3. A CSS property with a semantic equivalent (`background-color`, `box-shadow`) gets a suggestion to use the semantic name.
|
|
37
|
+
|
|
38
|
+
Semantic names: `fade`, `fill`, `color`, `theme`, `border`, `radius`, `shadow`, `outline`, `preset`, `text`, `gap`, `opacity`, `translate`, `rotate`, `scale`, `filter`, `image`, `background`, `width`, `height`, `inset` and `zIndex`.
|
|
39
|
+
|
|
40
|
+
`flow` is reported although the CSS property dataset lists it: no browser implements it, and Tasty has no `flow` transition.
|
|
41
|
+
|
|
42
|
+
Durations are checked by [`tasty/no-raw-transition-duration`](no-raw-transition-duration.md).
|
|
43
|
+
|
|
44
|
+
## Options
|
|
45
|
+
|
|
46
|
+
None.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# tasty/valid-value
|
|
2
|
+
|
|
3
|
+
Parses every string style value and reports what Tasty cannot read: unknown words, modifiers a property does not take, colors where none belong, unbalanced parentheses and `!important`.
|
|
4
|
+
|
|
5
|
+
**Presets:** error in recommended and strict. **Fix:** none.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Tasty's runtime parser files every word it does not recognize as a modifier and moves on, so a typo renders as nothing, with no error. This rule uses the plugin's own value parser, which reports every unrecognized token instead.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// ❌ Modifier 'topp' is not valid for 'padding'. Accepted: top, right, bottom, left, longhand.
|
|
15
|
+
padding: '2x topp'
|
|
16
|
+
|
|
17
|
+
// ❌ Property 'padding' does not accept color tokens, but found '#red'.
|
|
18
|
+
padding: '2x #red'
|
|
19
|
+
|
|
20
|
+
// ❌ Unbalanced parentheses in value.
|
|
21
|
+
fill: 'rgb(255, 0, 0'
|
|
22
|
+
|
|
23
|
+
// ❌ 'svgFill' takes a single paint: Tasty keeps one color and drops the rest.
|
|
24
|
+
svgFill: 'none #red'
|
|
25
|
+
|
|
26
|
+
// ❌ '#purple' is not expanded after url()
|
|
27
|
+
svgFill: 'url(#gradient) #purple'
|
|
28
|
+
|
|
29
|
+
// ✅ Valid
|
|
30
|
+
fill: '#purple.5'
|
|
31
|
+
padding: '2x top bottom'
|
|
32
|
+
border: '1bw solid #red top'
|
|
33
|
+
border: '1bw Highlight' // CSS named and system colors are colors
|
|
34
|
+
width: 'min 200px'
|
|
35
|
+
display: '-webkit-box' // with -webkit-line-clamp; also -webkit-inline-box
|
|
36
|
+
aspectRatio: '16/10' // <ratio> allows optional whitespace: same as '16 / 10'
|
|
37
|
+
svgFill: 'context-stroke'
|
|
38
|
+
svgFill: 'url(#gradient) currentColor' // a url() fallback is plain CSS
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
1. The value is split into groups (by commas) and parts (by spaced slashes), and each token is classified: color token, custom property, function, number with a unit, keyword, or unknown.
|
|
44
|
+
2. The result is compared with what the property expects:
|
|
45
|
+
- A property without modifiers (`fill`, `color`, `opacity`, `width`, `height`, …) must have no modifier words.
|
|
46
|
+
- A property with modifiers (`border`, `padding`, `radius`, …) takes only modifiers from its own set.
|
|
47
|
+
- A property without colors (`padding`, `margin`, `gap`, `width`, …) must have no color tokens.
|
|
48
|
+
- `svgFill` is the native SVG `fill`, so it also takes `context-fill` and `context-stroke`. It takes one paint: Tasty keeps one color and drops the rest. A `url()` may have one fallback, but Tasty passes that value through as plain CSS, so a `#name` fallback is reported: it is never expanded.
|
|
49
|
+
|
|
50
|
+
`recipe` (checked by [`tasty/valid-recipe`](valid-recipe.md)) and the at-rule keys are not parsed.
|
|
51
|
+
|
|
52
|
+
## Related rules
|
|
53
|
+
|
|
54
|
+
`valid-value` asks "does it parse?". Other rules check that the names exist in your config:
|
|
55
|
+
|
|
56
|
+
| Check | Rule |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| Does this value parse without unknown tokens? | `tasty/valid-value` |
|
|
59
|
+
| Is `#purple` a known color token? | [`tasty/valid-color-token`](valid-color-token.md) |
|
|
60
|
+
| Is `3cols` a known unit? | [`tasty/valid-custom-unit`](valid-custom-unit.md) |
|
|
61
|
+
| Is `$spacign` a known custom property? | [`tasty/valid-custom-property`](valid-custom-property.md) |
|
|
62
|
+
|
|
63
|
+
## Options
|
|
64
|
+
|
|
65
|
+
None.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Linting Tasty styles
|
|
2
|
+
|
|
3
|
+
## After you change styles
|
|
4
|
+
|
|
5
|
+
- Lint the changed files and fix every `tasty/*` problem.
|
|
6
|
+
- `tasty-lint:check <files>` runs only the Tasty rules.
|
|
7
|
+
- Each `tasty/<rule>` has a page in the `tasty-lint` docs index.
|
|
8
|
+
|
|
9
|
+
## Exceptions
|
|
10
|
+
|
|
11
|
+
- Disable a rule for one line only: `// eslint-disable-next-line tasty/<rule> -- <reason>`.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The check-edit hook: after an agent edits a file that imports Tasty, lints
|
|
3
|
+
* it with the recommended Tasty rules and hands the new problems back.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code runs it on PostToolUse for Edit, Write and MultiEdit, with the
|
|
6
|
+
* event as JSON on stdin. Exit 0 says nothing. Exit 2 puts stderr in front of
|
|
7
|
+
* the agent. Any other exit shows stderr to the person only, which is how a
|
|
8
|
+
* broken setup is reported: the agent cannot fix it.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
11
|
+
import { dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
|
+
import {
|
|
14
|
+
NO_PARSER,
|
|
15
|
+
collectProblems,
|
|
16
|
+
createLinter,
|
|
17
|
+
formatProblems,
|
|
18
|
+
importsFrom,
|
|
19
|
+
isLintable,
|
|
20
|
+
isTypeScript,
|
|
21
|
+
loadESLint,
|
|
22
|
+
loadTypeScriptParser,
|
|
23
|
+
newProblems,
|
|
24
|
+
rulePage,
|
|
25
|
+
} from './lint.mjs';
|
|
26
|
+
|
|
27
|
+
/** More problems than this are summed up, not listed. */
|
|
28
|
+
const LIMIT = 20;
|
|
29
|
+
|
|
30
|
+
const PACKAGE_ROOT = realpathSync(
|
|
31
|
+
fileURLToPath(new URL('../../../', import.meta.url)),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
function setupError(message) {
|
|
35
|
+
console.error(`tasty-lint (check-edit hook): ${message}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let event;
|
|
40
|
+
try {
|
|
41
|
+
event = JSON.parse(readFileSync(0, 'utf8') || '{}');
|
|
42
|
+
} catch {
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
const filePath = event?.tool_input?.file_path;
|
|
46
|
+
if (typeof filePath !== 'string' || !isLintable(filePath)) process.exit(0);
|
|
47
|
+
|
|
48
|
+
const cwd = typeof event.cwd === 'string' ? event.cwd : process.cwd();
|
|
49
|
+
const projectDir = process.env.HARNESSPM_PROJECT_DIR || cwd;
|
|
50
|
+
const file = resolve(cwd, filePath);
|
|
51
|
+
let text;
|
|
52
|
+
try {
|
|
53
|
+
text = readFileSync(file, 'utf8');
|
|
54
|
+
} catch {
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const build = (name) => pathToFileURL(join(PACKAGE_ROOT, 'dist', name)).href;
|
|
59
|
+
if (!existsSync(join(PACKAGE_ROOT, 'dist/index.js'))) {
|
|
60
|
+
setupError(`cannot find the plugin build in ${PACKAGE_ROOT}/dist.`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The cheap test first. Only a file without a Tasty import pays for loading
|
|
64
|
+
// the config, to look for the design system modules it lists.
|
|
65
|
+
if (!importsFrom(text)) {
|
|
66
|
+
const { loadConfig } = await import(build('config.js'));
|
|
67
|
+
let sources = [];
|
|
68
|
+
try {
|
|
69
|
+
sources = loadConfig(file).importSources;
|
|
70
|
+
} catch {
|
|
71
|
+
// A broken config is reported when a file that imports Tasty is linted.
|
|
72
|
+
}
|
|
73
|
+
if (!importsFrom(text, sources)) process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let problems;
|
|
77
|
+
try {
|
|
78
|
+
const { default: plugin, recommended } = await import(build('index.js'));
|
|
79
|
+
const dirs = [dirname(file), projectDir];
|
|
80
|
+
const ESLint = await loadESLint(dirs, PACKAGE_ROOT);
|
|
81
|
+
const parser = await loadTypeScriptParser(dirs);
|
|
82
|
+
if (!parser && isTypeScript(file)) setupError(NO_PARSER);
|
|
83
|
+
const eslint = createLinter({
|
|
84
|
+
ESLint,
|
|
85
|
+
plugin,
|
|
86
|
+
rules: recommended,
|
|
87
|
+
parser,
|
|
88
|
+
cwd: projectDir,
|
|
89
|
+
});
|
|
90
|
+
const lint = async (source) =>
|
|
91
|
+
collectProblems(
|
|
92
|
+
await eslint.lintText(source, { filePath: file }),
|
|
93
|
+
projectDir,
|
|
94
|
+
).problems;
|
|
95
|
+
|
|
96
|
+
problems = await lint(text);
|
|
97
|
+
// Report only what this edit added, so problems an older line already had
|
|
98
|
+
// do not come back after every edit of the file.
|
|
99
|
+
const original = event.tool_response?.originalFile;
|
|
100
|
+
if (problems.length > 0 && typeof original === 'string' && original) {
|
|
101
|
+
problems = newProblems(problems, text, await lint(original), original);
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
setupError(error instanceof Error ? error.message : String(error));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (problems.length === 0) process.exit(0);
|
|
108
|
+
|
|
109
|
+
const shown = problems.slice(0, LIMIT);
|
|
110
|
+
const relativeFile = shown[0].file;
|
|
111
|
+
// Paths print relative to the project root: the event's cwd is wherever the
|
|
112
|
+
// agent's shell went last, such as a folder in node_modules.
|
|
113
|
+
const pageOf = (name) =>
|
|
114
|
+
rulePage(name, { projectDir, packageRoot: PACKAGE_ROOT, cwd: projectDir });
|
|
115
|
+
const out = [
|
|
116
|
+
`tasty-lint: this edit left ${problems.length} Tasty lint problem${problems.length === 1 ? '' : 's'} in ${relativeFile}. Fix ${problems.length === 1 ? 'it' : 'them'}. For an intentional exception, put \`// eslint-disable-next-line tasty/<rule> -- <reason>\` above the line.`,
|
|
117
|
+
'',
|
|
118
|
+
formatProblems(shown, pageOf),
|
|
119
|
+
];
|
|
120
|
+
if (problems.length > LIMIT) {
|
|
121
|
+
out.push(
|
|
122
|
+
'',
|
|
123
|
+
`${problems.length - LIMIT} more: run \`npx harnesspm run tasty-lint:check ${relativeFile}\`.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
console.error(out.join('\n'));
|
|
127
|
+
process.exit(2);
|