@trackunit/eslint-plugin-trackunit 0.6.82 → 0.6.84
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/CHANGELOG.md +21 -0
- package/package.json +1 -1
- package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.js +24 -17
- package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js +8 -0
- package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.js +6 -0
- package/src/lib/utils/classname-utils.d.ts +33 -2
- package/src/lib/utils/classname-utils.js +94 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## 0.6.84 (2026-07-29)
|
|
2
|
+
|
|
3
|
+
### 🧱 Updated Dependencies
|
|
4
|
+
|
|
5
|
+
- Updated shared-utils to 1.15.84
|
|
6
|
+
|
|
7
|
+
## 0.6.83 (2026-07-28)
|
|
8
|
+
|
|
9
|
+
### 🩹 Fixes
|
|
10
|
+
|
|
11
|
+
- **eslint:** reuse classname detection in rules ([dcfeae4cafc](https://github.com/Trackunit/manager/commit/dcfeae4cafc))
|
|
12
|
+
|
|
13
|
+
### 🧱 Updated Dependencies
|
|
14
|
+
|
|
15
|
+
- Updated shared-utils to 1.15.83
|
|
16
|
+
|
|
17
|
+
### ❤️ Thank You
|
|
18
|
+
|
|
19
|
+
- Cursor @cursoragent
|
|
20
|
+
- Michael Buss Rønne
|
|
21
|
+
|
|
1
22
|
## 0.6.82 (2026-07-27)
|
|
2
23
|
|
|
3
24
|
### 🧱 Updated Dependencies
|
package/package.json
CHANGED
|
@@ -4,6 +4,18 @@ exports.cvaMergeBaseClassesAsArray = void 0;
|
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const classname_utils_1 = require("../../utils/classname-utils");
|
|
6
6
|
const createRule = utils_1.ESLintUtils.RuleCreator(name => `https://github.com/trackunit/manager/blob/main/libs/eslint/plugin-trackunit/src/lib/rules/${name}.ts`);
|
|
7
|
+
const isCvaMergeBaseContext = (context) => {
|
|
8
|
+
if (context.type === "function-call") {
|
|
9
|
+
return context.functionName === "cvaMerge" && context.argumentIndex === 0;
|
|
10
|
+
}
|
|
11
|
+
if (context.type === "array-element" && context.parentContext.type === "function-call") {
|
|
12
|
+
return context.parentContext.functionName === "cvaMerge" && context.parentContext.argumentIndex === 0;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
};
|
|
16
|
+
const isLiteralLocation = (location) => {
|
|
17
|
+
return location.fixNode.type === utils_1.AST_NODE_TYPES.Literal && typeof location.fixNode.value === "string";
|
|
18
|
+
};
|
|
7
19
|
/**
|
|
8
20
|
* ESLint rule that converts cvaMerge base styles to arrays if more than one class is used.
|
|
9
21
|
*
|
|
@@ -32,23 +44,26 @@ exports.cvaMergeBaseClassesAsArray = createRule({
|
|
|
32
44
|
create(context) {
|
|
33
45
|
return {
|
|
34
46
|
CallExpression(node) {
|
|
35
|
-
const calleeName = (0, classname_utils_1.getCalleeName)(node.callee);
|
|
36
|
-
if (calleeName !== "cvaMerge" || node.arguments.length === 0) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
47
|
const firstArg = node.arguments[0];
|
|
40
48
|
if (!firstArg) {
|
|
41
49
|
return;
|
|
42
50
|
}
|
|
51
|
+
const baseClassLocations = (0, classname_utils_1.findClassnameStringsInCall)(node)
|
|
52
|
+
.filter(location => isCvaMergeBaseContext(location.context))
|
|
53
|
+
.filter(isLiteralLocation);
|
|
43
54
|
// Handle string literal: cvaMerge("class1 class2")
|
|
44
|
-
if (firstArg.type === utils_1.AST_NODE_TYPES.Literal
|
|
45
|
-
const
|
|
55
|
+
if (firstArg.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
56
|
+
const baseClassLocation = baseClassLocations[0];
|
|
57
|
+
if (!baseClassLocation) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const value = baseClassLocation.value;
|
|
46
61
|
if (value.includes(" ")) {
|
|
47
62
|
context.report({
|
|
48
63
|
node,
|
|
49
64
|
messageId: "stringNeedsArray",
|
|
50
65
|
fix(fixer) {
|
|
51
|
-
return fixer.replaceText(
|
|
66
|
+
return fixer.replaceText(baseClassLocation.fixNode, (0, classname_utils_1.formatAsArray)((0, classname_utils_1.splitClasses)(value)));
|
|
52
67
|
},
|
|
53
68
|
});
|
|
54
69
|
}
|
|
@@ -56,21 +71,13 @@ exports.cvaMergeBaseClassesAsArray = createRule({
|
|
|
56
71
|
}
|
|
57
72
|
// Handle array: cvaMerge(["class1 class2", "class3"])
|
|
58
73
|
if (firstArg.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
59
|
-
const hasMultiClassElement =
|
|
60
|
-
typeof element.value === "string" &&
|
|
61
|
-
element.value.includes(" "));
|
|
74
|
+
const hasMultiClassElement = baseClassLocations.some(location => location.value.includes(" "));
|
|
62
75
|
if (hasMultiClassElement) {
|
|
63
76
|
context.report({
|
|
64
77
|
node,
|
|
65
78
|
messageId: "arrayNeedsSplit",
|
|
66
79
|
fix(fixer) {
|
|
67
|
-
|
|
68
|
-
const allClasses = [];
|
|
69
|
-
for (const element of firstArg.elements) {
|
|
70
|
-
if (element?.type === utils_1.AST_NODE_TYPES.Literal && typeof element.value === "string") {
|
|
71
|
-
allClasses.push(...(0, classname_utils_1.splitClasses)(element.value));
|
|
72
|
-
}
|
|
73
|
-
}
|
|
80
|
+
const allClasses = baseClassLocations.flatMap(location => (0, classname_utils_1.splitClasses)(location.value));
|
|
74
81
|
return fixer.replaceText(firstArg, (0, classname_utils_1.formatAsArray)(allClasses));
|
|
75
82
|
},
|
|
76
83
|
});
|
package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js
CHANGED
|
@@ -49,6 +49,14 @@ exports.noTemplateStringsInClassName = createRule({
|
|
|
49
49
|
context.report({ node, messageId: "templateStringInClassName" });
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
|
+
VariableDeclarator(node) {
|
|
53
|
+
const locations = (0, classname_utils_1.findClassnameStringsInVariable)(node);
|
|
54
|
+
for (const location of locations) {
|
|
55
|
+
if (location.fixNode.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
|
|
56
|
+
context.report({ node: location.reportNode, messageId: "templateStringInClassName" });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
52
60
|
};
|
|
53
61
|
},
|
|
54
62
|
});
|
|
@@ -265,6 +265,12 @@ exports.requireClassnameAlternatives = createRule({
|
|
|
265
265
|
checkAndReport(location.reportNode, location.value, location.fixNode);
|
|
266
266
|
}
|
|
267
267
|
},
|
|
268
|
+
VariableDeclarator(node) {
|
|
269
|
+
const locations = (0, classname_utils_1.findClassnameStringsInVariable)(node);
|
|
270
|
+
for (const location of locations) {
|
|
271
|
+
checkAndReport(location.reportNode, location.value, location.fixNode);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
268
274
|
};
|
|
269
275
|
},
|
|
270
276
|
});
|
|
@@ -71,11 +71,19 @@ export type ClassnameLocation = {
|
|
|
71
71
|
* Function names that accept Tailwind class strings as arguments.
|
|
72
72
|
* These are commonly used for class merging and conditional class application.
|
|
73
73
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
74
|
+
* Kept in sync with Prettier's Tailwind CSS plugin config by
|
|
75
|
+
* `classname-config-sync.spec.ts`.
|
|
76
76
|
*/
|
|
77
77
|
export declare const TAILWIND_FUNCTIONS: readonly ["cva", "cvaMerge", "tw", "twx", "tws", "twMerge", "twJoin", "cn", "clsx", "classNames"];
|
|
78
78
|
export type TailwindFunction = (typeof TAILWIND_FUNCTIONS)[number];
|
|
79
|
+
/**
|
|
80
|
+
* Variable name suffixes that indicate a classname value.
|
|
81
|
+
* Matches variables like: containerClass, buttonClassName, rootClasses
|
|
82
|
+
*
|
|
83
|
+
* Kept in sync with the VS Code `tailwindCSS.experimental.classRegex` setting by
|
|
84
|
+
* `classname-config-sync.spec.ts`, since Prettier has no equivalent option to check against.
|
|
85
|
+
*/
|
|
86
|
+
export declare const CLASSNAME_VARIABLE_SUFFIXES: readonly ["Class", "ClassName", "Classes"];
|
|
79
87
|
/**
|
|
80
88
|
* JSX attribute names that contain classname values
|
|
81
89
|
*/
|
|
@@ -134,6 +142,25 @@ export declare const findClassnameStringsInCall: (node: TSESTree.CallExpression)
|
|
|
134
142
|
* ```
|
|
135
143
|
*/
|
|
136
144
|
export declare const findClassnameStringsInAttribute: (node: TSESTree.JSXAttribute) => Array<ClassnameLocation>;
|
|
145
|
+
/**
|
|
146
|
+
* Find all classname string locations in a VariableDeclarator node.
|
|
147
|
+
*
|
|
148
|
+
* Use this in your ESLint rule's VariableDeclarator handler to process
|
|
149
|
+
* variables with classname-like names.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* VariableDeclarator(node) {
|
|
154
|
+
* const locations = findClassnameStringsInVariable(node);
|
|
155
|
+
* for (const location of locations) {
|
|
156
|
+
* if (hasBannedPattern(location.value)) {
|
|
157
|
+
* context.report({ node: location.reportNode, ... });
|
|
158
|
+
* }
|
|
159
|
+
* }
|
|
160
|
+
* }
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export declare const findClassnameStringsInVariable: (node: TSESTree.VariableDeclarator) => Array<ClassnameLocation>;
|
|
137
164
|
/**
|
|
138
165
|
* Split a classname string into individual class names.
|
|
139
166
|
* Handles whitespace-separated classes.
|
|
@@ -148,3 +175,7 @@ export declare const joinClasses: (classes: Array<string>) => string;
|
|
|
148
175
|
* Useful for auto-fix operations.
|
|
149
176
|
*/
|
|
150
177
|
export declare const formatAsArray: (classes: Array<string>) => string;
|
|
178
|
+
/**
|
|
179
|
+
* Create a quoted string with the same quote style as the original.
|
|
180
|
+
*/
|
|
181
|
+
export declare const quoteString: (value: string, originalNode: TSESTree.Literal) => string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.formatAsArray = exports.joinClasses = exports.splitClasses = exports.findClassnameStringsInAttribute = exports.findClassnameStringsInCall = exports.getCalleeName = exports.isClassnameAttribute = exports.isClassnameVariable = exports.isTailwindFunction = exports.CLASSNAME_ATTRIBUTES = exports.TAILWIND_FUNCTIONS = void 0;
|
|
3
|
+
exports.quoteString = exports.formatAsArray = exports.joinClasses = exports.splitClasses = exports.findClassnameStringsInVariable = exports.findClassnameStringsInAttribute = exports.findClassnameStringsInCall = exports.getCalleeName = exports.isClassnameAttribute = exports.isClassnameVariable = exports.isTailwindFunction = exports.CLASSNAME_ATTRIBUTES = exports.CLASSNAME_VARIABLE_SUFFIXES = exports.TAILWIND_FUNCTIONS = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
// =============================================================================
|
|
6
6
|
// Constants
|
|
@@ -9,8 +9,8 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
9
9
|
* Function names that accept Tailwind class strings as arguments.
|
|
10
10
|
* These are commonly used for class merging and conditional class application.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Kept in sync with Prettier's Tailwind CSS plugin config by
|
|
13
|
+
* `classname-config-sync.spec.ts`.
|
|
14
14
|
*/
|
|
15
15
|
exports.TAILWIND_FUNCTIONS = [
|
|
16
16
|
"cva",
|
|
@@ -25,10 +25,17 @@ exports.TAILWIND_FUNCTIONS = [
|
|
|
25
25
|
"classNames",
|
|
26
26
|
];
|
|
27
27
|
/**
|
|
28
|
-
* Variable name
|
|
28
|
+
* Variable name suffixes that indicate a classname value.
|
|
29
29
|
* Matches variables like: containerClass, buttonClassName, rootClasses
|
|
30
|
+
*
|
|
31
|
+
* Kept in sync with the VS Code `tailwindCSS.experimental.classRegex` setting by
|
|
32
|
+
* `classname-config-sync.spec.ts`, since Prettier has no equivalent option to check against.
|
|
33
|
+
*/
|
|
34
|
+
exports.CLASSNAME_VARIABLE_SUFFIXES = ["Class", "ClassName", "Classes"];
|
|
35
|
+
/**
|
|
36
|
+
* Variable name patterns that indicate a classname value, derived from CLASSNAME_VARIABLE_SUFFIXES.
|
|
30
37
|
*/
|
|
31
|
-
const CLASSNAME_VARIABLE_PATTERNS =
|
|
38
|
+
const CLASSNAME_VARIABLE_PATTERNS = exports.CLASSNAME_VARIABLE_SUFFIXES.map(suffix => new RegExp(`${suffix}$`));
|
|
32
39
|
/**
|
|
33
40
|
* JSX attribute names that contain classname values
|
|
34
41
|
*/
|
|
@@ -337,6 +344,49 @@ const extractFromJsxAttribute = (node) => {
|
|
|
337
344
|
return [];
|
|
338
345
|
};
|
|
339
346
|
// =============================================================================
|
|
347
|
+
// Variable Declaration Extraction
|
|
348
|
+
// =============================================================================
|
|
349
|
+
/**
|
|
350
|
+
* Extract classname locations from a variable declaration with a classname-like name.
|
|
351
|
+
*
|
|
352
|
+
* Example: const buttonClass = "flex items-center";
|
|
353
|
+
*/
|
|
354
|
+
const extractFromVariableDeclarator = (node) => {
|
|
355
|
+
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
const varName = node.id.name;
|
|
359
|
+
if (!(0, exports.isClassnameVariable)(varName)) {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
if (!node.init) {
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
const context = {
|
|
366
|
+
type: "variable",
|
|
367
|
+
variableName: varName,
|
|
368
|
+
};
|
|
369
|
+
// Handle string literal
|
|
370
|
+
const extracted = extractStringValue(node.init);
|
|
371
|
+
if (extracted) {
|
|
372
|
+
return [
|
|
373
|
+
{
|
|
374
|
+
value: extracted.value,
|
|
375
|
+
reportNode: node.init,
|
|
376
|
+
fixNode: extracted.node,
|
|
377
|
+
context,
|
|
378
|
+
},
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
// Handle array of strings
|
|
382
|
+
if (node.init.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
383
|
+
return extractFromArray(node.init, context);
|
|
384
|
+
}
|
|
385
|
+
// Note: Function calls in variable initializers are handled separately
|
|
386
|
+
// by the CallExpression handler
|
|
387
|
+
return [];
|
|
388
|
+
};
|
|
389
|
+
// =============================================================================
|
|
340
390
|
// Unified Entry Points
|
|
341
391
|
// =============================================================================
|
|
342
392
|
/**
|
|
@@ -383,6 +433,28 @@ const findClassnameStringsInAttribute = (node) => {
|
|
|
383
433
|
return extractFromJsxAttribute(node);
|
|
384
434
|
};
|
|
385
435
|
exports.findClassnameStringsInAttribute = findClassnameStringsInAttribute;
|
|
436
|
+
/**
|
|
437
|
+
* Find all classname string locations in a VariableDeclarator node.
|
|
438
|
+
*
|
|
439
|
+
* Use this in your ESLint rule's VariableDeclarator handler to process
|
|
440
|
+
* variables with classname-like names.
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```typescript
|
|
444
|
+
* VariableDeclarator(node) {
|
|
445
|
+
* const locations = findClassnameStringsInVariable(node);
|
|
446
|
+
* for (const location of locations) {
|
|
447
|
+
* if (hasBannedPattern(location.value)) {
|
|
448
|
+
* context.report({ node: location.reportNode, ... });
|
|
449
|
+
* }
|
|
450
|
+
* }
|
|
451
|
+
* }
|
|
452
|
+
* ```
|
|
453
|
+
*/
|
|
454
|
+
const findClassnameStringsInVariable = (node) => {
|
|
455
|
+
return extractFromVariableDeclarator(node);
|
|
456
|
+
};
|
|
457
|
+
exports.findClassnameStringsInVariable = findClassnameStringsInVariable;
|
|
386
458
|
// =============================================================================
|
|
387
459
|
// Utility Helpers for Fixing
|
|
388
460
|
// =============================================================================
|
|
@@ -409,4 +481,21 @@ const formatAsArray = (classes) => {
|
|
|
409
481
|
return JSON.stringify(classes);
|
|
410
482
|
};
|
|
411
483
|
exports.formatAsArray = formatAsArray;
|
|
484
|
+
/**
|
|
485
|
+
* Get the quote character used in a string literal.
|
|
486
|
+
*/
|
|
487
|
+
const getQuoteChar = (node) => {
|
|
488
|
+
if (typeof node.value === "string" && node.raw) {
|
|
489
|
+
return node.raw.charAt(0);
|
|
490
|
+
}
|
|
491
|
+
return '"';
|
|
492
|
+
};
|
|
493
|
+
/**
|
|
494
|
+
* Create a quoted string with the same quote style as the original.
|
|
495
|
+
*/
|
|
496
|
+
const quoteString = (value, originalNode) => {
|
|
497
|
+
const quote = getQuoteChar(originalNode);
|
|
498
|
+
return `${quote}${value}${quote}`;
|
|
499
|
+
};
|
|
500
|
+
exports.quoteString = quoteString;
|
|
412
501
|
//# sourceMappingURL=classname-utils.js.map
|