@tenphi/eslint-plugin-tasty 0.11.4 → 1.0.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 +15 -2
- package/dist/config.js +3 -2
- package/dist/config.js.map +1 -1
- package/dist/constants.js +49 -7
- package/dist/constants.js.map +1 -1
- package/dist/context.js +40 -5
- package/dist/context.js.map +1 -1
- package/dist/index.js +40 -37
- package/dist/index.js.map +1 -1
- package/dist/parsers/value-parser.js +34 -0
- package/dist/parsers/value-parser.js.map +1 -1
- package/dist/rules/no-runtime-styles-mutation.js.map +1 -1
- package/dist/rules/valid-color-token.js +1 -1
- package/dist/rules/valid-color-token.js.map +1 -1
- package/dist/rules/valid-custom-property.js +1 -1
- package/dist/rules/valid-custom-property.js.map +1 -1
- package/dist/rules/valid-directional-modifier.js +76 -13
- package/dist/rules/valid-directional-modifier.js.map +1 -1
- package/dist/rules/valid-styles-structure.js +50 -10
- package/dist/rules/valid-styles-structure.js.map +1 -1
- package/dist/rules/valid-value.js +6 -3
- package/dist/rules/valid-value.js.map +1 -1
- package/dist/types.d.ts +9 -1
- package/package.json +7 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
|
-
import { DIRECTIONAL_MODIFIERS } from "../constants.js";
|
|
2
|
+
import { DIRECTIONAL_MODIFIERS, DOCK_PROPERTIES, SINGLE_VALUE_DIRECTIONAL_PROPERTIES } from "../constants.js";
|
|
3
3
|
import { getKeyName, getStringValue } from "../utils.js";
|
|
4
4
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
5
|
+
import { parseValue } from "../parsers/value-parser.js";
|
|
5
6
|
|
|
6
7
|
//#region src/rules/valid-directional-modifier.ts
|
|
7
8
|
const ALL_DIRECTIONS = new Set([
|
|
@@ -14,28 +15,90 @@ const ALL_DIRECTIONS = new Set([
|
|
|
14
15
|
"bottom-left",
|
|
15
16
|
"bottom-right"
|
|
16
17
|
]);
|
|
18
|
+
/**
|
|
19
|
+
* Token types that count as a *value* rather than a modifier.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately excludes `keyword`: direction words, `solid`, `inherit` and every
|
|
22
|
+
* other bare word classify as keywords, and a CSS-wide keyword alongside a
|
|
23
|
+
* direction (`padding: 'inherit top'`) is legal while carrying no value of its own.
|
|
24
|
+
*
|
|
25
|
+
* Also excludes colors: a `fade` group carries a width plus up to two mask
|
|
26
|
+
* colors, so colors must not count toward the one-value limit.
|
|
27
|
+
*/
|
|
28
|
+
const VALUE_TOKEN_TYPES = new Set([
|
|
29
|
+
"custom-unit",
|
|
30
|
+
"css-unit",
|
|
31
|
+
"number",
|
|
32
|
+
"custom-prop",
|
|
33
|
+
"custom-prop-ref",
|
|
34
|
+
"css-function",
|
|
35
|
+
"tasty-function",
|
|
36
|
+
"group-expr"
|
|
37
|
+
]);
|
|
38
|
+
/** The source text of a token. `keyword` tokens carry `value` rather than `raw`. */
|
|
39
|
+
function tokenText(token) {
|
|
40
|
+
if ("raw" in token && typeof token.raw === "string") return token.raw;
|
|
41
|
+
if (token.type === "keyword") return token.value;
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
17
44
|
var valid_directional_modifier_default = createRule({
|
|
18
45
|
name: "valid-directional-modifier",
|
|
19
46
|
meta: {
|
|
20
47
|
type: "problem",
|
|
21
|
-
docs: { description: "Validate
|
|
22
|
-
messages: {
|
|
48
|
+
docs: { description: "Validate directional modifiers: only on properties that support them, and one value per group" },
|
|
49
|
+
messages: {
|
|
50
|
+
invalidDirectionalModifier: "Modifier '{{modifier}}' is not valid for '{{property}}'. Accepted: {{accepted}}.",
|
|
51
|
+
tooManyValues: "A group that names directions takes {{limit}}, but '{{group}}' has {{count}}. Values and modifiers are parsed into separate buckets, so their order is not recoverable — use comma-separated groups for per-side values, e.g. \"2x top, 4x right\"."
|
|
52
|
+
},
|
|
23
53
|
schema: []
|
|
24
54
|
},
|
|
25
55
|
defaultOptions: [],
|
|
26
56
|
create(context) {
|
|
27
57
|
const ctx = new TastyContext(context);
|
|
28
58
|
function checkValue(property, value, node) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
59
|
+
const allowedMods = DIRECTIONAL_MODIFIERS[property];
|
|
60
|
+
if (!allowedMods) return;
|
|
61
|
+
const { groups } = parseValue(value, {
|
|
62
|
+
skipUnitValidation: true,
|
|
63
|
+
skipFuncValidation: true
|
|
64
|
+
});
|
|
65
|
+
for (const group of groups) {
|
|
66
|
+
const tokens = group.parts.flatMap((part) => part.tokens);
|
|
67
|
+
const directions = [];
|
|
68
|
+
let valueCount = 0;
|
|
69
|
+
let hasSpanModifier = false;
|
|
70
|
+
for (const token of tokens) {
|
|
71
|
+
if (VALUE_TOKEN_TYPES.has(token.type)) {
|
|
72
|
+
valueCount++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const word = tokenText(token);
|
|
76
|
+
if (ALL_DIRECTIONS.has(word)) directions.push(word);
|
|
77
|
+
else if (word === "dock" && DOCK_PROPERTIES.has(property)) hasSpanModifier = true;
|
|
78
|
+
}
|
|
79
|
+
if (directions.length === 0) continue;
|
|
80
|
+
let reportedUnknown = false;
|
|
81
|
+
for (const direction of directions) if (!allowedMods.has(direction)) {
|
|
82
|
+
context.report({
|
|
83
|
+
node,
|
|
84
|
+
messageId: "invalidDirectionalModifier",
|
|
85
|
+
data: {
|
|
86
|
+
property,
|
|
87
|
+
modifier: direction,
|
|
88
|
+
accepted: [...allowedMods].join(", ")
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
reportedUnknown = true;
|
|
92
|
+
}
|
|
93
|
+
if (reportedUnknown) continue;
|
|
94
|
+
if (!SINGLE_VALUE_DIRECTIONAL_PROPERTIES.has(property)) continue;
|
|
95
|
+
if (valueCount > (hasSpanModifier ? 2 : 1)) context.report({
|
|
34
96
|
node,
|
|
35
|
-
messageId: "
|
|
97
|
+
messageId: "tooManyValues",
|
|
36
98
|
data: {
|
|
37
|
-
|
|
38
|
-
|
|
99
|
+
group: tokens.map(tokenText).filter(Boolean).join(" "),
|
|
100
|
+
count: `${valueCount}`,
|
|
101
|
+
limit: hasSpanModifier ? "at most two values" : "a single value"
|
|
39
102
|
}
|
|
40
103
|
});
|
|
41
104
|
}
|
|
@@ -46,13 +109,13 @@ var valid_directional_modifier_default = createRule({
|
|
|
46
109
|
if (prop.type !== "Property" || prop.computed) continue;
|
|
47
110
|
const key = getKeyName(prop.key);
|
|
48
111
|
if (key === null) continue;
|
|
49
|
-
if (
|
|
112
|
+
if (key === "$" || key.startsWith("@") || key.startsWith("$") || key.startsWith("#") || key.startsWith("&") || /^[A-Z]/.test(key)) continue;
|
|
50
113
|
const str = getStringValue(prop.value);
|
|
51
114
|
if (str) {
|
|
52
115
|
checkValue(key, str, prop.value);
|
|
53
116
|
continue;
|
|
54
117
|
}
|
|
55
|
-
if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
|
|
118
|
+
if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) for (const stateProp of prop.value.properties) {
|
|
56
119
|
if (stateProp.type !== "Property") continue;
|
|
57
120
|
const stateStr = getStringValue(stateProp.value);
|
|
58
121
|
if (stateStr) checkValue(key, stateStr, stateProp.value);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valid-directional-modifier.js","names":[],"sources":["../../src/rules/valid-directional-modifier.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport {
|
|
1
|
+
{"version":3,"file":"valid-directional-modifier.js","names":[],"sources":["../../src/rules/valid-directional-modifier.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport {\n DIRECTIONAL_MODIFIERS,\n DOCK_PROPERTIES,\n SINGLE_VALUE_DIRECTIONAL_PROPERTIES,\n} from '../constants.js';\nimport { parseValue } from '../parsers/index.js';\nimport type { ValueToken } from '../parsers/index.js';\n\ntype MessageIds = 'invalidDirectionalModifier' | 'tooManyValues';\n\nconst ALL_DIRECTIONS = new Set([\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n]);\n\n/**\n * Token types that count as a *value* rather than a modifier.\n *\n * Deliberately excludes `keyword`: direction words, `solid`, `inherit` and every\n * other bare word classify as keywords, and a CSS-wide keyword alongside a\n * direction (`padding: 'inherit top'`) is legal while carrying no value of its own.\n *\n * Also excludes colors: a `fade` group carries a width plus up to two mask\n * colors, so colors must not count toward the one-value limit.\n */\nconst VALUE_TOKEN_TYPES = new Set<ValueToken['type']>([\n 'custom-unit',\n 'css-unit',\n 'number',\n 'custom-prop',\n 'custom-prop-ref',\n 'css-function',\n 'tasty-function',\n 'group-expr',\n]);\n\n/** The source text of a token. `keyword` tokens carry `value` rather than `raw`. */\nfunction tokenText(token: ValueToken): string {\n if ('raw' in token && typeof token.raw === 'string') return token.raw;\n if (token.type === 'keyword') return token.value;\n\n return '';\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-directional-modifier',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate directional modifiers: only on properties that support them, and one value per group',\n },\n messages: {\n invalidDirectionalModifier:\n \"Modifier '{{modifier}}' is not valid for '{{property}}'. Accepted: {{accepted}}.\",\n tooManyValues:\n 'A group that names directions takes {{limit}}, but \\'{{group}}\\' has {{count}}. Values and modifiers are parsed into separate buckets, so their order is not recoverable — use comma-separated groups for per-side values, e.g. \"2x top, 4x right\".',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n property: string,\n value: string,\n node: TSESTree.Node,\n ): void {\n const allowedMods = DIRECTIONAL_MODIFIERS[property];\n // Only properties with a directional vocabulary are checked.\n //\n // This gate looks like it makes the \"wrong property\" case unreachable, and\n // it does — but removing it is not the fix. Direction words are ordinary CSS\n // *values* for a long tail of properties: `verticalAlign: 'bottom'`,\n // `textAlign: 'left'`, `transformOrigin: 'top center'`,\n // `backgroundPosition`, `objectPosition`, `float`, `clear`,\n // `scrollSnapAlign`, and `transition` (whose value names properties, which\n // can themselves be `left`/`top`). Checking every property reported all of\n // those as errors across a real design system. Catching `fill: '#purple\n // top'` is not worth an allowlist of every property that takes a\n // positional keyword.\n if (!allowedMods) return;\n\n // `parseValue` splits on commas (groups) and spaced slashes (parts), which\n // is what makes a per-group arity check possible at all.\n const { groups } = parseValue(value, {\n skipUnitValidation: true,\n skipFuncValidation: true,\n });\n\n for (const group of groups) {\n const tokens = group.parts.flatMap((part) => part.tokens);\n const directions: string[] = [];\n let valueCount = 0;\n let hasSpanModifier = false;\n\n for (const token of tokens) {\n if (VALUE_TOKEN_TYPES.has(token.type)) {\n valueCount++;\n continue;\n }\n\n const word = tokenText(token);\n\n if (ALL_DIRECTIONS.has(word)) {\n directions.push(word);\n } else if (word === 'dock' && DOCK_PROPERTIES.has(property)) {\n hasSpanModifier = true;\n }\n }\n\n if (directions.length === 0) continue;\n\n let reportedUnknown = false;\n for (const direction of directions) {\n if (!allowedMods.has(direction)) {\n context.report({\n node,\n messageId: 'invalidDirectionalModifier',\n data: {\n property,\n modifier: direction,\n accepted: [...allowedMods].join(', '),\n },\n });\n reportedUnknown = true;\n }\n }\n\n // Don't pile an arity complaint on top of an already-invalid modifier.\n if (reportedUnknown) continue;\n\n if (!SINGLE_VALUE_DIRECTIONAL_PROPERTIES.has(property)) continue;\n\n // Values and modifiers are bucketed separately by the parser, so the\n // pairing a reader infers from source order does not survive parsing: a\n // group naming directions carries one value, applied to every direction it\n // names. `dock` is the exception — its second value insets the sides the\n // dock spans.\n const limit = hasSpanModifier ? 2 : 1;\n\n if (valueCount > limit) {\n context.report({\n node,\n messageId: 'tooManyValues',\n data: {\n group: tokens.map(tokenText).filter(Boolean).join(' '),\n count: `${valueCount}`,\n limit: hasSpanModifier ? 'at most two values' : 'a single value',\n },\n });\n }\n }\n }\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) continue;\n\n // Sub-elements, at-rules, token declarations, nested selectors, and the\n // `$` affix (which holds a selector, not a value) are not style values.\n if (\n key === '$' ||\n key.startsWith('@') ||\n key.startsWith('$') ||\n key.startsWith('#') ||\n key.startsWith('&') ||\n /^[A-Z]/.test(key)\n ) {\n continue;\n }\n\n // Direct value\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(key, str, prop.value);\n continue;\n }\n\n // State map\n if (\n prop.value.type === 'ObjectExpression' &&\n ctx.isStateMap(prop.value, prop)\n ) {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkValue(key, stateStr, stateProp.value);\n }\n }\n }\n }\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,19 +1,35 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
|
+
import { RENAMED_SPECIAL_STYLE_KEYS } from "../constants.js";
|
|
2
3
|
import { getKeyName, getStringValue } from "../utils.js";
|
|
3
4
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
5
|
|
|
5
6
|
//#region src/rules/valid-styles-structure.ts
|
|
7
|
+
/**
|
|
8
|
+
* Whether an at-rule value is *definitely* not the descriptor object it must be.
|
|
9
|
+
*
|
|
10
|
+
* Only literal shapes are conclusive. An identifier, member expression, or call
|
|
11
|
+
* can perfectly well evaluate to an object — `'@font-face': fontFaceMap` is valid
|
|
12
|
+
* — so anything the plugin cannot see into is left alone.
|
|
13
|
+
*/
|
|
14
|
+
function isDefinitelyNotAnObject(node) {
|
|
15
|
+
let current = node;
|
|
16
|
+
while (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression") current = current.expression;
|
|
17
|
+
return current.type === "Literal" || current.type === "TemplateLiteral" || current.type === "ArrayExpression";
|
|
18
|
+
}
|
|
6
19
|
var valid_styles_structure_default = createRule({
|
|
7
20
|
name: "valid-styles-structure",
|
|
8
21
|
meta: {
|
|
9
22
|
type: "problem",
|
|
23
|
+
fixable: "code",
|
|
10
24
|
docs: { description: "Validate overall structure of styles object passed to tasty APIs" },
|
|
11
25
|
messages: {
|
|
12
26
|
stateKeyAtTopLevel: "State key '{{key}}' at top level is not valid. State maps belong inside property values, not at the root of the styles object.",
|
|
13
27
|
invalidKeyframesStructure: "@keyframes value must be an object of { name: { step: styles } }.",
|
|
14
|
-
invalidPropertiesStructure: "@
|
|
15
|
-
invalidFontFaceStructure: "@
|
|
16
|
-
invalidCounterStyleStructure: "@
|
|
28
|
+
invalidPropertiesStructure: "'@property' value must be an object of { name: { syntax, inherits, initialValue } }.",
|
|
29
|
+
invalidFontFaceStructure: "'@font-face' value must be an object of { familyName: descriptors | descriptors[] }.",
|
|
30
|
+
invalidCounterStyleStructure: "'@counter-style' value must be an object of { name: descriptors }.",
|
|
31
|
+
invalidFunctionStructure: "'@function' value must be an object of { '$$name': { args, result } }.",
|
|
32
|
+
renamedSpecialKey: "'{{oldKey}}' was renamed to '{{newKey}}' in Tasty v3 — at-rule keys now match the CSS at-rule names Tasty emits.",
|
|
17
33
|
recipeNotString: "'recipe' value must be a string."
|
|
18
34
|
},
|
|
19
35
|
schema: []
|
|
@@ -44,34 +60,58 @@ var valid_styles_structure_default = createRule({
|
|
|
44
60
|
});
|
|
45
61
|
continue;
|
|
46
62
|
}
|
|
63
|
+
const renamedTo = key ? RENAMED_SPECIAL_STYLE_KEYS[key] : void 0;
|
|
64
|
+
if (renamedTo) {
|
|
65
|
+
const keyNode = prop.key;
|
|
66
|
+
context.report({
|
|
67
|
+
node: keyNode,
|
|
68
|
+
messageId: "renamedSpecialKey",
|
|
69
|
+
data: {
|
|
70
|
+
oldKey: key,
|
|
71
|
+
newKey: renamedTo
|
|
72
|
+
},
|
|
73
|
+
fix(fixer) {
|
|
74
|
+
const quote = keyNode.type === "Literal" && typeof keyNode.raw === "string" && keyNode.raw.startsWith("\"") ? "\"" : "'";
|
|
75
|
+
return fixer.replaceText(keyNode, `${quote}${renamedTo}${quote}`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
47
80
|
if (key === "@keyframes") {
|
|
48
|
-
if (prop.value
|
|
81
|
+
if (isDefinitelyNotAnObject(prop.value)) context.report({
|
|
49
82
|
node: prop.value,
|
|
50
83
|
messageId: "invalidKeyframesStructure"
|
|
51
84
|
});
|
|
52
85
|
continue;
|
|
53
86
|
}
|
|
54
|
-
if (key === "@
|
|
55
|
-
if (prop.value
|
|
87
|
+
if (key === "@property") {
|
|
88
|
+
if (isDefinitelyNotAnObject(prop.value)) context.report({
|
|
56
89
|
node: prop.value,
|
|
57
90
|
messageId: "invalidPropertiesStructure"
|
|
58
91
|
});
|
|
59
92
|
continue;
|
|
60
93
|
}
|
|
61
|
-
if (key === "@
|
|
62
|
-
if (prop.value
|
|
94
|
+
if (key === "@font-face") {
|
|
95
|
+
if (isDefinitelyNotAnObject(prop.value)) context.report({
|
|
63
96
|
node: prop.value,
|
|
64
97
|
messageId: "invalidFontFaceStructure"
|
|
65
98
|
});
|
|
66
99
|
continue;
|
|
67
100
|
}
|
|
68
|
-
if (key === "@
|
|
69
|
-
if (prop.value
|
|
101
|
+
if (key === "@counter-style") {
|
|
102
|
+
if (isDefinitelyNotAnObject(prop.value)) context.report({
|
|
70
103
|
node: prop.value,
|
|
71
104
|
messageId: "invalidCounterStyleStructure"
|
|
72
105
|
});
|
|
73
106
|
continue;
|
|
74
107
|
}
|
|
108
|
+
if (key === "@function") {
|
|
109
|
+
if (isDefinitelyNotAnObject(prop.value)) context.report({
|
|
110
|
+
node: prop.value,
|
|
111
|
+
messageId: "invalidFunctionStructure"
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
75
115
|
if (key === "recipe") {
|
|
76
116
|
if (getStringValue(prop.value) === null && prop.value.type !== "Literal") {
|
|
77
117
|
if (prop.value.type !== "TemplateLiteral" || prop.value.expressions.length > 0) context.report({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valid-styles-structure.js","names":[],"sources":["../../src/rules/valid-styles-structure.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\n\ntype MessageIds =\n | 'stateKeyAtTopLevel'\n | 'invalidKeyframesStructure'\n | 'invalidPropertiesStructure'\n | 'invalidFontFaceStructure'\n | 'invalidCounterStyleStructure'\n | 'recipeNotString';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-styles-structure',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate overall structure of styles object passed to tasty APIs',\n },\n messages: {\n stateKeyAtTopLevel:\n \"State key '{{key}}' at top level is not valid. State maps belong inside property values, not at the root of the styles object.\",\n invalidKeyframesStructure:\n '@keyframes value must be an object of { name: { step: styles } }.',\n invalidPropertiesStructure:\n '@
|
|
1
|
+
{"version":3,"file":"valid-styles-structure.js","names":[],"sources":["../../src/rules/valid-styles-structure.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { RENAMED_SPECIAL_STYLE_KEYS } from '../constants.js';\nimport { getKeyName, getStringValue } from '../utils.js';\n\ntype MessageIds =\n | 'stateKeyAtTopLevel'\n | 'invalidKeyframesStructure'\n | 'invalidPropertiesStructure'\n | 'invalidFontFaceStructure'\n | 'invalidCounterStyleStructure'\n | 'invalidFunctionStructure'\n | 'renamedSpecialKey'\n | 'recipeNotString';\n\n/**\n * Whether an at-rule value is *definitely* not the descriptor object it must be.\n *\n * Only literal shapes are conclusive. An identifier, member expression, or call\n * can perfectly well evaluate to an object — `'@font-face': fontFaceMap` is valid\n * — so anything the plugin cannot see into is left alone.\n */\nfunction isDefinitelyNotAnObject(node: TSESTree.Node): boolean {\n let current = node;\n\n // Look through `as` / `satisfies` wrappers.\n while (\n current.type === 'TSAsExpression' ||\n current.type === 'TSSatisfiesExpression' ||\n current.type === 'TSNonNullExpression'\n ) {\n current = current.expression;\n }\n\n return (\n current.type === 'Literal' ||\n current.type === 'TemplateLiteral' ||\n current.type === 'ArrayExpression'\n );\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-styles-structure',\n meta: {\n type: 'problem',\n fixable: 'code',\n docs: {\n description:\n 'Validate overall structure of styles object passed to tasty APIs',\n },\n messages: {\n stateKeyAtTopLevel:\n \"State key '{{key}}' at top level is not valid. State maps belong inside property values, not at the root of the styles object.\",\n invalidKeyframesStructure:\n '@keyframes value must be an object of { name: { step: styles } }.',\n invalidPropertiesStructure:\n \"'@property' value must be an object of { name: { syntax, inherits, initialValue } }.\",\n invalidFontFaceStructure:\n \"'@font-face' value must be an object of { familyName: descriptors | descriptors[] }.\",\n invalidCounterStyleStructure:\n \"'@counter-style' value must be an object of { name: descriptors }.\",\n invalidFunctionStructure:\n \"'@function' value must be an object of { '$$name': { args, result } }.\",\n renamedSpecialKey:\n \"'{{oldKey}}' was renamed to '{{newKey}}' in Tasty v3 — at-rule keys now match the CSS at-rule names Tasty emits.\",\n recipeNotString: \"'recipe' value must be a string.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n const STATE_KEY_PATTERNS = [\n /^:/, // pseudo-class\n /^\\./, // class selector\n /^\\[/, // attribute selector\n ];\n\n function looksLikeStateKey(key: string): boolean {\n // '' default and '_' fallback floor are state-map keys, never top-level.\n if (key === '' || key === '_') return true;\n return STATE_KEY_PATTERNS.some((p) => p.test(key));\n }\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) continue;\n\n // Check for state keys at top level (common mistake)\n if (looksLikeStateKey(key)) {\n context.report({\n node: prop.key,\n messageId: 'stateKeyAtTopLevel',\n data: { key },\n });\n continue;\n }\n\n // Report the v2 camelCase at-rule spellings, with a fix.\n const renamedTo = key ? RENAMED_SPECIAL_STYLE_KEYS[key] : undefined;\n if (renamedTo) {\n const keyNode = prop.key;\n context.report({\n node: keyNode,\n messageId: 'renamedSpecialKey',\n data: { oldKey: key, newKey: renamedTo },\n fix(fixer) {\n // Always emit a quoted key: the new spellings are kebab-case and so\n // are not valid bare identifiers.\n const quote =\n keyNode.type === 'Literal' &&\n typeof keyNode.raw === 'string' &&\n keyNode.raw.startsWith('\"')\n ? '\"'\n : \"'\";\n\n return fixer.replaceText(keyNode, `${quote}${renamedTo}${quote}`);\n },\n });\n continue;\n }\n\n // Validate @keyframes structure\n if (key === '@keyframes') {\n if (isDefinitelyNotAnObject(prop.value)) {\n context.report({\n node: prop.value,\n messageId: 'invalidKeyframesStructure',\n });\n }\n continue;\n }\n\n // Validate @property structure\n if (key === '@property') {\n if (isDefinitelyNotAnObject(prop.value)) {\n context.report({\n node: prop.value,\n messageId: 'invalidPropertiesStructure',\n });\n }\n continue;\n }\n\n // Validate @font-face structure\n if (key === '@font-face') {\n if (isDefinitelyNotAnObject(prop.value)) {\n context.report({\n node: prop.value,\n messageId: 'invalidFontFaceStructure',\n });\n }\n continue;\n }\n\n // Validate @counter-style structure\n if (key === '@counter-style') {\n if (isDefinitelyNotAnObject(prop.value)) {\n context.report({\n node: prop.value,\n messageId: 'invalidCounterStyleStructure',\n });\n }\n continue;\n }\n\n // Validate @function structure\n if (key === '@function') {\n if (isDefinitelyNotAnObject(prop.value)) {\n context.report({\n node: prop.value,\n messageId: 'invalidFunctionStructure',\n });\n }\n continue;\n }\n\n // Validate recipe is a string\n if (key === 'recipe') {\n const str = getStringValue(prop.value);\n if (str === null && prop.value.type !== 'Literal') {\n // Allow string literals, template literals without expressions\n if (\n prop.value.type !== 'TemplateLiteral' ||\n prop.value.expressions.length > 0\n ) {\n context.report({\n node: prop.value,\n messageId: 'recipeNotString',\n });\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;;;;;;;;AAuBA,SAAS,wBAAwB,MAA8B;CAC7D,IAAI,UAAU;AAGd,QACE,QAAQ,SAAS,oBACjB,QAAQ,SAAS,2BACjB,QAAQ,SAAS,sBAEjB,WAAU,QAAQ;AAGpB,QACE,QAAQ,SAAS,aACjB,QAAQ,SAAS,qBACjB,QAAQ,SAAS;;AAIrB,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,oEACH;EACD,UAAU;GACR,oBACE;GACF,2BACE;GACF,4BACE;GACF,0BACE;GACF,8BACE;GACF,0BACE;GACF,mBACE;GACF,iBAAiB;GAClB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,MAAM,qBAAqB;GACzB;GACA;GACA;GACD;EAED,SAAS,kBAAkB,KAAsB;AAE/C,OAAI,QAAQ,MAAM,QAAQ,IAAK,QAAO;AACtC,UAAO,mBAAmB,MAAM,MAAM,EAAE,KAAK,IAAI,CAAC;;EAGpD,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,KAAM;AAGlB,QAAI,kBAAkB,IAAI,EAAE;AAC1B,aAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACX,MAAM,EAAE,KAAK;MACd,CAAC;AACF;;IAIF,MAAM,YAAY,MAAM,2BAA2B,OAAO;AAC1D,QAAI,WAAW;KACb,MAAM,UAAU,KAAK;AACrB,aAAQ,OAAO;MACb,MAAM;MACN,WAAW;MACX,MAAM;OAAE,QAAQ;OAAK,QAAQ;OAAW;MACxC,IAAI,OAAO;OAGT,MAAM,QACJ,QAAQ,SAAS,aACjB,OAAO,QAAQ,QAAQ,YACvB,QAAQ,IAAI,WAAW,KAAI,GACvB,OACA;AAEN,cAAO,MAAM,YAAY,SAAS,GAAG,QAAQ,YAAY,QAAQ;;MAEpE,CAAC;AACF;;AAIF,QAAI,QAAQ,cAAc;AACxB,SAAI,wBAAwB,KAAK,MAAM,CACrC,SAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACZ,CAAC;AAEJ;;AAIF,QAAI,QAAQ,aAAa;AACvB,SAAI,wBAAwB,KAAK,MAAM,CACrC,SAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACZ,CAAC;AAEJ;;AAIF,QAAI,QAAQ,cAAc;AACxB,SAAI,wBAAwB,KAAK,MAAM,CACrC,SAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACZ,CAAC;AAEJ;;AAIF,QAAI,QAAQ,kBAAkB;AAC5B,SAAI,wBAAwB,KAAK,MAAM,CACrC,SAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACZ,CAAC;AAEJ;;AAIF,QAAI,QAAQ,aAAa;AACvB,SAAI,wBAAwB,KAAK,MAAM,CACrC,SAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACZ,CAAC;AAEJ;;AAIF,QAAI,QAAQ,UAEV;SADY,eAAe,KAAK,MAAM,KAC1B,QAAQ,KAAK,MAAM,SAAS,WAEtC;UACE,KAAK,MAAM,SAAS,qBACpB,KAAK,MAAM,YAAY,SAAS,EAEhC,SAAQ,OAAO;OACb,MAAM,KAAK;OACX,WAAW;OACZ,CAAC;;;;;AAOZ,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -10,7 +10,10 @@ const SKIP_PROPERTIES = new Set([
|
|
|
10
10
|
"preset",
|
|
11
11
|
"transition",
|
|
12
12
|
"@keyframes",
|
|
13
|
-
"@
|
|
13
|
+
"@property",
|
|
14
|
+
"@font-face",
|
|
15
|
+
"@counter-style",
|
|
16
|
+
"@function",
|
|
14
17
|
"content",
|
|
15
18
|
"animation",
|
|
16
19
|
"animationName",
|
|
@@ -52,8 +55,8 @@ var valid_value_default = createRule({
|
|
|
52
55
|
const opts = {};
|
|
53
56
|
if (ctx.config.units === false) opts.skipUnitValidation = true;
|
|
54
57
|
else if (Array.isArray(ctx.config.units)) opts.knownUnits = new Set(ctx.config.units);
|
|
55
|
-
if (ctx.config.
|
|
56
|
-
else if (Array.isArray(ctx.config.
|
|
58
|
+
if (ctx.config.functions === false) opts.skipFuncValidation = true;
|
|
59
|
+
else if (Array.isArray(ctx.config.functions)) opts.knownFuncs = new Set(ctx.config.functions);
|
|
57
60
|
return opts;
|
|
58
61
|
}
|
|
59
62
|
function checkValue(value, property, node) {
|
|
@@ -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, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.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 '@properties',\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.funcs === false) {\n opts.skipFuncValidation = true;\n } else if (Array.isArray(ctx.config.funcs)) {\n opts.knownFuncs = new Set(ctx.config.funcs);\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 function processProperty(prop: TSESTree.Property): void {\n const key = !prop.computed ? getKeyName(prop.key) : null;\n\n if (key && (/^[A-Z]/.test(key) || key.startsWith('@'))) return;\n if (key && (key.startsWith('$') || key.startsWith('#'))) return;\n if (key && key.startsWith('&')) return;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(str, key, prop.value);\n return;\n }\n\n // State map\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkValue(stateStr, key, stateProp.value);\n }\n }\n }\n }\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') continue;\n processProperty(prop);\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\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, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.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 function processProperty(prop: TSESTree.Property): void {\n const key = !prop.computed ? getKeyName(prop.key) : null;\n\n if (key && (/^[A-Z]/.test(key) || key.startsWith('@'))) return;\n if (key && (key.startsWith('$') || key.startsWith('#'))) return;\n if (key && key.startsWith('&')) return;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(str, key, prop.value);\n return;\n }\n\n // State map\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkValue(stateStr, key, stateProp.value);\n }\n }\n }\n }\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') continue;\n processProperty(prop);\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\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/dist/types.d.ts
CHANGED
|
@@ -3,6 +3,14 @@ interface TastyValidationConfig {
|
|
|
3
3
|
extends?: string;
|
|
4
4
|
tokens?: false | string[];
|
|
5
5
|
units?: false | string[];
|
|
6
|
+
/**
|
|
7
|
+
* Valid custom parse-function names. Matches Tasty v3's `functions` config key.
|
|
8
|
+
*
|
|
9
|
+
* `funcs` is the v2 spelling and is still read as a deprecated alias so a shared
|
|
10
|
+
* `tasty.config.ts` keeps working through the upgrade.
|
|
11
|
+
*/
|
|
12
|
+
functions?: false | string[];
|
|
13
|
+
/** @deprecated Renamed to `functions` in Tasty v3. */
|
|
6
14
|
funcs?: false | string[];
|
|
7
15
|
states?: string[];
|
|
8
16
|
presets?: string[];
|
|
@@ -13,7 +21,7 @@ interface TastyValidationConfig {
|
|
|
13
21
|
interface ResolvedConfig {
|
|
14
22
|
tokens: false | string[];
|
|
15
23
|
units: false | string[];
|
|
16
|
-
|
|
24
|
+
functions: false | string[];
|
|
17
25
|
states: string[];
|
|
18
26
|
presets: string[];
|
|
19
27
|
recipes: string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenphi/eslint-plugin-tasty",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"homepage": "https://github.com/tenphi/eslint-plugin-tasty#readme",
|
|
41
41
|
"peerDependencies": {
|
|
42
|
+
"@tenphi/tasty": ">=3",
|
|
42
43
|
"eslint": ">=8.0.0"
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
@@ -60,6 +61,11 @@
|
|
|
60
61
|
"typescript-eslint": "^8.56.0",
|
|
61
62
|
"vitest": "^4.0.18"
|
|
62
63
|
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"@tenphi/tasty": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
63
69
|
"scripts": {
|
|
64
70
|
"build": "tsdown",
|
|
65
71
|
"test": "vitest run",
|