@tenphi/eslint-plugin-tasty 1.8.0 → 1.8.2

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.
@@ -1,6 +1,6 @@
1
1
  import { createRule } from "../create-rule.js";
2
2
  import { BUILT_IN_CUSTOM_PROPERTIES } from "../constants.js";
3
- import { getKeyName } from "../utils.js";
3
+ import { getKeyName, getStringValue, unwrapExpression } from "../utils.js";
4
4
  import { TastyContext } from "../context.js";
5
5
  import { stringStyleValueListeners } from "../style-values.js";
6
6
  import { scanValueWords } from "../value-words.js";
@@ -13,7 +13,7 @@ var valid_custom_property_default = createRule({
13
13
  docs: { description: "Validate $name custom property references" },
14
14
  messages: {
15
15
  invalidSyntax: "Invalid custom property '{{token}}'. Use '$name' with letters, digits, or hyphens (e.g. $accent-color).",
16
- unknownProperty: "Unknown custom property '{{token}}'. Declare it as a '$name' key in this styles object, or add it to your tasty config."
16
+ unknownProperty: "Unknown custom property '{{token}}'. Declare it as a '$name' key in a styles object or 'tokens' prop in this file, or add it to your tasty config."
17
17
  },
18
18
  schema: []
19
19
  },
@@ -29,6 +29,20 @@ var valid_custom_property_default = createRule({
29
29
  if (key && key.startsWith("$") && !key.startsWith("$$")) fileCustomProperties.add(key);
30
30
  }
31
31
  }
32
+ /**
33
+ * Tokens declare custom properties too: `tokens={{ $progress: … }}` on an
34
+ * element, a `tokens` option of a component definition, and a raw
35
+ * `'--progress'` entry of an inline `style`, which sets the same property.
36
+ */
37
+ function collectDeclaredTokens(node, prefix) {
38
+ for (const prop of node.properties) {
39
+ if (prop.type !== "Property") continue;
40
+ const key = prop.computed ? getStringValue(unwrapExpression(prop.key)) : getKeyName(prop.key);
41
+ if (key === null) continue;
42
+ const name = prefix === "$" ? key : key.startsWith("--") ? "$" + key.slice(2) : "";
43
+ if (/^\$[^$]/.test(name)) fileCustomProperties.add(name);
44
+ }
45
+ }
32
46
  function checkValue(value, node) {
33
47
  if (ctx.config.tokens === false) return;
34
48
  for (const word of scanValueWords(value)) {
@@ -44,7 +58,7 @@ var valid_custom_property_default = createRule({
44
58
  });
45
59
  }
46
60
  }
47
- return stringStyleValueListeners(ctx, checkValue, {
61
+ const listeners = stringStyleValueListeners(ctx, checkValue, {
48
62
  onStyleObject: collectLocalProperties,
49
63
  onProgramExit() {
50
64
  if (!Array.isArray(ctx.config.tokens) || ctx.config.tokens.length === 0) return;
@@ -59,6 +73,27 @@ var valid_custom_property_default = createRule({
59
73
  }
60
74
  }
61
75
  });
76
+ return {
77
+ ...listeners,
78
+ JSXAttribute(node) {
79
+ listeners.JSXAttribute(node);
80
+ if (node.name.type !== "JSXIdentifier") return;
81
+ const name = node.name.name;
82
+ if (name !== "tokens" && name !== "style") return;
83
+ if (node.value?.type !== "JSXExpressionContainer") return;
84
+ const value = unwrapExpression(node.value.expression);
85
+ if (value.type !== "ObjectExpression") return;
86
+ collectDeclaredTokens(value, name === "tokens" ? "$" : "--");
87
+ },
88
+ Property(node) {
89
+ if (node.computed || getKeyName(node.key) !== "tokens") return;
90
+ const value = unwrapExpression(node.value);
91
+ const call = node.parent.parent;
92
+ if (value.type !== "ObjectExpression" || call?.type !== "CallExpression") return;
93
+ const imp = ctx.isTastyCall(call);
94
+ if (imp?.importedName === "tasty" || imp && ctx.config.styleFunctions?.[imp.importedName]?.kind === "options") collectDeclaredTokens(value, "$");
95
+ }
96
+ };
62
97
  }
63
98
  });
64
99
 
@@ -1 +1 @@
1
- {"version":3,"file":"valid-custom-property.js","names":[],"sources":["../../src/rules/valid-custom-property.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport { BUILT_IN_CUSTOM_PROPERTIES } from '../constants.js';\nimport { getKeyName } from '../utils.js';\nimport { scanValueWords } from '../value-words.js';\n\ntype MessageIds = 'invalidSyntax' | 'unknownProperty';\n\ninterface PendingExistenceCheck {\n token: string;\n baseName: string;\n node: TSESTree.Node;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-custom-property',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Validate $name custom property references',\n },\n messages: {\n invalidSyntax:\n \"Invalid custom property '{{token}}'. Use '$name' with letters, digits, or hyphens (e.g. $accent-color).\",\n unknownProperty:\n \"Unknown custom property '{{token}}'. Declare it as a '$name' key in this styles object, or add it to your tasty config.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const fileCustomProperties = new Set<string>();\n const pendingChecks: PendingExistenceCheck[] = [];\n\n function collectLocalProperties(node: TSESTree.ObjectExpression): void {\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const key = getKeyName(prop.key);\n if (key && key.startsWith('$') && !key.startsWith('$$')) {\n fileCustomProperties.add(key);\n }\n }\n }\n\n function checkValue(value: string, node: TSESTree.Node): void {\n if (ctx.config.tokens === false) return;\n\n for (const word of scanValueWords(value)) {\n // A dynamic color opacity is another custom-property reference.\n const token =\n word.value.startsWith('#') && word.value.includes('.$')\n ? word.value.slice(word.value.indexOf('.$') + 1)\n : word.value;\n if (!token.startsWith('$')) continue;\n if (\n token.startsWith('$$') &&\n word.isFunction &&\n value[word.offset + word.value.length] === '('\n )\n continue;\n const baseName = token.startsWith('$$') ? '$' + token.slice(2) : token;\n\n if (BUILT_IN_CUSTOM_PROPERTIES.has(baseName)) continue;\n\n pendingChecks.push({ token, baseName, node });\n }\n }\n\n return stringStyleValueListeners(ctx, checkValue, {\n onStyleObject: collectLocalProperties,\n onProgramExit() {\n if (\n !Array.isArray(ctx.config.tokens) ||\n ctx.config.tokens.length === 0\n ) {\n return;\n }\n\n for (const { token, baseName, node } of pendingChecks) {\n if (fileCustomProperties.has(baseName)) continue;\n if (ctx.config.tokens.includes(baseName)) continue;\n\n context.report({\n node,\n messageId: 'unknownProperty',\n data: { token },\n });\n }\n },\n });\n },\n});\n"],"mappings":";;;;;;;;AAgBA,oCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6CACd;EACD,UAAU;GACR,eACE;GACF,iBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,uCAAuB,IAAI,KAAa;EAC9C,MAAM,gBAAyC,EAAE;EAEjD,SAAS,uBAAuB,MAAuC;AACrE,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAC/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,OAAO,IAAI,WAAW,IAAI,IAAI,CAAC,IAAI,WAAW,KAAK,CACrD,sBAAqB,IAAI,IAAI;;;EAKnC,SAAS,WAAW,OAAe,MAA2B;AAC5D,OAAI,IAAI,OAAO,WAAW,MAAO;AAEjC,QAAK,MAAM,QAAQ,eAAe,MAAM,EAAE;IAExC,MAAM,QACJ,KAAK,MAAM,WAAW,IAAI,IAAI,KAAK,MAAM,SAAS,KAAK,GACnD,KAAK,MAAM,MAAM,KAAK,MAAM,QAAQ,KAAK,GAAG,EAAE,GAC9C,KAAK;AACX,QAAI,CAAC,MAAM,WAAW,IAAI,CAAE;AAC5B,QACE,MAAM,WAAW,KAAK,IACtB,KAAK,cACL,MAAM,KAAK,SAAS,KAAK,MAAM,YAAY,IAE3C;IACF,MAAM,WAAW,MAAM,WAAW,KAAK,GAAG,MAAM,MAAM,MAAM,EAAE,GAAG;AAEjE,QAAI,2BAA2B,IAAI,SAAS,CAAE;AAE9C,kBAAc,KAAK;KAAE;KAAO;KAAU;KAAM,CAAC;;;AAIjD,SAAO,0BAA0B,KAAK,YAAY;GAChD,eAAe;GACf,gBAAgB;AACd,QACE,CAAC,MAAM,QAAQ,IAAI,OAAO,OAAO,IACjC,IAAI,OAAO,OAAO,WAAW,EAE7B;AAGF,SAAK,MAAM,EAAE,OAAO,UAAU,UAAU,eAAe;AACrD,SAAI,qBAAqB,IAAI,SAAS,CAAE;AACxC,SAAI,IAAI,OAAO,OAAO,SAAS,SAAS,CAAE;AAE1C,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,OAAO;MAChB,CAAC;;;GAGP,CAAC;;CAEL,CAAC"}
1
+ {"version":3,"file":"valid-custom-property.js","names":[],"sources":["../../src/rules/valid-custom-property.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport { BUILT_IN_CUSTOM_PROPERTIES } from '../constants.js';\nimport { getKeyName, getStringValue, unwrapExpression } from '../utils.js';\nimport { scanValueWords } from '../value-words.js';\n\ntype MessageIds = 'invalidSyntax' | 'unknownProperty';\n\ninterface PendingExistenceCheck {\n token: string;\n baseName: string;\n node: TSESTree.Node;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-custom-property',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Validate $name custom property references',\n },\n messages: {\n invalidSyntax:\n \"Invalid custom property '{{token}}'. Use '$name' with letters, digits, or hyphens (e.g. $accent-color).\",\n unknownProperty:\n \"Unknown custom property '{{token}}'. Declare it as a '$name' key in a styles object or 'tokens' prop in this file, or add it to your tasty config.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const fileCustomProperties = new Set<string>();\n const pendingChecks: PendingExistenceCheck[] = [];\n\n function collectLocalProperties(node: TSESTree.ObjectExpression): void {\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const key = getKeyName(prop.key);\n if (key && key.startsWith('$') && !key.startsWith('$$')) {\n fileCustomProperties.add(key);\n }\n }\n }\n\n /**\n * Tokens declare custom properties too: `tokens={{ $progress: … }}` on an\n * element, a `tokens` option of a component definition, and a raw\n * `'--progress'` entry of an inline `style`, which sets the same property.\n */\n function collectDeclaredTokens(\n node: TSESTree.ObjectExpression,\n prefix: '$' | '--',\n ): void {\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) continue;\n const name =\n prefix === '$' ? key : key.startsWith('--') ? '$' + key.slice(2) : '';\n if (/^\\$[^$]/.test(name)) fileCustomProperties.add(name);\n }\n }\n\n function checkValue(value: string, node: TSESTree.Node): void {\n if (ctx.config.tokens === false) return;\n\n for (const word of scanValueWords(value)) {\n // A dynamic color opacity is another custom-property reference.\n const token =\n word.value.startsWith('#') && word.value.includes('.$')\n ? word.value.slice(word.value.indexOf('.$') + 1)\n : word.value;\n if (!token.startsWith('$')) continue;\n if (\n token.startsWith('$$') &&\n word.isFunction &&\n value[word.offset + word.value.length] === '('\n )\n continue;\n const baseName = token.startsWith('$$') ? '$' + token.slice(2) : token;\n\n if (BUILT_IN_CUSTOM_PROPERTIES.has(baseName)) continue;\n\n pendingChecks.push({ token, baseName, node });\n }\n }\n\n const listeners = stringStyleValueListeners(ctx, checkValue, {\n onStyleObject: collectLocalProperties,\n onProgramExit() {\n if (\n !Array.isArray(ctx.config.tokens) ||\n ctx.config.tokens.length === 0\n ) {\n return;\n }\n\n for (const { token, baseName, node } of pendingChecks) {\n if (fileCustomProperties.has(baseName)) continue;\n if (ctx.config.tokens.includes(baseName)) continue;\n\n context.report({\n node,\n messageId: 'unknownProperty',\n data: { token },\n });\n }\n },\n });\n\n return {\n ...listeners,\n JSXAttribute(node: TSESTree.JSXAttribute) {\n listeners.JSXAttribute(node);\n if (node.name.type !== 'JSXIdentifier') return;\n const name = node.name.name;\n if (name !== 'tokens' && name !== 'style') return;\n if (node.value?.type !== 'JSXExpressionContainer') return;\n const value = unwrapExpression(node.value.expression);\n if (value.type !== 'ObjectExpression') return;\n collectDeclaredTokens(value, name === 'tokens' ? '$' : '--');\n },\n Property(node: TSESTree.Property) {\n if (node.computed || getKeyName(node.key) !== 'tokens') return;\n const value = unwrapExpression(node.value);\n const call = node.parent.parent;\n if (\n value.type !== 'ObjectExpression' ||\n call?.type !== 'CallExpression'\n )\n return;\n const imp = ctx.isTastyCall(call);\n if (\n imp?.importedName === 'tasty' ||\n (imp &&\n ctx.config.styleFunctions?.[imp.importedName]?.kind === 'options')\n ) {\n collectDeclaredTokens(value, '$');\n }\n },\n };\n },\n});\n"],"mappings":";;;;;;;;AAgBA,oCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6CACd;EACD,UAAU;GACR,eACE;GACF,iBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,uCAAuB,IAAI,KAAa;EAC9C,MAAM,gBAAyC,EAAE;EAEjD,SAAS,uBAAuB,MAAuC;AACrE,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAC/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,OAAO,IAAI,WAAW,IAAI,IAAI,CAAC,IAAI,WAAW,KAAK,CACrD,sBAAqB,IAAI,IAAI;;;;;;;;EAUnC,SAAS,sBACP,MACA,QACM;AACN,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,KAAM;IAClB,MAAM,OACJ,WAAW,MAAM,MAAM,IAAI,WAAW,KAAK,GAAG,MAAM,IAAI,MAAM,EAAE,GAAG;AACrE,QAAI,UAAU,KAAK,KAAK,CAAE,sBAAqB,IAAI,KAAK;;;EAI5D,SAAS,WAAW,OAAe,MAA2B;AAC5D,OAAI,IAAI,OAAO,WAAW,MAAO;AAEjC,QAAK,MAAM,QAAQ,eAAe,MAAM,EAAE;IAExC,MAAM,QACJ,KAAK,MAAM,WAAW,IAAI,IAAI,KAAK,MAAM,SAAS,KAAK,GACnD,KAAK,MAAM,MAAM,KAAK,MAAM,QAAQ,KAAK,GAAG,EAAE,GAC9C,KAAK;AACX,QAAI,CAAC,MAAM,WAAW,IAAI,CAAE;AAC5B,QACE,MAAM,WAAW,KAAK,IACtB,KAAK,cACL,MAAM,KAAK,SAAS,KAAK,MAAM,YAAY,IAE3C;IACF,MAAM,WAAW,MAAM,WAAW,KAAK,GAAG,MAAM,MAAM,MAAM,EAAE,GAAG;AAEjE,QAAI,2BAA2B,IAAI,SAAS,CAAE;AAE9C,kBAAc,KAAK;KAAE;KAAO;KAAU;KAAM,CAAC;;;EAIjD,MAAM,YAAY,0BAA0B,KAAK,YAAY;GAC3D,eAAe;GACf,gBAAgB;AACd,QACE,CAAC,MAAM,QAAQ,IAAI,OAAO,OAAO,IACjC,IAAI,OAAO,OAAO,WAAW,EAE7B;AAGF,SAAK,MAAM,EAAE,OAAO,UAAU,UAAU,eAAe;AACrD,SAAI,qBAAqB,IAAI,SAAS,CAAE;AACxC,SAAI,IAAI,OAAO,OAAO,SAAS,SAAS,CAAE;AAE1C,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,OAAO;MAChB,CAAC;;;GAGP,CAAC;AAEF,SAAO;GACL,GAAG;GACH,aAAa,MAA6B;AACxC,cAAU,aAAa,KAAK;AAC5B,QAAI,KAAK,KAAK,SAAS,gBAAiB;IACxC,MAAM,OAAO,KAAK,KAAK;AACvB,QAAI,SAAS,YAAY,SAAS,QAAS;AAC3C,QAAI,KAAK,OAAO,SAAS,yBAA0B;IACnD,MAAM,QAAQ,iBAAiB,KAAK,MAAM,WAAW;AACrD,QAAI,MAAM,SAAS,mBAAoB;AACvC,0BAAsB,OAAO,SAAS,WAAW,MAAM,KAAK;;GAE9D,SAAS,MAAyB;AAChC,QAAI,KAAK,YAAY,WAAW,KAAK,IAAI,KAAK,SAAU;IACxD,MAAM,QAAQ,iBAAiB,KAAK,MAAM;IAC1C,MAAM,OAAO,KAAK,OAAO;AACzB,QACE,MAAM,SAAS,sBACf,MAAM,SAAS,iBAEf;IACF,MAAM,MAAM,IAAI,YAAY,KAAK;AACjC,QACE,KAAK,iBAAiB,WACrB,OACC,IAAI,OAAO,iBAAiB,IAAI,eAAe,SAAS,UAE1D,uBAAsB,OAAO,IAAI;;GAGtC;;CAEJ,CAAC"}
@@ -4,6 +4,17 @@ import { TastyContext, styleObjectListeners } from "../context.js";
4
4
  import selectorParser from "postcss-selector-parser";
5
5
 
6
6
  //#region src/rules/valid-sub-element.ts
7
+ /** Expressions whose result is visibly not an object. */
8
+ const NON_OBJECT_EXPRESSIONS = new Set([
9
+ "TemplateLiteral",
10
+ "ArrayExpression",
11
+ "ArrowFunctionExpression",
12
+ "FunctionExpression",
13
+ "ClassExpression",
14
+ "UnaryExpression",
15
+ "BinaryExpression",
16
+ "UpdateExpression"
17
+ ]);
7
18
  const LEGACY_PSEUDO_ELEMENTS = new Set([
8
19
  ":before",
9
20
  ":after",
@@ -83,6 +94,28 @@ var valid_sub_element_default = createRule({
83
94
  });
84
95
  }
85
96
  }
97
+ /**
98
+ * The type to name in the report when a value can never be a style object,
99
+ * or `null` when it is `false` (disables the sub-element) or might be one.
100
+ * A `const` binding is followed to what it names; any other binding, member
101
+ * or call is opaque, and Tasty accepts a reference to a style object there.
102
+ */
103
+ function nonObjectType(node, seen) {
104
+ node = unwrapExpression(node);
105
+ if (node.type === "Literal") return node.value === false ? null : typeof node.value;
106
+ if (node.type === "Identifier") {
107
+ if (seen.has(node)) return null;
108
+ seen.add(node);
109
+ let scope = context.sourceCode.getScope(node);
110
+ while (scope && !scope.set.has(node.name)) scope = scope.upper;
111
+ const variable = scope?.set.get(node.name);
112
+ const def = variable?.defs.length === 1 ? variable.defs[0] : void 0;
113
+ if (def?.type !== "Variable" || def.parent.kind !== "const" || !def.node.init) return null;
114
+ const init = unwrapExpression(def.node.init);
115
+ return init.type === "ObjectExpression" ? null : nonObjectType(init, seen);
116
+ }
117
+ return NON_OBJECT_EXPRESSIONS.has(node.type) ? node.type : null;
118
+ }
86
119
  function checkStyleObject(node) {
87
120
  if (visited.has(node)) return;
88
121
  visited.add(node);
@@ -95,8 +128,8 @@ var valid_sub_element_default = createRule({
95
128
  checkAffix(key, value);
96
129
  checkStyleObject(value);
97
130
  } else {
98
- if (value.type === "Literal" && value.value === false) continue;
99
- const valueType = value.type === "Literal" ? typeof value.value : value.type;
131
+ const valueType = nonObjectType(value, /* @__PURE__ */ new Set());
132
+ if (valueType === null) continue;
100
133
  context.report({
101
134
  node: prop.value,
102
135
  messageId: "subElementNotObject",
@@ -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 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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"}
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\n/** Expressions whose result is visibly not an object. */\nconst NON_OBJECT_EXPRESSIONS = new Set<string>([\n 'TemplateLiteral',\n 'ArrayExpression',\n 'ArrowFunctionExpression',\n 'FunctionExpression',\n 'ClassExpression',\n 'UnaryExpression',\n 'BinaryExpression',\n 'UpdateExpression',\n]);\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 /**\n * The type to name in the report when a value can never be a style object,\n * or `null` when it is `false` (disables the sub-element) or might be one.\n * A `const` binding is followed to what it names; any other binding, member\n * or call is opaque, and Tasty accepts a reference to a style object there.\n */\n function nonObjectType(\n node: TSESTree.Node,\n seen: Set<TSESTree.Node>,\n ): string | null {\n node = unwrapExpression(node);\n if (node.type === 'Literal') {\n return node.value === false ? null : typeof node.value;\n }\n if (node.type === 'Identifier') {\n if (seen.has(node)) return null;\n seen.add(node);\n let scope: ReturnType<typeof context.sourceCode.getScope> | null =\n context.sourceCode.getScope(node);\n while (scope && !scope.set.has(node.name)) scope = scope.upper;\n const variable = scope?.set.get(node.name);\n const def = variable?.defs.length === 1 ? variable.defs[0] : undefined;\n if (\n def?.type !== 'Variable' ||\n def.parent.kind !== 'const' ||\n !def.node.init\n ) {\n return null;\n }\n const init = unwrapExpression(def.node.init);\n return init.type === 'ObjectExpression'\n ? null\n : nonObjectType(init, seen);\n }\n return NON_OBJECT_EXPRESSIONS.has(node.type) ? node.type : null;\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 const valueType = nonObjectType(value, new Set());\n if (valueType === null) continue;\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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"}
@@ -88,8 +88,8 @@ var valid_value_default = createRule({
88
88
  });
89
89
  continue;
90
90
  }
91
- if (!expectation.acceptsColor && (token.type === "color-token" || token.type === "color-ref")) {
92
- const colorName = token.type === "color-token" ? `#${token.name}` : `##${token.name}`;
91
+ if (!expectation.acceptsColor && (token.type === "color-token" || token.type === "color-ref" || token.type === "color-keyword")) {
92
+ const colorName = token.type === "color-token" ? `#${token.name}` : token.type === "color-ref" ? `##${token.name}` : token.raw;
93
93
  context.report({
94
94
  node,
95
95
  messageId: "unexpectedColor",
@@ -1 +1 @@
1
- {"version":3,"file":"valid-value.js","names":[],"sources":["../../src/rules/valid-value.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport { parseValue } from '../parsers/value-parser.js';\nimport type { ValueParserOptions } from '../parsers/value-parser.js';\nimport { getExpectation } from '../property-expectations.js';\n\nconst SKIP_PROPERTIES = new Set([\n 'recipe',\n 'preset',\n 'transition',\n '@keyframes',\n '@property',\n '@font-face',\n '@counter-style',\n '@function',\n 'content',\n 'animation',\n 'animationName',\n 'gridArea',\n 'gridAreas',\n 'gridColumn',\n 'gridColumnStart',\n 'gridColumnEnd',\n 'gridRow',\n 'gridRowStart',\n 'gridRowEnd',\n 'gridTemplate',\n 'gridTemplateAreas',\n 'gridTemplateColumns',\n 'gridTemplateRows',\n 'listStyle',\n 'willChange',\n]);\n\ntype MessageIds =\n | 'unbalancedParens'\n | 'importantNotAllowed'\n | 'unexpectedMod'\n | 'unexpectedColor'\n | 'invalidMod'\n | 'unknownToken'\n | 'parseError';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-value',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Parse style values through the value parser and validate against per-property expectations',\n },\n messages: {\n unbalancedParens: 'Unbalanced parentheses in value.',\n importantNotAllowed:\n 'Do not use !important in tasty styles. Use state specificity instead.',\n unexpectedMod:\n \"Unrecognized token '{{mod}}' in '{{property}}' value. This may be a typo.\",\n unexpectedColor:\n \"Property '{{property}}' does not accept color tokens, but found '{{color}}'.\",\n invalidMod:\n \"Modifier '{{mod}}' is not valid for '{{property}}'. Accepted: {{accepted}}.\",\n unknownToken:\n \"Unknown token '{{token}}' in '{{property}}' value. Check for a typo.\",\n parseError: '{{message}}',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function getParserOpts(): ValueParserOptions {\n const opts: ValueParserOptions = {};\n if (ctx.config.units === false) {\n opts.skipUnitValidation = true;\n } else if (Array.isArray(ctx.config.units)) {\n opts.knownUnits = new Set(ctx.config.units);\n }\n if (ctx.config.functions === false) {\n opts.skipFuncValidation = true;\n } else if (Array.isArray(ctx.config.functions)) {\n opts.knownFuncs = new Set(ctx.config.functions);\n }\n return opts;\n }\n\n function checkValue(\n value: string,\n property: string | null,\n node: TSESTree.Node,\n ): void {\n if (property && SKIP_PROPERTIES.has(property)) return;\n\n const result = parseValue(value, getParserOpts());\n\n const erroredRaws = new Set<string>();\n\n // Report parser-level errors (bracket balance, unknown units/functions)\n for (const error of result.errors) {\n if (error.message.includes('parenthes')) {\n context.report({ node, messageId: 'unbalancedParens' });\n return;\n }\n context.report({\n node,\n messageId: 'parseError',\n data: { message: error.message },\n });\n if (error.raw) {\n erroredRaws.add(error.raw);\n }\n }\n\n if (!property) return;\n\n const expectation = getExpectation(property);\n\n for (const group of result.groups) {\n for (const part of group.parts) {\n for (const token of part.tokens) {\n // Check !important\n if (token.type === 'important') {\n context.report({ node, messageId: 'importantNotAllowed' });\n continue;\n }\n\n // Check color tokens in non-color properties\n if (\n !expectation.acceptsColor &&\n (token.type === 'color-token' || token.type === 'color-ref')\n ) {\n const colorName =\n token.type === 'color-token'\n ? `#${token.name}`\n : `##${token.name}`;\n context.report({\n node,\n messageId: 'unexpectedColor',\n data: { property, color: colorName },\n });\n continue;\n }\n\n // Check color functions in non-color properties\n if (\n !expectation.acceptsColor &&\n token.type === 'css-function' &&\n isColorFunction(token.name)\n ) {\n context.report({\n node,\n messageId: 'unexpectedColor',\n data: { property, color: `${token.name}()` },\n });\n continue;\n }\n\n // Skip tokens already reported via parser errors\n if (\n (token.type === 'unknown' || token.type === 'css-function') &&\n 'raw' in token &&\n token.raw &&\n erroredRaws.has(token.raw)\n ) {\n continue;\n }\n\n // Check unknown tokens against property expectations\n if (token.type === 'unknown') {\n const raw = token.raw;\n\n if (expectation.acceptsMods === true) {\n // Passthrough: accept any unknown token\n } else if (expectation.acceptsMods === false) {\n context.report({\n node,\n messageId: 'unexpectedMod',\n data: { property, mod: raw },\n });\n } else if (Array.isArray(expectation.acceptsMods)) {\n if (!expectation.acceptsMods.includes(raw)) {\n context.report({\n node,\n messageId: 'invalidMod',\n data: {\n property,\n mod: raw,\n accepted: expectation.acceptsMods.join(', '),\n },\n });\n }\n }\n }\n }\n }\n }\n }\n\n return stringStyleValueListeners(ctx, (value, node, property) => {\n if (!/^[#$]/.test(property)) checkValue(value, property, node);\n });\n },\n});\n\nconst COLOR_FUNC_NAMES = new Set([\n 'rgb',\n 'rgba',\n 'hsl',\n 'hsla',\n 'hwb',\n 'lab',\n 'lch',\n 'oklab',\n 'oklch',\n 'color',\n 'color-mix',\n 'color-contrast',\n]);\n\nfunction isColorFunction(name: string): boolean {\n return COLOR_FUNC_NAMES.has(name);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"valid-value.js","names":[],"sources":["../../src/rules/valid-value.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport { parseValue } from '../parsers/value-parser.js';\nimport type { ValueParserOptions } from '../parsers/value-parser.js';\nimport { getExpectation } from '../property-expectations.js';\n\nconst SKIP_PROPERTIES = new Set([\n 'recipe',\n 'preset',\n 'transition',\n '@keyframes',\n '@property',\n '@font-face',\n '@counter-style',\n '@function',\n 'content',\n 'animation',\n 'animationName',\n 'gridArea',\n 'gridAreas',\n 'gridColumn',\n 'gridColumnStart',\n 'gridColumnEnd',\n 'gridRow',\n 'gridRowStart',\n 'gridRowEnd',\n 'gridTemplate',\n 'gridTemplateAreas',\n 'gridTemplateColumns',\n 'gridTemplateRows',\n 'listStyle',\n 'willChange',\n]);\n\ntype MessageIds =\n | 'unbalancedParens'\n | 'importantNotAllowed'\n | 'unexpectedMod'\n | 'unexpectedColor'\n | 'invalidMod'\n | 'unknownToken'\n | 'parseError';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-value',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Parse style values through the value parser and validate against per-property expectations',\n },\n messages: {\n unbalancedParens: 'Unbalanced parentheses in value.',\n importantNotAllowed:\n 'Do not use !important in tasty styles. Use state specificity instead.',\n unexpectedMod:\n \"Unrecognized token '{{mod}}' in '{{property}}' value. This may be a typo.\",\n unexpectedColor:\n \"Property '{{property}}' does not accept color tokens, but found '{{color}}'.\",\n invalidMod:\n \"Modifier '{{mod}}' is not valid for '{{property}}'. Accepted: {{accepted}}.\",\n unknownToken:\n \"Unknown token '{{token}}' in '{{property}}' value. Check for a typo.\",\n parseError: '{{message}}',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function getParserOpts(): ValueParserOptions {\n const opts: ValueParserOptions = {};\n if (ctx.config.units === false) {\n opts.skipUnitValidation = true;\n } else if (Array.isArray(ctx.config.units)) {\n opts.knownUnits = new Set(ctx.config.units);\n }\n if (ctx.config.functions === false) {\n opts.skipFuncValidation = true;\n } else if (Array.isArray(ctx.config.functions)) {\n opts.knownFuncs = new Set(ctx.config.functions);\n }\n return opts;\n }\n\n function checkValue(\n value: string,\n property: string | null,\n node: TSESTree.Node,\n ): void {\n if (property && SKIP_PROPERTIES.has(property)) return;\n\n const result = parseValue(value, getParserOpts());\n\n const erroredRaws = new Set<string>();\n\n // Report parser-level errors (bracket balance, unknown units/functions)\n for (const error of result.errors) {\n if (error.message.includes('parenthes')) {\n context.report({ node, messageId: 'unbalancedParens' });\n return;\n }\n context.report({\n node,\n messageId: 'parseError',\n data: { message: error.message },\n });\n if (error.raw) {\n erroredRaws.add(error.raw);\n }\n }\n\n if (!property) return;\n\n const expectation = getExpectation(property);\n\n for (const group of result.groups) {\n for (const part of group.parts) {\n for (const token of part.tokens) {\n // Check !important\n if (token.type === 'important') {\n context.report({ node, messageId: 'importantNotAllowed' });\n continue;\n }\n\n // Check color tokens and keywords in non-color properties\n if (\n !expectation.acceptsColor &&\n (token.type === 'color-token' ||\n token.type === 'color-ref' ||\n token.type === 'color-keyword')\n ) {\n const colorName =\n token.type === 'color-token'\n ? `#${token.name}`\n : token.type === 'color-ref'\n ? `##${token.name}`\n : token.raw;\n context.report({\n node,\n messageId: 'unexpectedColor',\n data: { property, color: colorName },\n });\n continue;\n }\n\n // Check color functions in non-color properties\n if (\n !expectation.acceptsColor &&\n token.type === 'css-function' &&\n isColorFunction(token.name)\n ) {\n context.report({\n node,\n messageId: 'unexpectedColor',\n data: { property, color: `${token.name}()` },\n });\n continue;\n }\n\n // Skip tokens already reported via parser errors\n if (\n (token.type === 'unknown' || token.type === 'css-function') &&\n 'raw' in token &&\n token.raw &&\n erroredRaws.has(token.raw)\n ) {\n continue;\n }\n\n // Check unknown tokens against property expectations\n if (token.type === 'unknown') {\n const raw = token.raw;\n\n if (expectation.acceptsMods === true) {\n // Passthrough: accept any unknown token\n } else if (expectation.acceptsMods === false) {\n context.report({\n node,\n messageId: 'unexpectedMod',\n data: { property, mod: raw },\n });\n } else if (Array.isArray(expectation.acceptsMods)) {\n if (!expectation.acceptsMods.includes(raw)) {\n context.report({\n node,\n messageId: 'invalidMod',\n data: {\n property,\n mod: raw,\n accepted: expectation.acceptsMods.join(', '),\n },\n });\n }\n }\n }\n }\n }\n }\n }\n\n return stringStyleValueListeners(ctx, (value, node, property) => {\n if (!/^[#$]/.test(property)) checkValue(value, property, node);\n });\n },\n});\n\nconst COLOR_FUNC_NAMES = new Set([\n 'rgb',\n 'rgba',\n 'hsl',\n 'hsla',\n 'hwb',\n 'lab',\n 'lch',\n 'oklab',\n 'oklch',\n 'color',\n 'color-mix',\n 'color-contrast',\n]);\n\nfunction isColorFunction(name: string): boolean {\n return COLOR_FUNC_NAMES.has(name);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/eslint-plugin-tasty",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
5
5
  "type": "module",
6
6
  "exports": {