@trackunit/eslint-plugin-trackunit 0.6.96-alpha-893dae34b91.0 → 0.6.96
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 +7 -0
- package/package.json +2 -1
- package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.js +5 -5
- package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js +3 -3
- package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.js +9 -26
- package/src/lib/utils/classname-utils.d.ts +0 -181
- package/src/lib/utils/classname-utils.js +0 -501
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/eslint-plugin-trackunit",
|
|
3
|
-
"version": "0.6.96
|
|
3
|
+
"version": "0.6.96",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
5
5
|
"repository": "https://github.com/Trackunit/manager",
|
|
6
6
|
"engines": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@nx/eslint-plugin": "23.1.0",
|
|
11
|
+
"@trackunit/css-classname-utils": "0.0.2",
|
|
11
12
|
"@typescript-eslint/eslint-plugin": "8.58.1",
|
|
12
13
|
"@typescript-eslint/utils": "8.58.1",
|
|
13
14
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.cvaMergeBaseClassesAsArray = void 0;
|
|
4
|
+
const css_classname_utils_1 = require("@trackunit/css-classname-utils");
|
|
4
5
|
const utils_1 = require("@typescript-eslint/utils");
|
|
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
7
|
const isCvaMergeBaseContext = (context) => {
|
|
8
8
|
if (context.type === "function-call") {
|
|
@@ -48,7 +48,7 @@ exports.cvaMergeBaseClassesAsArray = createRule({
|
|
|
48
48
|
if (!firstArg) {
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
|
-
const baseClassLocations = (0,
|
|
51
|
+
const baseClassLocations = (0, css_classname_utils_1.findClassnameStringsInCall)(node)
|
|
52
52
|
.filter(location => isCvaMergeBaseContext(location.context))
|
|
53
53
|
.filter(isLiteralLocation);
|
|
54
54
|
// Handle string literal: cvaMerge("class1 class2")
|
|
@@ -63,7 +63,7 @@ exports.cvaMergeBaseClassesAsArray = createRule({
|
|
|
63
63
|
node,
|
|
64
64
|
messageId: "stringNeedsArray",
|
|
65
65
|
fix(fixer) {
|
|
66
|
-
return fixer.replaceText(baseClassLocation.fixNode, (0,
|
|
66
|
+
return fixer.replaceText(baseClassLocation.fixNode, (0, css_classname_utils_1.formatAsArray)((0, css_classname_utils_1.splitClasses)(value)));
|
|
67
67
|
},
|
|
68
68
|
});
|
|
69
69
|
}
|
|
@@ -77,8 +77,8 @@ exports.cvaMergeBaseClassesAsArray = createRule({
|
|
|
77
77
|
node,
|
|
78
78
|
messageId: "arrayNeedsSplit",
|
|
79
79
|
fix(fixer) {
|
|
80
|
-
const allClasses = baseClassLocations.flatMap(location => (0,
|
|
81
|
-
return fixer.replaceText(firstArg, (0,
|
|
80
|
+
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));
|
|
82
82
|
},
|
|
83
83
|
});
|
|
84
84
|
}
|
package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.noTemplateStringsInClassName = void 0;
|
|
4
|
+
const css_classname_utils_1 = require("@trackunit/css-classname-utils");
|
|
4
5
|
const utils_1 = require("@typescript-eslint/utils");
|
|
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
7
|
/**
|
|
8
8
|
* ESLint rule to disallow template strings in className prop.
|
|
@@ -40,7 +40,7 @@ exports.noTemplateStringsInClassName = createRule({
|
|
|
40
40
|
*/
|
|
41
41
|
JSXAttribute(node) {
|
|
42
42
|
// Check if this is a classname attribute (className or class)
|
|
43
|
-
if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier || !(0,
|
|
43
|
+
if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier || !(0, css_classname_utils_1.isClassnameAttribute)(node.name.name)) {
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
// Check if the value is a template literal in a JSX expression container
|
|
@@ -50,7 +50,7 @@ exports.noTemplateStringsInClassName = createRule({
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
VariableDeclarator(node) {
|
|
53
|
-
const locations = (0,
|
|
53
|
+
const locations = (0, css_classname_utils_1.findClassnameStringsInVariable)(node);
|
|
54
54
|
for (const location of locations) {
|
|
55
55
|
if (location.fixNode.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
|
|
56
56
|
context.report({ node: location.reportNode, messageId: "templateStringInClassName" });
|
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.requireClassnameAlternatives = void 0;
|
|
4
|
+
const css_classname_utils_1 = require("@trackunit/css-classname-utils");
|
|
4
5
|
const utils_1 = require("@typescript-eslint/utils");
|
|
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
|
-
/**
|
|
8
|
-
* Replace a specific pattern in a string with its replacement.
|
|
9
|
-
* Handles both prefix-based patterns (e.g., bg-red-500 -> bg-danger-500)
|
|
10
|
-
* and exact matches.
|
|
11
|
-
*/
|
|
12
|
-
function replacePattern(value, oldPattern, newPattern, hasPrefixes) {
|
|
13
|
-
if (hasPrefixes) {
|
|
14
|
-
// With prefixes, replace -oldPattern- with -newPattern-
|
|
15
|
-
const pattern = new RegExp(`-${oldPattern}(-|$|/)`, "g");
|
|
16
|
-
return value.replace(pattern, `-${newPattern}$1`);
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
// Without prefixes, replace exact word boundaries
|
|
20
|
-
const pattern = new RegExp(`(^|\\s)${oldPattern}($|\\s)`, "g");
|
|
21
|
-
return value.replace(pattern, `$1${newPattern}$2`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
7
|
/**
|
|
25
8
|
* Build a regex pattern that matches class names based on configuration.
|
|
26
9
|
*
|
|
@@ -118,8 +101,8 @@ exports.requireClassnameAlternatives = createRule({
|
|
|
118
101
|
let result = value;
|
|
119
102
|
for (const [pattern, alts] of Object.entries(alternatives)) {
|
|
120
103
|
const singleAlternative = alts.length === 1 ? alts[0] : undefined;
|
|
121
|
-
if (singleAlternative) {
|
|
122
|
-
result =
|
|
104
|
+
if (singleAlternative !== undefined) {
|
|
105
|
+
result = (0, css_classname_utils_1.replaceClassToken)(result, pattern, singleAlternative, { prefixes });
|
|
123
106
|
}
|
|
124
107
|
}
|
|
125
108
|
return result;
|
|
@@ -175,7 +158,7 @@ exports.requireClassnameAlternatives = createRule({
|
|
|
175
158
|
if (typeof originalValue !== "string") {
|
|
176
159
|
return null;
|
|
177
160
|
}
|
|
178
|
-
const fixedValue =
|
|
161
|
+
const fixedValue = (0, css_classname_utils_1.replaceClassToken)(originalValue, oldPattern, newPattern, { prefixes });
|
|
179
162
|
const raw = stringLiteralNode.raw;
|
|
180
163
|
const quote = raw.charAt(0);
|
|
181
164
|
return fixer.replaceText(stringLiteralNode, `${quote}${fixedValue}${quote}`);
|
|
@@ -187,7 +170,7 @@ exports.requireClassnameAlternatives = createRule({
|
|
|
187
170
|
? quasi.value.raw.includes(`-${oldPattern}`)
|
|
188
171
|
: quasi.value.raw.includes(oldPattern);
|
|
189
172
|
if (containsPattern) {
|
|
190
|
-
const fixedRaw =
|
|
173
|
+
const fixedRaw = (0, css_classname_utils_1.replaceClassToken)(quasi.value.raw, oldPattern, newPattern, { prefixes });
|
|
191
174
|
const rangeStart = quasi.range[0] + (quasi.tail ? 1 : 0);
|
|
192
175
|
const rangeEnd = quasi.range[1] - (quasi.tail ? 1 : 0);
|
|
193
176
|
fixes.push(fixer.replaceTextRange([rangeStart, rangeEnd], quasi.tail ? fixedRaw : fixedRaw + (quasi.value.raw.endsWith(" ") ? "" : "")));
|
|
@@ -222,7 +205,7 @@ exports.requireClassnameAlternatives = createRule({
|
|
|
222
205
|
if (!patternIsAutoFixable) {
|
|
223
206
|
suggest = patternAlternatives.map(alternative => ({
|
|
224
207
|
messageId: "suggestReplacement",
|
|
225
|
-
data: { replacement:
|
|
208
|
+
data: { replacement: (0, css_classname_utils_1.replaceClassToken)(usage.match, usage.pattern, alternative, { prefixes }) },
|
|
226
209
|
fix: createSuggestionFixer(stringLiteralNode, usage.pattern, alternative),
|
|
227
210
|
}));
|
|
228
211
|
}
|
|
@@ -254,19 +237,19 @@ exports.requireClassnameAlternatives = createRule({
|
|
|
254
237
|
}
|
|
255
238
|
return {
|
|
256
239
|
JSXAttribute(node) {
|
|
257
|
-
const locations = (0,
|
|
240
|
+
const locations = (0, css_classname_utils_1.findClassnameStringsInAttribute)(node);
|
|
258
241
|
for (const location of locations) {
|
|
259
242
|
checkAndReport(location.reportNode, location.value, location.fixNode);
|
|
260
243
|
}
|
|
261
244
|
},
|
|
262
245
|
CallExpression(node) {
|
|
263
|
-
const locations = (0,
|
|
246
|
+
const locations = (0, css_classname_utils_1.findClassnameStringsInCall)(node);
|
|
264
247
|
for (const location of locations) {
|
|
265
248
|
checkAndReport(location.reportNode, location.value, location.fixNode);
|
|
266
249
|
}
|
|
267
250
|
},
|
|
268
251
|
VariableDeclarator(node) {
|
|
269
|
-
const locations = (0,
|
|
252
|
+
const locations = (0, css_classname_utils_1.findClassnameStringsInVariable)(node);
|
|
270
253
|
for (const location of locations) {
|
|
271
254
|
checkAndReport(location.reportNode, location.value, location.fixNode);
|
|
272
255
|
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { TSESTree } from "@typescript-eslint/utils";
|
|
2
|
-
/**
|
|
3
|
-
* Utility functions for finding and analyzing Tailwind CSS classnames in ESLint rules.
|
|
4
|
-
*
|
|
5
|
-
* This module provides a centralized way to detect classname usage patterns across:
|
|
6
|
-
* - JSX `className` and `class` attributes
|
|
7
|
-
* - Variables named with `...Class` or `...ClassName` suffixes
|
|
8
|
-
* - Tailwind utility functions: `cvaMerge`, `twMerge`, `cva`, `tw`, `twx`, `tws`, `cn`, `clsx`, `classNames`, `twJoin`
|
|
9
|
-
*
|
|
10
|
-
* ## Core Concepts
|
|
11
|
-
*
|
|
12
|
-
* ### ClassnameLocation
|
|
13
|
-
* Represents a single location where a classname string value is found.
|
|
14
|
-
* Contains the string value, the AST node for reporting/fixing, and metadata.
|
|
15
|
-
*
|
|
16
|
-
* ### ClassnameContext
|
|
17
|
-
* Describes where a classname was found (JSX attribute, function call, variable, etc.)
|
|
18
|
-
*
|
|
19
|
-
* ## Usage in ESLint Rules
|
|
20
|
-
*
|
|
21
|
-
* ```typescript
|
|
22
|
-
* import { findClassnameStrings, TAILWIND_FUNCTIONS, isClassnameVariable } from "./classname-utils";
|
|
23
|
-
*
|
|
24
|
-
* // In a CallExpression handler:
|
|
25
|
-
* CallExpression(node) {
|
|
26
|
-
* const locations = findClassnameStrings(node);
|
|
27
|
-
* for (const location of locations) {
|
|
28
|
-
* // Check location.value for banned patterns
|
|
29
|
-
* // Report on location.reportNode
|
|
30
|
-
* // Fix using location.fixNode
|
|
31
|
-
* }
|
|
32
|
-
* }
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
/**
|
|
36
|
-
* Context describing where a classname was found
|
|
37
|
-
*/
|
|
38
|
-
export type ClassnameContext = {
|
|
39
|
-
type: "jsx-attribute";
|
|
40
|
-
attributeName: string;
|
|
41
|
-
} | {
|
|
42
|
-
type: "function-call";
|
|
43
|
-
functionName: string;
|
|
44
|
-
argumentIndex: number;
|
|
45
|
-
} | {
|
|
46
|
-
type: "cva-variant";
|
|
47
|
-
functionName: string;
|
|
48
|
-
variantPath: Array<string>;
|
|
49
|
-
} | {
|
|
50
|
-
type: "variable";
|
|
51
|
-
variableName: string;
|
|
52
|
-
} | {
|
|
53
|
-
type: "array-element";
|
|
54
|
-
parentContext: ClassnameContext;
|
|
55
|
-
elementIndex: number;
|
|
56
|
-
};
|
|
57
|
-
/**
|
|
58
|
-
* Represents a location where a classname string value is found
|
|
59
|
-
*/
|
|
60
|
-
export type ClassnameLocation = {
|
|
61
|
-
/** The extracted string value (for template literals, only static parts) */
|
|
62
|
-
value: string;
|
|
63
|
-
/** The AST node to use when reporting errors */
|
|
64
|
-
reportNode: TSESTree.Node;
|
|
65
|
-
/** The AST node to use when creating fixes (string literal or template literal) */
|
|
66
|
-
fixNode: TSESTree.Literal | TSESTree.TemplateLiteral;
|
|
67
|
-
/** Context describing where this classname was found */
|
|
68
|
-
context: ClassnameContext;
|
|
69
|
-
};
|
|
70
|
-
/**
|
|
71
|
-
* Function names that accept Tailwind class strings as arguments.
|
|
72
|
-
* These are commonly used for class merging and conditional class application.
|
|
73
|
-
*
|
|
74
|
-
* Kept in sync with Prettier's Tailwind CSS plugin config by
|
|
75
|
-
* `classname-config-sync.spec.ts`.
|
|
76
|
-
*/
|
|
77
|
-
export declare const TAILWIND_FUNCTIONS: readonly ["cva", "cvaMerge", "tw", "twx", "tws", "twMerge", "twJoin", "cn", "clsx", "classNames"];
|
|
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"];
|
|
87
|
-
/**
|
|
88
|
-
* JSX attribute names that contain classname values
|
|
89
|
-
*/
|
|
90
|
-
export declare const CLASSNAME_ATTRIBUTES: readonly ["className", "class"];
|
|
91
|
-
/**
|
|
92
|
-
* Check if a function name is a known Tailwind utility function
|
|
93
|
-
*/
|
|
94
|
-
export declare const isTailwindFunction: (name: string) => name is TailwindFunction;
|
|
95
|
-
/**
|
|
96
|
-
* Check if a variable name indicates it contains classnames
|
|
97
|
-
*/
|
|
98
|
-
export declare const isClassnameVariable: (name: string) => boolean;
|
|
99
|
-
/**
|
|
100
|
-
* Check if a JSX attribute name is a classname attribute
|
|
101
|
-
*/
|
|
102
|
-
export declare const isClassnameAttribute: (name: string) => boolean;
|
|
103
|
-
/**
|
|
104
|
-
* Get the function name from a CallExpression callee
|
|
105
|
-
*/
|
|
106
|
-
export declare const getCalleeName: (callee: TSESTree.CallExpression["callee"]) => string | null;
|
|
107
|
-
/**
|
|
108
|
-
* Find all classname string locations in a CallExpression node.
|
|
109
|
-
*
|
|
110
|
-
* Use this in your ESLint rule's CallExpression handler to process
|
|
111
|
-
* Tailwind utility function calls like twMerge, cvaMerge, etc.
|
|
112
|
-
*
|
|
113
|
-
* @example
|
|
114
|
-
* ```typescript
|
|
115
|
-
* CallExpression(node) {
|
|
116
|
-
* const locations = findClassnameStringsInCall(node);
|
|
117
|
-
* for (const location of locations) {
|
|
118
|
-
* if (hasBannedPattern(location.value)) {
|
|
119
|
-
* context.report({ node: location.reportNode, ... });
|
|
120
|
-
* }
|
|
121
|
-
* }
|
|
122
|
-
* }
|
|
123
|
-
* ```
|
|
124
|
-
*/
|
|
125
|
-
export declare const findClassnameStringsInCall: (node: TSESTree.CallExpression) => Array<ClassnameLocation>;
|
|
126
|
-
/**
|
|
127
|
-
* Find all classname string locations in a JSXAttribute node.
|
|
128
|
-
*
|
|
129
|
-
* Use this in your ESLint rule's JSXAttribute handler to process
|
|
130
|
-
* className and class attributes.
|
|
131
|
-
*
|
|
132
|
-
* @example
|
|
133
|
-
* ```typescript
|
|
134
|
-
* JSXAttribute(node) {
|
|
135
|
-
* const locations = findClassnameStringsInAttribute(node);
|
|
136
|
-
* for (const location of locations) {
|
|
137
|
-
* if (hasBannedPattern(location.value)) {
|
|
138
|
-
* context.report({ node: location.reportNode, ... });
|
|
139
|
-
* }
|
|
140
|
-
* }
|
|
141
|
-
* }
|
|
142
|
-
* ```
|
|
143
|
-
*/
|
|
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>;
|
|
164
|
-
/**
|
|
165
|
-
* Split a classname string into individual class names.
|
|
166
|
-
* Handles whitespace-separated classes.
|
|
167
|
-
*/
|
|
168
|
-
export declare const splitClasses: (value: string) => Array<string>;
|
|
169
|
-
/**
|
|
170
|
-
* Join class names into a single string.
|
|
171
|
-
*/
|
|
172
|
-
export declare const joinClasses: (classes: Array<string>) => string;
|
|
173
|
-
/**
|
|
174
|
-
* Create a JSON-formatted array string from an array of class names.
|
|
175
|
-
* Useful for auto-fix operations.
|
|
176
|
-
*/
|
|
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,501 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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
|
-
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
-
// =============================================================================
|
|
6
|
-
// Constants
|
|
7
|
-
// =============================================================================
|
|
8
|
-
/**
|
|
9
|
-
* Function names that accept Tailwind class strings as arguments.
|
|
10
|
-
* These are commonly used for class merging and conditional class application.
|
|
11
|
-
*
|
|
12
|
-
* Kept in sync with Prettier's Tailwind CSS plugin config by
|
|
13
|
-
* `classname-config-sync.spec.ts`.
|
|
14
|
-
*/
|
|
15
|
-
exports.TAILWIND_FUNCTIONS = [
|
|
16
|
-
"cva",
|
|
17
|
-
"cvaMerge",
|
|
18
|
-
"tw",
|
|
19
|
-
"twx",
|
|
20
|
-
"tws",
|
|
21
|
-
"twMerge",
|
|
22
|
-
"twJoin",
|
|
23
|
-
"cn",
|
|
24
|
-
"clsx",
|
|
25
|
-
"classNames",
|
|
26
|
-
];
|
|
27
|
-
/**
|
|
28
|
-
* Variable name suffixes that indicate a classname value.
|
|
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.
|
|
37
|
-
*/
|
|
38
|
-
const CLASSNAME_VARIABLE_PATTERNS = exports.CLASSNAME_VARIABLE_SUFFIXES.map(suffix => new RegExp(`${suffix}$`));
|
|
39
|
-
/**
|
|
40
|
-
* JSX attribute names that contain classname values
|
|
41
|
-
*/
|
|
42
|
-
exports.CLASSNAME_ATTRIBUTES = ["className", "class"];
|
|
43
|
-
// =============================================================================
|
|
44
|
-
// Detection Helpers
|
|
45
|
-
// =============================================================================
|
|
46
|
-
/**
|
|
47
|
-
* Check if a function name is a known Tailwind utility function
|
|
48
|
-
*/
|
|
49
|
-
const isTailwindFunction = (name) => {
|
|
50
|
-
return exports.TAILWIND_FUNCTIONS.some(fn => fn === name);
|
|
51
|
-
};
|
|
52
|
-
exports.isTailwindFunction = isTailwindFunction;
|
|
53
|
-
/**
|
|
54
|
-
* Check if a variable name indicates it contains classnames
|
|
55
|
-
*/
|
|
56
|
-
const isClassnameVariable = (name) => {
|
|
57
|
-
return CLASSNAME_VARIABLE_PATTERNS.some(pattern => pattern.test(name));
|
|
58
|
-
};
|
|
59
|
-
exports.isClassnameVariable = isClassnameVariable;
|
|
60
|
-
/**
|
|
61
|
-
* Check if a JSX attribute name is a classname attribute
|
|
62
|
-
*/
|
|
63
|
-
const isClassnameAttribute = (name) => {
|
|
64
|
-
return exports.CLASSNAME_ATTRIBUTES.some(attr => attr === name);
|
|
65
|
-
};
|
|
66
|
-
exports.isClassnameAttribute = isClassnameAttribute;
|
|
67
|
-
/**
|
|
68
|
-
* Get the function name from a CallExpression callee
|
|
69
|
-
*/
|
|
70
|
-
const getCalleeName = (callee) => {
|
|
71
|
-
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
72
|
-
return callee.name;
|
|
73
|
-
}
|
|
74
|
-
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression && callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
75
|
-
return callee.property.name;
|
|
76
|
-
}
|
|
77
|
-
return null;
|
|
78
|
-
};
|
|
79
|
-
exports.getCalleeName = getCalleeName;
|
|
80
|
-
// =============================================================================
|
|
81
|
-
// String Extraction
|
|
82
|
-
// =============================================================================
|
|
83
|
-
/**
|
|
84
|
-
* Extract a string value from a Literal node
|
|
85
|
-
*/
|
|
86
|
-
const extractFromLiteral = (node) => {
|
|
87
|
-
if (typeof node.value === "string") {
|
|
88
|
-
return { value: node.value, node };
|
|
89
|
-
}
|
|
90
|
-
return null;
|
|
91
|
-
};
|
|
92
|
-
/**
|
|
93
|
-
* Extract the static parts of a TemplateLiteral as a single string.
|
|
94
|
-
* Template expressions are replaced with a single space to preserve word boundaries.
|
|
95
|
-
*/
|
|
96
|
-
const extractFromTemplateLiteral = (node) => {
|
|
97
|
-
// Join static parts with spaces (representing where expressions would be)
|
|
98
|
-
const value = node.quasis.map(quasi => quasi.value.raw).join(" ");
|
|
99
|
-
return { value, node };
|
|
100
|
-
};
|
|
101
|
-
/**
|
|
102
|
-
* Extract string value from a node that might contain a classname string.
|
|
103
|
-
* Handles Literal and TemplateLiteral nodes.
|
|
104
|
-
*/
|
|
105
|
-
const extractStringValue = (node) => {
|
|
106
|
-
switch (node.type) {
|
|
107
|
-
case utils_1.AST_NODE_TYPES.Literal:
|
|
108
|
-
return extractFromLiteral(node);
|
|
109
|
-
case utils_1.AST_NODE_TYPES.TemplateLiteral:
|
|
110
|
-
return extractFromTemplateLiteral(node);
|
|
111
|
-
default:
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
// =============================================================================
|
|
116
|
-
// Array Traversal
|
|
117
|
-
// =============================================================================
|
|
118
|
-
/**
|
|
119
|
-
* Extract classname locations from an ArrayExpression.
|
|
120
|
-
* Handles arrays like: ["flex", "items-center", "bg-primary-500"]
|
|
121
|
-
*/
|
|
122
|
-
const extractFromArray = (node, parentContext) => {
|
|
123
|
-
const locations = [];
|
|
124
|
-
node.elements.forEach((element, index) => {
|
|
125
|
-
if (!element || element.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const extracted = extractStringValue(element);
|
|
129
|
-
if (extracted) {
|
|
130
|
-
locations.push({
|
|
131
|
-
value: extracted.value,
|
|
132
|
-
reportNode: element,
|
|
133
|
-
fixNode: extracted.node,
|
|
134
|
-
context: { type: "array-element", parentContext, elementIndex: index },
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
return locations;
|
|
139
|
-
};
|
|
140
|
-
// =============================================================================
|
|
141
|
-
// CVA/CVAMerge Object Traversal
|
|
142
|
-
// =============================================================================
|
|
143
|
-
/**
|
|
144
|
-
* Recursively extract classname locations from a CVA options object.
|
|
145
|
-
*
|
|
146
|
-
* CVA structure:
|
|
147
|
-
* ```
|
|
148
|
-
* cva(baseClasses, {
|
|
149
|
-
* variants: {
|
|
150
|
-
* variantName: {
|
|
151
|
-
* variantValue: "classnames here",
|
|
152
|
-
* }
|
|
153
|
-
* },
|
|
154
|
-
* compoundVariants: [
|
|
155
|
-
* { variantName: "value", className: "classnames" }
|
|
156
|
-
* ],
|
|
157
|
-
* defaultVariants: { ... }
|
|
158
|
-
* })
|
|
159
|
-
* ```
|
|
160
|
-
*/
|
|
161
|
-
const extractFromCvaObject = (node, functionName, currentPath = []) => {
|
|
162
|
-
const locations = [];
|
|
163
|
-
for (const property of node.properties) {
|
|
164
|
-
if (property.type !== utils_1.AST_NODE_TYPES.Property) {
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const keyName = getPropertyKeyName(property.key);
|
|
168
|
-
if (!keyName) {
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
const propertyPath = [...currentPath, keyName];
|
|
172
|
-
// Skip defaultVariants as it doesn't contain classnames
|
|
173
|
-
if (keyName === "defaultVariants") {
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
// Recurse into nested objects (variants, compoundVariants entries, etc.)
|
|
177
|
-
if (property.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
178
|
-
locations.push(...extractFromCvaObject(property.value, functionName, propertyPath));
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
// Handle arrays (compoundVariants array, or array of class strings)
|
|
182
|
-
if (property.value.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
183
|
-
// Check if this is an array of objects (like compoundVariants)
|
|
184
|
-
const hasObjectElements = property.value.elements.some(el => el?.type === utils_1.AST_NODE_TYPES.ObjectExpression);
|
|
185
|
-
if (hasObjectElements) {
|
|
186
|
-
// Recurse into each object in the array
|
|
187
|
-
property.value.elements.forEach((element, index) => {
|
|
188
|
-
if (element?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
189
|
-
locations.push(...extractFromCvaObject(element, functionName, [...propertyPath, `[${index}]`]));
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
// This is an array of class strings
|
|
195
|
-
const arrayContext = {
|
|
196
|
-
type: "cva-variant",
|
|
197
|
-
functionName,
|
|
198
|
-
variantPath: propertyPath,
|
|
199
|
-
};
|
|
200
|
-
locations.push(...extractFromArray(property.value, arrayContext));
|
|
201
|
-
}
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
// Extract string values at leaf nodes
|
|
205
|
-
const extracted = extractStringValue(property.value);
|
|
206
|
-
if (extracted) {
|
|
207
|
-
locations.push({
|
|
208
|
-
value: extracted.value,
|
|
209
|
-
reportNode: property.value,
|
|
210
|
-
fixNode: extracted.node,
|
|
211
|
-
context: {
|
|
212
|
-
type: "cva-variant",
|
|
213
|
-
functionName,
|
|
214
|
-
variantPath: propertyPath,
|
|
215
|
-
},
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return locations;
|
|
220
|
-
};
|
|
221
|
-
/**
|
|
222
|
-
* Get the name of an object property key
|
|
223
|
-
*/
|
|
224
|
-
const getPropertyKeyName = (key) => {
|
|
225
|
-
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
226
|
-
return key.name;
|
|
227
|
-
}
|
|
228
|
-
if (key.type === utils_1.AST_NODE_TYPES.Literal && typeof key.value === "string") {
|
|
229
|
-
return key.value;
|
|
230
|
-
}
|
|
231
|
-
return null;
|
|
232
|
-
};
|
|
233
|
-
// =============================================================================
|
|
234
|
-
// Function Call Extraction
|
|
235
|
-
// =============================================================================
|
|
236
|
-
/**
|
|
237
|
-
* Extract classname locations from a Tailwind function call.
|
|
238
|
-
*
|
|
239
|
-
* Handles various argument patterns:
|
|
240
|
-
* - String literals: twMerge("flex items-center")
|
|
241
|
-
* - Template literals: twMerge(`flex ${condition && "hidden"}`)
|
|
242
|
-
* - Arrays: cvaMerge(["flex", "items-center"], options)
|
|
243
|
-
* - CVA options objects: cva(base, { variants: { ... } })
|
|
244
|
-
*/
|
|
245
|
-
const extractFromFunctionCall = (node) => {
|
|
246
|
-
const calleeName = (0, exports.getCalleeName)(node.callee);
|
|
247
|
-
if (!calleeName || !(0, exports.isTailwindFunction)(calleeName)) {
|
|
248
|
-
return [];
|
|
249
|
-
}
|
|
250
|
-
const locations = [];
|
|
251
|
-
const isCvaStyle = calleeName === "cva" || calleeName === "cvaMerge";
|
|
252
|
-
node.arguments.forEach((arg, argIndex) => {
|
|
253
|
-
const baseContext = {
|
|
254
|
-
type: "function-call",
|
|
255
|
-
functionName: calleeName,
|
|
256
|
-
argumentIndex: argIndex,
|
|
257
|
-
};
|
|
258
|
-
// Handle string literals
|
|
259
|
-
const extracted = extractStringValue(arg);
|
|
260
|
-
if (extracted) {
|
|
261
|
-
locations.push({
|
|
262
|
-
value: extracted.value,
|
|
263
|
-
reportNode: arg,
|
|
264
|
-
fixNode: extracted.node,
|
|
265
|
-
context: baseContext,
|
|
266
|
-
});
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
// Handle array arguments
|
|
270
|
-
if (arg.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
271
|
-
locations.push(...extractFromArray(arg, baseContext));
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
// Handle CVA options object (typically second argument)
|
|
275
|
-
if (isCvaStyle && arg.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
276
|
-
locations.push(...extractFromCvaObject(arg, calleeName));
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
return locations;
|
|
280
|
-
};
|
|
281
|
-
// =============================================================================
|
|
282
|
-
// JSX Attribute Extraction
|
|
283
|
-
// =============================================================================
|
|
284
|
-
/**
|
|
285
|
-
* Extract classname locations from a JSX attribute (className or class).
|
|
286
|
-
*
|
|
287
|
-
* Handles various value patterns:
|
|
288
|
-
* - String literals: className="flex items-center"
|
|
289
|
-
* - Expression with string: className={"flex items-center"}
|
|
290
|
-
* - Template literals: className={`flex ${condition && "hidden"}`}
|
|
291
|
-
*/
|
|
292
|
-
const extractFromJsxAttribute = (node) => {
|
|
293
|
-
if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
294
|
-
return [];
|
|
295
|
-
}
|
|
296
|
-
const attrName = node.name.name;
|
|
297
|
-
if (!(0, exports.isClassnameAttribute)(attrName)) {
|
|
298
|
-
return [];
|
|
299
|
-
}
|
|
300
|
-
const attrValue = node.value;
|
|
301
|
-
if (!attrValue) {
|
|
302
|
-
return [];
|
|
303
|
-
}
|
|
304
|
-
const context = {
|
|
305
|
-
type: "jsx-attribute",
|
|
306
|
-
attributeName: attrName,
|
|
307
|
-
};
|
|
308
|
-
// Direct string literal: className="flex items-center"
|
|
309
|
-
if (attrValue.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
310
|
-
const extracted = extractFromLiteral(attrValue);
|
|
311
|
-
if (extracted) {
|
|
312
|
-
return [
|
|
313
|
-
{
|
|
314
|
-
value: extracted.value,
|
|
315
|
-
reportNode: node,
|
|
316
|
-
fixNode: extracted.node,
|
|
317
|
-
context,
|
|
318
|
-
},
|
|
319
|
-
];
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
// JSX Expression: className={...}
|
|
323
|
-
if (attrValue.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
324
|
-
const expression = attrValue.expression;
|
|
325
|
-
// Skip empty expressions
|
|
326
|
-
if (expression.type === utils_1.AST_NODE_TYPES.JSXEmptyExpression) {
|
|
327
|
-
return [];
|
|
328
|
-
}
|
|
329
|
-
// String literal in expression: className={"flex items-center"}
|
|
330
|
-
const extracted = extractStringValue(expression);
|
|
331
|
-
if (extracted) {
|
|
332
|
-
return [
|
|
333
|
-
{
|
|
334
|
-
value: extracted.value,
|
|
335
|
-
reportNode: node,
|
|
336
|
-
fixNode: extracted.node,
|
|
337
|
-
context,
|
|
338
|
-
},
|
|
339
|
-
];
|
|
340
|
-
}
|
|
341
|
-
// Note: We don't recurse into function calls here as they're handled
|
|
342
|
-
// separately by the CallExpression handler in the rule
|
|
343
|
-
}
|
|
344
|
-
return [];
|
|
345
|
-
};
|
|
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
|
-
// =============================================================================
|
|
390
|
-
// Unified Entry Points
|
|
391
|
-
// =============================================================================
|
|
392
|
-
/**
|
|
393
|
-
* Find all classname string locations in a CallExpression node.
|
|
394
|
-
*
|
|
395
|
-
* Use this in your ESLint rule's CallExpression handler to process
|
|
396
|
-
* Tailwind utility function calls like twMerge, cvaMerge, etc.
|
|
397
|
-
*
|
|
398
|
-
* @example
|
|
399
|
-
* ```typescript
|
|
400
|
-
* CallExpression(node) {
|
|
401
|
-
* const locations = findClassnameStringsInCall(node);
|
|
402
|
-
* for (const location of locations) {
|
|
403
|
-
* if (hasBannedPattern(location.value)) {
|
|
404
|
-
* context.report({ node: location.reportNode, ... });
|
|
405
|
-
* }
|
|
406
|
-
* }
|
|
407
|
-
* }
|
|
408
|
-
* ```
|
|
409
|
-
*/
|
|
410
|
-
const findClassnameStringsInCall = (node) => {
|
|
411
|
-
return extractFromFunctionCall(node);
|
|
412
|
-
};
|
|
413
|
-
exports.findClassnameStringsInCall = findClassnameStringsInCall;
|
|
414
|
-
/**
|
|
415
|
-
* Find all classname string locations in a JSXAttribute node.
|
|
416
|
-
*
|
|
417
|
-
* Use this in your ESLint rule's JSXAttribute handler to process
|
|
418
|
-
* className and class attributes.
|
|
419
|
-
*
|
|
420
|
-
* @example
|
|
421
|
-
* ```typescript
|
|
422
|
-
* JSXAttribute(node) {
|
|
423
|
-
* const locations = findClassnameStringsInAttribute(node);
|
|
424
|
-
* for (const location of locations) {
|
|
425
|
-
* if (hasBannedPattern(location.value)) {
|
|
426
|
-
* context.report({ node: location.reportNode, ... });
|
|
427
|
-
* }
|
|
428
|
-
* }
|
|
429
|
-
* }
|
|
430
|
-
* ```
|
|
431
|
-
*/
|
|
432
|
-
const findClassnameStringsInAttribute = (node) => {
|
|
433
|
-
return extractFromJsxAttribute(node);
|
|
434
|
-
};
|
|
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;
|
|
458
|
-
// =============================================================================
|
|
459
|
-
// Utility Helpers for Fixing
|
|
460
|
-
// =============================================================================
|
|
461
|
-
/**
|
|
462
|
-
* Split a classname string into individual class names.
|
|
463
|
-
* Handles whitespace-separated classes.
|
|
464
|
-
*/
|
|
465
|
-
const splitClasses = (value) => {
|
|
466
|
-
return value.split(/\s+/).filter(Boolean);
|
|
467
|
-
};
|
|
468
|
-
exports.splitClasses = splitClasses;
|
|
469
|
-
/**
|
|
470
|
-
* Join class names into a single string.
|
|
471
|
-
*/
|
|
472
|
-
const joinClasses = (classes) => {
|
|
473
|
-
return classes.join(" ");
|
|
474
|
-
};
|
|
475
|
-
exports.joinClasses = joinClasses;
|
|
476
|
-
/**
|
|
477
|
-
* Create a JSON-formatted array string from an array of class names.
|
|
478
|
-
* Useful for auto-fix operations.
|
|
479
|
-
*/
|
|
480
|
-
const formatAsArray = (classes) => {
|
|
481
|
-
return JSON.stringify(classes);
|
|
482
|
-
};
|
|
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;
|
|
501
|
-
//# sourceMappingURL=classname-utils.js.map
|