@tenphi/eslint-plugin-tasty 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -178,13 +178,15 @@ 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 |
185
185
  | `tasty/valid-recipe` | error | Unknown recipe names |
186
186
  | `tasty/valid-transition` | warn | Unknown transition property names |
187
187
  | `tasty/no-nested-selector` | warn | `&`-prefixed nested selectors (use sub-elements) |
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)) |
188
190
  | `tasty/static-no-dynamic-values` | error | Dynamic values in `tastyStatic()` |
189
191
  | `tasty/static-valid-selector` | error | Invalid selector in `tastyStatic(selector, ...)` |
190
192
  | `tasty/require-default-state` | error | Missing default (`''`) or fallback floor (`_`) key in state mappings (skipped for extending calls) |
@@ -212,6 +214,29 @@ for typing shared signatures.
212
214
  | `tasty/no-unknown-state-alias` | warn | Unknown `@name` state aliases |
213
215
  | `tasty/no-styles-prop` | warn | Direct `styles` prop usage |
214
216
 
217
+ ### Structural selectors and state maps
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
+
226
+ Keep a sub-element's `$` selector focused on the element's identity and position.
227
+ `tasty/no-state-in-selector` warns on conditions such as `img:not([width])`,
228
+ `button[disabled]`, and `>@:hover`. Express those conditions in property state
229
+ maps, using `@own(...)` for the sub-element's own state. Root states stay outside
230
+ `@own(...)`.
231
+
232
+ Tags, combinators, classes, IDs, exact `[data-element="Name"]` identities, and
233
+ pseudo-elements remain valid structural selectors. `:is()` and `:where()` may
234
+ group structural selectors, but conditions inside them still produce a warning:
235
+ `:where(picture)` is accepted, while `:where(img:not([width]))` is reported.
236
+ The rule does not autofix because moving a condition can change its scope and
237
+ the behavior of other properties. See the [rule guide](docs/rules/no-state-in-selector.md)
238
+ for examples and migration advice.
239
+
215
240
  ### Static values and intentional spreads
216
241
 
217
242
  The recommended rules flag JavaScript-computed style values, including
