@trackunit/eslint-plugin-trackunit 0.6.129 β 0.6.130
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 +20 -0
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/lib/config/fragments/restricted-imports.js +5 -1
- package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.d.ts +12 -4
- package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.js +75 -28
- package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## 0.6.130 (2026-09-08)
|
|
2
|
+
|
|
3
|
+
### π Features
|
|
4
|
+
|
|
5
|
+
- β οΈ **css-class-variance-utilities:** replace cvaMerge with cva 1.0 ([#25413](https://github.com/Trackunit/manager/pull/25413), [#25396](https://github.com/Trackunit/manager/issues/25396), [#25410](https://github.com/Trackunit/manager/issues/25410), [#133](https://github.com/Trackunit/manager/issues/133))
|
|
6
|
+
|
|
7
|
+
### β οΈ Breaking Changes
|
|
8
|
+
|
|
9
|
+
- **css-class-variance-utilities:** replace cvaMerge with cva 1.0 ([#25413](https://github.com/Trackunit/manager/pull/25413), [#25396](https://github.com/Trackunit/manager/issues/25396), [#25410](https://github.com/Trackunit/manager/issues/25410), [#133](https://github.com/Trackunit/manager/issues/133))
|
|
10
|
+
|
|
11
|
+
### π§± Updated Dependencies
|
|
12
|
+
|
|
13
|
+
- Updated css-classname-utils to 0.0.33
|
|
14
|
+
- Updated shared-utils to 1.16.33
|
|
15
|
+
|
|
16
|
+
### β€οΈ Thank You
|
|
17
|
+
|
|
18
|
+
- Cursor @cursoragent
|
|
19
|
+
- Michael Buss RΓΈnne @man-trackunit
|
|
20
|
+
|
|
1
21
|
## 0.6.129 (2026-09-08)
|
|
2
22
|
|
|
3
23
|
### π§± Updated Dependencies
|
package/README.md
CHANGED
|
@@ -54,9 +54,9 @@ All custom rules are enabled automatically through the presets under the `@track
|
|
|
54
54
|
|
|
55
55
|
| Rule | Severity | Auto-fix | Description |
|
|
56
56
|
| ------------------------------------------------------ | -------- | -------- | ------------------------------------------------------------------------------------------ |
|
|
57
|
-
| `@trackunit/no-template-strings-in-classname-prop` | β | β | Disallows template literals in `className`. Use `
|
|
57
|
+
| `@trackunit/no-template-strings-in-classname-prop` | β | β | Disallows template literals in `className`. Use `cx()` or `cva()` instead. |
|
|
58
58
|
| `@trackunit/require-classname-alternatives` | β | yes | Replaces banned class names with approved alternatives. |
|
|
59
|
-
| `@trackunit/cva-merge-base-classes-as-array` | β | yes | Enforces array syntax for `
|
|
59
|
+
| `@trackunit/cva-merge-base-classes-as-array` | β | yes | Enforces array syntax for `cva` base classes when more than one class is used. |
|
|
60
60
|
| `@trackunit/design-guideline-button-icon-size-match` | β | yes | Ensures `Icon` inside `Button` uses the correct `size` prop. |
|
|
61
61
|
| `@trackunit/prefer-field-components` | β | yes | Prefers `SelectField`/`TextField` over manually combining `Label` + base input components. |
|
|
62
62
|
| `@trackunit/prefer-mouse-event-handler-in-react-props` | β | β | Enforces `MouseEventHandler<T>` typing for `onClick*` props. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/eslint-plugin-trackunit",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.130",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
5
5
|
"repository": "https://github.com/Trackunit/manager",
|
|
6
6
|
"engines": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@nx/eslint-plugin": "23.1.0",
|
|
11
|
-
"@trackunit/css-classname-utils": "0.0.
|
|
11
|
+
"@trackunit/css-classname-utils": "0.0.33",
|
|
12
12
|
"@typescript-eslint/eslint-plugin": "8.58.1",
|
|
13
13
|
"@typescript-eslint/utils": "8.58.1",
|
|
14
14
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -50,9 +50,13 @@ exports.restrictedImportRules = {
|
|
|
50
50
|
name: "@heroicons/react/24/outline",
|
|
51
51
|
message: "Please use the Icon component from react-components instead.",
|
|
52
52
|
},
|
|
53
|
+
{
|
|
54
|
+
name: "cva",
|
|
55
|
+
message: "Import cva/cx from @trackunit/css-class-variance-utilities instead. Ours are bound to a config whose onComplete hook runs every generated class string through tailwind-merge; the raw ones only concatenate, so conflicting utilities would both survive.",
|
|
56
|
+
},
|
|
53
57
|
{
|
|
54
58
|
name: "class-variance-authority",
|
|
55
|
-
message: "
|
|
59
|
+
message: "Removed β import cva/cx from @trackunit/css-class-variance-utilities instead. The old package is no longer a dependency.",
|
|
56
60
|
},
|
|
57
61
|
],
|
|
58
62
|
patterns: [
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
2
|
type MessageIds = "stringNeedsArray" | "arrayNeedsSplit";
|
|
3
3
|
/**
|
|
4
|
-
* ESLint rule that converts
|
|
4
|
+
* ESLint rule that converts cva base styles to arrays if more than one class is used.
|
|
5
|
+
*
|
|
6
|
+
* One class per array item keeps diffs and merge conflicts to the class that
|
|
7
|
+
* actually changed, and lets tailwind-merge see each utility individually.
|
|
8
|
+
*
|
|
9
|
+
* The export and published rule id keep the historical `cva-merge-β¦` name
|
|
10
|
+
* (GLU-2173). Renaming would be a breaking change for every consumer
|
|
11
|
+
* `eslint.config` that lists `@trackunit/cva-merge-base-classes-as-array`;
|
|
12
|
+
* the implementation only inspects `cva({ base })`.
|
|
5
13
|
*
|
|
6
14
|
* @example
|
|
7
15
|
* Invalid:
|
|
8
|
-
* const cvaMyComponent =
|
|
9
|
-
* const cvaMyComponent =
|
|
16
|
+
* const cvaMyComponent = cva({ base: "text-danger-600 mb-4 font-semibold" });
|
|
17
|
+
* const cvaMyComponent = cva({ base: ["text-danger-600 mb-4","font-semibold"] });
|
|
10
18
|
* Valid:
|
|
11
|
-
* const cvaMyComponent =
|
|
19
|
+
* const cvaMyComponent = cva({ base: ["text-danger-600", "mb-4", "font-semibold"] });
|
|
12
20
|
*/
|
|
13
21
|
export declare const cvaMergeBaseClassesAsArray: ESLintUtils.RuleModule<MessageIds, [], unknown, ESLintUtils.RuleListener> & {
|
|
14
22
|
name: string;
|
|
@@ -4,39 +4,83 @@ exports.cvaMergeBaseClassesAsArray = void 0;
|
|
|
4
4
|
const css_classname_utils_1 = require("@trackunit/css-classname-utils");
|
|
5
5
|
const utils_1 = require("@typescript-eslint/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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
/** Base classes live under `base` in a cva 1.0 config. */
|
|
8
|
+
const isBaseContext = (context) => {
|
|
9
|
+
if (context.type === "array-element") {
|
|
10
|
+
return isBaseContext(context.parentContext);
|
|
10
11
|
}
|
|
11
|
-
if (context.type === "
|
|
12
|
-
return context.
|
|
12
|
+
if (context.type === "cva-variant") {
|
|
13
|
+
return context.functionName === "cva" && context.variantPath.length === 1 && context.variantPath[0] === "base";
|
|
13
14
|
}
|
|
14
15
|
return false;
|
|
15
16
|
};
|
|
17
|
+
const propertyKeyName = (key) => {
|
|
18
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
19
|
+
return key.name;
|
|
20
|
+
}
|
|
21
|
+
if (key.type === utils_1.AST_NODE_TYPES.Literal && typeof key.value === "string") {
|
|
22
|
+
return key.value;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The node holding the base classes on a `cva({ base, ... })` call.
|
|
28
|
+
*/
|
|
29
|
+
const findBaseNode = (node) => {
|
|
30
|
+
const calleeName = (0, css_classname_utils_1.getCalleeName)(node.callee);
|
|
31
|
+
if (calleeName !== "cva") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const firstArgument = node.arguments[0];
|
|
35
|
+
if (!firstArgument || firstArgument.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const baseProperty = firstArgument.properties.find((property) => property.type === utils_1.AST_NODE_TYPES.Property && propertyKeyName(property.key) === "base");
|
|
39
|
+
const baseValue = baseProperty?.value;
|
|
40
|
+
if (baseValue?.type === utils_1.AST_NODE_TYPES.Literal || baseValue?.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
41
|
+
return baseValue;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
};
|
|
16
45
|
const isLiteralLocation = (location) => {
|
|
17
46
|
return location.fixNode.type === utils_1.AST_NODE_TYPES.Literal && typeof location.fixNode.value === "string";
|
|
18
47
|
};
|
|
48
|
+
const isStringLiteral = (element) => element !== null && element.type === utils_1.AST_NODE_TYPES.Literal && typeof element.value === "string";
|
|
19
49
|
/**
|
|
20
|
-
*
|
|
50
|
+
* `formatAsArray(allClasses)` below rebuilds the array from string literals only, then replaces
|
|
51
|
+
* the *whole* array node -- so a non-literal element (a spread, an identifier, a conditional, ...)
|
|
52
|
+
* would be silently dropped. Report but don't offer a fix in that case instead.
|
|
53
|
+
*/
|
|
54
|
+
const hasNonLiteralElement = (node) => node.type === utils_1.AST_NODE_TYPES.ArrayExpression && node.elements.some(element => !isStringLiteral(element));
|
|
55
|
+
/**
|
|
56
|
+
* ESLint rule that converts cva base styles to arrays if more than one class is used.
|
|
57
|
+
*
|
|
58
|
+
* One class per array item keeps diffs and merge conflicts to the class that
|
|
59
|
+
* actually changed, and lets tailwind-merge see each utility individually.
|
|
60
|
+
*
|
|
61
|
+
* The export and published rule id keep the historical `cva-merge-β¦` name
|
|
62
|
+
* (GLU-2173). Renaming would be a breaking change for every consumer
|
|
63
|
+
* `eslint.config` that lists `@trackunit/cva-merge-base-classes-as-array`;
|
|
64
|
+
* the implementation only inspects `cva({ base })`.
|
|
21
65
|
*
|
|
22
66
|
* @example
|
|
23
67
|
* Invalid:
|
|
24
|
-
* const cvaMyComponent =
|
|
25
|
-
* const cvaMyComponent =
|
|
68
|
+
* const cvaMyComponent = cva({ base: "text-danger-600 mb-4 font-semibold" });
|
|
69
|
+
* const cvaMyComponent = cva({ base: ["text-danger-600 mb-4","font-semibold"] });
|
|
26
70
|
* Valid:
|
|
27
|
-
* const cvaMyComponent =
|
|
71
|
+
* const cvaMyComponent = cva({ base: ["text-danger-600", "mb-4", "font-semibold"] });
|
|
28
72
|
*/
|
|
29
73
|
exports.cvaMergeBaseClassesAsArray = createRule({
|
|
30
74
|
name: "cva-merge-base-classes-as-array",
|
|
31
75
|
meta: {
|
|
32
76
|
type: "layout",
|
|
33
77
|
docs: {
|
|
34
|
-
description: "Convert
|
|
78
|
+
description: "Convert cva base styles to arrays if more than one class is used.",
|
|
35
79
|
},
|
|
36
80
|
fixable: "code",
|
|
37
81
|
messages: {
|
|
38
|
-
stringNeedsArray: "[
|
|
39
|
-
arrayNeedsSplit: "[
|
|
82
|
+
stringNeedsArray: "[cva] Base classes should be an array if multiple classes are used.",
|
|
83
|
+
arrayNeedsSplit: "[cva] Only one class per array item is allowed.",
|
|
40
84
|
},
|
|
41
85
|
schema: [],
|
|
42
86
|
},
|
|
@@ -44,15 +88,15 @@ exports.cvaMergeBaseClassesAsArray = createRule({
|
|
|
44
88
|
create(context) {
|
|
45
89
|
return {
|
|
46
90
|
CallExpression(node) {
|
|
47
|
-
const
|
|
48
|
-
if (!
|
|
91
|
+
const baseNode = findBaseNode(node);
|
|
92
|
+
if (!baseNode) {
|
|
49
93
|
return;
|
|
50
94
|
}
|
|
51
95
|
const baseClassLocations = (0, css_classname_utils_1.findClassnameStringsInCall)(node)
|
|
52
|
-
.filter(location =>
|
|
96
|
+
.filter(location => isBaseContext(location.context))
|
|
53
97
|
.filter(isLiteralLocation);
|
|
54
|
-
//
|
|
55
|
-
if (
|
|
98
|
+
// A single string: cva({ base: "class1 class2" })
|
|
99
|
+
if (baseNode.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
56
100
|
const baseClassLocation = baseClassLocations[0];
|
|
57
101
|
if (!baseClassLocation) {
|
|
58
102
|
return;
|
|
@@ -69,19 +113,22 @@ exports.cvaMergeBaseClassesAsArray = createRule({
|
|
|
69
113
|
}
|
|
70
114
|
return;
|
|
71
115
|
}
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
116
|
+
// An array: cva({ base: [...] })
|
|
117
|
+
const hasMultiClassElement = baseClassLocations.some(location => location.value.includes(" "));
|
|
118
|
+
if (hasMultiClassElement) {
|
|
119
|
+
context.report({
|
|
120
|
+
node,
|
|
121
|
+
messageId: "arrayNeedsSplit",
|
|
122
|
+
// A non-literal element (spread, identifier, conditional, ...) can't be rebuilt by
|
|
123
|
+
// formatAsArray, and offering a fix that silently drops it would be worse than
|
|
124
|
+
// reporting without one.
|
|
125
|
+
fix: hasNonLiteralElement(baseNode)
|
|
126
|
+
? null
|
|
127
|
+
: fixer => {
|
|
80
128
|
const allClasses = baseClassLocations.flatMap(location => (0, css_classname_utils_1.splitClasses)(location.value));
|
|
81
|
-
return fixer.replaceText(
|
|
129
|
+
return fixer.replaceText(baseNode, (0, css_classname_utils_1.formatAsArray)(allClasses));
|
|
82
130
|
},
|
|
83
|
-
|
|
84
|
-
}
|
|
131
|
+
});
|
|
85
132
|
}
|
|
86
133
|
},
|
|
87
134
|
};
|
package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js
CHANGED
|
@@ -24,7 +24,7 @@ exports.noTemplateStringsInClassName = createRule({
|
|
|
24
24
|
description: "Disallow the use of template strings in className prop to encourage better class management.",
|
|
25
25
|
},
|
|
26
26
|
messages: {
|
|
27
|
-
templateStringInClassName: "Do not use template strings in className prop. If you wish to merge classes from different sources, use
|
|
27
|
+
templateStringInClassName: "Do not use template strings in className prop. If you wish to merge classes from different sources, use cx() for simple inline merging or make a cva() function if it has variants.",
|
|
28
28
|
},
|
|
29
29
|
schema: [],
|
|
30
30
|
},
|