@tenphi/eslint-plugin-tasty 1.4.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 +9 -1
- 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/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) |
|
|
@@ -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
|
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"}
|
|
@@ -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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"}
|