@tenphi/eslint-plugin-tasty 0.1.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/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/_virtual/_rolldown/runtime.mjs +7 -0
- package/dist/config.mjs +118 -0
- package/dist/config.mjs.map +1 -0
- package/dist/configs.d.mts +8 -0
- package/dist/configs.mjs +36 -0
- package/dist/configs.mjs.map +1 -0
- package/dist/constants.mjs +700 -0
- package/dist/constants.mjs.map +1 -0
- package/dist/context.mjs +180 -0
- package/dist/context.mjs.map +1 -0
- package/dist/create-rule.mjs +8 -0
- package/dist/create-rule.mjs.map +1 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +83 -0
- package/dist/index.mjs.map +1 -0
- package/dist/parser.mjs +46 -0
- package/dist/parser.mjs.map +1 -0
- package/dist/property-expectations.mjs +168 -0
- package/dist/property-expectations.mjs.map +1 -0
- package/dist/rules/consistent-token-usage.mjs +88 -0
- package/dist/rules/consistent-token-usage.mjs.map +1 -0
- package/dist/rules/known-property.mjs +55 -0
- package/dist/rules/known-property.mjs.map +1 -0
- package/dist/rules/no-duplicate-state.mjs +51 -0
- package/dist/rules/no-duplicate-state.mjs.map +1 -0
- package/dist/rules/no-important.mjs +44 -0
- package/dist/rules/no-important.mjs.map +1 -0
- package/dist/rules/no-nested-selector.mjs +40 -0
- package/dist/rules/no-nested-selector.mjs.map +1 -0
- package/dist/rules/no-nested-state-map.mjs +44 -0
- package/dist/rules/no-nested-state-map.mjs.map +1 -0
- package/dist/rules/no-raw-color-values.mjs +84 -0
- package/dist/rules/no-raw-color-values.mjs.map +1 -0
- package/dist/rules/no-runtime-styles-mutation.mjs +52 -0
- package/dist/rules/no-runtime-styles-mutation.mjs.map +1 -0
- package/dist/rules/no-styles-prop.mjs +25 -0
- package/dist/rules/no-styles-prop.mjs.map +1 -0
- package/dist/rules/no-unknown-state-alias.mjs +49 -0
- package/dist/rules/no-unknown-state-alias.mjs.map +1 -0
- package/dist/rules/prefer-shorthand-property.mjs +45 -0
- package/dist/rules/prefer-shorthand-property.mjs.map +1 -0
- package/dist/rules/require-default-state.mjs +47 -0
- package/dist/rules/require-default-state.mjs.map +1 -0
- package/dist/rules/static-no-dynamic-values.mjs +54 -0
- package/dist/rules/static-no-dynamic-values.mjs.map +1 -0
- package/dist/rules/static-valid-selector.mjs +51 -0
- package/dist/rules/static-valid-selector.mjs.map +1 -0
- package/dist/rules/valid-boolean-property.mjs +45 -0
- package/dist/rules/valid-boolean-property.mjs.map +1 -0
- package/dist/rules/valid-color-token.mjs +84 -0
- package/dist/rules/valid-color-token.mjs.map +1 -0
- package/dist/rules/valid-custom-property.mjs +69 -0
- package/dist/rules/valid-custom-property.mjs.map +1 -0
- package/dist/rules/valid-custom-unit.mjs +62 -0
- package/dist/rules/valid-custom-unit.mjs.map +1 -0
- package/dist/rules/valid-directional-modifier.mjs +71 -0
- package/dist/rules/valid-directional-modifier.mjs.map +1 -0
- package/dist/rules/valid-preset.mjs +64 -0
- package/dist/rules/valid-preset.mjs.map +1 -0
- package/dist/rules/valid-radius-shape.mjs +77 -0
- package/dist/rules/valid-radius-shape.mjs.map +1 -0
- package/dist/rules/valid-recipe.mjs +51 -0
- package/dist/rules/valid-recipe.mjs.map +1 -0
- package/dist/rules/valid-state-key.mjs +71 -0
- package/dist/rules/valid-state-key.mjs.map +1 -0
- package/dist/rules/valid-styles-structure.mjs +79 -0
- package/dist/rules/valid-styles-structure.mjs.map +1 -0
- package/dist/rules/valid-sub-element.mjs +46 -0
- package/dist/rules/valid-sub-element.mjs.map +1 -0
- package/dist/rules/valid-transition.mjs +62 -0
- package/dist/rules/valid-transition.mjs.map +1 -0
- package/dist/rules/valid-value.mjs +123 -0
- package/dist/rules/valid-value.mjs.map +1 -0
- package/dist/types.d.mts +25 -0
- package/dist/utils.mjs +152 -0
- package/dist/utils.mjs.map +1 -0
- package/package.json +90 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"valid-value.mjs","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 { getKeyName, getStringValue } from '../utils.js';\nimport { getParser } from '../parser.js';\nimport { getExpectation } from '../property-expectations.js';\n\ntype MessageIds =\n | 'unbalancedParens'\n | 'importantNotAllowed'\n | 'unexpectedMod'\n | 'unexpectedColor'\n | 'invalidMod';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-value',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Parse style values through the tasty 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 },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkParenBalance(value: string, node: TSESTree.Node): boolean {\n let depth = 0;\n for (const char of value) {\n if (char === '(') depth++;\n if (char === ')') depth--;\n if (depth < 0) {\n context.report({ node, messageId: 'unbalancedParens' });\n return false;\n }\n }\n if (depth !== 0) {\n context.report({ node, messageId: 'unbalancedParens' });\n return false;\n }\n return true;\n }\n\n function checkValue(\n value: string,\n property: string | null,\n node: TSESTree.Node,\n ): void {\n if (!checkParenBalance(value, node)) return;\n\n if (value.includes('!important')) {\n context.report({ node, messageId: 'importantNotAllowed' });\n return;\n }\n\n if (!property) return;\n\n const parser = getParser(ctx.config);\n const result = parser.process(value);\n const expectation = getExpectation(property);\n\n for (const group of result.groups) {\n if (!expectation.acceptsColor && group.colors.length > 0) {\n for (const color of group.colors) {\n context.report({\n node,\n messageId: 'unexpectedColor',\n data: { property, color },\n });\n }\n }\n\n if (expectation.acceptsMods === false && group.mods.length > 0) {\n for (const mod of group.mods) {\n context.report({\n node,\n messageId: 'unexpectedMod',\n data: { property, mod },\n });\n }\n } else if (\n Array.isArray(expectation.acceptsMods) &&\n group.mods.length > 0\n ) {\n const allowed = new Set(expectation.acceptsMods);\n for (const mod of group.mods) {\n if (!allowed.has(mod)) {\n context.report({\n node,\n messageId: 'invalidMod',\n data: {\n property,\n mod,\n accepted: expectation.acceptsMods.join(', '),\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 return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n 'CallExpression ObjectExpression'(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 },\n});\n"],"mappings":";;;;;;;AAcA,0BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,8FACH;EACD,UAAU;GACR,kBAAkB;GAClB,qBACE;GACF,eACE;GACF,iBACE;GACF,YACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,kBAAkB,OAAe,MAA8B;GACtE,IAAI,QAAQ;AACZ,QAAK,MAAM,QAAQ,OAAO;AACxB,QAAI,SAAS,IAAK;AAClB,QAAI,SAAS,IAAK;AAClB,QAAI,QAAQ,GAAG;AACb,aAAQ,OAAO;MAAE;MAAM,WAAW;MAAoB,CAAC;AACvD,YAAO;;;AAGX,OAAI,UAAU,GAAG;AACf,YAAQ,OAAO;KAAE;KAAM,WAAW;KAAoB,CAAC;AACvD,WAAO;;AAET,UAAO;;EAGT,SAAS,WACP,OACA,UACA,MACM;AACN,OAAI,CAAC,kBAAkB,OAAO,KAAK,CAAE;AAErC,OAAI,MAAM,SAAS,aAAa,EAAE;AAChC,YAAQ,OAAO;KAAE;KAAM,WAAW;KAAuB,CAAC;AAC1D;;AAGF,OAAI,CAAC,SAAU;GAGf,MAAM,SADS,UAAU,IAAI,OAAO,CACd,QAAQ,MAAM;GACpC,MAAM,cAAc,eAAe,SAAS;AAE5C,QAAK,MAAM,SAAS,OAAO,QAAQ;AACjC,QAAI,CAAC,YAAY,gBAAgB,MAAM,OAAO,SAAS,EACrD,MAAK,MAAM,SAAS,MAAM,OACxB,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE;MAAU;MAAO;KAC1B,CAAC;AAIN,QAAI,YAAY,gBAAgB,SAAS,MAAM,KAAK,SAAS,EAC3D,MAAK,MAAM,OAAO,MAAM,KACtB,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE;MAAU;MAAK;KACxB,CAAC;aAGJ,MAAM,QAAQ,YAAY,YAAY,IACtC,MAAM,KAAK,SAAS,GACpB;KACA,MAAM,UAAU,IAAI,IAAI,YAAY,YAAY;AAChD,UAAK,MAAM,OAAO,MAAM,KACtB,KAAI,CAAC,QAAQ,IAAI,IAAI,CACnB,SAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OACJ;OACA;OACA,UAAU,YAAY,YAAY,KAAK,KAAK;OAC7C;MACF,CAAC;;;;EAOZ,SAAS,gBAAgB,MAA+B;GACtD,MAAM,MAAM,CAAC,KAAK,WAAW,WAAW,KAAK,IAAI,GAAG;AAEpD,OAAI,QAAQ,SAAS,KAAK,IAAI,IAAI,IAAI,WAAW,IAAI,EAAG;AACxD,OAAI,QAAQ,IAAI,WAAW,IAAI,IAAI,IAAI,WAAW,IAAI,EAAG;AACzD,OAAI,OAAO,IAAI,WAAW,IAAI,CAAE;GAEhC,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,OAAI,KAAK;AACP,eAAW,KAAK,KAAK,KAAK,MAAM;AAChC;;AAIF,OAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,QAAI,UAAU,SAAS,WAAY;IACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,QAAI,SACF,YAAW,UAAU,KAAK,UAAU,MAAM;;;AAMlD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,kCAAkC,MAAiC;AACjE,QAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,SAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,SAAI,KAAK,SAAS,WAAY;AAC9B,qBAAgB,KAAK;;;GAG1B;;CAEJ,CAAC"}
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
interface TastyValidationConfig {
|
|
3
|
+
extends?: string;
|
|
4
|
+
tokens?: false | string[];
|
|
5
|
+
units?: false | string[];
|
|
6
|
+
funcs?: false | string[];
|
|
7
|
+
states?: string[];
|
|
8
|
+
presets?: string[];
|
|
9
|
+
recipes?: string[];
|
|
10
|
+
styles?: string[];
|
|
11
|
+
importSources?: string[];
|
|
12
|
+
}
|
|
13
|
+
interface ResolvedConfig {
|
|
14
|
+
tokens: false | string[];
|
|
15
|
+
units: false | string[];
|
|
16
|
+
funcs: false | string[];
|
|
17
|
+
states: string[];
|
|
18
|
+
presets: string[];
|
|
19
|
+
recipes: string[];
|
|
20
|
+
styles: string[];
|
|
21
|
+
importSources: string[];
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { ResolvedConfig, TastyValidationConfig };
|
|
25
|
+
//# sourceMappingURL=types.d.mts.map
|
package/dist/utils.mjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { BUILT_IN_STATE_PREFIXES, BUILT_IN_UNITS, CSS_UNITS } from "./constants.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/utils.ts
|
|
4
|
+
/**
|
|
5
|
+
* Gets the string value of a property key node.
|
|
6
|
+
*/
|
|
7
|
+
function getKeyName(key) {
|
|
8
|
+
if (key.type === "Identifier") return key.name;
|
|
9
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
10
|
+
if (key.type === "Literal" && typeof key.value === "number") return String(key.value);
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Gets the string value of a node if it is a string literal.
|
|
15
|
+
*/
|
|
16
|
+
function getStringValue(node) {
|
|
17
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
18
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0].value.cooked ?? null;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Checks if a value node is a static literal.
|
|
23
|
+
*/
|
|
24
|
+
function isStaticValue(node) {
|
|
25
|
+
if (node.type === "Literal") return true;
|
|
26
|
+
if (node.type === "UnaryExpression" && node.operator === "-" && node.argument.type === "Literal") return true;
|
|
27
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0) return true;
|
|
28
|
+
if (node.type === "ArrayExpression") return node.elements.every((el) => el !== null && isStaticValue(el));
|
|
29
|
+
if (node.type === "ObjectExpression") return node.properties.every((prop) => prop.type === "Property" && !prop.computed && isStaticValue(prop.value));
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validates color token syntax.
|
|
34
|
+
* Returns null if valid, or an error message if invalid.
|
|
35
|
+
*/
|
|
36
|
+
function validateColorTokenSyntax(token) {
|
|
37
|
+
let name = token;
|
|
38
|
+
if (name.startsWith("##")) name = name.slice(2);
|
|
39
|
+
else if (name.startsWith("#")) name = name.slice(1);
|
|
40
|
+
else return "Color token must start with #";
|
|
41
|
+
if (name.length === 0) return "Empty color token name";
|
|
42
|
+
const dotIndex = name.indexOf(".");
|
|
43
|
+
if (dotIndex !== -1) {
|
|
44
|
+
const tokenName = name.slice(0, dotIndex);
|
|
45
|
+
const opacitySuffix = name.slice(dotIndex + 1);
|
|
46
|
+
if (tokenName.length === 0) return "Empty color token name before opacity";
|
|
47
|
+
if (opacitySuffix.startsWith("$")) return null;
|
|
48
|
+
if (opacitySuffix.length === 0) return "Trailing dot with no opacity value";
|
|
49
|
+
const opacity = Number(opacitySuffix);
|
|
50
|
+
if (isNaN(opacity)) return `Invalid opacity value '${opacitySuffix}'`;
|
|
51
|
+
if (opacity < 0) return "Opacity cannot be negative";
|
|
52
|
+
if (opacity > 100) return `Opacity '${opacitySuffix}' exceeds 100`;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Checks if a string looks like a raw hex color (not a token).
|
|
58
|
+
* Hex colors: #fff, #ffff, #ffffff, #ffffffff (3, 4, 6, or 8 hex chars).
|
|
59
|
+
*/
|
|
60
|
+
function isRawHexColor(value) {
|
|
61
|
+
if (!value.startsWith("#")) return false;
|
|
62
|
+
const hex = value.slice(1).split(".")[0];
|
|
63
|
+
if (![
|
|
64
|
+
3,
|
|
65
|
+
4,
|
|
66
|
+
6,
|
|
67
|
+
8
|
|
68
|
+
].includes(hex.length)) return false;
|
|
69
|
+
return /^[0-9a-fA-F]+$/.test(hex);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Extracts custom unit from a value token like "2x", "1.5r", "3cols".
|
|
73
|
+
* Returns the unit name, or null if not a custom-unit value.
|
|
74
|
+
*/
|
|
75
|
+
function extractCustomUnit(token) {
|
|
76
|
+
const match = token.match(/^-?[\d.]+([a-zA-Z]+)$/);
|
|
77
|
+
if (!match) return null;
|
|
78
|
+
return match[1];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Checks if a unit is valid (built-in, CSS, or in config).
|
|
82
|
+
*/
|
|
83
|
+
function isValidUnit(unit, config) {
|
|
84
|
+
if (config.units === false) return true;
|
|
85
|
+
if (BUILT_IN_UNITS.has(unit)) return true;
|
|
86
|
+
if (CSS_UNITS.has(unit)) return true;
|
|
87
|
+
if (Array.isArray(config.units) && config.units.includes(unit)) return true;
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Validates a state key string.
|
|
92
|
+
* Returns null if valid, or an error message if invalid.
|
|
93
|
+
*/
|
|
94
|
+
function validateStateKey(key) {
|
|
95
|
+
if (key === "") return null;
|
|
96
|
+
if (key.startsWith(":") || key.startsWith("::")) return null;
|
|
97
|
+
if (key.startsWith(".")) {
|
|
98
|
+
if (key.length <= 1) return "Empty class selector";
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
if (key.startsWith("[") && key.endsWith("]")) return null;
|
|
102
|
+
if (key.startsWith("@")) {
|
|
103
|
+
for (const prefix of BUILT_IN_STATE_PREFIXES) if (key === prefix || key.startsWith(prefix + "(")) return null;
|
|
104
|
+
if (key.startsWith("@(")) return null;
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
if (key.includes("&") || key.includes("|") || key.includes("^")) return validateCombinedStateKey(key);
|
|
108
|
+
if (key.startsWith("!")) {
|
|
109
|
+
if (key.length <= 1) return "Empty negated state";
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
if (key.startsWith("(") && key.endsWith(")")) return null;
|
|
113
|
+
if (/^[a-zA-Z][a-zA-Z0-9-]*$/.test(key)) return null;
|
|
114
|
+
if (/^[a-zA-Z][a-zA-Z0-9-]*[\^$*]?=/.test(key)) return null;
|
|
115
|
+
return `Invalid state key syntax '${key}'`;
|
|
116
|
+
}
|
|
117
|
+
function validateCombinedStateKey(key) {
|
|
118
|
+
const parts = key.split(/\s*[&|^]\s*/);
|
|
119
|
+
for (const part of parts) {
|
|
120
|
+
const trimmed = part.trim();
|
|
121
|
+
if (trimmed.length === 0) return "Empty part in combined state expression";
|
|
122
|
+
const partError = validateStateKey(trimmed.startsWith("!") ? trimmed : trimmed);
|
|
123
|
+
if (partError) return partError;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Checks if a state alias key (starting with @) is known.
|
|
129
|
+
*/
|
|
130
|
+
function isKnownStateAlias(key, config) {
|
|
131
|
+
for (const prefix of BUILT_IN_STATE_PREFIXES) if (key === prefix || key.startsWith(prefix + "(")) return true;
|
|
132
|
+
if (key.startsWith("@(")) return true;
|
|
133
|
+
return config.states.includes(key);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Checks if a CSS selector string is basically valid.
|
|
137
|
+
*/
|
|
138
|
+
function isValidSelector(selector) {
|
|
139
|
+
if (selector.length === 0) return "Selector cannot be empty";
|
|
140
|
+
let depth = 0;
|
|
141
|
+
for (const char of selector) {
|
|
142
|
+
if (char === "(" || char === "[") depth++;
|
|
143
|
+
if (char === ")" || char === "]") depth--;
|
|
144
|
+
if (depth < 0) return "Unbalanced brackets in selector";
|
|
145
|
+
}
|
|
146
|
+
if (depth !== 0) return "Unbalanced brackets in selector";
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
export { extractCustomUnit, getKeyName, getStringValue, isKnownStateAlias, isRawHexColor, isStaticValue, isValidSelector, isValidUnit, validateColorTokenSyntax, validateStateKey };
|
|
152
|
+
//# sourceMappingURL=utils.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.mjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport {\n BUILT_IN_UNITS,\n CSS_UNITS,\n BUILT_IN_STATE_PREFIXES,\n} from './constants.js';\nimport type { ResolvedConfig } from './types.js';\n\n/**\n * Gets the string value of a property key node.\n */\nexport function getKeyName(key: TSESTree.Node): string | null {\n if (key.type === 'Identifier') return key.name;\n if (key.type === 'Literal' && typeof key.value === 'string') return key.value;\n if (key.type === 'Literal' && typeof key.value === 'number')\n return String(key.value);\n return null;\n}\n\n/**\n * Gets the string value of a node if it is a string literal.\n */\nexport function getStringValue(node: TSESTree.Node): string | null {\n if (node.type === 'Literal' && typeof node.value === 'string') {\n return node.value;\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return node.quasis[0].value.cooked ?? null;\n }\n return null;\n}\n\n/**\n * Checks if a value node is a static literal.\n */\nexport function isStaticValue(node: TSESTree.Node): boolean {\n if (node.type === 'Literal') return true;\n if (\n node.type === 'UnaryExpression' &&\n node.operator === '-' &&\n node.argument.type === 'Literal'\n ) {\n return true;\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return true;\n }\n if (node.type === 'ArrayExpression') {\n return node.elements.every((el) => el !== null && isStaticValue(el));\n }\n if (node.type === 'ObjectExpression') {\n return node.properties.every(\n (prop) =>\n prop.type === 'Property' && !prop.computed && isStaticValue(prop.value),\n );\n }\n return false;\n}\n\n/**\n * Validates color token syntax.\n * Returns null if valid, or an error message if invalid.\n */\nexport function validateColorTokenSyntax(token: string): string | null {\n // Strip leading # or ##\n let name = token;\n if (name.startsWith('##')) {\n name = name.slice(2);\n } else if (name.startsWith('#')) {\n name = name.slice(1);\n } else {\n return 'Color token must start with #';\n }\n\n if (name.length === 0) return 'Empty color token name';\n\n // Check for opacity suffix\n const dotIndex = name.indexOf('.');\n if (dotIndex !== -1) {\n const tokenName = name.slice(0, dotIndex);\n const opacitySuffix = name.slice(dotIndex + 1);\n\n if (tokenName.length === 0) return 'Empty color token name before opacity';\n\n if (opacitySuffix.startsWith('$')) {\n // Dynamic opacity from CSS custom property — always valid\n return null;\n }\n\n if (opacitySuffix.length === 0) return 'Trailing dot with no opacity value';\n\n const opacity = Number(opacitySuffix);\n if (isNaN(opacity)) return `Invalid opacity value '${opacitySuffix}'`;\n if (opacity < 0) return 'Opacity cannot be negative';\n if (opacity > 100) return `Opacity '${opacitySuffix}' exceeds 100`;\n }\n\n return null;\n}\n\n/**\n * Checks if a string looks like a raw hex color (not a token).\n * Hex colors: #fff, #ffff, #ffffff, #ffffffff (3, 4, 6, or 8 hex chars).\n */\nexport function isRawHexColor(value: string): boolean {\n if (!value.startsWith('#')) return false;\n const hex = value.slice(1).split('.')[0];\n if (![3, 4, 6, 8].includes(hex.length)) return false;\n return /^[0-9a-fA-F]+$/.test(hex);\n}\n\n/**\n * Extracts custom unit from a value token like \"2x\", \"1.5r\", \"3cols\".\n * Returns the unit name, or null if not a custom-unit value.\n */\nexport function extractCustomUnit(token: string): string | null {\n const match = token.match(/^-?[\\d.]+([a-zA-Z]+)$/);\n if (!match) return null;\n return match[1];\n}\n\n/**\n * Checks if a unit is valid (built-in, CSS, or in config).\n */\nexport function isValidUnit(unit: string, config: ResolvedConfig): boolean {\n if (config.units === false) return true;\n if (BUILT_IN_UNITS.has(unit)) return true;\n if (CSS_UNITS.has(unit)) return true;\n if (Array.isArray(config.units) && config.units.includes(unit)) return true;\n return false;\n}\n\n/**\n * Validates a state key string.\n * Returns null if valid, or an error message if invalid.\n */\nexport function validateStateKey(key: string): string | null {\n if (key === '') return null; // Default state\n\n // Pseudo-class/element\n if (key.startsWith(':') || key.startsWith('::')) return null;\n\n // Class selector\n if (key.startsWith('.')) {\n if (key.length <= 1) return 'Empty class selector';\n return null;\n }\n\n // Attribute selector\n if (key.startsWith('[') && key.endsWith(']')) return null;\n\n // @ prefixed — check for built-in or alias\n if (key.startsWith('@')) {\n // Built-in functional state prefix: @media(...), @root(...), etc.\n for (const prefix of BUILT_IN_STATE_PREFIXES) {\n if (key === prefix || key.startsWith(prefix + '(')) return null;\n }\n // Container query shorthand: @(...)\n if (key.startsWith('@(')) return null;\n // Otherwise it's an alias — validated elsewhere\n return null;\n }\n\n // Combined expressions with operators\n if (key.includes('&') || key.includes('|') || key.includes('^')) {\n return validateCombinedStateKey(key);\n }\n\n // Negation\n if (key.startsWith('!')) {\n if (key.length <= 1) return 'Empty negated state';\n return null;\n }\n\n // Parenthesized group\n if (key.startsWith('(') && key.endsWith(')')) return null;\n\n // Boolean modifier: simple identifier like 'hovered'\n if (/^[a-zA-Z][a-zA-Z0-9-]*$/.test(key)) return null;\n\n // Value modifier: name=value or name^=value etc.\n if (/^[a-zA-Z][a-zA-Z0-9-]*[\\^$*]?=/.test(key)) return null;\n\n return `Invalid state key syntax '${key}'`;\n}\n\nfunction validateCombinedStateKey(key: string): string | null {\n // Split by & and | operators, validate each part\n const parts = key.split(/\\s*[&|^]\\s*/);\n for (const part of parts) {\n const trimmed = part.trim();\n if (trimmed.length === 0) return 'Empty part in combined state expression';\n // Each part should be a valid individual state\n const partError = validateStateKey(\n trimmed.startsWith('!') ? trimmed : trimmed,\n );\n if (partError) return partError;\n }\n return null;\n}\n\n/**\n * Checks if a state alias key (starting with @) is known.\n */\nexport function isKnownStateAlias(\n key: string,\n config: ResolvedConfig,\n): boolean {\n // Built-in prefixes\n for (const prefix of BUILT_IN_STATE_PREFIXES) {\n if (key === prefix || key.startsWith(prefix + '(')) return true;\n }\n // Container query shorthand\n if (key.startsWith('@(')) return true;\n // Config aliases\n return config.states.includes(key);\n}\n\n/**\n * Checks if a CSS selector string is basically valid.\n */\nexport function isValidSelector(selector: string): string | null {\n if (selector.length === 0) return 'Selector cannot be empty';\n\n // Check balanced brackets\n let depth = 0;\n for (const char of selector) {\n if (char === '(' || char === '[') depth++;\n if (char === ')' || char === ']') depth--;\n if (depth < 0) return 'Unbalanced brackets in selector';\n }\n if (depth !== 0) return 'Unbalanced brackets in selector';\n\n return null;\n}\n\n/**\n * Finds a property by key name in an object expression.\n */\nexport function findProperty(\n obj: TSESTree.ObjectExpression,\n name: string,\n): TSESTree.Property | undefined {\n for (const prop of obj.properties) {\n if (prop.type === 'Property' && !prop.computed) {\n const keyName = getKeyName(prop.key);\n if (keyName === name) return prop;\n }\n }\n return undefined;\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tenphi/eslint-plugin-tasty",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsdown",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"test:coverage": "vitest run --coverage",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "eslint src",
|
|
28
|
+
"lint:fix": "eslint src --fix",
|
|
29
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
30
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
31
|
+
"prepublishOnly": "pnpm run build",
|
|
32
|
+
"changeset": "changeset",
|
|
33
|
+
"version": "changeset version",
|
|
34
|
+
"release": "changeset publish"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/tenphi/eslint-plugin-tasty.git"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"eslint",
|
|
42
|
+
"eslintplugin",
|
|
43
|
+
"eslint-plugin",
|
|
44
|
+
"tasty",
|
|
45
|
+
"css-in-js",
|
|
46
|
+
"styling",
|
|
47
|
+
"design-system",
|
|
48
|
+
"linter"
|
|
49
|
+
],
|
|
50
|
+
"author": "Andrey Yamanov",
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/tenphi/eslint-plugin-tasty/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/tenphi/eslint-plugin-tasty#readme",
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@tenphi/tasty": ">=0.1.3",
|
|
58
|
+
"eslint": ">=8.0.0"
|
|
59
|
+
},
|
|
60
|
+
"peerDependenciesMeta": {
|
|
61
|
+
"@tenphi/tasty": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@typescript-eslint/utils": "^8.56.0"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@changesets/changelog-github": "^0.5.2",
|
|
70
|
+
"@changesets/cli": "^2.29.8",
|
|
71
|
+
"@eslint/js": "^10.0.1",
|
|
72
|
+
"@types/node": "^22.0.0",
|
|
73
|
+
"@typescript-eslint/parser": "^8.56.1",
|
|
74
|
+
"@typescript-eslint/rule-tester": "^8.56.1",
|
|
75
|
+
"eslint": "^10.0.0",
|
|
76
|
+
"eslint-config-prettier": "^10.1.8",
|
|
77
|
+
"prettier": "^3.8.1",
|
|
78
|
+
"tsdown": "^0.20.3",
|
|
79
|
+
"typescript": "^5.9.3",
|
|
80
|
+
"typescript-eslint": "^8.56.0",
|
|
81
|
+
"@tenphi/tasty": "^0.1.3",
|
|
82
|
+
"vitest": "^4.0.18"
|
|
83
|
+
},
|
|
84
|
+
"pnpm": {
|
|
85
|
+
"onlyBuiltDependencies": [
|
|
86
|
+
"esbuild"
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"packageManager": "pnpm@10.29.3"
|
|
90
|
+
}
|