@tenphi/eslint-plugin-tasty 1.15.0 → 1.16.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
CHANGED
|
@@ -263,7 +263,7 @@ not matched.
|
|
|
263
263
|
| `tasty/no-classname-prop` | warn | `className` on recognized Tasty components; keep styling in Tasty or use `data-element` for sub-elements ([details](docs/rules/no-classname-prop.md)) |
|
|
264
264
|
| `tasty/no-styles-prop` | warn | Instance `styles` on recognized Tasty components; use `tokens`, `mods`, exposed props/variants, or `tasty(Component, { styles })` ([details](docs/rules/no-styles-prop.md)) |
|
|
265
265
|
| `tasty/no-protected-restyle` | warn | Restyling a design system's `protectedComponents`: instance `styles` and `*Styles`, appearance style props, and `tasty()` wrappers that restyle them, followed through aliases, wrappers and relative re-exports. Inactive without the config ([details](docs/rules/no-protected-restyle.md)) |
|
|
266
|
-
| `tasty/consistent-token-usage` | warn | Nonzero raw pixel lengths, including compounds, expressions and numeric inputs to enhanced length handlers |
|
|
266
|
+
| `tasty/consistent-token-usage` | warn | Nonzero raw pixel lengths, including compounds, expressions and numeric inputs to enhanced length handlers. The `pixels: 'replaceable'` option reports only lengths with an exact unit, such as `8px` → `1x` ([details](docs/rules/consistent-token-usage.md)) |
|
|
267
267
|
| `tasty/prefer-auto-calc` | warn | `calc(...)` instead of Tasty auto-calc `(...)` |
|
|
268
268
|
| `tasty/prefer-state-negation` | warn | Top-level `:not(...)` in state keys instead of the `!` prefix (autofixable; nested CSS selectors stay intact) |
|
|
269
269
|
| `tasty/prefer-custom-property-syntax` | warn | `var(--prop)` / `$x-color` / `transparent` / `currentColor` instead of `$prop` / `#color` / `#clear` / `#current` |
|
|
@@ -565,8 +565,11 @@ are checked only for enhanced handlers that convert them to pixels; unitless
|
|
|
565
565
|
properties such as `opacity` and `zIndex` are unaffected. Known equivalents still
|
|
566
566
|
have suggestions (`8px` → `1x`, radius `6px` → `1r`, border `1px` → `1bw`). These
|
|
567
567
|
assume the conventional scale defaults; review them against your design system.
|
|
568
|
-
|
|
569
|
-
|
|
568
|
+
Font size, line height and letter spacing are on the type scale, so they get no
|
|
569
|
+
`x` suggestion.
|
|
570
|
+
Other pixels are report-only. With the `pixels: 'replaceable'` option, they are
|
|
571
|
+
not reported, and only lengths with an exact unit are. Zero, token definitions,
|
|
572
|
+
quoted CSS text, and URLs are allowed. Quotes and URLs are also excluded from token and raw-color checks.
|
|
570
573
|
Range-based fixes are withheld when JavaScript escapes or JSX entities make
|
|
571
574
|
source offsets differ from the decoded value.
|
|
572
575
|
|
|
@@ -17,6 +17,15 @@ import { replaceInStringValue } from "../fix-utils.js";
|
|
|
17
17
|
* happened to use.
|
|
18
18
|
*/
|
|
19
19
|
const BORDER_WIDTH_PROPERTIES = new Set(["border", ...LOGICAL_BORDER_STYLES]);
|
|
20
|
+
/**
|
|
21
|
+
* Lengths on the type scale, which presets set. `x` is the spacing scale, so a
|
|
22
|
+
* `16px` line height equals `2x` only by coincidence and gets no suggestion.
|
|
23
|
+
*/
|
|
24
|
+
const TYPE_SCALE_PROPERTIES = new Set([
|
|
25
|
+
"fontSize",
|
|
26
|
+
"lineHeight",
|
|
27
|
+
"letterSpacing"
|
|
28
|
+
]);
|
|
20
29
|
/** Enhanced handlers that convert numeric inputs to pixels. Unitless CSS stays out. */
|
|
21
30
|
const NUMERIC_PIXEL_PROPERTIES = new Set([
|
|
22
31
|
...LOGICAL_STYLES,
|
|
@@ -81,32 +90,40 @@ var consistent_token_usage_default = createRule({
|
|
|
81
90
|
preferToken: "Consider using '{{suggestion}}' instead of '{{raw}}'. For an intentional pixel value, explicitly disable tasty/consistent-token-usage on this line with a reason.",
|
|
82
91
|
replaceWithToken: "Replace '{{raw}}' with '{{suggestion}}'"
|
|
83
92
|
},
|
|
84
|
-
schema: [
|
|
93
|
+
schema: [{
|
|
94
|
+
type: "object",
|
|
95
|
+
properties: { pixels: {
|
|
96
|
+
type: "string",
|
|
97
|
+
enum: ["all", "replaceable"]
|
|
98
|
+
} },
|
|
99
|
+
additionalProperties: false
|
|
100
|
+
}]
|
|
85
101
|
},
|
|
86
|
-
defaultOptions: [],
|
|
87
|
-
create(context) {
|
|
102
|
+
defaultOptions: [{ pixels: "all" }],
|
|
103
|
+
create(context, [{ pixels = "all" }]) {
|
|
88
104
|
const ctx = new TastyContext(context);
|
|
89
105
|
function checkValue(property, value, node) {
|
|
90
106
|
if (/^[#$]/.test(property)) return;
|
|
91
|
-
const
|
|
107
|
+
const lengths = /* @__PURE__ */ new Map();
|
|
92
108
|
for (const word of scanValueWords(value)) {
|
|
93
109
|
if (!/^[+-]?(?:\d*\.\d+|\d+\.?\d*)(?:e[+-]?\d+)?px$/i.test(word.value)) continue;
|
|
94
110
|
const amount = Number(word.value.slice(0, -2));
|
|
95
111
|
if (amount === 0) continue;
|
|
96
112
|
const raw = word.value;
|
|
97
113
|
const canonical = `${amount}px`;
|
|
98
|
-
const suggestion = property === "radius" && amount === 6 ? "1r" : BORDER_WIDTH_PROPERTIES.has(property) && amount === 1 ? "1bw" : PX_TO_UNIT[canonical];
|
|
99
|
-
const edits =
|
|
114
|
+
const suggestion = property === "radius" && amount === 6 ? "1r" : BORDER_WIDTH_PROPERTIES.has(property) && amount === 1 ? "1bw" : TYPE_SCALE_PROPERTIES.has(property) ? void 0 : PX_TO_UNIT[canonical];
|
|
115
|
+
const edits = lengths.get(raw) ?? [];
|
|
100
116
|
edits.push({
|
|
101
117
|
start: word.offset,
|
|
102
118
|
end: word.offset + raw.length,
|
|
103
119
|
replacement: suggestion ?? ""
|
|
104
120
|
});
|
|
105
|
-
|
|
121
|
+
lengths.set(raw, edits);
|
|
106
122
|
}
|
|
107
|
-
for (const [raw, edits] of
|
|
123
|
+
for (const [raw, edits] of lengths) {
|
|
108
124
|
const suggestion = edits[0].replacement;
|
|
109
125
|
if (!suggestion) {
|
|
126
|
+
if (pixels === "replaceable") continue;
|
|
110
127
|
context.report({
|
|
111
128
|
node,
|
|
112
129
|
messageId: "rawPixelValue",
|
|
@@ -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 } from '../context.js';\nimport { styleValueListeners } from '../style-values.js';\nimport { LOGICAL_BORDER_STYLES, LOGICAL_STYLES } from '../constants.js';\nimport { getStringValue } from '../utils.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { scanValueWords } from '../value-words.js';\n\ntype MessageIds = 'preferToken' | 'rawPixelValue' | 'replaceWithToken';\n\n/**\n * Properties whose value carries a border width, so a `1px` in one is the\n * `1bw` token spelled out.\n *\n * The logical axes belong here for the same reason `border` does: they take the\n * same width/style/colour value, parsed by the same `parseBorderValue`. Leaving\n * them out would make the advice depend on which axis vocabulary the author\n * happened to use.\n */\nconst BORDER_WIDTH_PROPERTIES = new Set<string>([\n 'border',\n ...LOGICAL_BORDER_STYLES,\n]);\n\n/** Enhanced handlers that convert numeric inputs to pixels. Unitless CSS stays out. */\nconst NUMERIC_PIXEL_PROPERTIES = new Set<string>([\n ...LOGICAL_STYLES,\n 'gap',\n 'radius',\n 'outlineOffset',\n 'width',\n 'minWidth',\n 'maxWidth',\n 'height',\n 'minHeight',\n 'maxHeight',\n 'padding',\n 'paddingTop',\n 'paddingRight',\n 'paddingBottom',\n 'paddingLeft',\n 'margin',\n 'marginTop',\n 'marginRight',\n 'marginBottom',\n 'marginLeft',\n 'inset',\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'border',\n 'borderTop',\n 'borderRight',\n 'borderBottom',\n 'borderLeft',\n 'scrollPadding',\n 'scrollPaddingTop',\n 'scrollPaddingRight',\n 'scrollPaddingBottom',\n 'scrollPaddingLeft',\n 'scrollMargin',\n 'scrollMarginTop',\n 'scrollMarginRight',\n 'scrollMarginBottom',\n 'scrollMarginLeft',\n]);\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 rawPixelValue:\n \"Use a design token or custom unit instead of raw pixel value '{{raw}}'. For an intentional pixel value, explicitly disable tasty/consistent-token-usage on this line with a reason.\",\n preferToken:\n \"Consider using '{{suggestion}}' instead of '{{raw}}'. For an intentional pixel value, explicitly disable tasty/consistent-token-usage on this line with a reason.\",\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 // Definitions establish the scale itself; quoted text and URLs are opaque.\n if (/^[#$]/.test(property)) return;\n const
|
|
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 } from '../context.js';\nimport { styleValueListeners } from '../style-values.js';\nimport { LOGICAL_BORDER_STYLES, LOGICAL_STYLES } from '../constants.js';\nimport { getStringValue } from '../utils.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { scanValueWords } from '../value-words.js';\n\ntype MessageIds = 'preferToken' | 'rawPixelValue' | 'replaceWithToken';\n\n/**\n * Which pixel lengths to report: every one, or only those with an exact\n * replacement, so a project can enforce the known units without listing every\n * layout size it chose in pixels.\n */\ntype Pixels = 'all' | 'replaceable';\n\n/**\n * Properties whose value carries a border width, so a `1px` in one is the\n * `1bw` token spelled out.\n *\n * The logical axes belong here for the same reason `border` does: they take the\n * same width/style/colour value, parsed by the same `parseBorderValue`. Leaving\n * them out would make the advice depend on which axis vocabulary the author\n * happened to use.\n */\nconst BORDER_WIDTH_PROPERTIES = new Set<string>([\n 'border',\n ...LOGICAL_BORDER_STYLES,\n]);\n\n/**\n * Lengths on the type scale, which presets set. `x` is the spacing scale, so a\n * `16px` line height equals `2x` only by coincidence and gets no suggestion.\n */\nconst TYPE_SCALE_PROPERTIES = new Set<string>([\n 'fontSize',\n 'lineHeight',\n 'letterSpacing',\n]);\n\n/** Enhanced handlers that convert numeric inputs to pixels. Unitless CSS stays out. */\nconst NUMERIC_PIXEL_PROPERTIES = new Set<string>([\n ...LOGICAL_STYLES,\n 'gap',\n 'radius',\n 'outlineOffset',\n 'width',\n 'minWidth',\n 'maxWidth',\n 'height',\n 'minHeight',\n 'maxHeight',\n 'padding',\n 'paddingTop',\n 'paddingRight',\n 'paddingBottom',\n 'paddingLeft',\n 'margin',\n 'marginTop',\n 'marginRight',\n 'marginBottom',\n 'marginLeft',\n 'inset',\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'border',\n 'borderTop',\n 'borderRight',\n 'borderBottom',\n 'borderLeft',\n 'scrollPadding',\n 'scrollPaddingTop',\n 'scrollPaddingRight',\n 'scrollPaddingBottom',\n 'scrollPaddingLeft',\n 'scrollMargin',\n 'scrollMarginTop',\n 'scrollMarginRight',\n 'scrollMarginBottom',\n 'scrollMarginLeft',\n]);\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<[{ pixels?: Pixels }], 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 rawPixelValue:\n \"Use a design token or custom unit instead of raw pixel value '{{raw}}'. For an intentional pixel value, explicitly disable tasty/consistent-token-usage on this line with a reason.\",\n preferToken:\n \"Consider using '{{suggestion}}' instead of '{{raw}}'. For an intentional pixel value, explicitly disable tasty/consistent-token-usage on this line with a reason.\",\n replaceWithToken: \"Replace '{{raw}}' with '{{suggestion}}'\",\n },\n schema: [\n {\n type: 'object',\n properties: {\n pixels: { type: 'string', enum: ['all', 'replaceable'] },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ pixels: 'all' }],\n create(context, [{ pixels = 'all' }]) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n property: string,\n value: string,\n node: TSESTree.Node,\n ): void {\n // Definitions establish the scale itself; quoted text and URLs are opaque.\n if (/^[#$]/.test(property)) return;\n const lengths = new Map<\n string,\n { start: number; end: number; replacement: string }[]\n >();\n for (const word of scanValueWords(value)) {\n if (!/^[+-]?(?:\\d*\\.\\d+|\\d+\\.?\\d*)(?:e[+-]?\\d+)?px$/i.test(word.value))\n continue;\n const amount = Number(word.value.slice(0, -2));\n if (amount === 0) continue;\n const raw = word.value;\n const canonical = `${amount}px`;\n const suggestion =\n property === 'radius' && amount === 6\n ? '1r'\n : BORDER_WIDTH_PROPERTIES.has(property) && amount === 1\n ? '1bw'\n : TYPE_SCALE_PROPERTIES.has(property)\n ? undefined\n : PX_TO_UNIT[canonical];\n const edits = lengths.get(raw) ?? [];\n edits.push({\n start: word.offset,\n end: word.offset + raw.length,\n replacement: suggestion ?? '',\n });\n lengths.set(raw, edits);\n }\n for (const [raw, edits] of lengths) {\n const suggestion = edits[0].replacement;\n if (!suggestion) {\n if (pixels === 'replaceable') continue;\n context.report({ node, messageId: 'rawPixelValue', data: { raw } });\n continue;\n }\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion, raw },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw, suggestion },\n fix: (fixer) =>\n getStringValue(node) === null\n ? fixer.replaceText(node, `'${suggestion}'`)\n : replaceInStringValue(\n fixer,\n node,\n edits,\n context.sourceCode,\n ),\n },\n ],\n });\n }\n }\n\n return styleValueListeners(ctx, (property, node) => {\n const value = getStringValue(node);\n if (value !== null) {\n checkValue(property, value, node);\n } else if (NUMERIC_PIXEL_PROPERTIES.has(property)) {\n if (node.type === 'Literal' && typeof node.value === 'number') {\n checkValue(property, `${node.value}px`, node);\n } else if (\n node.type === 'UnaryExpression' &&\n (node.operator === '-' || node.operator === '+') &&\n node.argument.type === 'Literal' &&\n typeof node.argument.value === 'number'\n ) {\n const amount =\n node.operator === '-' ? -node.argument.value : node.argument.value;\n checkValue(property, `${amount}px`, node);\n }\n }\n });\n },\n});\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenphi/eslint-plugin-tasty",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@changesets/changelog-github": "^0.5.2",
|
|
54
54
|
"@changesets/cli": "^2.31.1",
|
|
55
55
|
"@eslint/js": "^10.0.1",
|
|
56
|
-
"@tenphi/tasty": "^3.
|
|
56
|
+
"@tenphi/tasty": "^3.17.1",
|
|
57
57
|
"@types/node": "^22.0.0",
|
|
58
58
|
"@types/semver": "^7.8.0",
|
|
59
59
|
"@typescript-eslint/parser": "^8.56.1",
|