@tenphi/eslint-plugin-tasty 1.4.0 → 1.5.1
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 +31 -2
- package/dist/configs.js +1 -0
- package/dist/configs.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/rules/no-style-spread.js +23 -3
- package/dist/rules/no-style-spread.js.map +1 -1
- package/dist/rules/prefer-element-selector.js +101 -0
- package/dist/rules/prefer-element-selector.js.map +1 -0
- package/dist/rules/valid-sub-element.js +84 -8
- package/dist/rules/valid-sub-element.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -178,7 +178,7 @@ for typing shared signatures.
|
|
|
178
178
|
| `tasty/valid-styles-structure` | error | Invalid styles object structure, and the v2 at-rule key spellings (auto-fixable) |
|
|
179
179
|
| `tasty/no-nested-state-map` | error | Nested state maps (not supported) |
|
|
180
180
|
| `tasty/no-important` | error | `!important` usage (breaks tasty specificity) |
|
|
181
|
-
| `tasty/valid-sub-element` | error | Sub-element values must be style objects |
|
|
181
|
+
| `tasty/valid-sub-element` | error | Sub-element values must be style objects; `$` selectors must not target the root element ([details](docs/rules/valid-sub-element.md)) |
|
|
182
182
|
| `tasty/valid-directional-modifier` | error | Directional modifiers on wrong properties, and more than one value in a group that names directions. Physical properties take `top`/`right`/`bottom`/`left`, logical ones `start`/`end`, and mixing the two is reported in either direction |
|
|
183
183
|
| `tasty/valid-radius-shape` | error | Unknown radius shape keywords |
|
|
184
184
|
| `tasty/valid-preset` | error | Unknown preset names |
|
|
@@ -186,6 +186,7 @@ for typing shared signatures.
|
|
|
186
186
|
| `tasty/valid-transition` | warn | Unknown transition property names |
|
|
187
187
|
| `tasty/no-nested-selector` | warn | `&`-prefixed nested selectors (use sub-elements) |
|
|
188
188
|
| `tasty/no-state-in-selector` | warn | Pseudo-class and attribute conditions in sub-element `$` selectors; put conditions in property state maps ([details](docs/rules/no-state-in-selector.md)) |
|
|
189
|
+
| `tasty/prefer-element-selector` | warn | Prefer element names such as `Primary > Search` over exact `data-element` attributes in sub-element `$` selectors (safely autofixable; [details](docs/rules/prefer-element-selector.md)) |
|
|
189
190
|
| `tasty/static-no-dynamic-values` | error | Dynamic values in `tastyStatic()` |
|
|
190
191
|
| `tasty/static-valid-selector` | error | Invalid selector in `tastyStatic(selector, ...)` |
|
|
191
192
|
| `tasty/require-default-state` | error | Missing default (`''`) or fallback floor (`_`) key in state mappings (skipped for extending calls) |
|
|
@@ -195,7 +196,7 @@ for typing shared signatures.
|
|
|
195
196
|
| `tasty/no-raw-color-values` | warn | Raw hex/rgb/`okhsl`/`okhst`/`oklch`/named colors instead of `#color` tokens |
|
|
196
197
|
| `tasty/no-raw-transition-duration` | warn | Hardcoded `transition` duration (`fill 0.2s`) in Tasty styles or a local `tasty()` component's `transition` prop. Suggests a configured duration token or the implicit per-name timing; zero and delays are left alone |
|
|
197
198
|
| `tasty/no-runtime-styles-mutation` | warn | JavaScript variables, calls, conditionals, computed keys, or interpolated templates in Tasty style values; use states and tokens instead |
|
|
198
|
-
| `tasty/no-style-spread` | warn | Object or array spreads inside runtime Tasty styles;
|
|
199
|
+
| `tasty/no-style-spread` | warn | Object or array spreads inside runtime Tasty styles; recommends `mergeStyles` for multiple root spreads; intentional spreads need a per-line suppression and reason |
|
|
199
200
|
| `tasty/consistent-token-usage` | warn | Raw px values when custom units or tokens exist |
|
|
200
201
|
| `tasty/prefer-auto-calc` | warn | `calc(...)` instead of Tasty auto-calc `(...)` |
|
|
201
202
|
| `tasty/prefer-state-negation` | warn | Top-level `:not(...)` in state keys instead of the `!` prefix (autofixable; nested CSS selectors stay intact) |
|
|
@@ -215,6 +216,13 @@ for typing shared signatures.
|
|
|
215
216
|
|
|
216
217
|
### Structural selectors and state maps
|
|
217
218
|
|
|
219
|
+
`tasty/valid-sub-element` reports selectors such as
|
|
220
|
+
`Level1: { $: '&:is(h1)', preset: 'h1' }` as errors: `&:is(h1)` selects the
|
|
221
|
+
component root. Use a root `preset` state map to style a root heading, or
|
|
222
|
+
`$: 'h1'` to select descendant headings. Root pseudo-elements such as
|
|
223
|
+
`&::before` remain supported. See the [sub-element rule guide](docs/rules/valid-sub-element.md)
|
|
224
|
+
for both alternatives.
|
|
225
|
+
|
|
218
226
|
Keep a sub-element's `$` selector focused on the element's identity and position.
|
|
219
227
|
`tasty/no-state-in-selector` warns on conditions such as `img:not([width])`,
|
|
220
228
|
`button[disabled]`, and `>@:hover`. Express those conditions in property state
|
|
@@ -251,6 +259,27 @@ const Card = tasty({
|
|
|
251
259
|
});
|
|
252
260
|
```
|
|
253
261
|
|
|
262
|
+
When a root style object contains two or more spreads, the rule reports one
|
|
263
|
+
composition warning at the first spread and recommends `mergeStyles`:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import { mergeStyles, type Styles } from '@tenphi/tasty';
|
|
267
|
+
|
|
268
|
+
// Warning: shallow composition can discard sub-element properties and states.
|
|
269
|
+
const finalStyles: Styles = { ...outerStyles, ...styles };
|
|
270
|
+
|
|
271
|
+
// Use Tasty's merge semantics, in the same override order.
|
|
272
|
+
const mergedStyles: Styles = mergeStyles(outerStyles, styles);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
For example, spreading `Label: { fill: '#accent' }` over
|
|
276
|
+
`Label: { padding: '2x', fill: '#surface' }` loses `padding`; `mergeStyles`
|
|
277
|
+
preserves it. State maps without a default key extend inherited entries, while
|
|
278
|
+
maps with a `''` default deliberately replace them. This is a behavioral choice,
|
|
279
|
+
so the rule provides no automatic rewrite. If shallow replacement is intentional,
|
|
280
|
+
suppress the warning at the first spread with a reason. Single spreads and spreads
|
|
281
|
+
inside state maps, sub-elements or arrays retain the ordinary spread warning.
|
|
282
|
+
|
|
254
283
|
`tastyStatic()` still rejects spreads and dynamic expressions as errors because
|
|
255
284
|
its build-time extractor cannot evaluate them. An ESLint suppression does not
|
|
256
285
|
make a spread valid there. To make warnings fail CI until addressed or explicitly
|
package/dist/configs.js
CHANGED
|
@@ -14,6 +14,7 @@ const recommended = {
|
|
|
14
14
|
"tasty/valid-radius-shape": "error",
|
|
15
15
|
"tasty/no-nested-selector": "error",
|
|
16
16
|
"tasty/no-state-in-selector": "warn",
|
|
17
|
+
"tasty/prefer-element-selector": "warn",
|
|
17
18
|
"tasty/static-no-dynamic-values": "error",
|
|
18
19
|
"tasty/static-valid-selector": "error",
|
|
19
20
|
"tasty/valid-preset": "error",
|
package/dist/configs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configs.js","names":[],"sources":["../src/configs.ts"],"sourcesContent":["import type { TSESLint } from '@typescript-eslint/utils';\n\nexport const recommended: TSESLint.SharedConfig.RulesRecord = {\n 'tasty/known-property': 'warn',\n 'tasty/valid-value': 'error',\n 'tasty/valid-color-token': 'error',\n 'tasty/valid-custom-unit': 'error',\n 'tasty/valid-boolean-property': 'error',\n 'tasty/valid-state-key': 'error',\n 'tasty/valid-styles-structure': 'error',\n 'tasty/no-nested-state-map': 'error',\n 'tasty/no-important': 'error',\n 'tasty/valid-sub-element': 'error',\n 'tasty/valid-directional-modifier': 'error',\n 'tasty/valid-radius-shape': 'error',\n 'tasty/no-nested-selector': 'error',\n 'tasty/no-state-in-selector': 'warn',\n 'tasty/static-no-dynamic-values': 'error',\n 'tasty/static-valid-selector': 'error',\n 'tasty/valid-preset': 'error',\n 'tasty/valid-recipe': 'error',\n 'tasty/valid-transition': 'warn',\n 'tasty/require-default-state': 'error',\n 'tasty/no-own-at-root': 'warn',\n 'tasty/valid-default-state-order': 'warn',\n 'tasty/prefer-shorthand-property': 'warn',\n 'tasty/no-raw-color-values': 'warn',\n 'tasty/no-raw-transition-duration': 'warn',\n 'tasty/no-runtime-styles-mutation': 'warn',\n 'tasty/no-style-spread': 'warn',\n 'tasty/consistent-token-usage': 'warn',\n 'tasty/prefer-auto-calc': 'warn',\n 'tasty/prefer-state-negation': 'warn',\n 'tasty/prefer-custom-property-syntax': 'warn',\n 'tasty/prefer-hide': 'warn',\n 'tasty/prefer-directional-shorthand': 'warn',\n 'tasty/prefer-longhand-property': 'error',\n};\n\nexport const strict: TSESLint.SharedConfig.RulesRecord = {\n ...recommended,\n 'tasty/valid-custom-property': 'warn',\n 'tasty/no-unknown-state-alias': 'warn',\n 'tasty/no-styles-prop': 'warn',\n 'tasty/valid-state-definition': 'warn',\n};\n"],"mappings":";AAEA,MAAa,cAAiD;CAC5D,wBAAwB;CACxB,qBAAqB;CACrB,2BAA2B;CAC3B,2BAA2B;CAC3B,gCAAgC;CAChC,yBAAyB;CACzB,gCAAgC;CAChC,6BAA6B;CAC7B,sBAAsB;CACtB,2BAA2B;CAC3B,oCAAoC;CACpC,4BAA4B;CAC5B,4BAA4B;CAC5B,8BAA8B;CAC9B,kCAAkC;CAClC,+BAA+B;CAC/B,sBAAsB;CACtB,sBAAsB;CACtB,0BAA0B;CAC1B,+BAA+B;CAC/B,wBAAwB;CACxB,mCAAmC;CACnC,mCAAmC;CACnC,6BAA6B;CAC7B,oCAAoC;CACpC,oCAAoC;CACpC,yBAAyB;CACzB,gCAAgC;CAChC,0BAA0B;CAC1B,+BAA+B;CAC/B,uCAAuC;CACvC,qBAAqB;CACrB,sCAAsC;CACtC,kCAAkC;CACnC;AAED,MAAa,SAA4C;CACvD,GAAG;CACH,+BAA+B;CAC/B,gCAAgC;CAChC,wBAAwB;CACxB,gCAAgC;CACjC"}
|
|
1
|
+
{"version":3,"file":"configs.js","names":[],"sources":["../src/configs.ts"],"sourcesContent":["import type { TSESLint } from '@typescript-eslint/utils';\n\nexport const recommended: TSESLint.SharedConfig.RulesRecord = {\n 'tasty/known-property': 'warn',\n 'tasty/valid-value': 'error',\n 'tasty/valid-color-token': 'error',\n 'tasty/valid-custom-unit': 'error',\n 'tasty/valid-boolean-property': 'error',\n 'tasty/valid-state-key': 'error',\n 'tasty/valid-styles-structure': 'error',\n 'tasty/no-nested-state-map': 'error',\n 'tasty/no-important': 'error',\n 'tasty/valid-sub-element': 'error',\n 'tasty/valid-directional-modifier': 'error',\n 'tasty/valid-radius-shape': 'error',\n 'tasty/no-nested-selector': 'error',\n 'tasty/no-state-in-selector': 'warn',\n 'tasty/prefer-element-selector': 'warn',\n 'tasty/static-no-dynamic-values': 'error',\n 'tasty/static-valid-selector': 'error',\n 'tasty/valid-preset': 'error',\n 'tasty/valid-recipe': 'error',\n 'tasty/valid-transition': 'warn',\n 'tasty/require-default-state': 'error',\n 'tasty/no-own-at-root': 'warn',\n 'tasty/valid-default-state-order': 'warn',\n 'tasty/prefer-shorthand-property': 'warn',\n 'tasty/no-raw-color-values': 'warn',\n 'tasty/no-raw-transition-duration': 'warn',\n 'tasty/no-runtime-styles-mutation': 'warn',\n 'tasty/no-style-spread': 'warn',\n 'tasty/consistent-token-usage': 'warn',\n 'tasty/prefer-auto-calc': 'warn',\n 'tasty/prefer-state-negation': 'warn',\n 'tasty/prefer-custom-property-syntax': 'warn',\n 'tasty/prefer-hide': 'warn',\n 'tasty/prefer-directional-shorthand': 'warn',\n 'tasty/prefer-longhand-property': 'error',\n};\n\nexport const strict: TSESLint.SharedConfig.RulesRecord = {\n ...recommended,\n 'tasty/valid-custom-property': 'warn',\n 'tasty/no-unknown-state-alias': 'warn',\n 'tasty/no-styles-prop': 'warn',\n 'tasty/valid-state-definition': 'warn',\n};\n"],"mappings":";AAEA,MAAa,cAAiD;CAC5D,wBAAwB;CACxB,qBAAqB;CACrB,2BAA2B;CAC3B,2BAA2B;CAC3B,gCAAgC;CAChC,yBAAyB;CACzB,gCAAgC;CAChC,6BAA6B;CAC7B,sBAAsB;CACtB,2BAA2B;CAC3B,oCAAoC;CACpC,4BAA4B;CAC5B,4BAA4B;CAC5B,8BAA8B;CAC9B,iCAAiC;CACjC,kCAAkC;CAClC,+BAA+B;CAC/B,sBAAsB;CACtB,sBAAsB;CACtB,0BAA0B;CAC1B,+BAA+B;CAC/B,wBAAwB;CACxB,mCAAmC;CACnC,mCAAmC;CACnC,6BAA6B;CAC7B,oCAAoC;CACpC,oCAAoC;CACpC,yBAAyB;CACzB,gCAAgC;CAChC,0BAA0B;CAC1B,+BAA+B;CAC/B,uCAAuC;CACvC,qBAAqB;CACrB,sCAAsC;CACtC,kCAAkC;CACnC;AAED,MAAa,SAA4C;CACvD,GAAG;CACH,+BAA+B;CAC/B,gCAAgC;CAChC,wBAAwB;CACxB,gCAAgC;CACjC"}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import valid_state_key_default from "./rules/valid-state-key.js";
|
|
|
6
6
|
import valid_styles_structure_default from "./rules/valid-styles-structure.js";
|
|
7
7
|
import no_nested_selector_default from "./rules/no-nested-selector.js";
|
|
8
8
|
import no_state_in_selector_default from "./rules/no-state-in-selector.js";
|
|
9
|
+
import prefer_element_selector_default from "./rules/prefer-element-selector.js";
|
|
9
10
|
import valid_custom_property_default from "./rules/valid-custom-property.js";
|
|
10
11
|
import valid_preset_default from "./rules/valid-preset.js";
|
|
11
12
|
import valid_recipe_default from "./rules/valid-recipe.js";
|
|
@@ -50,6 +51,7 @@ const ruleMap = {
|
|
|
50
51
|
"valid-styles-structure": valid_styles_structure_default,
|
|
51
52
|
"no-nested-selector": no_nested_selector_default,
|
|
52
53
|
"no-state-in-selector": no_state_in_selector_default,
|
|
54
|
+
"prefer-element-selector": prefer_element_selector_default,
|
|
53
55
|
"valid-custom-property": valid_custom_property_default,
|
|
54
56
|
"valid-preset": valid_preset_default,
|
|
55
57
|
"valid-recipe": valid_recipe_default,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["rules.knownProperty","rules.validValue","rules.validColorToken","rules.validCustomUnit","rules.validStateKey","rules.validStylesStructure","rules.noNestedSelector","rules.noStateInSelector","rules.validCustomProperty","rules.validPreset","rules.validRecipe","rules.validBooleanProperty","rules.validDirectionalModifier","rules.validRadiusShape","rules.noImportant","rules.validSubElement","rules.noNestedStateMap","rules.staticNoDynamicValues","rules.staticValidSelector","rules.preferShorthandProperty","rules.validTransition","rules.requireDefaultState","rules.noUnknownStateAlias","rules.noRawColorValues","rules.noRawTransitionDuration","rules.noRawMotionDuration","rules.noStylesProp","rules.consistentTokenUsage","rules.noRuntimeStylesMutation","rules.noStyleSpread","rules.validStateDefinition","rules.noOwnAtRoot","rules.validDefaultStateOrder","rules.preferAutoCalc","rules.preferStateNegation","rules.preferCustomPropertySyntax","rules.preferHide","rules.preferDirectionalShorthand","rules.preferLonghandProperty"],"sources":["../src/index.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { TSESLint } from '@typescript-eslint/utils';\nimport * as rules from './rules/index.js';\nimport { recommended, strict } from './configs.js';\n\nconst ruleMap: Record<string, TSESLint.RuleModule<string, unknown[]>> = {\n 'known-property': rules.knownProperty,\n 'valid-value': rules.validValue,\n 'valid-color-token': rules.validColorToken,\n 'valid-custom-unit': rules.validCustomUnit,\n 'valid-state-key': rules.validStateKey,\n 'valid-styles-structure': rules.validStylesStructure,\n 'no-nested-selector': rules.noNestedSelector,\n 'no-state-in-selector': rules.noStateInSelector,\n 'valid-custom-property': rules.validCustomProperty,\n 'valid-preset': rules.validPreset,\n 'valid-recipe': rules.validRecipe,\n 'valid-boolean-property': rules.validBooleanProperty,\n 'valid-directional-modifier': rules.validDirectionalModifier,\n 'valid-radius-shape': rules.validRadiusShape,\n 'no-important': rules.noImportant,\n 'valid-sub-element': rules.validSubElement,\n 'no-nested-state-map': rules.noNestedStateMap,\n 'static-no-dynamic-values': rules.staticNoDynamicValues,\n 'static-valid-selector': rules.staticValidSelector,\n 'prefer-shorthand-property': rules.preferShorthandProperty,\n 'valid-transition': rules.validTransition,\n 'require-default-state': rules.requireDefaultState,\n 'no-unknown-state-alias': rules.noUnknownStateAlias,\n 'no-raw-color-values': rules.noRawColorValues,\n 'no-raw-transition-duration': rules.noRawTransitionDuration,\n 'no-raw-motion-duration': rules.noRawMotionDuration,\n 'no-styles-prop': rules.noStylesProp,\n 'consistent-token-usage': rules.consistentTokenUsage,\n 'no-runtime-styles-mutation': rules.noRuntimeStylesMutation,\n 'no-style-spread': rules.noStyleSpread,\n 'valid-state-definition': rules.validStateDefinition,\n 'no-own-at-root': rules.noOwnAtRoot,\n 'valid-default-state-order': rules.validDefaultStateOrder,\n 'prefer-auto-calc': rules.preferAutoCalc,\n 'prefer-state-negation': rules.preferStateNegation,\n 'prefer-custom-property-syntax': rules.preferCustomPropertySyntax,\n 'prefer-hide': rules.preferHide,\n 'prefer-directional-shorthand': rules.preferDirectionalShorthand,\n 'prefer-longhand-property': rules.preferLonghandProperty,\n};\n\n// Read from package.json so `meta.version` cannot drift from the released\n// version, as the previous hardcoded literal had. `../package.json` resolves to\n// the package root from both `src/` and the unbundled `dist/`.\nconst { version } = createRequire(import.meta.url)('../package.json') as {\n version: string;\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version,\n },\n rules: ruleMap,\n configs: {\n recommended: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: recommended,\n },\n strict: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: strict,\n },\n },\n} satisfies TSESLint.FlatConfig.Plugin & {\n configs: Record<string, TSESLint.FlatConfig.Config>;\n};\n\nexport default plugin;\n\nexport { recommended, strict } from './configs.js';\nexport type {\n TastyValidationConfig,\n ResolvedConfig,\n StyleFunctionConfig,\n} from './types.js';\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","names":["rules.knownProperty","rules.validValue","rules.validColorToken","rules.validCustomUnit","rules.validStateKey","rules.validStylesStructure","rules.noNestedSelector","rules.noStateInSelector","rules.preferElementSelector","rules.validCustomProperty","rules.validPreset","rules.validRecipe","rules.validBooleanProperty","rules.validDirectionalModifier","rules.validRadiusShape","rules.noImportant","rules.validSubElement","rules.noNestedStateMap","rules.staticNoDynamicValues","rules.staticValidSelector","rules.preferShorthandProperty","rules.validTransition","rules.requireDefaultState","rules.noUnknownStateAlias","rules.noRawColorValues","rules.noRawTransitionDuration","rules.noRawMotionDuration","rules.noStylesProp","rules.consistentTokenUsage","rules.noRuntimeStylesMutation","rules.noStyleSpread","rules.validStateDefinition","rules.noOwnAtRoot","rules.validDefaultStateOrder","rules.preferAutoCalc","rules.preferStateNegation","rules.preferCustomPropertySyntax","rules.preferHide","rules.preferDirectionalShorthand","rules.preferLonghandProperty"],"sources":["../src/index.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { TSESLint } from '@typescript-eslint/utils';\nimport * as rules from './rules/index.js';\nimport { recommended, strict } from './configs.js';\n\nconst ruleMap: Record<string, TSESLint.RuleModule<string, unknown[]>> = {\n 'known-property': rules.knownProperty,\n 'valid-value': rules.validValue,\n 'valid-color-token': rules.validColorToken,\n 'valid-custom-unit': rules.validCustomUnit,\n 'valid-state-key': rules.validStateKey,\n 'valid-styles-structure': rules.validStylesStructure,\n 'no-nested-selector': rules.noNestedSelector,\n 'no-state-in-selector': rules.noStateInSelector,\n 'prefer-element-selector': rules.preferElementSelector,\n 'valid-custom-property': rules.validCustomProperty,\n 'valid-preset': rules.validPreset,\n 'valid-recipe': rules.validRecipe,\n 'valid-boolean-property': rules.validBooleanProperty,\n 'valid-directional-modifier': rules.validDirectionalModifier,\n 'valid-radius-shape': rules.validRadiusShape,\n 'no-important': rules.noImportant,\n 'valid-sub-element': rules.validSubElement,\n 'no-nested-state-map': rules.noNestedStateMap,\n 'static-no-dynamic-values': rules.staticNoDynamicValues,\n 'static-valid-selector': rules.staticValidSelector,\n 'prefer-shorthand-property': rules.preferShorthandProperty,\n 'valid-transition': rules.validTransition,\n 'require-default-state': rules.requireDefaultState,\n 'no-unknown-state-alias': rules.noUnknownStateAlias,\n 'no-raw-color-values': rules.noRawColorValues,\n 'no-raw-transition-duration': rules.noRawTransitionDuration,\n 'no-raw-motion-duration': rules.noRawMotionDuration,\n 'no-styles-prop': rules.noStylesProp,\n 'consistent-token-usage': rules.consistentTokenUsage,\n 'no-runtime-styles-mutation': rules.noRuntimeStylesMutation,\n 'no-style-spread': rules.noStyleSpread,\n 'valid-state-definition': rules.validStateDefinition,\n 'no-own-at-root': rules.noOwnAtRoot,\n 'valid-default-state-order': rules.validDefaultStateOrder,\n 'prefer-auto-calc': rules.preferAutoCalc,\n 'prefer-state-negation': rules.preferStateNegation,\n 'prefer-custom-property-syntax': rules.preferCustomPropertySyntax,\n 'prefer-hide': rules.preferHide,\n 'prefer-directional-shorthand': rules.preferDirectionalShorthand,\n 'prefer-longhand-property': rules.preferLonghandProperty,\n};\n\n// Read from package.json so `meta.version` cannot drift from the released\n// version, as the previous hardcoded literal had. `../package.json` resolves to\n// the package root from both `src/` and the unbundled `dist/`.\nconst { version } = createRequire(import.meta.url)('../package.json') as {\n version: string;\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version,\n },\n rules: ruleMap,\n configs: {\n recommended: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: recommended,\n },\n strict: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: strict,\n },\n },\n} satisfies TSESLint.FlatConfig.Plugin & {\n configs: Record<string, TSESLint.FlatConfig.Config>;\n};\n\nexport default plugin;\n\nexport { recommended, strict } from './configs.js';\nexport type {\n TastyValidationConfig,\n ResolvedConfig,\n StyleFunctionConfig,\n} from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,MAAM,UAAkE;CACtE,kBAAkBA;CAClB,eAAeC;CACf,qBAAqBC;CACrB,qBAAqBC;CACrB,mBAAmBC;CACnB,0BAA0BC;CAC1B,sBAAsBC;CACtB,wBAAwBC;CACxB,2BAA2BC;CAC3B,yBAAyBC;CACzB,gBAAgBC;CAChB,gBAAgBC;CAChB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,sBAAsBC;CACtB,gBAAgBC;CAChB,qBAAqBC;CACrB,uBAAuBC;CACvB,4BAA4BC;CAC5B,yBAAyBC;CACzB,6BAA6BC;CAC7B,oBAAoBC;CACpB,yBAAyBC;CACzB,0BAA0BC;CAC1B,uBAAuBC;CACvB,8BAA8BC;CAC9B,0BAA0BC;CAC1B,kBAAkBC;CAClB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,mBAAmBC;CACnB,0BAA0BC;CAC1B,kBAAkBC;CAClB,6BAA6BC;CAC7B,oBAAoBC;CACpB,yBAAyBC;CACzB,iCAAiCC;CACjC,eAAeC;CACf,gCAAgCC;CAChC,4BAA4BC;CAC7B;AAKD,MAAM,EAAE,YAAY,cAAc,OAAO,KAAK,IAAI,CAAC,kBAAkB;AAIrE,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN;EACD;CACD,OAAO;CACP,SAAS;EACP,aAAa;GACX,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACD,QAAQ;GACN,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACF;CACF"}
|
|
@@ -7,13 +7,17 @@ var no_style_spread_default = createRule({
|
|
|
7
7
|
name: "no-style-spread",
|
|
8
8
|
meta: {
|
|
9
9
|
type: "suggestion",
|
|
10
|
-
docs: { description: "Warn about spreads
|
|
11
|
-
messages: {
|
|
10
|
+
docs: { description: "Warn about style spreads and recommend mergeStyles for composition" },
|
|
11
|
+
messages: {
|
|
12
|
+
styleSpread: "List Tasty style properties explicitly instead of spreading. If this spread is intentional, suppress this warning on this line with an ESLint comment and a reason.",
|
|
13
|
+
preferMergeStyles: "Use mergeStyles(...) to compose Tasty style objects. Object spread is shallow and can discard sub-element properties and state-map entries. If shallow replacement is intentional, suppress this warning on this line with a reason."
|
|
14
|
+
},
|
|
12
15
|
schema: []
|
|
13
16
|
},
|
|
14
17
|
defaultOptions: [],
|
|
15
18
|
create(context) {
|
|
16
19
|
const ctx = new TastyContext(context);
|
|
20
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
17
21
|
function checkValue(node) {
|
|
18
22
|
const value = unwrapExpression(node);
|
|
19
23
|
if (value.type === "ObjectExpression") for (const prop of value.properties) if (prop.type === "SpreadElement") context.report({
|
|
@@ -31,15 +35,31 @@ var no_style_spread_default = createRule({
|
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
function handleStyleObject(node) {
|
|
38
|
+
if (visited.has(node)) return;
|
|
34
39
|
const styleCtx = ctx.getStyleContext(node);
|
|
35
40
|
if (!styleCtx || styleCtx.isStaticCall) return;
|
|
36
41
|
if (ctx.getRootStyleObject(node) !== node) return;
|
|
37
|
-
|
|
42
|
+
visited.add(node);
|
|
43
|
+
const spreads = node.properties.filter((prop) => prop.type === "SpreadElement");
|
|
44
|
+
if (spreads.length < 2) {
|
|
45
|
+
checkValue(node);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
context.report({
|
|
49
|
+
node: spreads[0],
|
|
50
|
+
messageId: "preferMergeStyles"
|
|
51
|
+
});
|
|
52
|
+
for (const prop of node.properties) if (prop.type === "Property") checkValue(prop.value);
|
|
38
53
|
}
|
|
39
54
|
return {
|
|
40
55
|
ImportDeclaration(node) {
|
|
41
56
|
ctx.trackImport(node);
|
|
42
57
|
},
|
|
58
|
+
VariableDeclarator(node) {
|
|
59
|
+
if (!node.init) return;
|
|
60
|
+
const value = unwrapExpression(node.init);
|
|
61
|
+
if (value.type === "ObjectExpression") handleStyleObject(value);
|
|
62
|
+
},
|
|
43
63
|
...styleObjectListeners(handleStyleObject)
|
|
44
64
|
};
|
|
45
65
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-style-spread.js","names":[],"sources":["../../src/rules/no-style-spread.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'styleSpread';\n\nexport default createRule<[], MessageIds>({\n name: 'no-style-spread',\n meta: {\n type: 'suggestion',\n docs: {\n description
|
|
1
|
+
{"version":3,"file":"no-style-spread.js","names":[],"sources":["../../src/rules/no-style-spread.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'styleSpread' | 'preferMergeStyles';\n\nexport default createRule<[], MessageIds>({\n name: 'no-style-spread',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Warn about style spreads and recommend mergeStyles for composition',\n },\n messages: {\n styleSpread:\n 'List Tasty style properties explicitly instead of spreading. If this spread is intentional, suppress this warning on this line with an ESLint comment and a reason.',\n preferMergeStyles:\n 'Use mergeStyles(...) to compose Tasty style objects. Object spread is shallow and can discard sub-element properties and state-map entries. If shallow replacement is intentional, suppress this warning on this line with a reason.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const visited = new WeakSet<TSESTree.ObjectExpression>();\n\n function checkValue(node: TSESTree.Node): void {\n const value = unwrapExpression(node);\n\n if (value.type === 'ObjectExpression') {\n for (const prop of value.properties) {\n if (prop.type === 'SpreadElement') {\n context.report({ node: prop, messageId: 'styleSpread' });\n } else {\n checkValue(prop.value);\n }\n }\n } else if (value.type === 'ArrayExpression') {\n for (const element of value.elements) {\n if (!element) continue;\n if (element.type === 'SpreadElement') {\n context.report({ node: element, messageId: 'styleSpread' });\n } else {\n checkValue(element);\n }\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression): void {\n if (visited.has(node)) return;\n const styleCtx = ctx.getStyleContext(node);\n // tastyStatic rejects spreads during extraction, so its existing error\n // gives the useful diagnostic without a second warning.\n if (!styleCtx || styleCtx.isStaticCall) return;\n if (ctx.getRootStyleObject(node) !== node) return;\n visited.add(node);\n\n const spreads = node.properties.filter(\n (prop): prop is TSESTree.SpreadElement => prop.type === 'SpreadElement',\n );\n if (spreads.length < 2) {\n checkValue(node);\n return;\n }\n\n // Report composition once, at its first spread so the existing per-line\n // suppression convention still works. Nested state maps/arrays are not\n // Styles objects and must keep their ordinary spread diagnostics.\n context.report({ node: spreads[0], messageId: 'preferMergeStyles' });\n for (const prop of node.properties) {\n if (prop.type === 'Property') checkValue(prop.value);\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n VariableDeclarator(node) {\n if (!node.init) return;\n // The shared selectors cover only one as/satisfies wrapper. Typed\n // composition can also have nested wrappers or a non-null assertion.\n const value = unwrapExpression(node.init);\n if (value.type === 'ObjectExpression') handleStyleObject(value);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,8BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,sEACH;EACD,UAAU;GACR,aACE;GACF,mBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,0BAAU,IAAI,SAAoC;EAExD,SAAS,WAAW,MAA2B;GAC7C,MAAM,QAAQ,iBAAiB,KAAK;AAEpC,OAAI,MAAM,SAAS,mBACjB,MAAK,MAAM,QAAQ,MAAM,WACvB,KAAI,KAAK,SAAS,gBAChB,SAAQ,OAAO;IAAE,MAAM;IAAM,WAAW;IAAe,CAAC;OAExD,YAAW,KAAK,MAAM;YAGjB,MAAM,SAAS,kBACxB,MAAK,MAAM,WAAW,MAAM,UAAU;AACpC,QAAI,CAAC,QAAS;AACd,QAAI,QAAQ,SAAS,gBACnB,SAAQ,OAAO;KAAE,MAAM;KAAS,WAAW;KAAe,CAAC;QAE3D,YAAW,QAAQ;;;EAM3B,SAAS,kBAAkB,MAAuC;AAChE,OAAI,QAAQ,IAAI,KAAK,CAAE;GACvB,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAG1C,OAAI,CAAC,YAAY,SAAS,aAAc;AACxC,OAAI,IAAI,mBAAmB,KAAK,KAAK,KAAM;AAC3C,WAAQ,IAAI,KAAK;GAEjB,MAAM,UAAU,KAAK,WAAW,QAC7B,SAAyC,KAAK,SAAS,gBACzD;AACD,OAAI,QAAQ,SAAS,GAAG;AACtB,eAAW,KAAK;AAChB;;AAMF,WAAQ,OAAO;IAAE,MAAM,QAAQ;IAAI,WAAW;IAAqB,CAAC;AACpE,QAAK,MAAM,QAAQ,KAAK,WACtB,KAAI,KAAK,SAAS,WAAY,YAAW,KAAK,MAAM;;AAIxD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,mBAAmB,MAAM;AACvB,QAAI,CAAC,KAAK,KAAM;IAGhB,MAAM,QAAQ,iBAAiB,KAAK,KAAK;AACzC,QAAI,MAAM,SAAS,mBAAoB,mBAAkB,MAAM;;GAEjE,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.js";
|
|
2
|
+
import { getKeyName, getStringValue, unwrapExpression } from "../utils.js";
|
|
3
|
+
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
|
+
import { replaceInStringValue } from "../fix-utils.js";
|
|
5
|
+
import selectorParser from "postcss-selector-parser";
|
|
6
|
+
|
|
7
|
+
//#region src/rules/prefer-element-selector.ts
|
|
8
|
+
function elementEdits(selector, owner) {
|
|
9
|
+
if (selector.includes("\\") || selector.includes("/*")) return [];
|
|
10
|
+
try {
|
|
11
|
+
const root = selectorParser().astSync(selector);
|
|
12
|
+
const edits = [];
|
|
13
|
+
for (const branch of root.nodes) {
|
|
14
|
+
if (branch.nodes.some((node, index) => {
|
|
15
|
+
switch (node.type) {
|
|
16
|
+
case "nesting": return index !== 0;
|
|
17
|
+
case "tag": return !/^[A-Za-z][a-zA-Z0-9-]*$/.test(node.value);
|
|
18
|
+
case "combinator": return !/^(?:\s+|[>+~])$/.test(node.value);
|
|
19
|
+
case "attribute": return node.toString().includes(",");
|
|
20
|
+
case "class":
|
|
21
|
+
case "universal": return false;
|
|
22
|
+
default: return true;
|
|
23
|
+
}
|
|
24
|
+
})) continue;
|
|
25
|
+
for (const [index, node] of branch.nodes.entries()) {
|
|
26
|
+
if (node.type !== "attribute") continue;
|
|
27
|
+
const previous = branch.nodes[index - 1];
|
|
28
|
+
if (previous && previous.type !== "nesting" && !(previous.type === "combinator" && /^[>+~]$/.test(previous.value))) continue;
|
|
29
|
+
const match = selector.slice(node.sourceIndex).match(/^\[\s*data-element\s*=\s*(?:"([A-Z][a-zA-Z0-9]*)"|'([A-Z][a-zA-Z0-9]*)'|([A-Z][a-zA-Z0-9]*))\s*\]/);
|
|
30
|
+
if (!match) continue;
|
|
31
|
+
if (/^[a-zA-Z0-9]/.test(selector.slice(node.sourceIndex + match[0].length))) continue;
|
|
32
|
+
const name = match[1] ?? match[2] ?? match[3];
|
|
33
|
+
if (index === branch.nodes.length - 1 && name !== owner) continue;
|
|
34
|
+
edits.push({
|
|
35
|
+
start: node.sourceIndex,
|
|
36
|
+
end: node.sourceIndex + match[0].length,
|
|
37
|
+
replacement: name
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return edits;
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
var prefer_element_selector_default = createRule({
|
|
47
|
+
name: "prefer-element-selector",
|
|
48
|
+
meta: {
|
|
49
|
+
type: "suggestion",
|
|
50
|
+
docs: { description: "Prefer Tasty element names over exact data-element selectors" },
|
|
51
|
+
fixable: "code",
|
|
52
|
+
messages: { preferElementSelector: "Use Tasty element names instead of explicit data-element attributes in this sub-element '$' selector." },
|
|
53
|
+
schema: []
|
|
54
|
+
},
|
|
55
|
+
defaultOptions: [],
|
|
56
|
+
create(context) {
|
|
57
|
+
const ctx = new TastyContext(context);
|
|
58
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
59
|
+
function checkAffix(owner, node) {
|
|
60
|
+
for (const prop of node.properties) {
|
|
61
|
+
if (prop.type !== "Property") continue;
|
|
62
|
+
if ((prop.computed ? getStringValue(unwrapExpression(prop.key)) : getKeyName(prop.key)) !== "$") continue;
|
|
63
|
+
const value = unwrapExpression(prop.value);
|
|
64
|
+
const selector = getStringValue(value);
|
|
65
|
+
if (selector === null) continue;
|
|
66
|
+
const edits = elementEdits(selector, owner);
|
|
67
|
+
if (!edits.length) continue;
|
|
68
|
+
context.report({
|
|
69
|
+
node: prop.value,
|
|
70
|
+
messageId: "preferElementSelector",
|
|
71
|
+
fix: (fixer) => replaceInStringValue(fixer, value, edits, context.sourceCode)
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function checkStyleObject(node) {
|
|
76
|
+
if (visited.has(node)) return;
|
|
77
|
+
visited.add(node);
|
|
78
|
+
for (const prop of node.properties) {
|
|
79
|
+
if (prop.type !== "Property") continue;
|
|
80
|
+
const key = prop.computed ? getStringValue(unwrapExpression(prop.key)) : getKeyName(prop.key);
|
|
81
|
+
if (key === null || !ctx.isSubElementKey(key)) continue;
|
|
82
|
+
const value = unwrapExpression(prop.value);
|
|
83
|
+
if (value.type !== "ObjectExpression") continue;
|
|
84
|
+
checkAffix(key, value);
|
|
85
|
+
checkStyleObject(value);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
ImportDeclaration(node) {
|
|
90
|
+
ctx.trackImport(node);
|
|
91
|
+
},
|
|
92
|
+
...styleObjectListeners((node) => {
|
|
93
|
+
if (ctx.isStyleObject(node)) checkStyleObject(node);
|
|
94
|
+
})
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
100
|
+
export { prefer_element_selector_default as default };
|
|
101
|
+
//# sourceMappingURL=prefer-element-selector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-element-selector.js","names":[],"sources":["../../src/rules/prefer-element-selector.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport selectorParser from 'postcss-selector-parser';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { replaceInStringValue, type StringEdit } from '../fix-utils.js';\nimport { getKeyName, getStringValue, unwrapExpression } from '../utils.js';\n\nfunction elementEdits(selector: string, owner: string): StringEdit[] {\n // Tasty's affix tokenizer does not implement the full CSS grammar. Keep\n // escapes, comments and functional pseudo arguments out of this rewrite.\n if (selector.includes('\\\\') || selector.includes('/*')) return [];\n try {\n const root = selectorParser().astSync(selector);\n const edits: StringEdit[] = [];\n for (const branch of root.nodes) {\n if (\n branch.nodes.some((node, index) => {\n switch (node.type) {\n case 'nesting':\n return index !== 0;\n case 'tag':\n return !/^[A-Za-z][a-zA-Z0-9-]*$/.test(node.value);\n case 'combinator':\n return !/^(?:\\s+|[>+~])$/.test(node.value);\n case 'attribute':\n return node.toString().includes(',');\n case 'class':\n case 'universal':\n return false;\n default:\n return true;\n }\n })\n )\n continue;\n\n for (const [index, node] of branch.nodes.entries()) {\n if (node.type !== 'attribute') continue;\n const previous = branch.nodes[index - 1];\n // Uppercase names insert a separator. Attributes within a compound\n // (including whitespace that Tasty ignores) must remain attributes.\n if (\n previous &&\n previous.type !== 'nesting' &&\n !(previous.type === 'combinator' && /^[>+~]$/.test(previous.value))\n )\n continue;\n\n // Match the raw spelling too: CSS flags, namespaces, escapes and\n // unsupported element names do not have equivalent Tasty shorthand.\n const match = selector\n .slice(node.sourceIndex)\n .match(\n /^\\[\\s*data-element\\s*=\\s*(?:\"([A-Z][a-zA-Z0-9]*)\"|'([A-Z][a-zA-Z0-9]*)'|([A-Z][a-zA-Z0-9]*))\\s*\\]/,\n );\n if (!match) continue;\n // Without authored whitespace, the replacement could merge with a\n // following tag/element name: [data-element=\"Primary\"]Search.\n if (\n /^[a-zA-Z0-9]/.test(\n selector.slice(node.sourceIndex + match[0].length),\n )\n )\n continue;\n const name = match[1] ?? match[2] ?? match[3];\n // A different trailing name would append the owner's descendant.\n // Keep that final attribute, while still shortening safe ancestors.\n if (index === branch.nodes.length - 1 && name !== owner) continue;\n edits.push({\n start: node.sourceIndex,\n end: node.sourceIndex + match[0].length,\n replacement: name,\n });\n }\n }\n return edits;\n } catch {\n return [];\n }\n}\n\nexport default createRule<[], 'preferElementSelector'>({\n name: 'prefer-element-selector',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Prefer Tasty element names over exact data-element selectors',\n },\n fixable: 'code',\n messages: {\n preferElementSelector:\n \"Use Tasty element names instead of explicit data-element attributes in this sub-element '$' selector.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const visited = new WeakSet<TSESTree.ObjectExpression>();\n\n function checkAffix(owner: string, node: TSESTree.ObjectExpression) {\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n const key = prop.computed\n ? getStringValue(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n if (key !== '$') continue;\n const value = unwrapExpression(prop.value);\n const selector = getStringValue(value);\n if (selector === null) continue;\n const edits = elementEdits(selector, owner);\n if (!edits.length) continue;\n context.report({\n node: prop.value,\n messageId: 'preferElementSelector',\n fix: (fixer) =>\n replaceInStringValue(fixer, value, edits, context.sourceCode),\n });\n }\n }\n\n function checkStyleObject(node: TSESTree.ObjectExpression) {\n if (visited.has(node)) return;\n visited.add(node);\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n const key = prop.computed\n ? getStringValue(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n if (key === null || !ctx.isSubElementKey(key)) continue;\n const value = unwrapExpression(prop.value);\n if (value.type !== 'ObjectExpression') continue;\n checkAffix(key, value);\n checkStyleObject(value);\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners((node) => {\n if (ctx.isStyleObject(node)) checkStyleObject(node);\n }),\n };\n },\n});\n"],"mappings":"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"}
|
|
@@ -1,27 +1,100 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
|
-
import { getKeyName, unwrapExpression } from "../utils.js";
|
|
2
|
+
import { getKeyName, getStringValue, unwrapExpression } from "../utils.js";
|
|
3
3
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
|
+
import selectorParser from "postcss-selector-parser";
|
|
4
5
|
|
|
5
6
|
//#region src/rules/valid-sub-element.ts
|
|
7
|
+
const LEGACY_PSEUDO_ELEMENTS = new Set([
|
|
8
|
+
":before",
|
|
9
|
+
":after",
|
|
10
|
+
":first-line",
|
|
11
|
+
":first-letter"
|
|
12
|
+
]);
|
|
13
|
+
function hasDescendantTarget(nodes) {
|
|
14
|
+
let lastCharacter = "";
|
|
15
|
+
for (const node of nodes.slice(1)) {
|
|
16
|
+
if (node.type === "combinator") {
|
|
17
|
+
if (node.value.trim()) return true;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (node.type === "tag" || node.type === "universal") {
|
|
21
|
+
if (lastCharacter) return true;
|
|
22
|
+
} else if (node.type === "class" || node.type === "attribute") {
|
|
23
|
+
if (lastCharacter && !/[\]@a-zA-Z0-9-]/.test(lastCharacter)) return true;
|
|
24
|
+
}
|
|
25
|
+
lastCharacter = node.toString().trim().slice(-1);
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function firstRootSelector(selector, key) {
|
|
30
|
+
try {
|
|
31
|
+
const root = selectorParser().astSync(selector);
|
|
32
|
+
for (const branch of root.nodes) {
|
|
33
|
+
const nodes = branch.nodes.filter((node) => node.type !== "comment");
|
|
34
|
+
if (nodes[0]?.type !== "nesting" || nodes.length < 2) continue;
|
|
35
|
+
if (nodes.some((node) => node.type === "tag" && node.value.startsWith("@"))) continue;
|
|
36
|
+
const trailingElement = branch.toString().trim().slice(1).trim().match(/(?:^|[\s>+~\]:])([A-Z][a-zA-Z0-9]*)$/);
|
|
37
|
+
if (trailingElement && trailingElement[1] !== key) continue;
|
|
38
|
+
if (hasDescendantTarget(nodes)) continue;
|
|
39
|
+
if (!nodes.some((node) => {
|
|
40
|
+
if (node.type !== "pseudo") return false;
|
|
41
|
+
let name = node.value.toLowerCase();
|
|
42
|
+
if (name.startsWith("::")) return true;
|
|
43
|
+
if (name.includes("\\")) {
|
|
44
|
+
const identifier = selectorParser().astSync(name.slice(1)).first?.first;
|
|
45
|
+
if (identifier?.type === "tag") name = ":" + identifier.value.toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
return LEGACY_PSEUDO_ELEMENTS.has(name);
|
|
48
|
+
})) return branch.toString().trim();
|
|
49
|
+
}
|
|
50
|
+
} catch {}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
6
53
|
var valid_sub_element_default = createRule({
|
|
7
54
|
name: "valid-sub-element",
|
|
8
55
|
meta: {
|
|
9
56
|
type: "problem",
|
|
10
|
-
docs: { description: "Validate sub-element
|
|
11
|
-
messages: {
|
|
57
|
+
docs: { description: "Validate sub-element values and prevent root-targeting affixes" },
|
|
58
|
+
messages: {
|
|
59
|
+
subElementNotObject: "Sub-element '{{name}}' value must be a style object, not a {{type}}.",
|
|
60
|
+
subElementTargetsRoot: "Sub-element '{{name}}' selector '{{selector}}' styles the containing element instead of a sub-element. Use property state maps in that containing scope, or a descendant '$' selector such as 'h1'. Pseudo-elements such as '&::before' are allowed."
|
|
61
|
+
},
|
|
12
62
|
schema: []
|
|
13
63
|
},
|
|
14
64
|
defaultOptions: [],
|
|
15
65
|
create(context) {
|
|
16
66
|
const ctx = new TastyContext(context);
|
|
17
|
-
|
|
18
|
-
|
|
67
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
68
|
+
function checkAffix(name, node) {
|
|
19
69
|
for (const prop of node.properties) {
|
|
20
|
-
if (prop.type !== "Property"
|
|
21
|
-
|
|
70
|
+
if (prop.type !== "Property") continue;
|
|
71
|
+
if ((prop.computed ? getStringValue(unwrapExpression(prop.key)) : getKeyName(prop.key)) !== "$") continue;
|
|
72
|
+
const selector = getStringValue(unwrapExpression(prop.value));
|
|
73
|
+
if (selector === null) continue;
|
|
74
|
+
const rootSelector = firstRootSelector(selector, name);
|
|
75
|
+
if (rootSelector === null) continue;
|
|
76
|
+
context.report({
|
|
77
|
+
node: prop.value,
|
|
78
|
+
messageId: "subElementTargetsRoot",
|
|
79
|
+
data: {
|
|
80
|
+
name,
|
|
81
|
+
selector: rootSelector
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function checkStyleObject(node) {
|
|
87
|
+
if (visited.has(node)) return;
|
|
88
|
+
visited.add(node);
|
|
89
|
+
for (const prop of node.properties) {
|
|
90
|
+
if (prop.type !== "Property") continue;
|
|
91
|
+
const key = prop.computed ? getStringValue(unwrapExpression(prop.key)) : getKeyName(prop.key);
|
|
22
92
|
if (key === null || !/^[A-Z]/.test(key)) continue;
|
|
23
93
|
const value = unwrapExpression(prop.value);
|
|
24
|
-
if (value.type
|
|
94
|
+
if (value.type === "ObjectExpression") {
|
|
95
|
+
checkAffix(key, value);
|
|
96
|
+
checkStyleObject(value);
|
|
97
|
+
} else {
|
|
25
98
|
if (value.type === "Literal" && value.value === false) continue;
|
|
26
99
|
const valueType = value.type === "Literal" ? typeof value.value : value.type;
|
|
27
100
|
context.report({
|
|
@@ -35,6 +108,9 @@ var valid_sub_element_default = createRule({
|
|
|
35
108
|
}
|
|
36
109
|
}
|
|
37
110
|
}
|
|
111
|
+
function handleStyleObject(node) {
|
|
112
|
+
if (ctx.isStyleObject(node)) checkStyleObject(node);
|
|
113
|
+
}
|
|
38
114
|
return {
|
|
39
115
|
ImportDeclaration(node) {
|
|
40
116
|
ctx.trackImport(node);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valid-sub-element.js","names":[],"sources":["../../src/rules/valid-sub-element.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'subElementNotObject';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-sub-element',\n meta: {\n type: 'problem',\n docs: {\n description
|
|
1
|
+
{"version":3,"file":"valid-sub-element.js","names":[],"sources":["../../src/rules/valid-sub-element.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport selectorParser from 'postcss-selector-parser';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue, unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'subElementNotObject' | 'subElementTargetsRoot';\n\nconst LEGACY_PSEUDO_ELEMENTS = new Set([\n ':before',\n ':after',\n ':first-line',\n ':first-letter',\n]);\n\nfunction hasDescendantTarget(nodes: selectorParser.Node[]): boolean {\n let lastCharacter = '';\n for (const node of nodes.slice(1)) {\n if (node.type === 'combinator') {\n if (node.value.trim()) return true;\n // The affix renderer skips authored whitespace. Tags insert their own\n // separator; pseudos attach directly, and classes/attributes attach\n // after a tag, class, attribute or placeholder.\n continue;\n }\n if (node.type === 'tag' || node.type === 'universal') {\n if (lastCharacter) return true;\n } else if (node.type === 'class' || node.type === 'attribute') {\n if (lastCharacter && !/[\\]@a-zA-Z0-9-]/.test(lastCharacter)) return true;\n }\n lastCharacter = node.toString().trim().slice(-1);\n }\n return false;\n}\n\nfunction firstRootSelector(selector: string, key: string): string | null {\n try {\n const root = selectorParser().astSync(selector);\n for (const branch of root.nodes) {\n const nodes = branch.nodes.filter((node) => node.type !== 'comment');\n if (nodes[0]?.type !== 'nesting' || nodes.length < 2) continue;\n // An explicit placeholder creates a descendant selector even with an\n // ampersand prefix. A different trailing uppercase name injects the key\n // as its descendant; the sub-element's own name does not.\n if (\n nodes.some((node) => node.type === 'tag' && node.value.startsWith('@'))\n )\n continue;\n const trailingElement = branch\n .toString()\n .trim()\n .slice(1)\n .trim()\n .match(/(?:^|[\\s>+~\\]:])([A-Z][a-zA-Z0-9]*)$/);\n if (trailingElement && trailingElement[1] !== key) continue;\n // Follow Tasty's affix normalization, rather than CSS whitespace alone.\n // Nested :is() arguments do not change the outer selector's target.\n if (hasDescendantTarget(nodes)) continue;\n const hasPseudoElement = nodes.some((node) => {\n if (node.type !== 'pseudo') return false;\n let name = node.value.toLowerCase();\n if (name.startsWith('::')) return true;\n if (name.includes('\\\\')) {\n const identifier = selectorParser().astSync(name.slice(1)).first\n ?.first;\n if (identifier?.type === 'tag')\n name = ':' + identifier.value.toLowerCase();\n }\n return LEGACY_PSEUDO_ELEMENTS.has(name);\n });\n if (!hasPseudoElement) return branch.toString().trim();\n }\n } catch {\n // Invalid/incomplete selector text belongs to syntax validation.\n }\n return null;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-sub-element',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate sub-element values and prevent root-targeting affixes',\n },\n messages: {\n subElementNotObject:\n \"Sub-element '{{name}}' value must be a style object, not a {{type}}.\",\n subElementTargetsRoot:\n \"Sub-element '{{name}}' selector '{{selector}}' styles the containing element instead of a sub-element. Use property state maps in that containing scope, or a descendant '$' selector such as 'h1'. Pseudo-elements such as '&::before' are allowed.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const visited = new WeakSet<TSESTree.ObjectExpression>();\n\n function checkAffix(name: string, node: TSESTree.ObjectExpression) {\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n const key = prop.computed\n ? getStringValue(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n if (key !== '$') continue;\n const selector = getStringValue(unwrapExpression(prop.value));\n if (selector === null) continue;\n const rootSelector = firstRootSelector(selector, name);\n if (rootSelector === null) continue;\n context.report({\n node: prop.value,\n messageId: 'subElementTargetsRoot',\n data: { name, selector: rootSelector },\n });\n }\n }\n\n // Recurse only through owned sub-elements. Some shared/JSX listeners match\n // just the root, and descriptor objects and state maps are not styles.\n function checkStyleObject(node: TSESTree.ObjectExpression) {\n if (visited.has(node)) return;\n visited.add(node);\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n\n const key = prop.computed\n ? getStringValue(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n if (key === null || !/^[A-Z]/.test(key)) continue;\n\n const value = unwrapExpression(prop.value);\n if (value.type === 'ObjectExpression') {\n checkAffix(key, value);\n checkStyleObject(value);\n } else {\n if (value.type === 'Literal' && value.value === false) {\n continue;\n }\n\n const valueType =\n value.type === 'Literal' ? typeof value.value : value.type;\n\n context.report({\n node: prop.value,\n messageId: 'subElementNotObject',\n data: { name: key, type: valueType },\n });\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (ctx.isStyleObject(node)) checkStyleObject(node);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":"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"}
|