tailwind-typescript-plugin 1.4.0-beta.23 → 1.4.0-beta.25
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 +30 -0
- package/lib/core/interfaces.d.ts +17 -1
- package/lib/core/interfaces.d.ts.map +1 -1
- package/lib/extractors/BaseExtractor.d.ts +7 -1
- package/lib/extractors/BaseExtractor.d.ts.map +1 -1
- package/lib/extractors/BaseExtractor.js +10 -0
- package/lib/extractors/BaseExtractor.js.map +1 -1
- package/lib/extractors/BaseExtractor.spec.d.ts +2 -0
- package/lib/extractors/BaseExtractor.spec.d.ts.map +1 -0
- package/lib/extractors/BaseExtractor.spec.js +421 -0
- package/lib/extractors/BaseExtractor.spec.js.map +1 -0
- package/lib/extractors/CvaExtractor.spec.d.ts +2 -0
- package/lib/extractors/CvaExtractor.spec.d.ts.map +1 -0
- package/lib/extractors/CvaExtractor.spec.js +1177 -0
- package/lib/extractors/CvaExtractor.spec.js.map +1 -0
- package/lib/extractors/ExpressionExtractor.spec.d.ts +2 -0
- package/lib/extractors/ExpressionExtractor.spec.d.ts.map +1 -0
- package/lib/extractors/ExpressionExtractor.spec.js +316 -0
- package/lib/extractors/ExpressionExtractor.spec.js.map +1 -0
- package/lib/extractors/JsxAttributeExtractor.d.ts +5 -0
- package/lib/extractors/JsxAttributeExtractor.d.ts.map +1 -1
- package/lib/extractors/JsxAttributeExtractor.js +6 -0
- package/lib/extractors/JsxAttributeExtractor.js.map +1 -1
- package/lib/extractors/JsxAttributeExtractor.spec.d.ts +2 -0
- package/lib/extractors/JsxAttributeExtractor.spec.d.ts.map +1 -0
- package/lib/extractors/JsxAttributeExtractor.spec.js +430 -0
- package/lib/extractors/JsxAttributeExtractor.spec.js.map +1 -0
- package/lib/extractors/TailwindVariantsExtractor.spec.d.ts +2 -0
- package/lib/extractors/TailwindVariantsExtractor.spec.d.ts.map +1 -0
- package/lib/extractors/TailwindVariantsExtractor.spec.js +1407 -0
- package/lib/extractors/TailwindVariantsExtractor.spec.js.map +1 -0
- package/lib/extractors/TemplateExpressionExtractor.spec.d.ts +2 -0
- package/lib/extractors/TemplateExpressionExtractor.spec.d.ts.map +1 -0
- package/lib/extractors/TemplateExpressionExtractor.spec.js +240 -0
- package/lib/extractors/TemplateExpressionExtractor.spec.js.map +1 -0
- package/lib/extractors/VariableReferenceExtractor.spec.d.ts +2 -0
- package/lib/extractors/VariableReferenceExtractor.spec.d.ts.map +1 -0
- package/lib/extractors/VariableReferenceExtractor.spec.js +138 -0
- package/lib/extractors/VariableReferenceExtractor.spec.js.map +1 -0
- package/lib/extractors/VueAttributeExtractor.d.ts +6 -0
- package/lib/extractors/VueAttributeExtractor.d.ts.map +1 -1
- package/lib/extractors/VueAttributeExtractor.js +7 -0
- package/lib/extractors/VueAttributeExtractor.js.map +1 -1
- package/lib/plugin/TailwindTypescriptPlugin.d.ts +9 -0
- package/lib/plugin/TailwindTypescriptPlugin.d.ts.map +1 -1
- package/lib/plugin/TailwindTypescriptPlugin.js +36 -11
- package/lib/plugin/TailwindTypescriptPlugin.js.map +1 -1
- package/lib/services/ClassNameExtractionService.d.ts +8 -3
- package/lib/services/ClassNameExtractionService.d.ts.map +1 -1
- package/lib/services/ClassNameExtractionService.js +26 -25
- package/lib/services/ClassNameExtractionService.js.map +1 -1
- package/lib/services/ClassNameExtractionService.spec.js +0 -7
- package/lib/services/ClassNameExtractionService.spec.js.map +1 -1
- package/lib/services/ConfigSchemaValidator.d.ts +40 -0
- package/lib/services/ConfigSchemaValidator.d.ts.map +1 -0
- package/lib/services/ConfigSchemaValidator.js +139 -0
- package/lib/services/ConfigSchemaValidator.js.map +1 -0
- package/lib/services/ConfigSchemaValidator.spec.d.ts +2 -0
- package/lib/services/ConfigSchemaValidator.spec.d.ts.map +1 -0
- package/lib/services/ConfigSchemaValidator.spec.js +344 -0
- package/lib/services/ConfigSchemaValidator.spec.js.map +1 -0
- package/lib/services/ConflictClassDetection.spec.js +48 -0
- package/lib/services/ConflictClassDetection.spec.js.map +1 -1
- package/lib/services/DiagnosticService.spec.d.ts +2 -0
- package/lib/services/DiagnosticService.spec.d.ts.map +1 -0
- package/lib/services/DiagnosticService.spec.js +259 -0
- package/lib/services/DiagnosticService.spec.js.map +1 -0
- package/lib/services/FileDiagnosticCache.spec.d.ts +2 -0
- package/lib/services/FileDiagnosticCache.spec.d.ts.map +1 -0
- package/lib/services/FileDiagnosticCache.spec.js +213 -0
- package/lib/services/FileDiagnosticCache.spec.js.map +1 -0
- package/lib/services/PerformanceCache.spec.d.ts +2 -0
- package/lib/services/PerformanceCache.spec.d.ts.map +1 -0
- package/lib/services/PerformanceCache.spec.js +168 -0
- package/lib/services/PerformanceCache.spec.js.map +1 -0
- package/lib/services/PluginConfigService.d.ts +19 -0
- package/lib/services/PluginConfigService.d.ts.map +1 -1
- package/lib/services/PluginConfigService.js +29 -0
- package/lib/services/PluginConfigService.js.map +1 -1
- package/lib/services/ValidationService.spec.d.ts +2 -0
- package/lib/services/ValidationService.spec.d.ts.map +1 -0
- package/lib/services/ValidationService.spec.js +289 -0
- package/lib/services/ValidationService.spec.js.map +1 -0
- package/lib/utils/FrameworkDetector.d.ts +1 -2
- package/lib/utils/FrameworkDetector.d.ts.map +1 -1
- package/lib/utils/FrameworkDetector.js +0 -5
- package/lib/utils/FrameworkDetector.js.map +1 -1
- package/lib/utils/FrameworkDetector.spec.js +1 -9
- package/lib/utils/FrameworkDetector.spec.js.map +1 -1
- package/package.json +10 -3
- package/lib/extractors/SvelteAttributeExtractor.d.ts +0 -17
- package/lib/extractors/SvelteAttributeExtractor.d.ts.map +0 -1
- package/lib/extractors/SvelteAttributeExtractor.js +0 -27
- package/lib/extractors/SvelteAttributeExtractor.js.map +0 -1
|
@@ -0,0 +1,1177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const ts = __importStar(require("typescript/lib/tsserverlibrary"));
|
|
37
|
+
const CvaExtractor_1 = require("./CvaExtractor");
|
|
38
|
+
describe('CvaExtractor', () => {
|
|
39
|
+
let extractor;
|
|
40
|
+
const createContext = (code, overrides = {}) => {
|
|
41
|
+
const sourceFile = ts.createSourceFile('test.tsx', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
42
|
+
return {
|
|
43
|
+
typescript: ts,
|
|
44
|
+
sourceFile,
|
|
45
|
+
utilityFunctions: [],
|
|
46
|
+
...overrides
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Creates a context with a full TypeScript program and TypeChecker.
|
|
51
|
+
* This allows testing code paths that require type information.
|
|
52
|
+
*/
|
|
53
|
+
const createContextWithTypeChecker = (code, overrides = {}) => {
|
|
54
|
+
const fileName = '/test.tsx';
|
|
55
|
+
// Create a virtual file system
|
|
56
|
+
const files = {
|
|
57
|
+
[fileName]: code,
|
|
58
|
+
// Add a minimal cva type declaration
|
|
59
|
+
'/node_modules/class-variance-authority/index.d.ts': `
|
|
60
|
+
export declare function cva<T>(base?: string, config?: T): (...args: any[]) => string;
|
|
61
|
+
`
|
|
62
|
+
};
|
|
63
|
+
// Create a compiler host
|
|
64
|
+
const compilerHost = {
|
|
65
|
+
getSourceFile: (name, languageVersion) => {
|
|
66
|
+
const content = files[name];
|
|
67
|
+
if (content !== undefined) {
|
|
68
|
+
return ts.createSourceFile(name, content, languageVersion, true);
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
},
|
|
72
|
+
getDefaultLibFileName: () => '/lib.d.ts',
|
|
73
|
+
writeFile: () => { },
|
|
74
|
+
getCurrentDirectory: () => '/',
|
|
75
|
+
getCanonicalFileName: (f) => f,
|
|
76
|
+
useCaseSensitiveFileNames: () => true,
|
|
77
|
+
getNewLine: () => '\n',
|
|
78
|
+
fileExists: (name) => name in files,
|
|
79
|
+
readFile: (name) => files[name],
|
|
80
|
+
directoryExists: () => true,
|
|
81
|
+
getDirectories: () => []
|
|
82
|
+
};
|
|
83
|
+
// Create a program
|
|
84
|
+
const program = ts.createProgram([fileName], {
|
|
85
|
+
target: ts.ScriptTarget.Latest,
|
|
86
|
+
module: ts.ModuleKind.ESNext,
|
|
87
|
+
jsx: ts.JsxEmit.React,
|
|
88
|
+
strict: true,
|
|
89
|
+
moduleResolution: ts.ModuleResolutionKind.NodeJs
|
|
90
|
+
}, compilerHost);
|
|
91
|
+
const sourceFile = program.getSourceFile(fileName);
|
|
92
|
+
const typeChecker = program.getTypeChecker();
|
|
93
|
+
return {
|
|
94
|
+
typescript: ts,
|
|
95
|
+
sourceFile,
|
|
96
|
+
typeChecker,
|
|
97
|
+
utilityFunctions: [],
|
|
98
|
+
...overrides
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
const findCallExpression = (sourceFile) => {
|
|
102
|
+
let result;
|
|
103
|
+
const visit = (node) => {
|
|
104
|
+
if (ts.isCallExpression(node)) {
|
|
105
|
+
result = node;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
ts.forEachChild(node, visit);
|
|
109
|
+
};
|
|
110
|
+
visit(sourceFile);
|
|
111
|
+
return result;
|
|
112
|
+
};
|
|
113
|
+
const findAllCallExpressions = (sourceFile) => {
|
|
114
|
+
const results = [];
|
|
115
|
+
const visit = (node) => {
|
|
116
|
+
if (ts.isCallExpression(node)) {
|
|
117
|
+
results.push(node);
|
|
118
|
+
}
|
|
119
|
+
ts.forEachChild(node, visit);
|
|
120
|
+
};
|
|
121
|
+
visit(sourceFile);
|
|
122
|
+
return results;
|
|
123
|
+
};
|
|
124
|
+
const findLastCallExpression = (sourceFile) => {
|
|
125
|
+
const calls = findAllCallExpressions(sourceFile);
|
|
126
|
+
return calls[calls.length - 1];
|
|
127
|
+
};
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
extractor = new CvaExtractor_1.CvaExtractor();
|
|
130
|
+
});
|
|
131
|
+
describe('canHandle', () => {
|
|
132
|
+
it('should return true for call expressions', () => {
|
|
133
|
+
const code = "import { cva } from 'class-variance-authority'; cva('flex');";
|
|
134
|
+
const context = createContext(code);
|
|
135
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
136
|
+
expect(extractor.canHandle(callExpr, context)).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
it('should return false for non-call expressions', () => {
|
|
139
|
+
const code = 'const x = "flex";';
|
|
140
|
+
const context = createContext(code);
|
|
141
|
+
expect(extractor.canHandle(context.sourceFile.statements[0], context)).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('extract - type guards', () => {
|
|
145
|
+
it('should return empty array for non-call expression node', () => {
|
|
146
|
+
const code = 'const x = "flex";';
|
|
147
|
+
const context = createContext(code);
|
|
148
|
+
const classes = extractor.extract(context.sourceFile.statements[0], context);
|
|
149
|
+
expect(classes).toHaveLength(0);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('extract - basic cva calls', () => {
|
|
153
|
+
it('should extract classes from cva base string', () => {
|
|
154
|
+
const code = `
|
|
155
|
+
import { cva } from 'class-variance-authority';
|
|
156
|
+
const button = cva('flex items-center');
|
|
157
|
+
`;
|
|
158
|
+
const context = createContext(code);
|
|
159
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
160
|
+
const classes = extractor.extract(callExpr, context);
|
|
161
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
162
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
163
|
+
});
|
|
164
|
+
it('should extract classes from cva base array', () => {
|
|
165
|
+
const code = `
|
|
166
|
+
import { cva } from 'class-variance-authority';
|
|
167
|
+
const button = cva(['flex', 'items-center', 'justify-center']);
|
|
168
|
+
`;
|
|
169
|
+
const context = createContext(code);
|
|
170
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
171
|
+
const classes = extractor.extract(callExpr, context);
|
|
172
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
173
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
174
|
+
expect(classes.map(c => c.className)).toContain('justify-center');
|
|
175
|
+
});
|
|
176
|
+
it('should return empty array when no cva import', () => {
|
|
177
|
+
const code = `
|
|
178
|
+
const button = cva('flex items-center');
|
|
179
|
+
`;
|
|
180
|
+
const context = createContext(code);
|
|
181
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
182
|
+
const classes = extractor.extract(callExpr, context);
|
|
183
|
+
expect(classes).toHaveLength(0);
|
|
184
|
+
});
|
|
185
|
+
it('should return empty array for cva call with no arguments', () => {
|
|
186
|
+
const code = `
|
|
187
|
+
import { cva } from 'class-variance-authority';
|
|
188
|
+
const button = cva();
|
|
189
|
+
`;
|
|
190
|
+
const context = createContext(code);
|
|
191
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
192
|
+
const classes = extractor.extract(callExpr, context);
|
|
193
|
+
expect(classes).toHaveLength(0);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe('extract - cva variants', () => {
|
|
197
|
+
it('should extract classes from variants object', () => {
|
|
198
|
+
const code = `
|
|
199
|
+
import { cva } from 'class-variance-authority';
|
|
200
|
+
const button = cva('base', {
|
|
201
|
+
variants: {
|
|
202
|
+
intent: {
|
|
203
|
+
primary: 'bg-blue-500 text-white',
|
|
204
|
+
secondary: 'bg-gray-500'
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
`;
|
|
209
|
+
const context = createContext(code);
|
|
210
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
211
|
+
const classes = extractor.extract(callExpr, context);
|
|
212
|
+
expect(classes.map(c => c.className)).toContain('base');
|
|
213
|
+
expect(classes.map(c => c.className)).toContain('bg-blue-500');
|
|
214
|
+
expect(classes.map(c => c.className)).toContain('text-white');
|
|
215
|
+
expect(classes.map(c => c.className)).toContain('bg-gray-500');
|
|
216
|
+
});
|
|
217
|
+
it('should extract classes from variant arrays', () => {
|
|
218
|
+
const code = `
|
|
219
|
+
import { cva } from 'class-variance-authority';
|
|
220
|
+
const button = cva('base', {
|
|
221
|
+
variants: {
|
|
222
|
+
size: {
|
|
223
|
+
sm: ['text-sm', 'py-1', 'px-2'],
|
|
224
|
+
lg: ['text-lg', 'py-3', 'px-4']
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
`;
|
|
229
|
+
const context = createContext(code);
|
|
230
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
231
|
+
const classes = extractor.extract(callExpr, context);
|
|
232
|
+
expect(classes.map(c => c.className)).toContain('text-sm');
|
|
233
|
+
expect(classes.map(c => c.className)).toContain('py-1');
|
|
234
|
+
expect(classes.map(c => c.className)).toContain('text-lg');
|
|
235
|
+
expect(classes.map(c => c.className)).toContain('py-3');
|
|
236
|
+
});
|
|
237
|
+
it('should handle boolean variants with true/false keys', () => {
|
|
238
|
+
const code = `
|
|
239
|
+
import { cva } from 'class-variance-authority';
|
|
240
|
+
const button = cva('base', {
|
|
241
|
+
variants: {
|
|
242
|
+
disabled: {
|
|
243
|
+
true: 'opacity-50 cursor-not-allowed',
|
|
244
|
+
false: 'cursor-pointer'
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
`;
|
|
249
|
+
const context = createContext(code);
|
|
250
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
251
|
+
const classes = extractor.extract(callExpr, context);
|
|
252
|
+
expect(classes.map(c => c.className)).toContain('opacity-50');
|
|
253
|
+
expect(classes.map(c => c.className)).toContain('cursor-not-allowed');
|
|
254
|
+
expect(classes.map(c => c.className)).toContain('cursor-pointer');
|
|
255
|
+
});
|
|
256
|
+
it('should handle null variant values', () => {
|
|
257
|
+
const code = `
|
|
258
|
+
import { cva } from 'class-variance-authority';
|
|
259
|
+
const button = cva('base', {
|
|
260
|
+
variants: {
|
|
261
|
+
intent: {
|
|
262
|
+
primary: 'bg-blue-500',
|
|
263
|
+
none: null
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
`;
|
|
268
|
+
const context = createContext(code);
|
|
269
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
270
|
+
const classes = extractor.extract(callExpr, context);
|
|
271
|
+
expect(classes.map(c => c.className)).toContain('bg-blue-500');
|
|
272
|
+
// null should be skipped
|
|
273
|
+
expect(classes.map(c => c.className)).not.toContain('null');
|
|
274
|
+
});
|
|
275
|
+
it('should handle non-object variant initializers', () => {
|
|
276
|
+
const code = `
|
|
277
|
+
import { cva } from 'class-variance-authority';
|
|
278
|
+
const button = cva('base', {
|
|
279
|
+
variants: 'not-an-object'
|
|
280
|
+
});
|
|
281
|
+
`;
|
|
282
|
+
const context = createContext(code);
|
|
283
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
284
|
+
// Should not crash
|
|
285
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
describe('extract - compoundVariants', () => {
|
|
289
|
+
it('should extract classes from compoundVariants with class property', () => {
|
|
290
|
+
const code = `
|
|
291
|
+
import { cva } from 'class-variance-authority';
|
|
292
|
+
const button = cva('base', {
|
|
293
|
+
variants: {
|
|
294
|
+
intent: { primary: 'bg-blue-500' }
|
|
295
|
+
},
|
|
296
|
+
compoundVariants: [
|
|
297
|
+
{
|
|
298
|
+
intent: 'primary',
|
|
299
|
+
class: 'font-bold uppercase'
|
|
300
|
+
}
|
|
301
|
+
]
|
|
302
|
+
});
|
|
303
|
+
`;
|
|
304
|
+
const context = createContext(code);
|
|
305
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
306
|
+
const classes = extractor.extract(callExpr, context);
|
|
307
|
+
expect(classes.map(c => c.className)).toContain('font-bold');
|
|
308
|
+
expect(classes.map(c => c.className)).toContain('uppercase');
|
|
309
|
+
});
|
|
310
|
+
it('should extract classes from compoundVariants with className property', () => {
|
|
311
|
+
const code = `
|
|
312
|
+
import { cva } from 'class-variance-authority';
|
|
313
|
+
const button = cva('base', {
|
|
314
|
+
compoundVariants: [
|
|
315
|
+
{
|
|
316
|
+
intent: 'primary',
|
|
317
|
+
className: 'shadow-lg'
|
|
318
|
+
}
|
|
319
|
+
]
|
|
320
|
+
});
|
|
321
|
+
`;
|
|
322
|
+
const context = createContext(code);
|
|
323
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
324
|
+
const classes = extractor.extract(callExpr, context);
|
|
325
|
+
expect(classes.map(c => c.className)).toContain('shadow-lg');
|
|
326
|
+
});
|
|
327
|
+
it('should handle compoundVariants with array values', () => {
|
|
328
|
+
const code = `
|
|
329
|
+
import { cva } from 'class-variance-authority';
|
|
330
|
+
const button = cva('base', {
|
|
331
|
+
compoundVariants: [
|
|
332
|
+
{
|
|
333
|
+
intent: 'primary',
|
|
334
|
+
class: ['font-bold', 'uppercase']
|
|
335
|
+
}
|
|
336
|
+
]
|
|
337
|
+
});
|
|
338
|
+
`;
|
|
339
|
+
const context = createContext(code);
|
|
340
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
341
|
+
const classes = extractor.extract(callExpr, context);
|
|
342
|
+
expect(classes.map(c => c.className)).toContain('font-bold');
|
|
343
|
+
expect(classes.map(c => c.className)).toContain('uppercase');
|
|
344
|
+
});
|
|
345
|
+
it('should handle non-array compoundVariants', () => {
|
|
346
|
+
const code = `
|
|
347
|
+
import { cva } from 'class-variance-authority';
|
|
348
|
+
const button = cva('base', {
|
|
349
|
+
compoundVariants: 'not-an-array'
|
|
350
|
+
});
|
|
351
|
+
`;
|
|
352
|
+
const context = createContext(code);
|
|
353
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
354
|
+
// Should not crash
|
|
355
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
describe('extract - defaultVariants', () => {
|
|
359
|
+
it('should skip defaultVariants property', () => {
|
|
360
|
+
const code = `
|
|
361
|
+
import { cva } from 'class-variance-authority';
|
|
362
|
+
const button = cva('base', {
|
|
363
|
+
variants: {
|
|
364
|
+
intent: { primary: 'bg-blue-500' }
|
|
365
|
+
},
|
|
366
|
+
defaultVariants: {
|
|
367
|
+
intent: 'primary'
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
`;
|
|
371
|
+
const context = createContext(code);
|
|
372
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
373
|
+
const classes = extractor.extract(callExpr, context);
|
|
374
|
+
// Should not include 'primary' as a class (it's a variant value, not a class)
|
|
375
|
+
expect(classes.map(c => c.className)).not.toContain('primary');
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
describe('extract - import aliasing', () => {
|
|
379
|
+
it('should handle aliased imports', () => {
|
|
380
|
+
const code = `
|
|
381
|
+
import { cva as createVariants } from 'class-variance-authority';
|
|
382
|
+
const button = createVariants('flex items-center');
|
|
383
|
+
`;
|
|
384
|
+
const context = createContext(code);
|
|
385
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
386
|
+
const classes = extractor.extract(callExpr, context);
|
|
387
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
388
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
389
|
+
});
|
|
390
|
+
it('should handle member expression calls', () => {
|
|
391
|
+
const code = `
|
|
392
|
+
import { cva } from 'class-variance-authority';
|
|
393
|
+
const utils = { cva };
|
|
394
|
+
const button = utils.cva('flex items-center');
|
|
395
|
+
`;
|
|
396
|
+
const context = createContext(code);
|
|
397
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
398
|
+
// Find the utils.cva call
|
|
399
|
+
const cvaCall = calls.find(c => c.getText().includes('utils.cva'));
|
|
400
|
+
if (cvaCall) {
|
|
401
|
+
const classes = extractor.extract(cvaCall, context);
|
|
402
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
describe('extract - template literals', () => {
|
|
407
|
+
it('should extract classes from no-substitution template literal', () => {
|
|
408
|
+
const code = `
|
|
409
|
+
import { cva } from 'class-variance-authority';
|
|
410
|
+
const button = cva(\`flex items-center\`);
|
|
411
|
+
`;
|
|
412
|
+
const context = createContext(code);
|
|
413
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
414
|
+
const classes = extractor.extract(callExpr, context);
|
|
415
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
416
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
417
|
+
});
|
|
418
|
+
it('should extract static parts from template expression', () => {
|
|
419
|
+
const code = `
|
|
420
|
+
import { cva } from 'class-variance-authority';
|
|
421
|
+
const dynamic = 'test';
|
|
422
|
+
const button = cva(\`flex \${dynamic} items-center\`);
|
|
423
|
+
`;
|
|
424
|
+
const context = createContext(code);
|
|
425
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
426
|
+
const cvaCall = calls.find(c => c.getText().includes('cva('));
|
|
427
|
+
if (cvaCall) {
|
|
428
|
+
const classes = extractor.extract(cvaCall, context);
|
|
429
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
430
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
describe('extract - expression types in values', () => {
|
|
435
|
+
it('should extract from ternary expression in base', () => {
|
|
436
|
+
const code = `
|
|
437
|
+
import { cva } from 'class-variance-authority';
|
|
438
|
+
const condition = true;
|
|
439
|
+
const button = cva(condition ? 'flex' : 'block');
|
|
440
|
+
`;
|
|
441
|
+
const context = createContext(code);
|
|
442
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
443
|
+
const cvaCall = calls.find(c => c.getText().includes('cva('));
|
|
444
|
+
if (cvaCall) {
|
|
445
|
+
const classes = extractor.extract(cvaCall, context);
|
|
446
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
447
|
+
expect(classes.map(c => c.className)).toContain('block');
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
it('should extract from binary expression (logical AND)', () => {
|
|
451
|
+
const code = `
|
|
452
|
+
import { cva } from 'class-variance-authority';
|
|
453
|
+
const condition = true;
|
|
454
|
+
const button = cva(condition && 'flex');
|
|
455
|
+
`;
|
|
456
|
+
const context = createContext(code);
|
|
457
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
458
|
+
const cvaCall = calls.find(c => c.getText().includes('cva('));
|
|
459
|
+
if (cvaCall) {
|
|
460
|
+
const classes = extractor.extract(cvaCall, context);
|
|
461
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
it('should extract from parenthesized expression', () => {
|
|
465
|
+
const code = `
|
|
466
|
+
import { cva } from 'class-variance-authority';
|
|
467
|
+
const button = cva(('flex items-center'));
|
|
468
|
+
`;
|
|
469
|
+
const context = createContext(code);
|
|
470
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
471
|
+
const classes = extractor.extract(callExpr, context);
|
|
472
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
473
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
474
|
+
});
|
|
475
|
+
it('should extract from as expression', () => {
|
|
476
|
+
const code = `
|
|
477
|
+
import { cva } from 'class-variance-authority';
|
|
478
|
+
const button = cva('flex items-center' as string);
|
|
479
|
+
`;
|
|
480
|
+
const context = createContext(code);
|
|
481
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
482
|
+
const classes = extractor.extract(callExpr, context);
|
|
483
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
484
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
485
|
+
});
|
|
486
|
+
it('should extract from non-null expression', () => {
|
|
487
|
+
const code = `
|
|
488
|
+
import { cva } from 'class-variance-authority';
|
|
489
|
+
const maybeClasses = 'flex items-center';
|
|
490
|
+
const button = cva(maybeClasses!);
|
|
491
|
+
`;
|
|
492
|
+
const context = createContext(code);
|
|
493
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
494
|
+
const cvaCall = calls.find(c => c.getText().includes('cva('));
|
|
495
|
+
// Should not crash
|
|
496
|
+
expect(() => {
|
|
497
|
+
if (cvaCall)
|
|
498
|
+
extractor.extract(cvaCall, context);
|
|
499
|
+
}).not.toThrow();
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
describe('extract - array with expressions', () => {
|
|
503
|
+
it('should extract from array with ternary expressions', () => {
|
|
504
|
+
const code = `
|
|
505
|
+
import { cva } from 'class-variance-authority';
|
|
506
|
+
const condition = true;
|
|
507
|
+
const button = cva(['flex', condition ? 'visible' : 'hidden']);
|
|
508
|
+
`;
|
|
509
|
+
const context = createContext(code);
|
|
510
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
511
|
+
const cvaCall = calls.find(c => c.getText().includes('cva('));
|
|
512
|
+
if (cvaCall) {
|
|
513
|
+
const classes = extractor.extract(cvaCall, context);
|
|
514
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
515
|
+
expect(classes.map(c => c.className)).toContain('visible');
|
|
516
|
+
expect(classes.map(c => c.className)).toContain('hidden');
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
describe('extract - isVariant marking', () => {
|
|
521
|
+
it('should mark base classes without isVariant', () => {
|
|
522
|
+
const code = `
|
|
523
|
+
import { cva } from 'class-variance-authority';
|
|
524
|
+
const button = cva('base-class', {
|
|
525
|
+
variants: {
|
|
526
|
+
intent: {
|
|
527
|
+
primary: 'variant-class'
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
`;
|
|
532
|
+
const context = createContext(code);
|
|
533
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
534
|
+
const classes = extractor.extract(callExpr, context);
|
|
535
|
+
const baseClass = classes.find(c => c.className === 'base-class');
|
|
536
|
+
const variantClass = classes.find(c => c.className === 'variant-class');
|
|
537
|
+
expect(baseClass?.isVariant).toBeFalsy();
|
|
538
|
+
expect(variantClass?.isVariant).toBe(true);
|
|
539
|
+
});
|
|
540
|
+
it('should mark compoundVariant classes with isVariant', () => {
|
|
541
|
+
const code = `
|
|
542
|
+
import { cva } from 'class-variance-authority';
|
|
543
|
+
const button = cva('base', {
|
|
544
|
+
compoundVariants: [
|
|
545
|
+
{ intent: 'primary', class: 'compound-class' }
|
|
546
|
+
]
|
|
547
|
+
});
|
|
548
|
+
`;
|
|
549
|
+
const context = createContext(code);
|
|
550
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
551
|
+
const classes = extractor.extract(callExpr, context);
|
|
552
|
+
const compoundClass = classes.find(c => c.className === 'compound-class');
|
|
553
|
+
expect(compoundClass?.isVariant).toBe(true);
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
describe('extract - attributeId', () => {
|
|
557
|
+
it('should set attributeId for all classes', () => {
|
|
558
|
+
const code = `
|
|
559
|
+
import { cva } from 'class-variance-authority';
|
|
560
|
+
const button = cva('flex items-center');
|
|
561
|
+
`;
|
|
562
|
+
const context = createContext(code);
|
|
563
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
564
|
+
const classes = extractor.extract(callExpr, context);
|
|
565
|
+
expect(classes[0].attributeId).toBeDefined();
|
|
566
|
+
expect(classes[0].attributeId).toMatch(/^cva:\d+-\d+$/);
|
|
567
|
+
expect(classes[0].attributeId).toBe(classes[1].attributeId);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
describe('extract - property name handling', () => {
|
|
571
|
+
it('should handle string literal property names', () => {
|
|
572
|
+
const code = `
|
|
573
|
+
import { cva } from 'class-variance-authority';
|
|
574
|
+
const button = cva('base', {
|
|
575
|
+
'variants': {
|
|
576
|
+
'intent': {
|
|
577
|
+
'primary': 'bg-blue-500'
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
`;
|
|
582
|
+
const context = createContext(code);
|
|
583
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
584
|
+
const classes = extractor.extract(callExpr, context);
|
|
585
|
+
expect(classes.map(c => c.className)).toContain('bg-blue-500');
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
describe('extract - config object validation', () => {
|
|
589
|
+
it('should handle non-object config argument', () => {
|
|
590
|
+
const code = `
|
|
591
|
+
import { cva } from 'class-variance-authority';
|
|
592
|
+
const button = cva('base', 'not-an-object');
|
|
593
|
+
`;
|
|
594
|
+
const context = createContext(code);
|
|
595
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
596
|
+
const classes = extractor.extract(callExpr, context);
|
|
597
|
+
expect(classes.map(c => c.className)).toContain('base');
|
|
598
|
+
});
|
|
599
|
+
it('should handle spread elements in config', () => {
|
|
600
|
+
const code = `
|
|
601
|
+
import { cva } from 'class-variance-authority';
|
|
602
|
+
const button = cva('base', {
|
|
603
|
+
...otherConfig,
|
|
604
|
+
variants: { intent: { primary: 'bg-blue-500' } }
|
|
605
|
+
});
|
|
606
|
+
`;
|
|
607
|
+
const context = createContext(code);
|
|
608
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
609
|
+
// Should not crash on spread elements
|
|
610
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
describe('caching', () => {
|
|
614
|
+
it('should cache import detection results', () => {
|
|
615
|
+
const code = `
|
|
616
|
+
import { cva } from 'class-variance-authority';
|
|
617
|
+
const button = cva('flex');
|
|
618
|
+
`;
|
|
619
|
+
const context = createContext(code);
|
|
620
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
621
|
+
// Call twice to test caching
|
|
622
|
+
extractor.extract(callExpr, context);
|
|
623
|
+
const classes = extractor.extract(callExpr, context);
|
|
624
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
625
|
+
});
|
|
626
|
+
it('should clear cache when clearCache is called', () => {
|
|
627
|
+
const code = `
|
|
628
|
+
import { cva } from 'class-variance-authority';
|
|
629
|
+
const button = cva('flex');
|
|
630
|
+
`;
|
|
631
|
+
const context = createContext(code);
|
|
632
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
633
|
+
extractor.extract(callExpr, context);
|
|
634
|
+
extractor.clearCache();
|
|
635
|
+
// Should still work after cache clear
|
|
636
|
+
const classes = extractor.extract(callExpr, context);
|
|
637
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
describe('edge cases', () => {
|
|
641
|
+
it('should return empty array for non-cva call expression', () => {
|
|
642
|
+
const code = `
|
|
643
|
+
import { cva } from 'class-variance-authority';
|
|
644
|
+
const x = otherFunction('flex');
|
|
645
|
+
`;
|
|
646
|
+
const context = createContext(code);
|
|
647
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
648
|
+
const classes = extractor.extract(callExpr, context);
|
|
649
|
+
expect(classes).toHaveLength(0);
|
|
650
|
+
});
|
|
651
|
+
it('should handle import without import clause', () => {
|
|
652
|
+
const code = `
|
|
653
|
+
import 'class-variance-authority';
|
|
654
|
+
const button = cva('flex');
|
|
655
|
+
`;
|
|
656
|
+
const context = createContext(code);
|
|
657
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
658
|
+
const classes = extractor.extract(callExpr, context);
|
|
659
|
+
expect(classes).toHaveLength(0);
|
|
660
|
+
});
|
|
661
|
+
it('should handle empty variants object', () => {
|
|
662
|
+
const code = `
|
|
663
|
+
import { cva } from 'class-variance-authority';
|
|
664
|
+
const button = cva('base', { variants: {} });
|
|
665
|
+
`;
|
|
666
|
+
const context = createContext(code);
|
|
667
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
668
|
+
const classes = extractor.extract(callExpr, context);
|
|
669
|
+
expect(classes.map(c => c.className)).toContain('base');
|
|
670
|
+
});
|
|
671
|
+
it('should handle unknown config properties', () => {
|
|
672
|
+
const code = `
|
|
673
|
+
import { cva } from 'class-variance-authority';
|
|
674
|
+
const button = cva('base', {
|
|
675
|
+
unknownProp: 'some-class'
|
|
676
|
+
});
|
|
677
|
+
`;
|
|
678
|
+
const context = createContext(code);
|
|
679
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
680
|
+
const classes = extractor.extract(callExpr, context);
|
|
681
|
+
// Unknown props are processed as potential class containers
|
|
682
|
+
expect(classes.map(c => c.className)).toContain('some-class');
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
describe('extract with TypeChecker - cva function calls', () => {
|
|
686
|
+
it('should extract class from cva-created function call with class property', () => {
|
|
687
|
+
const code = `
|
|
688
|
+
import { cva } from 'class-variance-authority';
|
|
689
|
+
const button = cva('base-class', {
|
|
690
|
+
variants: {
|
|
691
|
+
intent: {
|
|
692
|
+
primary: 'bg-blue-500'
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
const result = button({ intent: 'primary', class: 'extra-class' });
|
|
697
|
+
`;
|
|
698
|
+
const context = createContextWithTypeChecker(code);
|
|
699
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
700
|
+
const classes = extractor.extract(callExpr, context);
|
|
701
|
+
expect(classes.map(c => c.className)).toContain('extra-class');
|
|
702
|
+
});
|
|
703
|
+
it('should extract class from cva-created function call with className property', () => {
|
|
704
|
+
const code = `
|
|
705
|
+
import { cva } from 'class-variance-authority';
|
|
706
|
+
const button = cva('base-class');
|
|
707
|
+
const result = button({ className: 'override-class' });
|
|
708
|
+
`;
|
|
709
|
+
const context = createContextWithTypeChecker(code);
|
|
710
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
711
|
+
const classes = extractor.extract(callExpr, context);
|
|
712
|
+
expect(classes.map(c => c.className)).toContain('override-class');
|
|
713
|
+
});
|
|
714
|
+
it('should extract multiple classes from cva function call', () => {
|
|
715
|
+
const code = `
|
|
716
|
+
import { cva } from 'class-variance-authority';
|
|
717
|
+
const button = cva('base');
|
|
718
|
+
const result = button({ class: 'flex items-center gap-2' });
|
|
719
|
+
`;
|
|
720
|
+
const context = createContextWithTypeChecker(code);
|
|
721
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
722
|
+
const classes = extractor.extract(callExpr, context);
|
|
723
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
724
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
725
|
+
expect(classes.map(c => c.className)).toContain('gap-2');
|
|
726
|
+
});
|
|
727
|
+
it('should return empty array when cva function call has no arguments', () => {
|
|
728
|
+
const code = `
|
|
729
|
+
import { cva } from 'class-variance-authority';
|
|
730
|
+
const button = cva('base');
|
|
731
|
+
const result = button();
|
|
732
|
+
`;
|
|
733
|
+
const context = createContextWithTypeChecker(code);
|
|
734
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
735
|
+
const classes = extractor.extract(callExpr, context);
|
|
736
|
+
expect(classes).toHaveLength(0);
|
|
737
|
+
});
|
|
738
|
+
it('should return empty array when cva function call arg is not object literal', () => {
|
|
739
|
+
const code = `
|
|
740
|
+
import { cva } from 'class-variance-authority';
|
|
741
|
+
const button = cva('base');
|
|
742
|
+
const options = { class: 'flex' };
|
|
743
|
+
const result = button(options);
|
|
744
|
+
`;
|
|
745
|
+
const context = createContextWithTypeChecker(code);
|
|
746
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
747
|
+
const classes = extractor.extract(callExpr, context);
|
|
748
|
+
// Variable reference - can't extract without resolving
|
|
749
|
+
expect(classes).toHaveLength(0);
|
|
750
|
+
});
|
|
751
|
+
it('should skip utility functions even with TypeChecker', () => {
|
|
752
|
+
const code = `
|
|
753
|
+
import { cva } from 'class-variance-authority';
|
|
754
|
+
const button = cva('base');
|
|
755
|
+
const result = button({ class: 'flex' });
|
|
756
|
+
`;
|
|
757
|
+
const context = createContextWithTypeChecker(code, {
|
|
758
|
+
utilityFunctions: ['button']
|
|
759
|
+
});
|
|
760
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
761
|
+
const classes = extractor.extract(callExpr, context);
|
|
762
|
+
// button is marked as utility function, should not extract
|
|
763
|
+
expect(classes).toHaveLength(0);
|
|
764
|
+
});
|
|
765
|
+
it('should cache symbol lookup results', () => {
|
|
766
|
+
const code = `
|
|
767
|
+
import { cva } from 'class-variance-authority';
|
|
768
|
+
const button = cva('base');
|
|
769
|
+
const result1 = button({ class: 'flex' });
|
|
770
|
+
const result2 = button({ class: 'grid' });
|
|
771
|
+
`;
|
|
772
|
+
const context = createContextWithTypeChecker(code);
|
|
773
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
774
|
+
const buttonCall1 = calls[1]; // First button() call
|
|
775
|
+
const buttonCall2 = calls[2]; // Second button() call
|
|
776
|
+
// Both calls should work
|
|
777
|
+
const classes1 = extractor.extract(buttonCall1, context);
|
|
778
|
+
const classes2 = extractor.extract(buttonCall2, context);
|
|
779
|
+
expect(classes1.map(c => c.className)).toContain('flex');
|
|
780
|
+
expect(classes2.map(c => c.className)).toContain('grid');
|
|
781
|
+
});
|
|
782
|
+
it('should set attributeId for cva function call classes', () => {
|
|
783
|
+
const code = `
|
|
784
|
+
import { cva } from 'class-variance-authority';
|
|
785
|
+
const button = cva('base');
|
|
786
|
+
const result = button({ class: 'flex items-center' });
|
|
787
|
+
`;
|
|
788
|
+
const context = createContextWithTypeChecker(code);
|
|
789
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
790
|
+
const classes = extractor.extract(callExpr, context);
|
|
791
|
+
expect(classes[0].attributeId).toBeDefined();
|
|
792
|
+
expect(classes[0].attributeId).toMatch(/^cva-call:\d+-\d+$/);
|
|
793
|
+
});
|
|
794
|
+
it('should not extract from non-cva function calls', () => {
|
|
795
|
+
const code = `
|
|
796
|
+
import { cva } from 'class-variance-authority';
|
|
797
|
+
const notCva = () => {};
|
|
798
|
+
const result = notCva({ class: 'flex' });
|
|
799
|
+
`;
|
|
800
|
+
const context = createContextWithTypeChecker(code);
|
|
801
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
802
|
+
const classes = extractor.extract(callExpr, context);
|
|
803
|
+
expect(classes).toHaveLength(0);
|
|
804
|
+
});
|
|
805
|
+
it('should handle property access expression calls', () => {
|
|
806
|
+
const code = `
|
|
807
|
+
import { cva } from 'class-variance-authority';
|
|
808
|
+
const variants = {
|
|
809
|
+
button: cva('base-class')
|
|
810
|
+
};
|
|
811
|
+
const result = variants.button({ class: 'extra-class' });
|
|
812
|
+
`;
|
|
813
|
+
const context = createContextWithTypeChecker(code);
|
|
814
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
815
|
+
// This tests the property access expression path in isCvaCreatedFunctionCall
|
|
816
|
+
// The result depends on whether TypeChecker can resolve the symbol
|
|
817
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
818
|
+
});
|
|
819
|
+
it('should skip non-identifier and non-property-access expressions', () => {
|
|
820
|
+
const code = `
|
|
821
|
+
import { cva } from 'class-variance-authority';
|
|
822
|
+
const getButton = () => cva('base');
|
|
823
|
+
const result = getButton()({ class: 'flex' });
|
|
824
|
+
`;
|
|
825
|
+
const context = createContextWithTypeChecker(code);
|
|
826
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
827
|
+
const outerCall = calls[calls.length - 1]; // getButton()({ class: 'flex' })
|
|
828
|
+
const classes = extractor.extract(outerCall, context);
|
|
829
|
+
// Call expression as the expression (not identifier) - should return empty
|
|
830
|
+
expect(classes).toHaveLength(0);
|
|
831
|
+
});
|
|
832
|
+
it('should handle symbol without declarations', () => {
|
|
833
|
+
// This is an edge case that's hard to trigger, but we can at least
|
|
834
|
+
// verify the code doesn't crash
|
|
835
|
+
const code = `
|
|
836
|
+
import { cva } from 'class-variance-authority';
|
|
837
|
+
const button = cva('base');
|
|
838
|
+
button({ class: 'flex' });
|
|
839
|
+
`;
|
|
840
|
+
const context = createContextWithTypeChecker(code);
|
|
841
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
842
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
describe('extract with TypeChecker - edge cases', () => {
|
|
846
|
+
it('should handle class property with array value', () => {
|
|
847
|
+
const code = `
|
|
848
|
+
import { cva } from 'class-variance-authority';
|
|
849
|
+
const button = cva('base');
|
|
850
|
+
const result = button({ class: ['flex', 'items-center'] });
|
|
851
|
+
`;
|
|
852
|
+
const context = createContextWithTypeChecker(code);
|
|
853
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
854
|
+
const classes = extractor.extract(callExpr, context);
|
|
855
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
856
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
857
|
+
});
|
|
858
|
+
it('should skip non-property-assignment in function call arg', () => {
|
|
859
|
+
const code = `
|
|
860
|
+
import { cva } from 'class-variance-authority';
|
|
861
|
+
const button = cva('base');
|
|
862
|
+
const extra = 'flex';
|
|
863
|
+
const result = button({ class: 'grid', extra });
|
|
864
|
+
`;
|
|
865
|
+
const context = createContextWithTypeChecker(code);
|
|
866
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
867
|
+
const classes = extractor.extract(callExpr, context);
|
|
868
|
+
// Should only extract 'grid' from class property
|
|
869
|
+
expect(classes.map(c => c.className)).toContain('grid');
|
|
870
|
+
});
|
|
871
|
+
it('should handle aliased cva import with TypeChecker', () => {
|
|
872
|
+
const code = `
|
|
873
|
+
import { cva as createVariant } from 'class-variance-authority';
|
|
874
|
+
const button = createVariant('base-class');
|
|
875
|
+
const result = button({ class: 'extra-class' });
|
|
876
|
+
`;
|
|
877
|
+
const context = createContextWithTypeChecker(code);
|
|
878
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
879
|
+
const classes = extractor.extract(callExpr, context);
|
|
880
|
+
expect(classes.map(c => c.className)).toContain('extra-class');
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
describe('100% coverage - edge cases', () => {
|
|
884
|
+
it('should return false for cva call with call expression (not identifier/property access)', () => {
|
|
885
|
+
// Line 88: return false when expression is neither identifier nor property access
|
|
886
|
+
const code = `
|
|
887
|
+
import { cva } from 'class-variance-authority';
|
|
888
|
+
const getCva = () => cva;
|
|
889
|
+
const button = getCva()('base-class');
|
|
890
|
+
`;
|
|
891
|
+
const context = createContext(code);
|
|
892
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
893
|
+
// getCva()('base-class') - the outer call has a call expression as its expression
|
|
894
|
+
const outerCall = calls[calls.length - 1];
|
|
895
|
+
const classes = extractor.extract(outerCall, context);
|
|
896
|
+
// Should not match as cva call since expression is a call expression
|
|
897
|
+
expect(classes).toHaveLength(0);
|
|
898
|
+
});
|
|
899
|
+
it('should skip computed property names in config (line 197)', () => {
|
|
900
|
+
const code = `
|
|
901
|
+
import { cva } from 'class-variance-authority';
|
|
902
|
+
const prop = 'variants';
|
|
903
|
+
const button = cva('base', { [prop]: { size: { sm: 'text-sm' } } });
|
|
904
|
+
`;
|
|
905
|
+
const context = createContext(code);
|
|
906
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
907
|
+
const classes = extractor.extract(callExpr, context);
|
|
908
|
+
// Base class should be extracted, but computed property is skipped
|
|
909
|
+
expect(classes.map(c => c.className)).toContain('base');
|
|
910
|
+
});
|
|
911
|
+
it('should skip shorthand properties in variants (line 246)', () => {
|
|
912
|
+
const code = `
|
|
913
|
+
import { cva } from 'class-variance-authority';
|
|
914
|
+
const size = { sm: 'text-sm' };
|
|
915
|
+
const button = cva('base', { variants: { size } });
|
|
916
|
+
`;
|
|
917
|
+
const context = createContext(code);
|
|
918
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
919
|
+
const classes = extractor.extract(callExpr, context);
|
|
920
|
+
// Only base should be extracted
|
|
921
|
+
expect(classes.map(c => c.className)).toContain('base');
|
|
922
|
+
});
|
|
923
|
+
it('should skip spread in variant options (line 254)', () => {
|
|
924
|
+
const code = `
|
|
925
|
+
import { cva } from 'class-variance-authority';
|
|
926
|
+
const options = { sm: 'text-sm' };
|
|
927
|
+
const button = cva('base', {
|
|
928
|
+
variants: {
|
|
929
|
+
size: {
|
|
930
|
+
...options,
|
|
931
|
+
lg: 'text-lg'
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
`;
|
|
936
|
+
const context = createContext(code);
|
|
937
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
938
|
+
const classes = extractor.extract(callExpr, context);
|
|
939
|
+
expect(classes.map(c => c.className)).toContain('base');
|
|
940
|
+
expect(classes.map(c => c.className)).toContain('text-lg');
|
|
941
|
+
});
|
|
942
|
+
it('should skip non-object elements in compoundVariants (line 296)', () => {
|
|
943
|
+
const code = `
|
|
944
|
+
import { cva } from 'class-variance-authority';
|
|
945
|
+
const button = cva('base', {
|
|
946
|
+
compoundVariants: [
|
|
947
|
+
'not-an-object',
|
|
948
|
+
{ intent: 'primary', class: 'font-bold' }
|
|
949
|
+
]
|
|
950
|
+
});
|
|
951
|
+
`;
|
|
952
|
+
const context = createContext(code);
|
|
953
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
954
|
+
const classes = extractor.extract(callExpr, context);
|
|
955
|
+
expect(classes.map(c => c.className)).toContain('base');
|
|
956
|
+
expect(classes.map(c => c.className)).toContain('font-bold');
|
|
957
|
+
});
|
|
958
|
+
it('should skip spread in compoundVariants properties (line 302)', () => {
|
|
959
|
+
const code = `
|
|
960
|
+
import { cva } from 'class-variance-authority';
|
|
961
|
+
const conditions = { intent: 'primary' };
|
|
962
|
+
const button = cva('base', {
|
|
963
|
+
compoundVariants: [
|
|
964
|
+
{
|
|
965
|
+
...conditions,
|
|
966
|
+
class: 'font-bold'
|
|
967
|
+
}
|
|
968
|
+
]
|
|
969
|
+
});
|
|
970
|
+
`;
|
|
971
|
+
const context = createContext(code);
|
|
972
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
973
|
+
const classes = extractor.extract(callExpr, context);
|
|
974
|
+
expect(classes.map(c => c.className)).toContain('font-bold');
|
|
975
|
+
});
|
|
976
|
+
it('should handle identifier in extractFromValue (lines 340-341)', () => {
|
|
977
|
+
// This tests the identifier branch in extractFromValue
|
|
978
|
+
const code = `
|
|
979
|
+
import { cva } from 'class-variance-authority';
|
|
980
|
+
const myBase = 'flex items-center';
|
|
981
|
+
const button = cva(myBase);
|
|
982
|
+
`;
|
|
983
|
+
const context = createContext(code);
|
|
984
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
985
|
+
// Without TypeChecker, variable references won't be resolved
|
|
986
|
+
// but the code path should be hit
|
|
987
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
988
|
+
});
|
|
989
|
+
it('should handle identifier in array (lines 355-357)', () => {
|
|
990
|
+
const code = `
|
|
991
|
+
import { cva } from 'class-variance-authority';
|
|
992
|
+
const extraClass = 'extra';
|
|
993
|
+
const button = cva(['flex', extraClass, 'items-center']);
|
|
994
|
+
`;
|
|
995
|
+
const context = createContext(code);
|
|
996
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
997
|
+
const classes = extractor.extract(callExpr, context);
|
|
998
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
999
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
1000
|
+
});
|
|
1001
|
+
it('should handle return [] at end of extractFromValue (line 461)', () => {
|
|
1002
|
+
// This tests when node doesn't match any known expression type
|
|
1003
|
+
const code = `
|
|
1004
|
+
import { cva } from 'class-variance-authority';
|
|
1005
|
+
const button = cva('base', {
|
|
1006
|
+
variants: {
|
|
1007
|
+
size: {
|
|
1008
|
+
sm: someFunction()
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
`;
|
|
1013
|
+
const context = createContext(code);
|
|
1014
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
1015
|
+
// someFunction() is a CallExpression which isn't handled in extractFromValue
|
|
1016
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
1017
|
+
});
|
|
1018
|
+
it('should return false when symbol has no declarations (line 525)', () => {
|
|
1019
|
+
// This is hard to trigger directly, but we can test through a function
|
|
1020
|
+
// that references something TypeChecker can't resolve
|
|
1021
|
+
const code = `
|
|
1022
|
+
import { cva } from 'class-variance-authority';
|
|
1023
|
+
declare const button: ReturnType<typeof cva>;
|
|
1024
|
+
const result = button({ class: 'flex' });
|
|
1025
|
+
`;
|
|
1026
|
+
const context = createContextWithTypeChecker(code);
|
|
1027
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1028
|
+
// Should not crash even if symbol resolution is tricky
|
|
1029
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
1030
|
+
});
|
|
1031
|
+
it('should return null for numeric literal property name (line 610)', () => {
|
|
1032
|
+
const code = `
|
|
1033
|
+
import { cva } from 'class-variance-authority';
|
|
1034
|
+
const button = cva('base', {
|
|
1035
|
+
variants: {
|
|
1036
|
+
size: {
|
|
1037
|
+
100: 'text-xs'
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
`;
|
|
1042
|
+
const context = createContext(code);
|
|
1043
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
1044
|
+
const classes = extractor.extract(callExpr, context);
|
|
1045
|
+
// Numeric keys should still work (they're treated as identifiers)
|
|
1046
|
+
expect(classes.map(c => c.className)).toContain('base');
|
|
1047
|
+
expect(classes.map(c => c.className)).toContain('text-xs');
|
|
1048
|
+
});
|
|
1049
|
+
it('should handle export default cva pattern (lines 541-544)', () => {
|
|
1050
|
+
// Export assignments are a specific declaration type
|
|
1051
|
+
const code = `
|
|
1052
|
+
import { cva } from 'class-variance-authority';
|
|
1053
|
+
const button = cva('base');
|
|
1054
|
+
export default button;
|
|
1055
|
+
const result = button({ class: 'flex' });
|
|
1056
|
+
`;
|
|
1057
|
+
const context = createContextWithTypeChecker(code);
|
|
1058
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1059
|
+
const classes = extractor.extract(callExpr, context);
|
|
1060
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
1061
|
+
});
|
|
1062
|
+
it('should return false when TypeChecker returns no symbol (line 503)', () => {
|
|
1063
|
+
// Create a scenario where getSymbolAtLocation returns undefined
|
|
1064
|
+
const code = `
|
|
1065
|
+
import { cva } from 'class-variance-authority';
|
|
1066
|
+
const result = undefinedFunction({ class: 'flex' });
|
|
1067
|
+
`;
|
|
1068
|
+
const context = createContextWithTypeChecker(code);
|
|
1069
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1070
|
+
const classes = extractor.extract(callExpr, context);
|
|
1071
|
+
expect(classes).toHaveLength(0);
|
|
1072
|
+
});
|
|
1073
|
+
it('should handle non-identifier/non-property-access in isCvaCreatedFunctionCall (line 486)', () => {
|
|
1074
|
+
// When the call expression is something like fn()()
|
|
1075
|
+
const code = `
|
|
1076
|
+
import { cva } from 'class-variance-authority';
|
|
1077
|
+
const getVariant = () => cva('base');
|
|
1078
|
+
const result = getVariant()({ class: 'flex' });
|
|
1079
|
+
`;
|
|
1080
|
+
const context = createContextWithTypeChecker(code);
|
|
1081
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
1082
|
+
// The outer call getVariant()({ class: 'flex' }) has call expression as its expression
|
|
1083
|
+
const outerCall = calls[calls.length - 1];
|
|
1084
|
+
const classes = extractor.extract(outerCall, context);
|
|
1085
|
+
expect(classes).toHaveLength(0);
|
|
1086
|
+
});
|
|
1087
|
+
it('should handle type assertion expression in .ts file', () => {
|
|
1088
|
+
// Create context with .ts extension to test isTypeAssertionExpression
|
|
1089
|
+
const fileName = '/test.ts';
|
|
1090
|
+
const code = `
|
|
1091
|
+
import { cva } from 'class-variance-authority';
|
|
1092
|
+
const button = cva(<string>'base-class');
|
|
1093
|
+
`;
|
|
1094
|
+
const files = {
|
|
1095
|
+
[fileName]: code,
|
|
1096
|
+
'/node_modules/class-variance-authority/index.d.ts': `
|
|
1097
|
+
export declare function cva<T>(base?: string, config?: T): (...args: any[]) => string;
|
|
1098
|
+
`
|
|
1099
|
+
};
|
|
1100
|
+
const compilerHost = {
|
|
1101
|
+
getSourceFile: (name, languageVersion) => {
|
|
1102
|
+
const content = files[name];
|
|
1103
|
+
if (content !== undefined) {
|
|
1104
|
+
return ts.createSourceFile(name, content, languageVersion, true, name.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
1105
|
+
}
|
|
1106
|
+
return undefined;
|
|
1107
|
+
},
|
|
1108
|
+
getDefaultLibFileName: () => '/lib.d.ts',
|
|
1109
|
+
writeFile: () => { },
|
|
1110
|
+
getCurrentDirectory: () => '/',
|
|
1111
|
+
getCanonicalFileName: (f) => f,
|
|
1112
|
+
useCaseSensitiveFileNames: () => true,
|
|
1113
|
+
getNewLine: () => '\n',
|
|
1114
|
+
fileExists: (name) => name in files,
|
|
1115
|
+
readFile: (name) => files[name],
|
|
1116
|
+
directoryExists: () => true,
|
|
1117
|
+
getDirectories: () => []
|
|
1118
|
+
};
|
|
1119
|
+
const program = ts.createProgram([fileName], {
|
|
1120
|
+
target: ts.ScriptTarget.Latest,
|
|
1121
|
+
module: ts.ModuleKind.ESNext,
|
|
1122
|
+
strict: true,
|
|
1123
|
+
moduleResolution: ts.ModuleResolutionKind.NodeJs
|
|
1124
|
+
}, compilerHost);
|
|
1125
|
+
const sourceFile = program.getSourceFile(fileName);
|
|
1126
|
+
const context = {
|
|
1127
|
+
typescript: ts,
|
|
1128
|
+
sourceFile,
|
|
1129
|
+
typeChecker: program.getTypeChecker(),
|
|
1130
|
+
utilityFunctions: []
|
|
1131
|
+
};
|
|
1132
|
+
const callExpr = findCallExpression(sourceFile);
|
|
1133
|
+
const classes = extractor.extract(callExpr, context);
|
|
1134
|
+
expect(classes.map(c => c.className)).toContain('base-class');
|
|
1135
|
+
});
|
|
1136
|
+
it('should return false in isCvaCall for element access expression (line 88)', () => {
|
|
1137
|
+
// Element access expression like cvaFunctions['default']() is neither identifier nor property access
|
|
1138
|
+
const code = `
|
|
1139
|
+
import { cva } from 'class-variance-authority';
|
|
1140
|
+
const cvaFunctions = { default: cva };
|
|
1141
|
+
const button = cvaFunctions['default']('base-class');
|
|
1142
|
+
`;
|
|
1143
|
+
const context = createContext(code);
|
|
1144
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
1145
|
+
// cvaFunctions['default']('base-class') has element access expression
|
|
1146
|
+
const lastCall = calls[calls.length - 1];
|
|
1147
|
+
const classes = extractor.extract(lastCall, context);
|
|
1148
|
+
// Element access expressions return false in isCvaCall
|
|
1149
|
+
expect(classes).toHaveLength(0);
|
|
1150
|
+
});
|
|
1151
|
+
it('should handle identifier variable reference in array with TypeChecker (line 357)', () => {
|
|
1152
|
+
const code = `
|
|
1153
|
+
import { cva } from 'class-variance-authority';
|
|
1154
|
+
const myClass = 'my-custom-class';
|
|
1155
|
+
const button = cva(['flex', myClass, 'items-center']);
|
|
1156
|
+
`;
|
|
1157
|
+
const context = createContextWithTypeChecker(code);
|
|
1158
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
1159
|
+
const classes = extractor.extract(callExpr, context);
|
|
1160
|
+
// With TypeChecker, we might be able to resolve some variable references
|
|
1161
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
1162
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
1163
|
+
});
|
|
1164
|
+
it('should handle export default with direct cva call', () => {
|
|
1165
|
+
// Test export default cva(...) pattern
|
|
1166
|
+
const code = `
|
|
1167
|
+
import { cva } from 'class-variance-authority';
|
|
1168
|
+
export default cva('exported-base');
|
|
1169
|
+
`;
|
|
1170
|
+
const context = createContextWithTypeChecker(code);
|
|
1171
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
1172
|
+
const classes = extractor.extract(callExpr, context);
|
|
1173
|
+
expect(classes.map(c => c.className)).toContain('exported-base');
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
});
|
|
1177
|
+
//# sourceMappingURL=CvaExtractor.spec.js.map
|