@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 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 `twMerge()` or `cvaMerge()` instead. |
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 `cvaMerge` base classes when more than one class is used. |
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.129",
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.32",
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: "Import our wrappers from, @trackunit/css-class-variance-utilities or twMerge instead. [cva -> cvaMerge], [cx -> twMerge]",
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 cvaMerge base styles to arrays if more than one class is used.
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 = cvaMerge("text-danger-600 mb-4 font-semibold");
9
- * const cvaMyComponent = cvaMerge(["text-danger-600 mb-4","font-semibold"]);
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 = cvaMerge(["text-danger-600", "mb-4", "font-semibold"]);
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
- const isCvaMergeBaseContext = (context) => {
8
- if (context.type === "function-call") {
9
- return context.functionName === "cvaMerge" && context.argumentIndex === 0;
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 === "array-element" && context.parentContext.type === "function-call") {
12
- return context.parentContext.functionName === "cvaMerge" && context.parentContext.argumentIndex === 0;
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
- * ESLint rule that converts cvaMerge base styles to arrays if more than one class is used.
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 = cvaMerge("text-danger-600 mb-4 font-semibold");
25
- * const cvaMyComponent = cvaMerge(["text-danger-600 mb-4","font-semibold"]);
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 = cvaMerge(["text-danger-600", "mb-4", "font-semibold"]);
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 cvaMerge base styles to arrays if more than one class is used.",
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: "[cvaMerge] The first argument of cvaMerge should be an array if multiple classes are used.",
39
- arrayNeedsSplit: "[cvaMerge] Only one class per array item is allowed.",
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 firstArg = node.arguments[0];
48
- if (!firstArg) {
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 => isCvaMergeBaseContext(location.context))
96
+ .filter(location => isBaseContext(location.context))
53
97
  .filter(isLiteralLocation);
54
- // Handle string literal: cvaMerge("class1 class2")
55
- if (firstArg.type === utils_1.AST_NODE_TYPES.Literal) {
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
- // Handle array: cvaMerge(["class1 class2", "class3"])
73
- if (firstArg.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
74
- const hasMultiClassElement = baseClassLocations.some(location => location.value.includes(" "));
75
- if (hasMultiClassElement) {
76
- context.report({
77
- node,
78
- messageId: "arrayNeedsSplit",
79
- fix(fixer) {
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(firstArg, (0, css_classname_utils_1.formatAsArray)(allClasses));
129
+ return fixer.replaceText(baseNode, (0, css_classname_utils_1.formatAsArray)(allClasses));
82
130
  },
83
- });
84
- }
131
+ });
85
132
  }
86
133
  },
87
134
  };
@@ -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 twMerge() for simple inline merging or make a cvaMerge() function if it has variants.",
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
  },