eslint-plugin-tailwind-variants 1.0.2 → 2.0.1

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
@@ -43,14 +43,25 @@ npm i -D vue-eslint-parser
43
43
  import tailwindVariants from "eslint-plugin-tailwind-variants";
44
44
 
45
45
  export default defineConfig([
46
- tailwindVariants.configs.recommended,
46
+ ...tailwindVariants.configs.recommended,
47
47
  ]);
48
48
  ```
49
49
 
50
+ ### Editor setup
51
+
52
+ If you are using the ESLint plugin in VS Code add the following to your `settings.json` to enable `css` linting:
53
+
54
+ ```json
55
+ {
56
+ "eslint.validate": ["css"]
57
+ }
58
+ ```
59
+
50
60
  ### Rules
51
61
 
52
62
  | Name | Description | `recommended` | autofix |
53
63
  | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------- |
54
64
  | [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
65
  | [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 | ✔ | |
66
+ | [limited-inline-classes](docs/rules/limited-inline-classes.md) | enforce limited number of inline class names and prohibit cn() usage | ✔ | |
67
+ | [sort-custom-properties](docs/rules/sort-custom-properties.md) | enforce consistent ordering of CSS custom properties (CSS variables) | ✔ | ✔ |
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ESLint, Linter } from "eslint";
2
2
  type PluginConfigs = {
3
- recommended: Linter.Config;
3
+ recommended: Linter.Config[];
4
4
  };
5
5
  declare const plugin: ESLint.Plugin;
6
6
  export declare const configs: PluginConfigs;
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import css from "@eslint/css";
1
2
  import { name as packageName, version as packageVersion, } from "../package.json";
2
3
  import { rules } from "./rules/index.js";
3
4
  const pluginName = "tailwind-variants";
@@ -9,16 +10,31 @@ const plugin = {
9
10
  rules,
10
11
  };
11
12
  export const configs = {
12
- recommended: {
13
- plugins: {
14
- [pluginName]: plugin,
13
+ recommended: [
14
+ {
15
+ plugins: {
16
+ [pluginName]: plugin,
17
+ },
18
+ rules: {
19
+ [`${pluginName}/limited-inline-classes`]: "error",
20
+ [`${pluginName}/require-variants-call-styles-name`]: "error",
21
+ [`${pluginName}/require-variants-suffix`]: "error",
22
+ },
15
23
  },
16
- rules: {
17
- [`${pluginName}/limited-inline-classes`]: "error",
18
- [`${pluginName}/require-variants-call-styles-name`]: "error",
19
- [`${pluginName}/require-variants-suffix`]: "error",
24
+ {
25
+ files: ["**/*.css"],
26
+ language: "css/css",
27
+ plugins: { css },
28
+ rules: {
29
+ [`${pluginName}/sort-custom-properties`]: [
30
+ "error",
31
+ {
32
+ emptyLineBetweenGroups: true,
33
+ },
34
+ ],
35
+ },
20
36
  },
21
- },
37
+ ],
22
38
  };
23
39
  export { plugin };
24
40
  export default {
@@ -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 Options = [
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 Options = [
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": "1.0.2",
3
+ "version": "2.0.1",
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": {