eslint-plugin-tailwind-variants 1.0.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +21 -8
- package/dist/rules/index.js +2 -0
- package/dist/rules/limited-inline-classes.d.ts +2 -3
- package/dist/rules/require-variants-call-styles-name.d.ts +2 -2
- package/dist/rules/require-variants-suffix.d.ts +2 -2
- package/dist/rules/sort-custom-properties.d.ts +37 -0
- package/dist/rules/sort-custom-properties.js +192 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ npm i -D vue-eslint-parser
|
|
|
43
43
|
import tailwindVariants from "eslint-plugin-tailwind-variants";
|
|
44
44
|
|
|
45
45
|
export default defineConfig([
|
|
46
|
-
|
|
46
|
+
...tailwindVariants.configs.recommended,
|
|
47
47
|
]);
|
|
48
48
|
```
|
|
49
49
|
|
|
@@ -53,4 +53,5 @@ export default defineConfig([
|
|
|
53
53
|
| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------- |
|
|
54
54
|
| [require-variants-call-styles-name](docs/rules/require-variants-call-styles-name.md) | enforce that when calling a function returned by tailwind-variants (`tv()`), the result is assigned to a variable named `styles` (or a configurable name) | ✔ | ✔ |
|
|
55
55
|
| [require-variants-suffix](docs/rules/require-variants-suffix.md) | require variables assigned from tv() to end with a specific suffix | ✔ | ✔ |
|
|
56
|
-
| [limited-inline-classes](docs/rules/limited-inline-classes.md) | enforce limited number of inline class names and prohibit cn() usage | ✔ | |
|
|
56
|
+
| [limited-inline-classes](docs/rules/limited-inline-classes.md) | enforce limited number of inline class names and prohibit cn() usage | ✔ | |
|
|
57
|
+
| [sort-custom-properties](docs/rules/sort-custom-properties.md) | enforce consistent ordering of CSS custom properties (CSS variables) | ✔ | ✔ |
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -9,16 +9,29 @@ const plugin = {
|
|
|
9
9
|
rules,
|
|
10
10
|
};
|
|
11
11
|
export const configs = {
|
|
12
|
-
recommended:
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
recommended: [
|
|
13
|
+
{
|
|
14
|
+
plugins: {
|
|
15
|
+
[pluginName]: plugin,
|
|
16
|
+
},
|
|
17
|
+
rules: {
|
|
18
|
+
[`${pluginName}/limited-inline-classes`]: "error",
|
|
19
|
+
[`${pluginName}/require-variants-call-styles-name`]: "error",
|
|
20
|
+
[`${pluginName}/require-variants-suffix`]: "error",
|
|
21
|
+
},
|
|
15
22
|
},
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
{
|
|
24
|
+
files: ["**/*.css"],
|
|
25
|
+
rules: {
|
|
26
|
+
[`${pluginName}/sort-custom-properties`]: [
|
|
27
|
+
"error",
|
|
28
|
+
{
|
|
29
|
+
emptyLineBetweenGroups: true,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
20
33
|
},
|
|
21
|
-
|
|
34
|
+
],
|
|
22
35
|
};
|
|
23
36
|
export { plugin };
|
|
24
37
|
export default {
|
package/dist/rules/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { rule as limitedInlineClasses } from "./limited-inline-classes.js";
|
|
2
2
|
import { rule as requireVariantsCallStylesName } from "./require-variants-call-styles-name.js";
|
|
3
3
|
import { rule as requireVariantsSuffix } from "./require-variants-suffix.js";
|
|
4
|
+
import { rule as sortCustomProperties } from "./sort-custom-properties.js";
|
|
4
5
|
export const rules = {
|
|
5
6
|
"limited-inline-classes": limitedInlineClasses,
|
|
6
7
|
"require-variants-call-styles-name": requireVariantsCallStylesName,
|
|
7
8
|
"require-variants-suffix": requireVariantsSuffix,
|
|
9
|
+
"sort-custom-properties": sortCustomProperties,
|
|
8
10
|
// as unknown due to ESLint and TSESLint types not aligning perfectly
|
|
9
11
|
};
|
|
@@ -3,8 +3,8 @@ export declare const MESSAGE_IDS: {
|
|
|
3
3
|
readonly limitedInlineClasses: "limitedInlineClasses";
|
|
4
4
|
readonly noCnInClassName: "noCnInClassName";
|
|
5
5
|
};
|
|
6
|
-
type MessageIds = (typeof MESSAGE_IDS)[keyof typeof MESSAGE_IDS];
|
|
7
|
-
type Options = [
|
|
6
|
+
export type MessageIds = (typeof MESSAGE_IDS)[keyof typeof MESSAGE_IDS];
|
|
7
|
+
export type Options = [
|
|
8
8
|
{
|
|
9
9
|
/**
|
|
10
10
|
* Directory pattern to match
|
|
@@ -21,4 +21,3 @@ type Options = [
|
|
|
21
21
|
export declare const rule: ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
22
22
|
name: string;
|
|
23
23
|
};
|
|
24
|
-
export {};
|
|
@@ -2,7 +2,8 @@ import { ESLintUtils } from "@typescript-eslint/utils";
|
|
|
2
2
|
export declare const MESSAGE_IDS: {
|
|
3
3
|
readonly requireVariantsCallStylesName: "requireVariantsCallStylesName";
|
|
4
4
|
};
|
|
5
|
-
type
|
|
5
|
+
export type MessageIds = (typeof MESSAGE_IDS)[keyof typeof MESSAGE_IDS];
|
|
6
|
+
export type Options = [
|
|
6
7
|
{
|
|
7
8
|
/**
|
|
8
9
|
* Name required for variables assigned from tv()
|
|
@@ -14,4 +15,3 @@ type Options = [
|
|
|
14
15
|
export declare const rule: ESLintUtils.RuleModule<"requireVariantsCallStylesName", Options, unknown, ESLintUtils.RuleListener> & {
|
|
15
16
|
name: string;
|
|
16
17
|
};
|
|
17
|
-
export {};
|
|
@@ -2,7 +2,8 @@ import { ESLintUtils } from "@typescript-eslint/utils";
|
|
|
2
2
|
export declare const MESSAGE_IDS: {
|
|
3
3
|
readonly requireVariantsSuffix: "requireVariantsSuffix";
|
|
4
4
|
};
|
|
5
|
-
type
|
|
5
|
+
export type MessageIds = (typeof MESSAGE_IDS)[keyof typeof MESSAGE_IDS];
|
|
6
|
+
export type Options = [
|
|
6
7
|
{
|
|
7
8
|
/**
|
|
8
9
|
* Suffix required for variables assigned from tv()
|
|
@@ -14,4 +15,3 @@ type Options = [
|
|
|
14
15
|
export declare const rule: ESLintUtils.RuleModule<"requireVariantsSuffix", Options, unknown, ESLintUtils.RuleListener> & {
|
|
15
16
|
name: string;
|
|
16
17
|
};
|
|
17
|
-
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
export declare const MESSAGE_IDS: {
|
|
3
|
+
readonly missingEmptyLineBetweenGroups: "missingEmptyLineBetweenGroups";
|
|
4
|
+
readonly patternTooLong: "patternTooLong";
|
|
5
|
+
readonly unsortedCustomProperties: "unsortedCustomProperties";
|
|
6
|
+
};
|
|
7
|
+
export type MessageIds = (typeof MESSAGE_IDS)[keyof typeof MESSAGE_IDS];
|
|
8
|
+
export type Options = [
|
|
9
|
+
{
|
|
10
|
+
/**
|
|
11
|
+
* Add empty line between different prefix groups
|
|
12
|
+
* @default false
|
|
13
|
+
*/
|
|
14
|
+
emptyLineBetweenGroups?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Order of patterns (RegExp strings) for custom properties.
|
|
17
|
+
* Properties matching the first pattern appear first.
|
|
18
|
+
* @default [
|
|
19
|
+
* "^--spacing-",
|
|
20
|
+
* "^--size-",
|
|
21
|
+
* "^--font-",
|
|
22
|
+
* "^--weight-",
|
|
23
|
+
* "^--leading-",
|
|
24
|
+
* "^--tracking-",
|
|
25
|
+
* "^--radius-",
|
|
26
|
+
* "^--shadow-",
|
|
27
|
+
* "^--animate-",
|
|
28
|
+
* "^--transition-",
|
|
29
|
+
* "^--color-",
|
|
30
|
+
* ]
|
|
31
|
+
*/
|
|
32
|
+
order?: string[];
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
export declare const rule: ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
36
|
+
name: string;
|
|
37
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
const createRule = ESLintUtils.RuleCreator((name) => name);
|
|
3
|
+
const BLOCK_SELECTOR = "Rule > Block, AtRule[name='theme'] > Block, AtRule[name='utility'] > Block";
|
|
4
|
+
const defaultOrder = [
|
|
5
|
+
"^--spacing-",
|
|
6
|
+
"^--size-",
|
|
7
|
+
"^--font-",
|
|
8
|
+
"^--weight-",
|
|
9
|
+
"^--leading-",
|
|
10
|
+
"^--tracking-",
|
|
11
|
+
"^--radius-",
|
|
12
|
+
"^--shadow-",
|
|
13
|
+
"^--animate-",
|
|
14
|
+
"^--transition-",
|
|
15
|
+
"^--color-",
|
|
16
|
+
];
|
|
17
|
+
export const MESSAGE_IDS = {
|
|
18
|
+
missingEmptyLineBetweenGroups: "missingEmptyLineBetweenGroups",
|
|
19
|
+
patternTooLong: "patternTooLong",
|
|
20
|
+
unsortedCustomProperties: "unsortedCustomProperties",
|
|
21
|
+
};
|
|
22
|
+
export const rule = createRule({
|
|
23
|
+
name: "sort-custom-properties",
|
|
24
|
+
meta: {
|
|
25
|
+
docs: {
|
|
26
|
+
description: "Enforce sorting of CSS custom properties based on RegEx patterns within declaration blocks.",
|
|
27
|
+
},
|
|
28
|
+
fixable: "code",
|
|
29
|
+
messages: {
|
|
30
|
+
missingEmptyLineBetweenGroups: "Expected empty line between different custom property prefix groups",
|
|
31
|
+
patternTooLong: "The pattern '{{pattern}}' is too long and may cause performance issues",
|
|
32
|
+
unsortedCustomProperties: "Custom properties should be sorted by the defined order: {{order}}",
|
|
33
|
+
},
|
|
34
|
+
schema: [
|
|
35
|
+
{
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
properties: {
|
|
38
|
+
emptyLineBetweenGroups: {
|
|
39
|
+
default: false,
|
|
40
|
+
description: "Add empty line between different prefix groups",
|
|
41
|
+
type: "boolean",
|
|
42
|
+
},
|
|
43
|
+
order: {
|
|
44
|
+
default: defaultOrder,
|
|
45
|
+
description: "Array of RegEx patterns defining the sort order",
|
|
46
|
+
items: {
|
|
47
|
+
type: "string",
|
|
48
|
+
},
|
|
49
|
+
type: "array",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
type: "object",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
type: "layout",
|
|
56
|
+
},
|
|
57
|
+
defaultOptions: [
|
|
58
|
+
{
|
|
59
|
+
emptyLineBetweenGroups: false,
|
|
60
|
+
order: defaultOrder,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
create: (context) => {
|
|
64
|
+
const options = context.options[0] || {};
|
|
65
|
+
const order = options.order || defaultOrder;
|
|
66
|
+
const emptyLineBetweenGroups = options.emptyLineBetweenGroups || false;
|
|
67
|
+
const { sourceCode } = context;
|
|
68
|
+
const compiledOrder = order.map((pattern) => {
|
|
69
|
+
if (pattern.length > 100) {
|
|
70
|
+
context.report({
|
|
71
|
+
data: { pattern },
|
|
72
|
+
loc: { column: 1, line: 1 },
|
|
73
|
+
messageId: MESSAGE_IDS.patternTooLong,
|
|
74
|
+
});
|
|
75
|
+
return /(?!)/; // Matches nothing
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return new RegExp(pattern);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Fallback: escape special chars and treat as a prefix match
|
|
82
|
+
return new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const getMatchingOrderIndex = (propName) => {
|
|
86
|
+
for (let i = 0; i < compiledOrder.length; i++) {
|
|
87
|
+
if (compiledOrder[i].test(propName)) {
|
|
88
|
+
return i;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return compiledOrder.length; // Unmatched properties go to the end
|
|
92
|
+
};
|
|
93
|
+
const blockStack = [];
|
|
94
|
+
return {
|
|
95
|
+
[`${BLOCK_SELECTOR}:exit`]() {
|
|
96
|
+
const currentBlockProperties = blockStack.pop();
|
|
97
|
+
if (!currentBlockProperties || currentBlockProperties.length < 2) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
let isSorted = true;
|
|
101
|
+
let needsEmptyLines = false;
|
|
102
|
+
for (let i = 1; i < currentBlockProperties.length; i++) {
|
|
103
|
+
const prev = currentBlockProperties[i - 1];
|
|
104
|
+
const curr = currentBlockProperties[i];
|
|
105
|
+
if (prev.orderIndex > curr.orderIndex) {
|
|
106
|
+
isSorted = false;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
else if (prev.orderIndex === curr.orderIndex &&
|
|
110
|
+
prev.property > curr.property) {
|
|
111
|
+
isSorted = false;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
if (emptyLineBetweenGroups && prev.orderIndex !== curr.orderIndex) {
|
|
115
|
+
const prevNode = prev.node;
|
|
116
|
+
const currNode = curr.node;
|
|
117
|
+
const linesBetween = currNode.loc.start.line - prevNode.loc.end.line;
|
|
118
|
+
if (linesBetween < 2) {
|
|
119
|
+
needsEmptyLines = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (isSorted && !needsEmptyLines)
|
|
124
|
+
return;
|
|
125
|
+
const messageId = !isSorted
|
|
126
|
+
? MESSAGE_IDS.unsortedCustomProperties
|
|
127
|
+
: MESSAGE_IDS.missingEmptyLineBetweenGroups;
|
|
128
|
+
context.report({
|
|
129
|
+
...(messageId === MESSAGE_IDS.unsortedCustomProperties && {
|
|
130
|
+
data: {
|
|
131
|
+
order: order.join(", "),
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
fix: (fixer) => {
|
|
135
|
+
const sorted = [...currentBlockProperties].sort((a, b) => {
|
|
136
|
+
if (a.orderIndex !== b.orderIndex) {
|
|
137
|
+
return a.orderIndex - b.orderIndex;
|
|
138
|
+
}
|
|
139
|
+
return a.property.localeCompare(b.property);
|
|
140
|
+
});
|
|
141
|
+
const getFullLine = (node) => {
|
|
142
|
+
const lines = sourceCode.lines;
|
|
143
|
+
const startLine = node.loc.start.line - 1;
|
|
144
|
+
return lines[startLine];
|
|
145
|
+
};
|
|
146
|
+
const fixes = currentBlockProperties.map((prop, index) => {
|
|
147
|
+
const sortedNode = sorted[index].node;
|
|
148
|
+
const sortedLine = getFullLine(sortedNode);
|
|
149
|
+
const currentLineStart = sourceCode.getIndexFromLoc({
|
|
150
|
+
column: 1,
|
|
151
|
+
line: prop.node.loc.start.line,
|
|
152
|
+
});
|
|
153
|
+
const currentLineEnd = sourceCode.getIndexFromLoc({
|
|
154
|
+
column: 1,
|
|
155
|
+
line: prop.node.loc.start.line + 1,
|
|
156
|
+
});
|
|
157
|
+
let replacement = "";
|
|
158
|
+
if (emptyLineBetweenGroups && index > 0) {
|
|
159
|
+
const prevOrderIndex = sorted[index - 1].orderIndex;
|
|
160
|
+
const currOrderIndex = sorted[index].orderIndex;
|
|
161
|
+
if (prevOrderIndex !== currOrderIndex) {
|
|
162
|
+
replacement = "\n";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
replacement += sortedLine + "\n";
|
|
166
|
+
return fixer.replaceTextRange([currentLineStart, currentLineEnd], replacement);
|
|
167
|
+
});
|
|
168
|
+
return fixes;
|
|
169
|
+
},
|
|
170
|
+
messageId: messageId,
|
|
171
|
+
node: currentBlockProperties[0].node,
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
[`:matches(${BLOCK_SELECTOR}) > Declaration`](node) {
|
|
175
|
+
if (!node.property.startsWith("--"))
|
|
176
|
+
return;
|
|
177
|
+
const orderIndex = getMatchingOrderIndex(node.property);
|
|
178
|
+
const currentBlock = blockStack[blockStack.length - 1];
|
|
179
|
+
if (!currentBlock)
|
|
180
|
+
return;
|
|
181
|
+
currentBlock.push({
|
|
182
|
+
node,
|
|
183
|
+
orderIndex,
|
|
184
|
+
property: node.property,
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
[BLOCK_SELECTOR]() {
|
|
188
|
+
blockStack.push([]);
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-tailwind-variants",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "ESLint plugin for Tailwind Variants",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"eslint": ">9.0.0"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"@eslint/css": "^0.14.1",
|
|
44
45
|
"@typescript-eslint/utils": "^8.51.0"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|