@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/eslint-plugin-trackunit",
3
- "version": "0.6.82",
3
+ "version": "0.6.84",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "repository": "https://github.com/Trackunit/manager",
6
6
  "engines": {
@@ -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 && typeof firstArg.value === "string") {
45
- const value = firstArg.value;
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(firstArg, (0, classname_utils_1.formatAsArray)((0, classname_utils_1.splitClasses)(value)));
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 = firstArg.elements.some(element => element?.type === utils_1.AST_NODE_TYPES.Literal &&
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
- // Extract all string values and split them into individual classes
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
  });
@@ -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
- * Corresponds to the `tailwindFunctions` config in .prettierrc for Prettier's
75
- * Tailwind CSS plugin.
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
- * Corresponds to the `tailwindFunctions` config in .prettierrc for Prettier's
13
- * Tailwind CSS plugin.
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 patterns that indicate a classname value.
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 = [/Class$/, /ClassName$/, /Classes$/];
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