package/dist/configs.js CHANGED
@@ -13,6 +13,8 @@ const recommended = {
13
13
  "tasty/valid-directional-modifier": "error",
14
14
  "tasty/valid-radius-shape": "error",
15
15
  "tasty/no-nested-selector": "error",
16
+ "tasty/no-state-in-selector": "warn",
17
+ "tasty/prefer-element-selector": "warn",
16
18
  "tasty/static-no-dynamic-values": "error",
17
19
  "tasty/static-valid-selector": "error",
18
20
  "tasty/valid-preset": "error",
@@ -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/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,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
@@ -5,6 +5,8 @@ import valid_custom_unit_default from "./rules/valid-custom-unit.js";
5
5
  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
+ 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";
8
10
  import valid_custom_property_default from "./rules/valid-custom-property.js";
9
11
  import valid_preset_default from "./rules/valid-preset.js";
10
12
  import valid_recipe_default from "./rules/valid-recipe.js";
@@ -48,6 +50,8 @@ const ruleMap = {
48
50
  "valid-state-key": valid_state_key_default,
49
51
  "valid-styles-structure": valid_styles_structure_default,
50
52
  "no-nested-selector": no_nested_selector_default,
53
+ "no-state-in-selector": no_state_in_selector_default,
54
+ "prefer-element-selector": prefer_element_selector_default,
51
55
  "valid-custom-property": valid_custom_property_default,
52
56
  "valid-preset": valid_preset_default,
53
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.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 '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,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"}
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"}
@@ -0,0 +1,85 @@
1
+ import { createRule } from "../create-rule.js";
2
+ import { getKeyName, getStringValue, unwrapExpression } from "../utils.js";
3
+ import { TastyContext, styleObjectListeners } from "../context.js";
4
+ import selectorParser from "postcss-selector-parser";
5
+
6
+ //#region src/rules/no-state-in-selector.ts
7
+ const STRUCTURAL_PSEUDOS = new Set([":is", ":where"]);
8
+ const LEGACY_PSEUDO_ELEMENTS = new Set([
9
+ ":before",
10
+ ":after",
11
+ ":first-line",
12
+ ":first-letter"
13
+ ]);
14
+ function firstStateCondition(selector) {
15
+ let root;
16
+ try {
17
+ root = selectorParser().astSync(selector);
18
+ } catch {
19
+ return null;
20
+ }
21
+ let condition = null;
22
+ root.walk((node) => {
23
+ if (node.type === "attribute") {
24
+ if (node.attribute === "data-element" && !node.namespace && node.operator === "=" && node.value) return;
25
+ } else if (node.type === "pseudo") {
26
+ let name = node.value.toLowerCase();
27
+ if (name.includes("\\")) {
28
+ const colons = name.startsWith("::") ? "::" : ":";
29
+ const identifier = selectorParser().astSync(name.slice(colons.length)).first?.first;
30
+ if (identifier?.type === "tag") name = colons + identifier.value.toLowerCase();
31
+ }
32
+ if (name.startsWith("::") || LEGACY_PSEUDO_ELEMENTS.has(name) || STRUCTURAL_PSEUDOS.has(name)) return;
33
+ } else return;
34
+ condition = node.toString().trim();
35
+ return false;
36
+ });
37
+ return condition;
38
+ }
39
+ var no_state_in_selector_default = createRule({
40
+ name: "no-state-in-selector",
41
+ meta: {
42
+ type: "suggestion",
43
+ docs: { description: "Keep sub-element selectors structural and express conditions in property state maps" },
44
+ messages: { stateInSelector: "Move state condition '{{condition}}' out of the sub-element '$' selector and into property state maps. Use @own(...) for the sub-element's own state." },
45
+ schema: []
46
+ },
47
+ defaultOptions: [],
48
+ create(context) {
49
+ const ctx = new TastyContext(context);
50
+ const visited = /* @__PURE__ */ new WeakSet();
51
+ function handleStyleObject(node) {
52
+ if (visited.has(node) || !ctx.isStyleObject(node)) return;
53
+ visited.add(node);
54
+ const isSubElement = ctx.isInsideSubElement(node);
55
+ for (const prop of node.properties) {
56
+ if (prop.type !== "Property") continue;
57
+ const key = prop.computed ? getStringValue(unwrapExpression(prop.key)) : getKeyName(prop.key);
58
+ if (!prop.computed && key && ctx.isSubElementKey(key)) {
59
+ const child = unwrapExpression(prop.value);
60
+ if (child.type === "ObjectExpression") handleStyleObject(child);
61
+ }
62
+ if (!isSubElement || key !== "$") continue;
63
+ const selector = getStringValue(unwrapExpression(prop.value));
64
+ if (selector === null) continue;
65
+ const condition = firstStateCondition(selector);
66
+ if (condition === null) continue;
67
+ context.report({
68
+ node: prop.value,
69
+ messageId: "stateInSelector",
70
+ data: { condition }
71
+ });
72
+ }
73
+ }
74
+ return {
75
+ ImportDeclaration(node) {
76
+ ctx.trackImport(node);
77
+ },
78
+ ...styleObjectListeners(handleStyleObject)
79
+ };
80
+ }
81
+ });
82
+
83
+ //#endregion
84
+ export { no_state_in_selector_default as default };
85
+ //# sourceMappingURL=no-state-in-selector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-state-in-selector.js","names":[],"sources":["../../src/rules/no-state-in-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 { getKeyName, getStringValue, unwrapExpression } from '../utils.js';\n\nconst STRUCTURAL_PSEUDOS = new Set([':is', ':where']);\nconst LEGACY_PSEUDO_ELEMENTS = new Set([\n ':before',\n ':after',\n ':first-line',\n ':first-letter',\n]);\n\nfunction firstStateCondition(selector: string): string | null {\n let root: selectorParser.Root;\n try {\n root = selectorParser().astSync(selector);\n } catch {\n // This recommendation is not a CSS syntax validator. Incomplete selectors\n // must not crash lint or produce a misleading state-map recommendation.\n return null;\n }\n\n let condition: string | null = null;\n root.walk((node) => {\n if (node.type === 'attribute') {\n // An exact data-element identity is structural, like Tasty's uppercase\n // element names. Other attributes express conditions on that element.\n if (\n node.attribute === 'data-element' &&\n !node.namespace &&\n node.operator === '=' &&\n node.value\n ) {\n return;\n }\n } else if (node.type === 'pseudo') {\n let name = node.value.toLowerCase();\n if (name.includes('\\\\')) {\n // The parser decodes tag identifiers but preserves pseudo spelling.\n const colons = name.startsWith('::') ? '::' : ':';\n const identifier = selectorParser().astSync(name.slice(colons.length))\n .first?.first;\n if (identifier?.type === 'tag') {\n name = colons + identifier.value.toLowerCase();\n }\n }\n if (\n name.startsWith('::') ||\n LEGACY_PSEUDO_ELEMENTS.has(name) ||\n STRUCTURAL_PSEUDOS.has(name)\n ) {\n // Continue walking: :where(img:hover) and ::slotted(img[width])\n // still contain conditions, although the outer wrapper is structural.\n return;\n }\n } else {\n return;\n }\n\n condition = node.toString().trim();\n return false;\n });\n return condition;\n}\n\nexport default createRule<[], 'stateInSelector'>({\n name: 'no-state-in-selector',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Keep sub-element selectors structural and express conditions in property state maps',\n },\n messages: {\n stateInSelector:\n \"Move state condition '{{condition}}' out of the sub-element '$' selector and into property state maps. Use @own(...) for the sub-element's own state.\",\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 handleStyleObject(node: TSESTree.ObjectExpression) {\n if (visited.has(node) || !ctx.isStyleObject(node)) return;\n visited.add(node);\n const isSubElement = ctx.isInsideSubElement(node);\n\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 // Some listeners match only the shared/JSX root. Follow its owned\n // sub-elements explicitly, without entering state maps or metadata.\n if (!prop.computed && key && ctx.isSubElementKey(key)) {\n const child = unwrapExpression(prop.value);\n if (child.type === 'ObjectExpression') handleStyleObject(child);\n }\n if (!isSubElement || key !== '$') continue;\n\n const selector = getStringValue(unwrapExpression(prop.value));\n if (selector === null) continue;\n const condition = firstStateCondition(selector);\n if (condition === null) continue;\n\n context.report({\n node: prop.value,\n messageId: 'stateInSelector',\n data: { condition },\n });\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAMA,MAAM,qBAAqB,IAAI,IAAI,CAAC,OAAO,SAAS,CAAC;AACrD,MAAM,yBAAyB,IAAI,IAAI;CACrC;CACA;CACA;CACA;CACD,CAAC;AAEF,SAAS,oBAAoB,UAAiC;CAC5D,IAAI;AACJ,KAAI;AACF,SAAO,gBAAgB,CAAC,QAAQ,SAAS;SACnC;AAGN,SAAO;;CAGT,IAAI,YAA2B;AAC/B,MAAK,MAAM,SAAS;AAClB,MAAI,KAAK,SAAS,aAGhB;OACE,KAAK,cAAc,kBACnB,CAAC,KAAK,aACN,KAAK,aAAa,OAClB,KAAK,MAEL;aAEO,KAAK,SAAS,UAAU;GACjC,IAAI,OAAO,KAAK,MAAM,aAAa;AACnC,OAAI,KAAK,SAAS,KAAK,EAAE;IAEvB,MAAM,SAAS,KAAK,WAAW,KAAK,GAAG,OAAO;IAC9C,MAAM,aAAa,gBAAgB,CAAC,QAAQ,KAAK,MAAM,OAAO,OAAO,CAAC,CACnE,OAAO;AACV,QAAI,YAAY,SAAS,MACvB,QAAO,SAAS,WAAW,MAAM,aAAa;;AAGlD,OACE,KAAK,WAAW,KAAK,IACrB,uBAAuB,IAAI,KAAK,IAChC,mBAAmB,IAAI,KAAK,CAI5B;QAGF;AAGF,cAAY,KAAK,UAAU,CAAC,MAAM;AAClC,SAAO;GACP;AACF,QAAO;;AAGT,mCAAe,WAAkC;CAC/C,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,uFACH;EACD,UAAU,EACR,iBACE,yJACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,0BAAU,IAAI,SAAoC;EAExD,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,QAAQ,IAAI,KAAK,IAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AACnD,WAAQ,IAAI,KAAK;GACjB,MAAM,eAAe,IAAI,mBAAmB,KAAK;AAEjD,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;IAC9B,MAAM,MAAM,KAAK,WACb,eAAe,iBAAiB,KAAK,IAAI,CAAC,GAC1C,WAAW,KAAK,IAAI;AAGxB,QAAI,CAAC,KAAK,YAAY,OAAO,IAAI,gBAAgB,IAAI,EAAE;KACrD,MAAM,QAAQ,iBAAiB,KAAK,MAAM;AAC1C,SAAI,MAAM,SAAS,mBAAoB,mBAAkB,MAAM;;AAEjE,QAAI,CAAC,gBAAgB,QAAQ,IAAK;IAElC,MAAM,WAAW,eAAe,iBAAiB,KAAK,MAAM,CAAC;AAC7D,QAAI,aAAa,KAAM;IACvB,MAAM,YAAY,oBAAoB,SAAS;AAC/C,QAAI,cAAc,KAAM;AAExB,YAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,WAAW;KACpB,CAAC;;;AAIN,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,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":";;;;;;;AAOA,SAAS,aAAa,UAAkB,OAA6B;AAGnE,KAAI,SAAS,SAAS,KAAK,IAAI,SAAS,SAAS,KAAK,CAAE,QAAO,EAAE;AACjE,KAAI;EACF,MAAM,OAAO,gBAAgB,CAAC,QAAQ,SAAS;EAC/C,MAAM,QAAsB,EAAE;AAC9B,OAAK,MAAM,UAAU,KAAK,OAAO;AAC/B,OACE,OAAO,MAAM,MAAM,MAAM,UAAU;AACjC,YAAQ,KAAK,MAAb;KACE,KAAK,UACH,QAAO,UAAU;KACnB,KAAK,MACH,QAAO,CAAC,0BAA0B,KAAK,KAAK,MAAM;KACpD,KAAK,aACH,QAAO,CAAC,kBAAkB,KAAK,KAAK,MAAM;KAC5C,KAAK,YACH,QAAO,KAAK,UAAU,CAAC,SAAS,IAAI;KACtC,KAAK;KACL,KAAK,YACH,QAAO;KACT,QACE,QAAO;;KAEX,CAEF;AAEF,QAAK,MAAM,CAAC,OAAO,SAAS,OAAO,MAAM,SAAS,EAAE;AAClD,QAAI,KAAK,SAAS,YAAa;IAC/B,MAAM,WAAW,OAAO,MAAM,QAAQ;AAGtC,QACE,YACA,SAAS,SAAS,aAClB,EAAE,SAAS,SAAS,gBAAgB,UAAU,KAAK,SAAS,MAAM,EAElE;IAIF,MAAM,QAAQ,SACX,MAAM,KAAK,YAAY,CACvB,MACC,oGACD;AACH,QAAI,CAAC,MAAO;AAGZ,QACE,eAAe,KACb,SAAS,MAAM,KAAK,cAAc,MAAM,GAAG,OAAO,CACnD,CAED;IACF,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM,MAAM;AAG3C,QAAI,UAAU,OAAO,MAAM,SAAS,KAAK,SAAS,MAAO;AACzD,UAAM,KAAK;KACT,OAAO,KAAK;KACZ,KAAK,KAAK,cAAc,MAAM,GAAG;KACjC,aAAa;KACd,CAAC;;;AAGN,SAAO;SACD;AACN,SAAO,EAAE;;;AAIb,sCAAe,WAAwC;CACrD,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,gEACH;EACD,SAAS;EACT,UAAU,EACR,uBACE,yGACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,0BAAU,IAAI,SAAoC;EAExD,SAAS,WAAW,OAAe,MAAiC;AAClE,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;AAI9B,SAHY,KAAK,WACb,eAAe,iBAAiB,KAAK,IAAI,CAAC,GAC1C,WAAW,KAAK,IAAI,MACZ,IAAK;IACjB,MAAM,QAAQ,iBAAiB,KAAK,MAAM;IAC1C,MAAM,WAAW,eAAe,MAAM;AACtC,QAAI,aAAa,KAAM;IACvB,MAAM,QAAQ,aAAa,UAAU,MAAM;AAC3C,QAAI,CAAC,MAAM,OAAQ;AACnB,YAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,UACJ,qBAAqB,OAAO,OAAO,OAAO,QAAQ,WAAW;KAChE,CAAC;;;EAIN,SAAS,iBAAiB,MAAiC;AACzD,OAAI,QAAQ,IAAI,KAAK,CAAE;AACvB,WAAQ,IAAI,KAAK;AACjB,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;IAC9B,MAAM,MAAM,KAAK,WACb,eAAe,iBAAiB,KAAK,IAAI,CAAC,GAC1C,WAAW,KAAK,IAAI;AACxB,QAAI,QAAQ,QAAQ,CAAC,IAAI,gBAAgB,IAAI,CAAE;IAC/C,MAAM,QAAQ,iBAAiB,KAAK,MAAM;AAC1C,QAAI,MAAM,SAAS,mBAAoB;AACvC,eAAW,KAAK,MAAM;AACtB,qBAAiB,MAAM;;;AAI3B,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,sBAAsB,SAAS;AAChC,QAAI,IAAI,cAAc,KAAK,CAAE,kBAAiB,KAAK;KACnD;GACH;;CAEJ,CAAC"}
@@ -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 key format in style objects" },
11
- messages: { subElementNotObject: "Sub-element '{{name}}' value must be a style object, not a {{type}}." },
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
- function handleStyleObject(node) {
18
- if (!ctx.isStyleObject(node)) return;
67
+ const visited = /* @__PURE__ */ new WeakSet();
68
+ function checkAffix(name, node) {
19
69
  for (const prop of node.properties) {
20
- if (prop.type !== "Property" || prop.computed) continue;
21
- const key = getKeyName(prop.key);
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 !== "ObjectExpression") {
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: 'Validate sub-element key format in style objects',\n },\n messages: {\n subElementNotObject:\n \"Sub-element '{{name}}' value must be a style object, not a {{type}}.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = 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 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 return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,gCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,oDACd;EACD,UAAU,EACR,qBACE,wEACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,QAAQ,CAAC,SAAS,KAAK,IAAI,CAAE;IAEzC,MAAM,QAAQ,iBAAiB,KAAK,MAAM;AAC1C,QAAI,MAAM,SAAS,oBAAoB;AACrC,SAAI,MAAM,SAAS,aAAa,MAAM,UAAU,MAC9C;KAGF,MAAM,YACJ,MAAM,SAAS,YAAY,OAAO,MAAM,QAAQ,MAAM;AAExD,aAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACX,MAAM;OAAE,MAAM;OAAK,MAAM;OAAW;MACrC,CAAC;;;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
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":";;;;;;AAQA,MAAM,yBAAyB,IAAI,IAAI;CACrC;CACA;CACA;CACA;CACD,CAAC;AAEF,SAAS,oBAAoB,OAAuC;CAClE,IAAI,gBAAgB;AACpB,MAAK,MAAM,QAAQ,MAAM,MAAM,EAAE,EAAE;AACjC,MAAI,KAAK,SAAS,cAAc;AAC9B,OAAI,KAAK,MAAM,MAAM,CAAE,QAAO;AAI9B;;AAEF,MAAI,KAAK,SAAS,SAAS,KAAK,SAAS,aACvC;OAAI,cAAe,QAAO;aACjB,KAAK,SAAS,WAAW,KAAK,SAAS,aAChD;OAAI,iBAAiB,CAAC,kBAAkB,KAAK,cAAc,CAAE,QAAO;;AAEtE,kBAAgB,KAAK,UAAU,CAAC,MAAM,CAAC,MAAM,GAAG;;AAElD,QAAO;;AAGT,SAAS,kBAAkB,UAAkB,KAA4B;AACvE,KAAI;EACF,MAAM,OAAO,gBAAgB,CAAC,QAAQ,SAAS;AAC/C,OAAK,MAAM,UAAU,KAAK,OAAO;GAC/B,MAAM,QAAQ,OAAO,MAAM,QAAQ,SAAS,KAAK,SAAS,UAAU;AACpE,OAAI,MAAM,IAAI,SAAS,aAAa,MAAM,SAAS,EAAG;AAItD,OACE,MAAM,MAAM,SAAS,KAAK,SAAS,SAAS,KAAK,MAAM,WAAW,IAAI,CAAC,CAEvE;GACF,MAAM,kBAAkB,OACrB,UAAU,CACV,MAAM,CACN,MAAM,EAAE,CACR,MAAM,CACN,MAAM,uCAAuC;AAChD,OAAI,mBAAmB,gBAAgB,OAAO,IAAK;AAGnD,OAAI,oBAAoB,MAAM,CAAE;AAahC,OAAI,CAZqB,MAAM,MAAM,SAAS;AAC5C,QAAI,KAAK,SAAS,SAAU,QAAO;IACnC,IAAI,OAAO,KAAK,MAAM,aAAa;AACnC,QAAI,KAAK,WAAW,KAAK,CAAE,QAAO;AAClC,QAAI,KAAK,SAAS,KAAK,EAAE;KACvB,MAAM,aAAa,gBAAgB,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC,CAAC,OACvD;AACJ,SAAI,YAAY,SAAS,MACvB,QAAO,MAAM,WAAW,MAAM,aAAa;;AAE/C,WAAO,uBAAuB,IAAI,KAAK;KACvC,CACqB,QAAO,OAAO,UAAU,CAAC,MAAM;;SAElD;AAGR,QAAO;;AAGT,gCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,kEACH;EACD,UAAU;GACR,qBACE;GACF,uBACE;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,MAAc,MAAiC;AACjE,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;AAI9B,SAHY,KAAK,WACb,eAAe,iBAAiB,KAAK,IAAI,CAAC,GAC1C,WAAW,KAAK,IAAI,MACZ,IAAK;IACjB,MAAM,WAAW,eAAe,iBAAiB,KAAK,MAAM,CAAC;AAC7D,QAAI,aAAa,KAAM;IACvB,MAAM,eAAe,kBAAkB,UAAU,KAAK;AACtD,QAAI,iBAAiB,KAAM;AAC3B,YAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM;MAAE;MAAM,UAAU;MAAc;KACvC,CAAC;;;EAMN,SAAS,iBAAiB,MAAiC;AACzD,OAAI,QAAQ,IAAI,KAAK,CAAE;AACvB,WAAQ,IAAI,KAAK;AAEjB,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;IAE9B,MAAM,MAAM,KAAK,WACb,eAAe,iBAAiB,KAAK,IAAI,CAAC,GAC1C,WAAW,KAAK,IAAI;AACxB,QAAI,QAAQ,QAAQ,CAAC,SAAS,KAAK,IAAI,CAAE;IAEzC,MAAM,QAAQ,iBAAiB,KAAK,MAAM;AAC1C,QAAI,MAAM,SAAS,oBAAoB;AACrC,gBAAW,KAAK,MAAM;AACtB,sBAAiB,MAAM;WAClB;AACL,SAAI,MAAM,SAAS,aAAa,MAAM,UAAU,MAC9C;KAGF,MAAM,YACJ,MAAM,SAAS,YAAY,OAAO,MAAM,QAAQ,MAAM;AAExD,aAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACX,MAAM;OAAE,MAAM;OAAK,MAAM;OAAW;MACrC,CAAC;;;;EAKR,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,IAAI,cAAc,KAAK,CAAE,kBAAiB,KAAK;;AAGrD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/eslint-plugin-tasty",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,7 +44,8 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@typescript-eslint/utils": "^8.56.0",
47
- "jiti": "^2.6.1"
47
+ "jiti": "^2.6.1",
48
+ "postcss-selector-parser": "^7.1.6"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@changesets/changelog-github": "^0.5.2",