@tenphi/eslint-plugin-tasty 0.11.2 → 0.11.4
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/dist/property-expectations.js +12 -5
- package/dist/property-expectations.js.map +1 -1
- package/dist/rules/consistent-token-usage.js +1 -1
- package/dist/rules/consistent-token-usage.js.map +1 -1
- package/dist/rules/no-important.js +1 -1
- package/dist/rules/no-important.js.map +1 -1
- package/dist/rules/no-raw-color-values.js +1 -1
- package/dist/rules/no-raw-color-values.js.map +1 -1
- package/dist/rules/prefer-shorthand-property.js +28 -3
- package/dist/rules/prefer-shorthand-property.js.map +1 -1
- package/dist/rules/valid-transition.js +10 -1
- package/dist/rules/valid-transition.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,13 +5,20 @@ const DIRECTIONAL_MODS = [
|
|
|
5
5
|
"bottom",
|
|
6
6
|
"left"
|
|
7
7
|
];
|
|
8
|
-
const
|
|
8
|
+
const LONGHAND_MOD = "longhand";
|
|
9
|
+
const INSET_MODS = [
|
|
10
|
+
...DIRECTIONAL_MODS,
|
|
11
|
+
"dock",
|
|
12
|
+
LONGHAND_MOD
|
|
13
|
+
];
|
|
14
|
+
const BOX_DIRECTIONAL_MODS = [...DIRECTIONAL_MODS, LONGHAND_MOD];
|
|
9
15
|
const RADIUS_DIRECTIONAL_MODS = [
|
|
10
16
|
...DIRECTIONAL_MODS,
|
|
11
17
|
"top-left",
|
|
12
18
|
"top-right",
|
|
13
19
|
"bottom-left",
|
|
14
|
-
"bottom-right"
|
|
20
|
+
"bottom-right",
|
|
21
|
+
LONGHAND_MOD
|
|
15
22
|
];
|
|
16
23
|
const BORDER_STYLE_MODS = [
|
|
17
24
|
"solid",
|
|
@@ -83,7 +90,7 @@ const PROPERTY_EXPECTATIONS = {
|
|
|
83
90
|
},
|
|
84
91
|
border: {
|
|
85
92
|
acceptsColor: true,
|
|
86
|
-
acceptsMods: [...
|
|
93
|
+
acceptsMods: [...BOX_DIRECTIONAL_MODS, ...BORDER_STYLE_MODS]
|
|
87
94
|
},
|
|
88
95
|
outline: {
|
|
89
96
|
acceptsColor: true,
|
|
@@ -101,13 +108,13 @@ const PROPERTY_EXPECTATIONS = {
|
|
|
101
108
|
},
|
|
102
109
|
padding: {
|
|
103
110
|
acceptsColor: false,
|
|
104
|
-
acceptsMods:
|
|
111
|
+
acceptsMods: BOX_DIRECTIONAL_MODS
|
|
105
112
|
},
|
|
106
113
|
paddingInline: VALUE_ONLY,
|
|
107
114
|
paddingBlock: VALUE_ONLY,
|
|
108
115
|
margin: {
|
|
109
116
|
acceptsColor: false,
|
|
110
|
-
acceptsMods:
|
|
117
|
+
acceptsMods: BOX_DIRECTIONAL_MODS
|
|
111
118
|
},
|
|
112
119
|
fade: {
|
|
113
120
|
acceptsColor: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"property-expectations.js","names":[],"sources":["../src/property-expectations.ts"],"sourcesContent":["/**\n * Per-property expectations for parser bucket validation.\n *\n * After parsing a value through StyleParser.process(), each group contains\n * `colors`, `values`, and `mods` arrays. This map defines what is expected\n * for each tasty property so we can flag unexpected tokens.\n *\n * - `acceptsColor`: whether Color bucket tokens are valid\n * - `acceptsMods`: whether Mod bucket tokens are valid, and if so which ones\n * - `false` = no mods accepted (any mod is an error)\n * - `true` = any mod accepted (pass-through)\n * - `string[]` = only these specific mods are accepted\n *\n * Properties NOT listed here default to PASSTHROUGH (accept everything).\n * Only add properties that have actual restrictions.\n */\n\nexport interface PropertyExpectation {\n acceptsColor: boolean;\n acceptsMods: boolean | string[];\n}\n\nconst DIRECTIONAL_MODS = ['top', 'right', 'bottom', 'left'];\n// `inset` additionally accepts `dock`: pin one edge and span its full\n// length (tasty >= 2.10). Other directional properties do not.\nconst INSET_MODS = [...DIRECTIONAL_MODS, 'dock'];\nconst RADIUS_DIRECTIONAL_MODS = [\n ...DIRECTIONAL_MODS,\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n];\nconst BORDER_STYLE_MODS = [\n 'solid',\n 'dashed',\n 'dotted',\n 'double',\n 'groove',\n 'ridge',\n 'inset',\n 'outset',\n 'none',\n 'hidden',\n];\nconst DIMENSION_MODS = ['min', 'max', 'fixed'];\nconst FLOW_MODS = [\n 'row',\n 'column',\n 'wrap',\n 'nowrap',\n 'dense',\n 'row-reverse',\n 'column-reverse',\n];\nconst OVERFLOW_MODS = [\n 'visible',\n 'hidden',\n 'scroll',\n 'clip',\n 'auto',\n 'overlay',\n];\nconst POSITION_MODS = ['static', 'relative', 'absolute', 'fixed', 'sticky'];\n\nconst COLOR_ONLY: PropertyExpectation = {\n acceptsColor: true,\n acceptsMods: false,\n};\n\nconst VALUE_ONLY: PropertyExpectation = {\n acceptsColor: false,\n acceptsMods: false,\n};\n\nconst PASSTHROUGH: PropertyExpectation = {\n acceptsColor: true,\n acceptsMods: true,\n};\n\nexport const PROPERTY_EXPECTATIONS: Record<string, PropertyExpectation> = {\n fill: { acceptsColor: true, acceptsMods: ['none', 'transparent'] },\n color: { acceptsColor: true, acceptsMods: ['none', 'transparent'] },\n caretColor: COLOR_ONLY,\n accentColor: COLOR_ONLY,\n shadow: { acceptsColor: true, acceptsMods: ['inset'] },\n\n border: {\n acceptsColor: true,\n acceptsMods: [...
|
|
1
|
+
{"version":3,"file":"property-expectations.js","names":[],"sources":["../src/property-expectations.ts"],"sourcesContent":["/**\n * Per-property expectations for parser bucket validation.\n *\n * After parsing a value through StyleParser.process(), each group contains\n * `colors`, `values`, and `mods` arrays. This map defines what is expected\n * for each tasty property so we can flag unexpected tokens.\n *\n * - `acceptsColor`: whether Color bucket tokens are valid\n * - `acceptsMods`: whether Mod bucket tokens are valid, and if so which ones\n * - `false` = no mods accepted (any mod is an error)\n * - `true` = any mod accepted (pass-through)\n * - `string[]` = only these specific mods are accepted\n *\n * Properties NOT listed here default to PASSTHROUGH (accept everything).\n * Only add properties that have actual restrictions.\n */\n\nexport interface PropertyExpectation {\n acceptsColor: boolean;\n acceptsMods: boolean | string[];\n}\n\nconst DIRECTIONAL_MODS = ['top', 'right', 'bottom', 'left'];\n// Output modifier accepted by every box property (`padding`, `margin`,\n// `inset`, `border`, `radius`): emit the individual CSS longhands instead of\n// the shorthand, so children can selectively inherit a single side/corner.\nconst LONGHAND_MOD = 'longhand';\n// `inset` additionally accepts `dock`: pin one edge and span its full\n// length (tasty >= 2.10). Other directional properties do not.\nconst INSET_MODS = [...DIRECTIONAL_MODS, 'dock', LONGHAND_MOD];\nconst BOX_DIRECTIONAL_MODS = [...DIRECTIONAL_MODS, LONGHAND_MOD];\nconst RADIUS_DIRECTIONAL_MODS = [\n ...DIRECTIONAL_MODS,\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n LONGHAND_MOD,\n];\nconst BORDER_STYLE_MODS = [\n 'solid',\n 'dashed',\n 'dotted',\n 'double',\n 'groove',\n 'ridge',\n 'inset',\n 'outset',\n 'none',\n 'hidden',\n];\nconst DIMENSION_MODS = ['min', 'max', 'fixed'];\nconst FLOW_MODS = [\n 'row',\n 'column',\n 'wrap',\n 'nowrap',\n 'dense',\n 'row-reverse',\n 'column-reverse',\n];\nconst OVERFLOW_MODS = [\n 'visible',\n 'hidden',\n 'scroll',\n 'clip',\n 'auto',\n 'overlay',\n];\nconst POSITION_MODS = ['static', 'relative', 'absolute', 'fixed', 'sticky'];\n\nconst COLOR_ONLY: PropertyExpectation = {\n acceptsColor: true,\n acceptsMods: false,\n};\n\nconst VALUE_ONLY: PropertyExpectation = {\n acceptsColor: false,\n acceptsMods: false,\n};\n\nconst PASSTHROUGH: PropertyExpectation = {\n acceptsColor: true,\n acceptsMods: true,\n};\n\nexport const PROPERTY_EXPECTATIONS: Record<string, PropertyExpectation> = {\n fill: { acceptsColor: true, acceptsMods: ['none', 'transparent'] },\n color: { acceptsColor: true, acceptsMods: ['none', 'transparent'] },\n caretColor: COLOR_ONLY,\n accentColor: COLOR_ONLY,\n shadow: { acceptsColor: true, acceptsMods: ['inset'] },\n\n border: {\n acceptsColor: true,\n acceptsMods: [...BOX_DIRECTIONAL_MODS, ...BORDER_STYLE_MODS],\n },\n outline: {\n acceptsColor: true,\n acceptsMods: BORDER_STYLE_MODS,\n },\n\n radius: {\n acceptsColor: false,\n acceptsMods: [\n ...RADIUS_DIRECTIONAL_MODS,\n 'round',\n 'ellipse',\n 'leaf',\n 'backleaf',\n ],\n },\n\n padding: { acceptsColor: false, acceptsMods: BOX_DIRECTIONAL_MODS },\n paddingInline: VALUE_ONLY,\n paddingBlock: VALUE_ONLY,\n margin: { acceptsColor: false, acceptsMods: BOX_DIRECTIONAL_MODS },\n fade: { acceptsColor: true, acceptsMods: DIRECTIONAL_MODS },\n inset: { acceptsColor: false, acceptsMods: INSET_MODS },\n\n width: { acceptsColor: false, acceptsMods: DIMENSION_MODS },\n height: { acceptsColor: false, acceptsMods: DIMENSION_MODS },\n\n gap: VALUE_ONLY,\n columnGap: VALUE_ONLY,\n rowGap: VALUE_ONLY,\n flexBasis: VALUE_ONLY,\n flexGrow: VALUE_ONLY,\n flexShrink: VALUE_ONLY,\n flex: VALUE_ONLY,\n order: VALUE_ONLY,\n zIndex: VALUE_ONLY,\n opacity: VALUE_ONLY,\n aspectRatio: VALUE_ONLY,\n lineClamp: VALUE_ONLY,\n tabSize: VALUE_ONLY,\n\n flow: { acceptsColor: false, acceptsMods: FLOW_MODS },\n display: {\n acceptsColor: false,\n acceptsMods: [\n 'block',\n 'inline',\n 'inline-block',\n 'flex',\n 'inline-flex',\n 'grid',\n 'inline-grid',\n 'none',\n 'contents',\n 'table',\n 'table-row',\n 'table-cell',\n 'list-item',\n ],\n },\n overflow: { acceptsColor: false, acceptsMods: OVERFLOW_MODS },\n position: { acceptsColor: false, acceptsMods: POSITION_MODS },\n};\n\n/**\n * Get expectations for a property. Properties not in the map\n * are treated as passthrough (accept everything).\n */\nexport function getExpectation(property: string): PropertyExpectation {\n return PROPERTY_EXPECTATIONS[property] ?? PASSTHROUGH;\n}\n"],"mappings":";AAsBA,MAAM,mBAAmB;CAAC;CAAO;CAAS;CAAU;CAAO;AAI3D,MAAM,eAAe;AAGrB,MAAM,aAAa;CAAC,GAAG;CAAkB;CAAQ;CAAa;AAC9D,MAAM,uBAAuB,CAAC,GAAG,kBAAkB,aAAa;AAChE,MAAM,0BAA0B;CAC9B,GAAG;CACH;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,oBAAoB;CACxB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,iBAAiB;CAAC;CAAO;CAAO;CAAQ;AAC9C,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,gBAAgB;CACpB;CACA;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,gBAAgB;CAAC;CAAU;CAAY;CAAY;CAAS;CAAS;AAE3E,MAAM,aAAkC;CACtC,cAAc;CACd,aAAa;CACd;AAED,MAAM,aAAkC;CACtC,cAAc;CACd,aAAa;CACd;AAED,MAAM,cAAmC;CACvC,cAAc;CACd,aAAa;CACd;AAED,MAAa,wBAA6D;CACxE,MAAM;EAAE,cAAc;EAAM,aAAa,CAAC,QAAQ,cAAc;EAAE;CAClE,OAAO;EAAE,cAAc;EAAM,aAAa,CAAC,QAAQ,cAAc;EAAE;CACnE,YAAY;CACZ,aAAa;CACb,QAAQ;EAAE,cAAc;EAAM,aAAa,CAAC,QAAQ;EAAE;CAEtD,QAAQ;EACN,cAAc;EACd,aAAa,CAAC,GAAG,sBAAsB,GAAG,kBAAkB;EAC7D;CACD,SAAS;EACP,cAAc;EACd,aAAa;EACd;CAED,QAAQ;EACN,cAAc;EACd,aAAa;GACX,GAAG;GACH;GACA;GACA;GACA;GACD;EACF;CAED,SAAS;EAAE,cAAc;EAAO,aAAa;EAAsB;CACnE,eAAe;CACf,cAAc;CACd,QAAQ;EAAE,cAAc;EAAO,aAAa;EAAsB;CAClE,MAAM;EAAE,cAAc;EAAM,aAAa;EAAkB;CAC3D,OAAO;EAAE,cAAc;EAAO,aAAa;EAAY;CAEvD,OAAO;EAAE,cAAc;EAAO,aAAa;EAAgB;CAC3D,QAAQ;EAAE,cAAc;EAAO,aAAa;EAAgB;CAE5D,KAAK;CACL,WAAW;CACX,QAAQ;CACR,WAAW;CACX,UAAU;CACV,YAAY;CACZ,MAAM;CACN,OAAO;CACP,QAAQ;CACR,SAAS;CACT,aAAa;CACb,WAAW;CACX,SAAS;CAET,MAAM;EAAE,cAAc;EAAO,aAAa;EAAW;CACrD,SAAS;EACP,cAAc;EACd,aAAa;GACX;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACD;EACF;CACD,UAAU;EAAE,cAAc;EAAO,aAAa;EAAe;CAC7D,UAAU;EAAE,cAAc;EAAO,aAAa;EAAe;CAC9D;;;;;AAMD,SAAgB,eAAe,UAAuC;AACpE,QAAO,sBAAsB,aAAa"}
|
|
@@ -115,7 +115,7 @@ var consistent_token_usage_default = createRule({
|
|
|
115
115
|
checkValue(key, str, prop.value);
|
|
116
116
|
continue;
|
|
117
117
|
}
|
|
118
|
-
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) {
|
|
119
119
|
if (stateProp.type !== "Property") continue;
|
|
120
120
|
const stateStr = getStringValue(stateProp.value);
|
|
121
121
|
if (stateStr) checkValue(key, stateStr, stateProp.value);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consistent-token-usage.js","names":[],"sources":["../../src/rules/consistent-token-usage.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 { replaceStringValue, replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'preferToken' | 'replaceWithToken';\n\nconst PX_TO_UNIT: Record<string, string> = {\n '8px': '1x',\n '16px': '2x',\n '24px': '3x',\n '32px': '4x',\n '40px': '5x',\n '48px': '6x',\n '56px': '7x',\n '64px': '8x',\n};\n\nexport default createRule<[], MessageIds>({\n name: 'consistent-token-usage',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Suggest using design tokens and custom units instead of raw CSS values',\n },\n messages: {\n preferToken: \"Consider using '{{suggestion}}' instead of '{{raw}}'.\",\n replaceWithToken: \"Replace '{{raw}}' with '{{suggestion}}'\",\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 trimmed = value.trim();\n\n // Check pixel values that map to gap multiples\n if (trimmed in PX_TO_UNIT) {\n const suggestion = PX_TO_UNIT[trimmed];\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion, raw: trimmed },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: trimmed, suggestion },\n fix(fixer) {\n return replaceStringValue(fixer, node, suggestion);\n },\n },\n ],\n });\n return;\n }\n\n // Check 6px in radius context\n if (property === 'radius' && trimmed === '6px') {\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion: '1r', raw: '6px' },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: '6px', suggestion: '1r' },\n fix(fixer) {\n return replaceStringValue(fixer, node, '1r');\n },\n },\n ],\n });\n return;\n }\n\n // Check 1px in border context\n if (property === 'border' && trimmed.includes('1px')) {\n const edits: { start: number; end: number; replacement: string }[] = [];\n const re = /\\b1px\\b/g;\n let match: RegExpExecArray | null;\n while ((match = re.exec(value)) !== null) {\n edits.push({\n start: match.index,\n end: match.index + match[0].length,\n replacement: '1bw',\n });\n }\n if (edits.length === 0) return;\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion: '1bw', raw: '1px' },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: '1px', suggestion: '1bw' },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n edits,\n context.sourceCode,\n );\n },\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 const str = getStringValue(prop.value);\n if (str) {\n checkValue(key, str, prop.value);\n continue;\n }\n\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(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":";;;;;;AAQA,MAAM,aAAqC;CACzC,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACT;AAED,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,0EACH;EACD,UAAU;GACR,aAAa;GACb,kBAAkB;GACnB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,UACA,OACA,MACM;GACN,MAAM,UAAU,MAAM,MAAM;AAG5B,OAAI,WAAW,YAAY;IACzB,MAAM,aAAa,WAAW;AAC9B,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE;MAAY,KAAK;MAAS;KAClC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAS;OAAY;MAClC,IAAI,OAAO;AACT,cAAO,mBAAmB,OAAO,MAAM,WAAW;;MAErD,CACF;KACF,CAAC;AACF;;AAIF,OAAI,aAAa,YAAY,YAAY,OAAO;AAC9C,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE,YAAY;MAAM,KAAK;MAAO;KACtC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAO,YAAY;OAAM;MACtC,IAAI,OAAO;AACT,cAAO,mBAAmB,OAAO,MAAM,KAAK;;MAE/C,CACF;KACF,CAAC;AACF;;AAIF,OAAI,aAAa,YAAY,QAAQ,SAAS,MAAM,EAAE;IACpD,MAAM,QAA+D,EAAE;IACvE,MAAM,KAAK;IACX,IAAI;AACJ,YAAQ,QAAQ,GAAG,KAAK,MAAM,MAAM,KAClC,OAAM,KAAK;KACT,OAAO,MAAM;KACb,KAAK,MAAM,QAAQ,MAAM,GAAG;KAC5B,aAAa;KACd,CAAC;AAEJ,QAAI,MAAM,WAAW,EAAG;AACxB,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE,YAAY;MAAO,KAAK;MAAO;KACvC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAO,YAAY;OAAO;MACvC,IAAI,OAAO;AACT,cAAO,qBACL,OACA,MACA,OACA,QAAQ,WACT;;MAEJ,CACF;KACF,CAAC;;;EAIN,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;IAElB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,gBAAW,KAAK,KAAK,KAAK,MAAM;AAChC;;
|
|
1
|
+
{"version":3,"file":"consistent-token-usage.js","names":[],"sources":["../../src/rules/consistent-token-usage.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 { replaceStringValue, replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'preferToken' | 'replaceWithToken';\n\nconst PX_TO_UNIT: Record<string, string> = {\n '8px': '1x',\n '16px': '2x',\n '24px': '3x',\n '32px': '4x',\n '40px': '5x',\n '48px': '6x',\n '56px': '7x',\n '64px': '8x',\n};\n\nexport default createRule<[], MessageIds>({\n name: 'consistent-token-usage',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Suggest using design tokens and custom units instead of raw CSS values',\n },\n messages: {\n preferToken: \"Consider using '{{suggestion}}' instead of '{{raw}}'.\",\n replaceWithToken: \"Replace '{{raw}}' with '{{suggestion}}'\",\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 trimmed = value.trim();\n\n // Check pixel values that map to gap multiples\n if (trimmed in PX_TO_UNIT) {\n const suggestion = PX_TO_UNIT[trimmed];\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion, raw: trimmed },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: trimmed, suggestion },\n fix(fixer) {\n return replaceStringValue(fixer, node, suggestion);\n },\n },\n ],\n });\n return;\n }\n\n // Check 6px in radius context\n if (property === 'radius' && trimmed === '6px') {\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion: '1r', raw: '6px' },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: '6px', suggestion: '1r' },\n fix(fixer) {\n return replaceStringValue(fixer, node, '1r');\n },\n },\n ],\n });\n return;\n }\n\n // Check 1px in border context\n if (property === 'border' && trimmed.includes('1px')) {\n const edits: { start: number; end: number; replacement: string }[] = [];\n const re = /\\b1px\\b/g;\n let match: RegExpExecArray | null;\n while ((match = re.exec(value)) !== null) {\n edits.push({\n start: match.index,\n end: match.index + match[0].length,\n replacement: '1bw',\n });\n }\n if (edits.length === 0) return;\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion: '1bw', raw: '1px' },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: '1px', suggestion: '1bw' },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n edits,\n context.sourceCode,\n );\n },\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 const str = getStringValue(prop.value);\n if (str) {\n checkValue(key, str, prop.value);\n continue;\n }\n\n // Only recurse into genuine state maps. Sub-element objects\n // (`Icon: { … }`) are style objects in their own right and are visited\n // separately by the listener, so treating them as state maps here both\n // double-reports and attributes the value to the wrong property.\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":";;;;;;AAQA,MAAM,aAAqC;CACzC,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACT;AAED,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,0EACH;EACD,UAAU;GACR,aAAa;GACb,kBAAkB;GACnB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,UACA,OACA,MACM;GACN,MAAM,UAAU,MAAM,MAAM;AAG5B,OAAI,WAAW,YAAY;IACzB,MAAM,aAAa,WAAW;AAC9B,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE;MAAY,KAAK;MAAS;KAClC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAS;OAAY;MAClC,IAAI,OAAO;AACT,cAAO,mBAAmB,OAAO,MAAM,WAAW;;MAErD,CACF;KACF,CAAC;AACF;;AAIF,OAAI,aAAa,YAAY,YAAY,OAAO;AAC9C,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE,YAAY;MAAM,KAAK;MAAO;KACtC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAO,YAAY;OAAM;MACtC,IAAI,OAAO;AACT,cAAO,mBAAmB,OAAO,MAAM,KAAK;;MAE/C,CACF;KACF,CAAC;AACF;;AAIF,OAAI,aAAa,YAAY,QAAQ,SAAS,MAAM,EAAE;IACpD,MAAM,QAA+D,EAAE;IACvE,MAAM,KAAK;IACX,IAAI;AACJ,YAAQ,QAAQ,GAAG,KAAK,MAAM,MAAM,KAClC,OAAM,KAAK;KACT,OAAO,MAAM;KACb,KAAK,MAAM,QAAQ,MAAM,GAAG;KAC5B,aAAa;KACd,CAAC;AAEJ,QAAI,MAAM,WAAW,EAAG;AACxB,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE,YAAY;MAAO,KAAK;MAAO;KACvC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAO,YAAY;OAAO;MACvC,IAAI,OAAO;AACT,cAAO,qBACL,OACA,MACA,OACA,QAAQ,WACT;;MAEJ,CACF;KACF,CAAC;;;EAIN,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;IAElB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,gBAAW,KAAK,KAAK,KAAK,MAAM;AAChC;;AAOF,QACE,KAAK,MAAM,SAAS,sBACpB,IAAI,WAAW,KAAK,OAAO,KAAK,CAEhC,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SACF,YAAW,KAAK,UAAU,UAAU,MAAM;;;;AAOpD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -34,7 +34,7 @@ var no_important_default = createRule({
|
|
|
34
34
|
for (const prop of node.properties) {
|
|
35
35
|
if (prop.type !== "Property") continue;
|
|
36
36
|
checkNode(prop.value);
|
|
37
|
-
if (prop.value.type === "ObjectExpression") {
|
|
37
|
+
if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) {
|
|
38
38
|
for (const stateProp of prop.value.properties) if (stateProp.type === "Property") checkNode(stateProp.value);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-important.js","names":[],"sources":["../../src/rules/no-important.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue } from '../utils.js';\nimport { replaceStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'noImportant';\n\nexport default createRule<[], MessageIds>({\n name: 'no-important',\n meta: {\n type: 'problem',\n fixable: 'code',\n docs: {\n description: 'Disallow !important in tasty style values',\n },\n messages: {\n noImportant:\n 'Do not use !important in tasty styles. The tasty system manages specificity via doubled selectors and state ordering.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkNode(node: TSESTree.Node): void {\n const str = getStringValue(node);\n if (str && str.includes('!important')) {\n const stripped = str.replace(/\\s*!important\\s*/g, '').trim();\n context.report({\n node,\n messageId: 'noImportant',\n fix(fixer) {\n return replaceStringValue(fixer, node, stripped);\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\n checkNode(prop.value);\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type === 'Property') {\n checkNode(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":";;;;;;AAQA,2BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aAAa,6CACd;EACD,UAAU,EACR,aACE,yHACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,UAAU,MAA2B;GAC5C,MAAM,MAAM,eAAe,KAAK;AAChC,OAAI,OAAO,IAAI,SAAS,aAAa,EAAE;IACrC,MAAM,WAAW,IAAI,QAAQ,qBAAqB,GAAG,CAAC,MAAM;AAC5D,YAAQ,OAAO;KACb;KACA,WAAW;KACX,IAAI,OAAO;AACT,aAAO,mBAAmB,OAAO,MAAM,SAAS;;KAEnD,CAAC;;;EAIN,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;AAE9B,cAAU,KAAK,MAAM;
|
|
1
|
+
{"version":3,"file":"no-important.js","names":[],"sources":["../../src/rules/no-important.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue } from '../utils.js';\nimport { replaceStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'noImportant';\n\nexport default createRule<[], MessageIds>({\n name: 'no-important',\n meta: {\n type: 'problem',\n fixable: 'code',\n docs: {\n description: 'Disallow !important in tasty style values',\n },\n messages: {\n noImportant:\n 'Do not use !important in tasty styles. The tasty system manages specificity via doubled selectors and state ordering.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkNode(node: TSESTree.Node): void {\n const str = getStringValue(node);\n if (str && str.includes('!important')) {\n const stripped = str.replace(/\\s*!important\\s*/g, '').trim();\n context.report({\n node,\n messageId: 'noImportant',\n fix(fixer) {\n return replaceStringValue(fixer, node, stripped);\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\n checkNode(prop.value);\n\n // Only recurse into genuine state maps. Sub-element objects\n // (`Icon: { … }`) are style objects in their own right and are visited\n // separately by the listener, so recursing here reports them twice.\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') {\n checkNode(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":";;;;;;AAQA,2BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aAAa,6CACd;EACD,UAAU,EACR,aACE,yHACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,UAAU,MAA2B;GAC5C,MAAM,MAAM,eAAe,KAAK;AAChC,OAAI,OAAO,IAAI,SAAS,aAAa,EAAE;IACrC,MAAM,WAAW,IAAI,QAAQ,qBAAqB,GAAG,CAAC,MAAM;AAC5D,YAAQ,OAAO;KACb;KACA,WAAW;KACX,IAAI,OAAO;AACT,aAAO,mBAAmB,OAAO,MAAM,SAAS;;KAEnD,CAAC;;;EAIN,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;AAE9B,cAAU,KAAK,MAAM;AAKrB,QACE,KAAK,MAAM,SAAS,sBACpB,IAAI,WAAW,KAAK,OAAO,KAAK,EAEhC;UAAK,MAAM,aAAa,KAAK,MAAM,WACjC,KAAI,UAAU,SAAS,WACrB,WAAU,UAAU,MAAM;;;;AAOpC,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -80,7 +80,7 @@ var no_raw_color_values_default = createRule({
|
|
|
80
80
|
const key = getKeyName(prop.key);
|
|
81
81
|
const str = getStringValue(prop.value);
|
|
82
82
|
if (str) checkValue(str, prop.value, key);
|
|
83
|
-
if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
|
|
83
|
+
if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) for (const stateProp of prop.value.properties) {
|
|
84
84
|
if (stateProp.type !== "Property") continue;
|
|
85
85
|
const stateStr = getStringValue(stateProp.value);
|
|
86
86
|
if (stateStr) checkValue(stateStr, stateProp.value, key);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-raw-color-values.js","names":[],"sources":["../../src/rules/no-raw-color-values.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue, getKeyName } from '../utils.js';\nimport { COLOR_BEARING_PROPERTIES, NAMED_CSS_COLORS } from '../constants.js';\n\ntype MessageIds = 'rawHexColor' | 'rawColorFunction' | 'rawNamedColor';\n\nconst HEX_COLOR_REGEX = /#([0-9a-fA-F]{3,8})\\b/g;\nconst COLOR_FUNC_REGEX =\n /\\b(rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|okhsl|okhsv|okhst|color|device-cmyk|light-dark)\\s*\\(/gi;\nconst NAMED_COLOR_REGEX = new RegExp(\n `\\\\b(${[...NAMED_CSS_COLORS].sort((a, b) => b.length - a.length).join('|')})\\\\b`,\n 'gi',\n);\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-color-values',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Suggest using color tokens instead of raw hex/rgb/okhsl/named colors',\n },\n messages: {\n rawHexColor: \"Use a color token instead of raw hex color '{{value}}'.\",\n rawColorFunction: 'Use a color token instead of raw {{func}}() color.',\n rawNamedColor: \"Use a color token instead of raw named color '{{name}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function isInTokenDefinition(node: TSESTree.Node): boolean {\n // Check if this is inside a :root or token-defining context\n let current: TSESTree.Node | undefined = node;\n while (current) {\n if (current.type === 'CallExpression') {\n const imp = ctx.isTastyCall(current);\n if (imp && imp.importedName === 'tastyStatic') {\n const firstArg = current.arguments[0];\n const selectorStr = getStringValue(firstArg);\n if (selectorStr === ':root') return true;\n }\n break;\n }\n current = current.parent;\n }\n return false;\n }\n\n function checkValue(\n value: string,\n node: TSESTree.Node,\n propertyKey?: string | null,\n ): void {\n if (isInTokenDefinition(node)) return;\n\n // Check hex colors\n HEX_COLOR_REGEX.lastIndex = 0;\n let match;\n while ((match = HEX_COLOR_REGEX.exec(value)) !== null) {\n const hex = match[1];\n if ([3, 4, 6, 8].includes(hex.length)) {\n context.report({\n node,\n messageId: 'rawHexColor',\n data: { value: match[0] },\n });\n }\n }\n\n // Check color functions (rgb, hsl, okhsl, okhst, oklch, …)\n COLOR_FUNC_REGEX.lastIndex = 0;\n while ((match = COLOR_FUNC_REGEX.exec(value)) !== null) {\n context.report({\n node,\n messageId: 'rawColorFunction',\n data: { func: match[1] },\n });\n }\n\n // Check named CSS colors — scoped to color-bearing properties to avoid\n // false positives on string properties like `content` or `cursor`. Skip\n // matches that are part of a `#token` or `$prop` reference (e.g. `#purple`,\n // `$red`) — those are Tasty tokens/custom properties, not raw colors.\n if (propertyKey && COLOR_BEARING_PROPERTIES.has(propertyKey)) {\n NAMED_COLOR_REGEX.lastIndex = 0;\n while ((match = NAMED_COLOR_REGEX.exec(value)) !== null) {\n const prev = match.index > 0 ? value[match.index - 1] : '';\n if (prev === '#' || prev === '$') continue;\n context.report({\n node,\n messageId: 'rawNamedColor',\n data: { name: match[1] },\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 const key = getKeyName(prop.key);\n\n const str = getStringValue(prop.value);\n if (str) checkValue(str, prop.value, key);\n\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) checkValue(stateStr, stateProp.value, key);\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,MAAM,kBAAkB;AACxB,MAAM,mBACJ;AACF,MAAM,oBAAoB,IAAI,OAC5B,OAAO,CAAC,GAAG,iBAAiB,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,OAC3E,KACD;AAED,kCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,wEACH;EACD,UAAU;GACR,aAAa;GACb,kBAAkB;GAClB,eAAe;GAChB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,oBAAoB,MAA8B;GAEzD,IAAI,UAAqC;AACzC,UAAO,SAAS;AACd,QAAI,QAAQ,SAAS,kBAAkB;KACrC,MAAM,MAAM,IAAI,YAAY,QAAQ;AACpC,SAAI,OAAO,IAAI,iBAAiB,eAAe;MAC7C,MAAM,WAAW,QAAQ,UAAU;AAEnC,UADoB,eAAe,SAAS,KACxB,QAAS,QAAO;;AAEtC;;AAEF,cAAU,QAAQ;;AAEpB,UAAO;;EAGT,SAAS,WACP,OACA,MACA,aACM;AACN,OAAI,oBAAoB,KAAK,CAAE;AAG/B,mBAAgB,YAAY;GAC5B,IAAI;AACJ,WAAQ,QAAQ,gBAAgB,KAAK,MAAM,MAAM,MAAM;IACrD,MAAM,MAAM,MAAM;AAClB,QAAI;KAAC;KAAG;KAAG;KAAG;KAAE,CAAC,SAAS,IAAI,OAAO,CACnC,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,OAAO,MAAM,IAAI;KAC1B,CAAC;;AAKN,oBAAiB,YAAY;AAC7B,WAAQ,QAAQ,iBAAiB,KAAK,MAAM,MAAM,KAChD,SAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM,EAAE,MAAM,MAAM,IAAI;IACzB,CAAC;AAOJ,OAAI,eAAe,yBAAyB,IAAI,YAAY,EAAE;AAC5D,sBAAkB,YAAY;AAC9B,YAAQ,QAAQ,kBAAkB,KAAK,MAAM,MAAM,MAAM;KACvD,MAAM,OAAO,MAAM,QAAQ,IAAI,MAAM,MAAM,QAAQ,KAAK;AACxD,SAAI,SAAS,OAAO,SAAS,IAAK;AAClC,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,MAAM,MAAM,IAAI;MACzB,CAAC;;;;EAKR,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;IAC9B,MAAM,MAAM,WAAW,KAAK,IAAI;IAEhC,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,IAAK,YAAW,KAAK,KAAK,OAAO,IAAI;
|
|
1
|
+
{"version":3,"file":"no-raw-color-values.js","names":[],"sources":["../../src/rules/no-raw-color-values.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue, getKeyName } from '../utils.js';\nimport { COLOR_BEARING_PROPERTIES, NAMED_CSS_COLORS } from '../constants.js';\n\ntype MessageIds = 'rawHexColor' | 'rawColorFunction' | 'rawNamedColor';\n\nconst HEX_COLOR_REGEX = /#([0-9a-fA-F]{3,8})\\b/g;\nconst COLOR_FUNC_REGEX =\n /\\b(rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|okhsl|okhsv|okhst|color|device-cmyk|light-dark)\\s*\\(/gi;\nconst NAMED_COLOR_REGEX = new RegExp(\n `\\\\b(${[...NAMED_CSS_COLORS].sort((a, b) => b.length - a.length).join('|')})\\\\b`,\n 'gi',\n);\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-color-values',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Suggest using color tokens instead of raw hex/rgb/okhsl/named colors',\n },\n messages: {\n rawHexColor: \"Use a color token instead of raw hex color '{{value}}'.\",\n rawColorFunction: 'Use a color token instead of raw {{func}}() color.',\n rawNamedColor: \"Use a color token instead of raw named color '{{name}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function isInTokenDefinition(node: TSESTree.Node): boolean {\n // Check if this is inside a :root or token-defining context\n let current: TSESTree.Node | undefined = node;\n while (current) {\n if (current.type === 'CallExpression') {\n const imp = ctx.isTastyCall(current);\n if (imp && imp.importedName === 'tastyStatic') {\n const firstArg = current.arguments[0];\n const selectorStr = getStringValue(firstArg);\n if (selectorStr === ':root') return true;\n }\n break;\n }\n current = current.parent;\n }\n return false;\n }\n\n function checkValue(\n value: string,\n node: TSESTree.Node,\n propertyKey?: string | null,\n ): void {\n if (isInTokenDefinition(node)) return;\n\n // Check hex colors\n HEX_COLOR_REGEX.lastIndex = 0;\n let match;\n while ((match = HEX_COLOR_REGEX.exec(value)) !== null) {\n const hex = match[1];\n if ([3, 4, 6, 8].includes(hex.length)) {\n context.report({\n node,\n messageId: 'rawHexColor',\n data: { value: match[0] },\n });\n }\n }\n\n // Check color functions (rgb, hsl, okhsl, okhst, oklch, …)\n COLOR_FUNC_REGEX.lastIndex = 0;\n while ((match = COLOR_FUNC_REGEX.exec(value)) !== null) {\n context.report({\n node,\n messageId: 'rawColorFunction',\n data: { func: match[1] },\n });\n }\n\n // Check named CSS colors — scoped to color-bearing properties to avoid\n // false positives on string properties like `content` or `cursor`. Skip\n // matches that are part of a `#token` or `$prop` reference (e.g. `#purple`,\n // `$red`) — those are Tasty tokens/custom properties, not raw colors.\n if (propertyKey && COLOR_BEARING_PROPERTIES.has(propertyKey)) {\n NAMED_COLOR_REGEX.lastIndex = 0;\n while ((match = NAMED_COLOR_REGEX.exec(value)) !== null) {\n const prev = match.index > 0 ? value[match.index - 1] : '';\n if (prev === '#' || prev === '$') continue;\n context.report({\n node,\n messageId: 'rawNamedColor',\n data: { name: match[1] },\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 const key = getKeyName(prop.key);\n\n const str = getStringValue(prop.value);\n if (str) checkValue(str, prop.value, key);\n\n // Only recurse into genuine state maps. Sub-element objects\n // (`Icon: { … }`) are style objects in their own right and are visited\n // separately by the listener, so treating them as state maps here both\n // double-reports and passes the sub-element name as the property.\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) checkValue(stateStr, stateProp.value, key);\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,MAAM,kBAAkB;AACxB,MAAM,mBACJ;AACF,MAAM,oBAAoB,IAAI,OAC5B,OAAO,CAAC,GAAG,iBAAiB,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,OAC3E,KACD;AAED,kCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,wEACH;EACD,UAAU;GACR,aAAa;GACb,kBAAkB;GAClB,eAAe;GAChB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,oBAAoB,MAA8B;GAEzD,IAAI,UAAqC;AACzC,UAAO,SAAS;AACd,QAAI,QAAQ,SAAS,kBAAkB;KACrC,MAAM,MAAM,IAAI,YAAY,QAAQ;AACpC,SAAI,OAAO,IAAI,iBAAiB,eAAe;MAC7C,MAAM,WAAW,QAAQ,UAAU;AAEnC,UADoB,eAAe,SAAS,KACxB,QAAS,QAAO;;AAEtC;;AAEF,cAAU,QAAQ;;AAEpB,UAAO;;EAGT,SAAS,WACP,OACA,MACA,aACM;AACN,OAAI,oBAAoB,KAAK,CAAE;AAG/B,mBAAgB,YAAY;GAC5B,IAAI;AACJ,WAAQ,QAAQ,gBAAgB,KAAK,MAAM,MAAM,MAAM;IACrD,MAAM,MAAM,MAAM;AAClB,QAAI;KAAC;KAAG;KAAG;KAAG;KAAE,CAAC,SAAS,IAAI,OAAO,CACnC,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,OAAO,MAAM,IAAI;KAC1B,CAAC;;AAKN,oBAAiB,YAAY;AAC7B,WAAQ,QAAQ,iBAAiB,KAAK,MAAM,MAAM,KAChD,SAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM,EAAE,MAAM,MAAM,IAAI;IACzB,CAAC;AAOJ,OAAI,eAAe,yBAAyB,IAAI,YAAY,EAAE;AAC5D,sBAAkB,YAAY;AAC9B,YAAQ,QAAQ,kBAAkB,KAAK,MAAM,MAAM,MAAM;KACvD,MAAM,OAAO,MAAM,QAAQ,IAAI,MAAM,MAAM,QAAQ,KAAK;AACxD,SAAI,SAAS,OAAO,SAAS,IAAK;AAClC,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,MAAM,MAAM,IAAI;MACzB,CAAC;;;;EAKR,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;IAC9B,MAAM,MAAM,WAAW,KAAK,IAAI;IAEhC,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,IAAK,YAAW,KAAK,KAAK,OAAO,IAAI;AAMzC,QACE,KAAK,MAAM,SAAS,sBACpB,IAAI,WAAW,KAAK,OAAO,KAAK,CAEhC,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SAAU,YAAW,UAAU,UAAU,OAAO,IAAI;;;;AAMhE,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
2
|
import { SHORTHAND_MAPPING } from "../constants.js";
|
|
3
|
-
import { getKeyName } from "../utils.js";
|
|
3
|
+
import { getKeyName, getStringValue } from "../utils.js";
|
|
4
4
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
5
5
|
|
|
6
6
|
//#region src/rules/prefer-shorthand-property.ts
|
|
7
|
+
const CSS_WIDE_KEYWORDS = new Set([
|
|
8
|
+
"inherit",
|
|
9
|
+
"initial",
|
|
10
|
+
"unset",
|
|
11
|
+
"revert",
|
|
12
|
+
"revert-layer"
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* `font` resolves to `<value>, var(--font-sans, var(--font-sans-fallback))`,
|
|
16
|
+
* so it cannot express a CSS-wide keyword — `font: 'inherit'` emits
|
|
17
|
+
* `font-family: inherit, …`, which is invalid and drops the inherit.
|
|
18
|
+
*
|
|
19
|
+
* `preset` is the property that handles these: a CSS-wide keyword used as the
|
|
20
|
+
* preset name short-circuits token lookup and is emitted verbatim across the
|
|
21
|
+
* whole typography group, so `preset: 'inherit'` yields a real
|
|
22
|
+
* `font-family: inherit`. Point at that instead of `font`.
|
|
23
|
+
*/
|
|
24
|
+
function shorthandHint(key, prop) {
|
|
25
|
+
const mapping = SHORTHAND_MAPPING[key];
|
|
26
|
+
if (!mapping) return null;
|
|
27
|
+
if (key !== "fontFamily") return mapping.hint;
|
|
28
|
+
const value = getStringValue(prop.value)?.trim().toLowerCase();
|
|
29
|
+
return value && CSS_WIDE_KEYWORDS.has(value) ? `preset: '${value}'` : mapping.hint;
|
|
30
|
+
}
|
|
7
31
|
var prefer_shorthand_property_default = createRule({
|
|
8
32
|
name: "prefer-shorthand-property",
|
|
9
33
|
meta: {
|
|
@@ -23,12 +47,13 @@ var prefer_shorthand_property_default = createRule({
|
|
|
23
47
|
const key = getKeyName(prop.key);
|
|
24
48
|
if (key === null) continue;
|
|
25
49
|
const mapping = SHORTHAND_MAPPING[key];
|
|
26
|
-
|
|
50
|
+
const hint = shorthandHint(key, prop);
|
|
51
|
+
if (mapping && hint) context.report({
|
|
27
52
|
node: prop.key,
|
|
28
53
|
messageId: "preferShorthand",
|
|
29
54
|
data: {
|
|
30
55
|
native: key,
|
|
31
|
-
alternative:
|
|
56
|
+
alternative: hint
|
|
32
57
|
},
|
|
33
58
|
fix(fixer) {
|
|
34
59
|
if (!mapping.safeFix) return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-shorthand-property.js","names":[],"sources":["../../src/rules/prefer-shorthand-property.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName } from '../utils.js';\nimport { SHORTHAND_MAPPING } from '../constants.js';\n\ntype MessageIds = 'preferShorthand';\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-shorthand-property',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Suggest tasty shorthand when a native CSS property with a tasty alternative is used',\n },\n messages: {\n preferShorthand:\n \"Prefer tasty shorthand '{{alternative}}' instead of '{{native}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n const mapping = SHORTHAND_MAPPING[key];\n if (mapping) {\n context.report({\n node: prop.key,\n messageId: 'preferShorthand',\n data: { native: key, alternative:
|
|
1
|
+
{"version":3,"file":"prefer-shorthand-property.js","names":[],"sources":["../../src/rules/prefer-shorthand-property.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 { SHORTHAND_MAPPING } from '../constants.js';\n\ntype MessageIds = 'preferShorthand';\n\nconst CSS_WIDE_KEYWORDS = new Set([\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n]);\n\n/**\n * `font` resolves to `<value>, var(--font-sans, var(--font-sans-fallback))`,\n * so it cannot express a CSS-wide keyword — `font: 'inherit'` emits\n * `font-family: inherit, …`, which is invalid and drops the inherit.\n *\n * `preset` is the property that handles these: a CSS-wide keyword used as the\n * preset name short-circuits token lookup and is emitted verbatim across the\n * whole typography group, so `preset: 'inherit'` yields a real\n * `font-family: inherit`. Point at that instead of `font`.\n */\nfunction shorthandHint(key: string, prop: TSESTree.Property): string | null {\n const mapping = SHORTHAND_MAPPING[key];\n if (!mapping) return null;\n if (key !== 'fontFamily') return mapping.hint;\n\n const value = getStringValue(prop.value)?.trim().toLowerCase();\n return value && CSS_WIDE_KEYWORDS.has(value)\n ? `preset: '${value}'`\n : mapping.hint;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-shorthand-property',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Suggest tasty shorthand when a native CSS property with a tasty alternative is used',\n },\n messages: {\n preferShorthand:\n \"Prefer tasty shorthand '{{alternative}}' instead of '{{native}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n const mapping = SHORTHAND_MAPPING[key];\n const hint = shorthandHint(key, prop);\n if (mapping && hint) {\n context.report({\n node: prop.key,\n messageId: 'preferShorthand',\n data: { native: key, alternative: hint },\n fix(fixer) {\n // Only auto-fix the carry-over subset where the value passes\n // through unchanged (e.g. backgroundColor → fill, borderRadius →\n // radius). Directional / min/max / border-* renames change\n // semantics and stay report-only.\n if (!mapping.safeFix) return null;\n return fixer.replaceText(prop.key, mapping.property);\n },\n });\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,MAAM,oBAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;;;;AAYF,SAAS,cAAc,KAAa,MAAwC;CAC1E,MAAM,UAAU,kBAAkB;AAClC,KAAI,CAAC,QAAS,QAAO;AACrB,KAAI,QAAQ,aAAc,QAAO,QAAQ;CAEzC,MAAM,QAAQ,eAAe,KAAK,MAAM,EAAE,MAAM,CAAC,aAAa;AAC9D,QAAO,SAAS,kBAAkB,IAAI,MAAM,GACxC,YAAY,MAAM,KAClB,QAAQ;;AAGd,wCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,uFACH;EACD,UAAU,EACR,iBACE,qEACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;IAElB,MAAM,UAAU,kBAAkB;IAClC,MAAM,OAAO,cAAc,KAAK,KAAK;AACrC,QAAI,WAAW,KACb,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM;MAAE,QAAQ;MAAK,aAAa;MAAM;KACxC,IAAI,OAAO;AAKT,UAAI,CAAC,QAAQ,QAAS,QAAO;AAC7B,aAAO,MAAM,YAAY,KAAK,KAAK,QAAQ,SAAS;;KAEvD,CAAC;;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -5,6 +5,15 @@ import { TastyContext, styleObjectListeners } from "../context.js";
|
|
|
5
5
|
import { replaceInStringValue } from "../fix-utils.js";
|
|
6
6
|
|
|
7
7
|
//#region src/rules/valid-transition.ts
|
|
8
|
+
/**
|
|
9
|
+
* `KNOWN_CSS_PROPERTIES` is keyed by camelCase style keys, but a `transition`
|
|
10
|
+
* value names *CSS* properties, which are kebab-case (`text-decoration-color`).
|
|
11
|
+
* Tasty emits an unmapped name verbatim into the CSS `transition`, so the
|
|
12
|
+
* kebab form is the correct spelling — normalize before the lookup.
|
|
13
|
+
*/
|
|
14
|
+
function kebabToCamel(name) {
|
|
15
|
+
return name.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
16
|
+
}
|
|
8
17
|
var valid_transition_default = createRule({
|
|
9
18
|
name: "valid-transition",
|
|
10
19
|
meta: {
|
|
@@ -69,7 +78,7 @@ var valid_transition_default = createRule({
|
|
|
69
78
|
offset += group.length + 1;
|
|
70
79
|
continue;
|
|
71
80
|
}
|
|
72
|
-
if (!SEMANTIC_TRANSITIONS.has(name) && !KNOWN_CSS_PROPERTIES.has(name) && name !== "all" && name !== "none") context.report({
|
|
81
|
+
if (!SEMANTIC_TRANSITIONS.has(name) && !KNOWN_CSS_PROPERTIES.has(name) && !KNOWN_CSS_PROPERTIES.has(kebabToCamel(name)) && !name.startsWith("--") && name !== "all" && name !== "none") context.report({
|
|
73
82
|
node,
|
|
74
83
|
messageId: "unknownTransition",
|
|
75
84
|
data: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valid-transition.js","names":[],"sources":["../../src/rules/valid-transition.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 SEMANTIC_TRANSITIONS,\n KNOWN_CSS_PROPERTIES,\n TRANSITION_SEMANTIC_MAPPING,\n} from '../constants.js';\nimport { replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds =\n | 'unknownTransition'\n | 'preferSemanticTransition'\n | 'useSemantic';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-transition',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Validate transition property values use valid semantic transition names',\n },\n messages: {\n unknownTransition:\n \"Unknown transition name '{{name}}'. Use a semantic name ({{known}}) or a CSS property name.\",\n preferSemanticTransition:\n \"Transition '{{native}}' has a Tasty semantic equivalent — use '{{semantic}}' instead.\",\n useSemantic: \"Use '{{semantic}}' instead of '{{native}}'\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkTransitionValue(value: string, node: TSESTree.Node): void {\n const groups = value.split(',');\n let offset = 0;\n\n for (const group of groups) {\n const parts = group.trim().split(/\\s+/);\n if (parts.length === 0 || parts[0] === '') {\n offset += group.length + 1;\n continue;\n }\n\n const name = parts[0];\n\n // $$ prefix is always valid (custom property reference: $$name -> --name)\n if (name.startsWith('$$')) {\n offset += group.length + 1;\n continue;\n }\n\n // ## prefix is always valid (color property reference: ##name -> --name-color)\n if (name.startsWith('##')) {\n offset += group.length + 1;\n continue;\n }\n\n const semantic = TRANSITION_SEMANTIC_MAPPING[name];\n if (semantic) {\n const nameStartInGroup = group.indexOf(name);\n const start = offset + nameStartInGroup;\n const end = start + name.length;\n context.report({\n node,\n messageId: 'preferSemanticTransition',\n data: { native: name, semantic },\n suggest: [\n {\n messageId: 'useSemantic',\n data: { native: name, semantic },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: semantic }],\n context.sourceCode,\n );\n },\n },\n ],\n });\n offset += group.length + 1;\n continue;\n }\n\n if (\n !SEMANTIC_TRANSITIONS.has(name) &&\n !KNOWN_CSS_PROPERTIES.has(name) &&\n name !== 'all' &&\n name !== 'none'\n ) {\n context.report({\n node,\n messageId: 'unknownTransition',\n data: {\n name,\n known: [...SEMANTIC_TRANSITIONS].join(', '),\n },\n });\n }\n\n offset += group.length + 1;\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 !== 'transition') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkTransitionValue(str, prop.value);\n continue;\n }\n\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 checkTransitionValue(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":"
|
|
1
|
+
{"version":3,"file":"valid-transition.js","names":[],"sources":["../../src/rules/valid-transition.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 SEMANTIC_TRANSITIONS,\n KNOWN_CSS_PROPERTIES,\n TRANSITION_SEMANTIC_MAPPING,\n} from '../constants.js';\nimport { replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds =\n | 'unknownTransition'\n | 'preferSemanticTransition'\n | 'useSemantic';\n\n/**\n * `KNOWN_CSS_PROPERTIES` is keyed by camelCase style keys, but a `transition`\n * value names *CSS* properties, which are kebab-case (`text-decoration-color`).\n * Tasty emits an unmapped name verbatim into the CSS `transition`, so the\n * kebab form is the correct spelling — normalize before the lookup.\n */\nfunction kebabToCamel(name: string): string {\n return name.replace(/-([a-z])/g, (_, char: string) => char.toUpperCase());\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-transition',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Validate transition property values use valid semantic transition names',\n },\n messages: {\n unknownTransition:\n \"Unknown transition name '{{name}}'. Use a semantic name ({{known}}) or a CSS property name.\",\n preferSemanticTransition:\n \"Transition '{{native}}' has a Tasty semantic equivalent — use '{{semantic}}' instead.\",\n useSemantic: \"Use '{{semantic}}' instead of '{{native}}'\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkTransitionValue(value: string, node: TSESTree.Node): void {\n const groups = value.split(',');\n let offset = 0;\n\n for (const group of groups) {\n const parts = group.trim().split(/\\s+/);\n if (parts.length === 0 || parts[0] === '') {\n offset += group.length + 1;\n continue;\n }\n\n const name = parts[0];\n\n // $$ prefix is always valid (custom property reference: $$name -> --name)\n if (name.startsWith('$$')) {\n offset += group.length + 1;\n continue;\n }\n\n // ## prefix is always valid (color property reference: ##name -> --name-color)\n if (name.startsWith('##')) {\n offset += group.length + 1;\n continue;\n }\n\n const semantic = TRANSITION_SEMANTIC_MAPPING[name];\n if (semantic) {\n const nameStartInGroup = group.indexOf(name);\n const start = offset + nameStartInGroup;\n const end = start + name.length;\n context.report({\n node,\n messageId: 'preferSemanticTransition',\n data: { native: name, semantic },\n suggest: [\n {\n messageId: 'useSemantic',\n data: { native: name, semantic },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: semantic }],\n context.sourceCode,\n );\n },\n },\n ],\n });\n offset += group.length + 1;\n continue;\n }\n\n if (\n !SEMANTIC_TRANSITIONS.has(name) &&\n !KNOWN_CSS_PROPERTIES.has(name) &&\n !KNOWN_CSS_PROPERTIES.has(kebabToCamel(name)) &&\n !name.startsWith('--') &&\n name !== 'all' &&\n name !== 'none'\n ) {\n context.report({\n node,\n messageId: 'unknownTransition',\n data: {\n name,\n known: [...SEMANTIC_TRANSITIONS].join(', '),\n },\n });\n }\n\n offset += group.length + 1;\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 !== 'transition') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkTransitionValue(str, prop.value);\n continue;\n }\n\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 checkTransitionValue(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":";;;;;;;;;;;;;AAsBA,SAAS,aAAa,MAAsB;AAC1C,QAAO,KAAK,QAAQ,cAAc,GAAG,SAAiB,KAAK,aAAa,CAAC;;AAG3E,+BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,2EACH;EACD,UAAU;GACR,mBACE;GACF,0BACE;GACF,aAAa;GACd;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,qBAAqB,OAAe,MAA2B;GACtE,MAAM,SAAS,MAAM,MAAM,IAAI;GAC/B,IAAI,SAAS;AAEb,QAAK,MAAM,SAAS,QAAQ;IAC1B,MAAM,QAAQ,MAAM,MAAM,CAAC,MAAM,MAAM;AACvC,QAAI,MAAM,WAAW,KAAK,MAAM,OAAO,IAAI;AACzC,eAAU,MAAM,SAAS;AACzB;;IAGF,MAAM,OAAO,MAAM;AAGnB,QAAI,KAAK,WAAW,KAAK,EAAE;AACzB,eAAU,MAAM,SAAS;AACzB;;AAIF,QAAI,KAAK,WAAW,KAAK,EAAE;AACzB,eAAU,MAAM,SAAS;AACzB;;IAGF,MAAM,WAAW,4BAA4B;AAC7C,QAAI,UAAU;KACZ,MAAM,mBAAmB,MAAM,QAAQ,KAAK;KAC5C,MAAM,QAAQ,SAAS;KACvB,MAAM,MAAM,QAAQ,KAAK;AACzB,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OAAE,QAAQ;OAAM;OAAU;MAChC,SAAS,CACP;OACE,WAAW;OACX,MAAM;QAAE,QAAQ;QAAM;QAAU;OAChC,IAAI,OAAO;AACT,eAAO,qBACL,OACA,MACA,CAAC;SAAE;SAAO;SAAK,aAAa;SAAU,CAAC,EACvC,QAAQ,WACT;;OAEJ,CACF;MACF,CAAC;AACF,eAAU,MAAM,SAAS;AACzB;;AAGF,QACE,CAAC,qBAAqB,IAAI,KAAK,IAC/B,CAAC,qBAAqB,IAAI,KAAK,IAC/B,CAAC,qBAAqB,IAAI,aAAa,KAAK,CAAC,IAC7C,CAAC,KAAK,WAAW,KAAK,IACtB,SAAS,SACT,SAAS,OAET,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MACJ;MACA,OAAO,CAAC,GAAG,qBAAqB,CAAC,KAAK,KAAK;MAC5C;KACF,CAAC;AAGJ,cAAU,MAAM,SAAS;;;EAI7B,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;AAG/C,QADY,WAAW,KAAK,IAAI,KACpB,aAAc;IAE1B,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,0BAAqB,KAAK,KAAK,MAAM;AACrC;;AAGF,QAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SACF,sBAAqB,UAAU,UAAU,MAAM;;;;AAOzD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|