tailwind-typescript-plugin 1.4.1-beta.3 → 1.4.1-beta.31
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 +155 -0
- package/README.md +369 -71
- package/lib/core/interfaces.d.ts +40 -13
- package/lib/core/interfaces.d.ts.map +1 -1
- package/lib/core/types.d.ts +112 -1
- package/lib/core/types.d.ts.map +1 -1
- package/lib/extractors/BaseExtractor.d.ts +56 -3
- package/lib/extractors/BaseExtractor.d.ts.map +1 -1
- package/lib/extractors/BaseExtractor.js +194 -5
- 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.js +1 -1
- package/lib/extractors/CvaExtractor.js.map +1 -1
- 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.d.ts.map +1 -1
- package/lib/extractors/ExpressionExtractor.js +35 -5
- package/lib/extractors/ExpressionExtractor.js.map +1 -1
- 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 +7 -1
- package/lib/extractors/JsxAttributeExtractor.d.ts.map +1 -1
- package/lib/extractors/JsxAttributeExtractor.js +21 -8
- 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.d.ts.map +1 -1
- package/lib/extractors/TailwindVariantsExtractor.js +1 -5
- package/lib/extractors/TailwindVariantsExtractor.js.map +1 -1
- 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.d.ts.map +1 -1
- package/lib/extractors/VariableReferenceExtractor.js +21 -0
- package/lib/extractors/VariableReferenceExtractor.js.map +1 -1
- 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 +202 -0
- package/lib/extractors/VueAttributeExtractor.d.ts.map +1 -0
- package/lib/extractors/VueAttributeExtractor.js +1691 -0
- package/lib/extractors/VueAttributeExtractor.js.map +1 -0
- package/lib/extractors/VueExpressionExtractor.d.ts +34 -0
- package/lib/extractors/VueExpressionExtractor.d.ts.map +1 -0
- package/lib/extractors/VueExpressionExtractor.js +171 -0
- package/lib/extractors/VueExpressionExtractor.js.map +1 -0
- package/lib/infrastructure/TailwindValidator.css-vars.spec.js +1 -11
- package/lib/infrastructure/TailwindValidator.css-vars.spec.js.map +1 -1
- package/lib/infrastructure/TailwindValidator.d.ts +10 -3
- package/lib/infrastructure/TailwindValidator.d.ts.map +1 -1
- package/lib/infrastructure/TailwindValidator.js +68 -28
- package/lib/infrastructure/TailwindValidator.js.map +1 -1
- package/lib/infrastructure/TailwindValidator.spec.js +50 -17
- package/lib/infrastructure/TailwindValidator.spec.js.map +1 -1
- package/lib/plugin/TailwindTypescriptPlugin.d.ts +22 -1
- package/lib/plugin/TailwindTypescriptPlugin.d.ts.map +1 -1
- package/lib/plugin/TailwindTypescriptPlugin.js +133 -50
- package/lib/plugin/TailwindTypescriptPlugin.js.map +1 -1
- package/lib/services/ClassNameExtractionService.d.ts +27 -6
- package/lib/services/ClassNameExtractionService.d.ts.map +1 -1
- package/lib/services/ClassNameExtractionService.js +80 -17
- package/lib/services/ClassNameExtractionService.js.map +1 -1
- package/lib/services/ClassNameExtractionService.spec.d.ts +2 -0
- package/lib/services/ClassNameExtractionService.spec.d.ts.map +1 -0
- package/lib/services/ClassNameExtractionService.spec.js +215 -0
- package/lib/services/ClassNameExtractionService.spec.js.map +1 -0
- package/lib/services/CodeActionService.spec.js +1 -2
- package/lib/services/CodeActionService.spec.js.map +1 -1
- package/lib/services/CompletionService.d.ts +121 -0
- package/lib/services/CompletionService.d.ts.map +1 -0
- package/lib/services/CompletionService.js +573 -0
- package/lib/services/CompletionService.js.map +1 -0
- package/lib/services/CompletionService.spec.d.ts +2 -0
- package/lib/services/CompletionService.spec.d.ts.map +1 -0
- package/lib/services/CompletionService.spec.js +1182 -0
- package/lib/services/CompletionService.spec.js.map +1 -0
- 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 +53 -5
- package/lib/services/ConflictClassDetection.spec.js.map +1 -1
- package/lib/services/DiagnosticService.d.ts +8 -8
- package/lib/services/DiagnosticService.d.ts.map +1 -1
- package/lib/services/DiagnosticService.js +29 -13
- package/lib/services/DiagnosticService.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/DuplicateClassDetection.spec.js +20 -21
- package/lib/services/DuplicateClassDetection.spec.js.map +1 -1
- 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 +66 -15
- package/lib/services/PluginConfigService.d.ts.map +1 -1
- package/lib/services/PluginConfigService.js +230 -73
- package/lib/services/PluginConfigService.js.map +1 -1
- package/lib/services/PluginConfigService.spec.d.ts +2 -0
- package/lib/services/PluginConfigService.spec.d.ts.map +1 -0
- package/lib/services/PluginConfigService.spec.js +93 -0
- package/lib/services/PluginConfigService.spec.js.map +1 -0
- package/lib/services/ValidationService.d.ts +7 -5
- package/lib/services/ValidationService.d.ts.map +1 -1
- package/lib/services/ValidationService.js +40 -43
- package/lib/services/ValidationService.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 +23 -0
- package/lib/utils/FrameworkDetector.d.ts.map +1 -0
- package/lib/utils/FrameworkDetector.js +47 -0
- package/lib/utils/FrameworkDetector.js.map +1 -0
- package/lib/utils/FrameworkDetector.spec.d.ts +2 -0
- package/lib/utils/FrameworkDetector.spec.d.ts.map +1 -0
- package/lib/utils/FrameworkDetector.spec.js +67 -0
- package/lib/utils/FrameworkDetector.spec.js.map +1 -0
- package/package.json +11 -4
- package/lib/extractors/StringLiteralExtractor.d.ts +0 -12
- package/lib/extractors/StringLiteralExtractor.d.ts.map +0 -1
- package/lib/extractors/StringLiteralExtractor.js +0 -21
- package/lib/extractors/StringLiteralExtractor.js.map +0 -1
- package/lib/services/ClassNameExtractionService.original.d.ts +0 -20
- package/lib/services/ClassNameExtractionService.original.d.ts.map +0 -1
- package/lib/services/ClassNameExtractionService.original.js +0 -48
- package/lib/services/ClassNameExtractionService.original.js.map +0 -1
|
@@ -0,0 +1,1407 @@
|
|
|
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 TailwindVariantsExtractor_1 = require("./TailwindVariantsExtractor");
|
|
38
|
+
describe('TailwindVariantsExtractor', () => {
|
|
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 tv type declaration
|
|
59
|
+
'/node_modules/tailwind-variants/index.d.ts': `
|
|
60
|
+
export declare function tv<T>(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 TailwindVariantsExtractor_1.TailwindVariantsExtractor();
|
|
130
|
+
});
|
|
131
|
+
describe('canHandle', () => {
|
|
132
|
+
it('should return true for call expressions', () => {
|
|
133
|
+
const code = "import { tv } from 'tailwind-variants'; tv({ base: '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 - basic tv calls', () => {
|
|
145
|
+
it('should extract classes from tv base string', () => {
|
|
146
|
+
const code = `
|
|
147
|
+
import { tv } from 'tailwind-variants';
|
|
148
|
+
const button = tv({ base: 'flex items-center' });
|
|
149
|
+
`;
|
|
150
|
+
const context = createContext(code);
|
|
151
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
152
|
+
const classes = extractor.extract(callExpr, context);
|
|
153
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
154
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
155
|
+
});
|
|
156
|
+
it('should extract classes from tv base array', () => {
|
|
157
|
+
const code = `
|
|
158
|
+
import { tv } from 'tailwind-variants';
|
|
159
|
+
const button = tv({ base: ['flex', 'items-center', 'justify-center'] });
|
|
160
|
+
`;
|
|
161
|
+
const context = createContext(code);
|
|
162
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
163
|
+
const classes = extractor.extract(callExpr, context);
|
|
164
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
165
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
166
|
+
expect(classes.map(c => c.className)).toContain('justify-center');
|
|
167
|
+
});
|
|
168
|
+
it('should return empty array when no tv import', () => {
|
|
169
|
+
const code = `
|
|
170
|
+
const button = tv({ base: 'flex items-center' });
|
|
171
|
+
`;
|
|
172
|
+
const context = createContext(code);
|
|
173
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
174
|
+
const classes = extractor.extract(callExpr, context);
|
|
175
|
+
expect(classes).toHaveLength(0);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe('extract - tv variants', () => {
|
|
179
|
+
it('should extract classes from variants object', () => {
|
|
180
|
+
const code = `
|
|
181
|
+
import { tv } from 'tailwind-variants';
|
|
182
|
+
const button = tv({
|
|
183
|
+
base: 'base-class',
|
|
184
|
+
variants: {
|
|
185
|
+
color: {
|
|
186
|
+
primary: 'bg-blue-500 text-white',
|
|
187
|
+
secondary: 'bg-gray-500'
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
`;
|
|
192
|
+
const context = createContext(code);
|
|
193
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
194
|
+
const classes = extractor.extract(callExpr, context);
|
|
195
|
+
expect(classes.map(c => c.className)).toContain('base-class');
|
|
196
|
+
expect(classes.map(c => c.className)).toContain('bg-blue-500');
|
|
197
|
+
expect(classes.map(c => c.className)).toContain('text-white');
|
|
198
|
+
expect(classes.map(c => c.className)).toContain('bg-gray-500');
|
|
199
|
+
});
|
|
200
|
+
it('should extract classes from variant arrays', () => {
|
|
201
|
+
const code = `
|
|
202
|
+
import { tv } from 'tailwind-variants';
|
|
203
|
+
const button = tv({
|
|
204
|
+
variants: {
|
|
205
|
+
size: {
|
|
206
|
+
sm: ['text-sm', 'py-1', 'px-2'],
|
|
207
|
+
lg: ['text-lg', 'py-3', 'px-4']
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
`;
|
|
212
|
+
const context = createContext(code);
|
|
213
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
214
|
+
const classes = extractor.extract(callExpr, context);
|
|
215
|
+
expect(classes.map(c => c.className)).toContain('text-sm');
|
|
216
|
+
expect(classes.map(c => c.className)).toContain('py-1');
|
|
217
|
+
expect(classes.map(c => c.className)).toContain('text-lg');
|
|
218
|
+
expect(classes.map(c => c.className)).toContain('py-3');
|
|
219
|
+
});
|
|
220
|
+
it('should handle boolean variants', () => {
|
|
221
|
+
const code = `
|
|
222
|
+
import { tv } from 'tailwind-variants';
|
|
223
|
+
const button = tv({
|
|
224
|
+
variants: {
|
|
225
|
+
disabled: {
|
|
226
|
+
true: 'opacity-50 cursor-not-allowed',
|
|
227
|
+
false: 'cursor-pointer'
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
`;
|
|
232
|
+
const context = createContext(code);
|
|
233
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
234
|
+
const classes = extractor.extract(callExpr, context);
|
|
235
|
+
expect(classes.map(c => c.className)).toContain('opacity-50');
|
|
236
|
+
expect(classes.map(c => c.className)).toContain('cursor-not-allowed');
|
|
237
|
+
expect(classes.map(c => c.className)).toContain('cursor-pointer');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
describe('extract - compoundVariants', () => {
|
|
241
|
+
it('should extract classes from compoundVariants', () => {
|
|
242
|
+
const code = `
|
|
243
|
+
import { tv } from 'tailwind-variants';
|
|
244
|
+
const button = tv({
|
|
245
|
+
variants: {
|
|
246
|
+
color: { primary: 'bg-blue-500' }
|
|
247
|
+
},
|
|
248
|
+
compoundVariants: [
|
|
249
|
+
{
|
|
250
|
+
color: 'primary',
|
|
251
|
+
class: 'font-bold uppercase'
|
|
252
|
+
}
|
|
253
|
+
]
|
|
254
|
+
});
|
|
255
|
+
`;
|
|
256
|
+
const context = createContext(code);
|
|
257
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
258
|
+
const classes = extractor.extract(callExpr, context);
|
|
259
|
+
expect(classes.map(c => c.className)).toContain('font-bold');
|
|
260
|
+
expect(classes.map(c => c.className)).toContain('uppercase');
|
|
261
|
+
});
|
|
262
|
+
it('should extract classes from compoundVariants with className property', () => {
|
|
263
|
+
const code = `
|
|
264
|
+
import { tv } from 'tailwind-variants';
|
|
265
|
+
const button = tv({
|
|
266
|
+
compoundVariants: [
|
|
267
|
+
{
|
|
268
|
+
color: 'primary',
|
|
269
|
+
className: 'shadow-lg'
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
});
|
|
273
|
+
`;
|
|
274
|
+
const context = createContext(code);
|
|
275
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
276
|
+
const classes = extractor.extract(callExpr, context);
|
|
277
|
+
expect(classes.map(c => c.className)).toContain('shadow-lg');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
describe('extract - slots', () => {
|
|
281
|
+
it('should extract classes from slots', () => {
|
|
282
|
+
const code = `
|
|
283
|
+
import { tv } from 'tailwind-variants';
|
|
284
|
+
const card = tv({
|
|
285
|
+
slots: {
|
|
286
|
+
base: 'flex flex-col',
|
|
287
|
+
header: 'font-bold text-lg',
|
|
288
|
+
body: 'p-4',
|
|
289
|
+
footer: 'border-t'
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
`;
|
|
293
|
+
const context = createContext(code);
|
|
294
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
295
|
+
const classes = extractor.extract(callExpr, context);
|
|
296
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
297
|
+
expect(classes.map(c => c.className)).toContain('flex-col');
|
|
298
|
+
expect(classes.map(c => c.className)).toContain('font-bold');
|
|
299
|
+
expect(classes.map(c => c.className)).toContain('p-4');
|
|
300
|
+
expect(classes.map(c => c.className)).toContain('border-t');
|
|
301
|
+
});
|
|
302
|
+
it('should extract classes from slot arrays', () => {
|
|
303
|
+
const code = `
|
|
304
|
+
import { tv } from 'tailwind-variants';
|
|
305
|
+
const card = tv({
|
|
306
|
+
slots: {
|
|
307
|
+
base: ['flex', 'flex-col', 'gap-4']
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
`;
|
|
311
|
+
const context = createContext(code);
|
|
312
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
313
|
+
const classes = extractor.extract(callExpr, context);
|
|
314
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
315
|
+
expect(classes.map(c => c.className)).toContain('flex-col');
|
|
316
|
+
expect(classes.map(c => c.className)).toContain('gap-4');
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
describe('extract - import variations', () => {
|
|
320
|
+
it('should handle aliased imports', () => {
|
|
321
|
+
const code = `
|
|
322
|
+
import { tv as createVariants } from 'tailwind-variants';
|
|
323
|
+
const button = createVariants({ base: 'flex items-center' });
|
|
324
|
+
`;
|
|
325
|
+
const context = createContext(code);
|
|
326
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
327
|
+
const classes = extractor.extract(callExpr, context);
|
|
328
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
329
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
330
|
+
});
|
|
331
|
+
it('should handle lite version import', () => {
|
|
332
|
+
const code = `
|
|
333
|
+
import { tv } from 'tailwind-variants/lite';
|
|
334
|
+
const button = tv({ base: 'flex items-center' });
|
|
335
|
+
`;
|
|
336
|
+
const context = createContext(code);
|
|
337
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
338
|
+
const classes = extractor.extract(callExpr, context);
|
|
339
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
340
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
describe('caching', () => {
|
|
344
|
+
it('should cache import detection results', () => {
|
|
345
|
+
const code = `
|
|
346
|
+
import { tv } from 'tailwind-variants';
|
|
347
|
+
const button = tv({ base: 'flex' });
|
|
348
|
+
`;
|
|
349
|
+
const context = createContext(code);
|
|
350
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
351
|
+
// Call twice to test caching
|
|
352
|
+
extractor.extract(callExpr, context);
|
|
353
|
+
const classes = extractor.extract(callExpr, context);
|
|
354
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
355
|
+
});
|
|
356
|
+
it('should clear cache when clearCache is called', () => {
|
|
357
|
+
const code = `
|
|
358
|
+
import { tv } from 'tailwind-variants';
|
|
359
|
+
const button = tv({ base: 'flex' });
|
|
360
|
+
`;
|
|
361
|
+
const context = createContext(code);
|
|
362
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
363
|
+
extractor.extract(callExpr, context);
|
|
364
|
+
extractor.clearCache();
|
|
365
|
+
// Should still work after cache clear
|
|
366
|
+
const classes = extractor.extract(callExpr, context);
|
|
367
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
describe('edge cases', () => {
|
|
371
|
+
it('should return empty array for non-tv call expression', () => {
|
|
372
|
+
const code = `
|
|
373
|
+
import { tv } from 'tailwind-variants';
|
|
374
|
+
const x = otherFunction({ base: 'flex' });
|
|
375
|
+
`;
|
|
376
|
+
const context = createContext(code);
|
|
377
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
378
|
+
const classes = extractor.extract(callExpr, context);
|
|
379
|
+
expect(classes).toHaveLength(0);
|
|
380
|
+
});
|
|
381
|
+
it('should return empty array for tv call with no arguments', () => {
|
|
382
|
+
const code = `
|
|
383
|
+
import { tv } from 'tailwind-variants';
|
|
384
|
+
const button = tv();
|
|
385
|
+
`;
|
|
386
|
+
const context = createContext(code);
|
|
387
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
388
|
+
const classes = extractor.extract(callExpr, context);
|
|
389
|
+
expect(classes).toHaveLength(0);
|
|
390
|
+
});
|
|
391
|
+
it('should return empty array for tv call with non-object argument', () => {
|
|
392
|
+
const code = `
|
|
393
|
+
import { tv } from 'tailwind-variants';
|
|
394
|
+
const button = tv('not-an-object');
|
|
395
|
+
`;
|
|
396
|
+
const context = createContext(code);
|
|
397
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
398
|
+
const classes = extractor.extract(callExpr, context);
|
|
399
|
+
expect(classes).toHaveLength(0);
|
|
400
|
+
});
|
|
401
|
+
it('should handle empty variants object', () => {
|
|
402
|
+
const code = `
|
|
403
|
+
import { tv } from 'tailwind-variants';
|
|
404
|
+
const button = tv({ base: 'base-class', variants: {} });
|
|
405
|
+
`;
|
|
406
|
+
const context = createContext(code);
|
|
407
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
408
|
+
const classes = extractor.extract(callExpr, context);
|
|
409
|
+
expect(classes.map(c => c.className)).toContain('base-class');
|
|
410
|
+
});
|
|
411
|
+
it('should handle null variant values', () => {
|
|
412
|
+
const code = `
|
|
413
|
+
import { tv } from 'tailwind-variants';
|
|
414
|
+
const button = tv({
|
|
415
|
+
variants: {
|
|
416
|
+
color: {
|
|
417
|
+
primary: 'bg-blue-500',
|
|
418
|
+
none: null
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
`;
|
|
423
|
+
const context = createContext(code);
|
|
424
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
425
|
+
// Should not crash
|
|
426
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
427
|
+
});
|
|
428
|
+
it('should mark variant classes with isVariant', () => {
|
|
429
|
+
const code = `
|
|
430
|
+
import { tv } from 'tailwind-variants';
|
|
431
|
+
const button = tv({
|
|
432
|
+
base: 'base-class',
|
|
433
|
+
variants: {
|
|
434
|
+
color: {
|
|
435
|
+
primary: 'variant-class'
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
`;
|
|
440
|
+
const context = createContext(code);
|
|
441
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
442
|
+
const classes = extractor.extract(callExpr, context);
|
|
443
|
+
const baseClass = classes.find(c => c.className === 'base-class');
|
|
444
|
+
const variantClass = classes.find(c => c.className === 'variant-class');
|
|
445
|
+
// Base classes should not be marked as variants
|
|
446
|
+
expect(baseClass?.isVariant).toBeFalsy();
|
|
447
|
+
// Variant classes should be marked
|
|
448
|
+
expect(variantClass?.isVariant).toBe(true);
|
|
449
|
+
});
|
|
450
|
+
it('should set attributeId for duplicate detection', () => {
|
|
451
|
+
const code = `
|
|
452
|
+
import { tv } from 'tailwind-variants';
|
|
453
|
+
const button = tv({ base: 'flex items-center' });
|
|
454
|
+
`;
|
|
455
|
+
const context = createContext(code);
|
|
456
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
457
|
+
const classes = extractor.extract(callExpr, context);
|
|
458
|
+
expect(classes[0].attributeId).toBeDefined();
|
|
459
|
+
expect(classes[0].attributeId).toMatch(/^tv:\d+-\d+$/);
|
|
460
|
+
// All classes from same tv() call should have same attributeId
|
|
461
|
+
expect(classes[0].attributeId).toBe(classes[1].attributeId);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
describe('extract - type guards', () => {
|
|
465
|
+
it('should return empty array for non-call-expression nodes', () => {
|
|
466
|
+
const code = `
|
|
467
|
+
import { tv } from 'tailwind-variants';
|
|
468
|
+
const x = 'flex';
|
|
469
|
+
`;
|
|
470
|
+
const context = createContext(code);
|
|
471
|
+
// Pass a non-call-expression node
|
|
472
|
+
const classes = extractor.extract(context.sourceFile.statements[1], context);
|
|
473
|
+
expect(classes).toHaveLength(0);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
describe('extract - member expression tv calls', () => {
|
|
477
|
+
it('should handle property access expression for tv', () => {
|
|
478
|
+
const code = `
|
|
479
|
+
import { tv } from 'tailwind-variants';
|
|
480
|
+
const utils = { tv };
|
|
481
|
+
const button = utils.tv({ base: 'flex items-center' });
|
|
482
|
+
`;
|
|
483
|
+
const context = createContext(code);
|
|
484
|
+
// Find the last call expression (utils.tv(...))
|
|
485
|
+
const callExpressions = [];
|
|
486
|
+
const visit = (node) => {
|
|
487
|
+
if (ts.isCallExpression(node)) {
|
|
488
|
+
callExpressions.push(node);
|
|
489
|
+
}
|
|
490
|
+
ts.forEachChild(node, visit);
|
|
491
|
+
};
|
|
492
|
+
visit(context.sourceFile);
|
|
493
|
+
const callExpr = callExpressions[callExpressions.length - 1];
|
|
494
|
+
const classes = extractor.extract(callExpr, context);
|
|
495
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
496
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
describe('extract - template expressions', () => {
|
|
500
|
+
it('should extract from no-substitution template literal', () => {
|
|
501
|
+
const code = `
|
|
502
|
+
import { tv } from 'tailwind-variants';
|
|
503
|
+
const button = tv({ base: \`flex items-center\` });
|
|
504
|
+
`;
|
|
505
|
+
const context = createContext(code);
|
|
506
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
507
|
+
const classes = extractor.extract(callExpr, context);
|
|
508
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
509
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
510
|
+
});
|
|
511
|
+
it('should extract static parts from template expression with substitutions', () => {
|
|
512
|
+
const code = `
|
|
513
|
+
import { tv } from 'tailwind-variants';
|
|
514
|
+
const color = 'blue';
|
|
515
|
+
const button = tv({ base: \`flex \${color} items-center\` });
|
|
516
|
+
`;
|
|
517
|
+
const context = createContext(code);
|
|
518
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
519
|
+
const classes = extractor.extract(callExpr, context);
|
|
520
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
521
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
522
|
+
});
|
|
523
|
+
it('should extract from template expression head', () => {
|
|
524
|
+
const code = `
|
|
525
|
+
import { tv } from 'tailwind-variants';
|
|
526
|
+
const button = tv({ base: \`static-head \${dynamic}\` });
|
|
527
|
+
`;
|
|
528
|
+
const context = createContext(code);
|
|
529
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
530
|
+
const classes = extractor.extract(callExpr, context);
|
|
531
|
+
expect(classes.map(c => c.className)).toContain('static-head');
|
|
532
|
+
});
|
|
533
|
+
it('should extract from template spans', () => {
|
|
534
|
+
const code = `
|
|
535
|
+
import { tv } from 'tailwind-variants';
|
|
536
|
+
const button = tv({ base: \`\${first} middle-span \${second} end-span\` });
|
|
537
|
+
`;
|
|
538
|
+
const context = createContext(code);
|
|
539
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
540
|
+
const classes = extractor.extract(callExpr, context);
|
|
541
|
+
expect(classes.map(c => c.className)).toContain('middle-span');
|
|
542
|
+
expect(classes.map(c => c.className)).toContain('end-span');
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
describe('extract - expression types in values', () => {
|
|
546
|
+
it('should extract from ternary expressions', () => {
|
|
547
|
+
const code = `
|
|
548
|
+
import { tv } from 'tailwind-variants';
|
|
549
|
+
const button = tv({
|
|
550
|
+
variants: {
|
|
551
|
+
active: {
|
|
552
|
+
true: condition ? 'bg-blue-500' : 'bg-gray-500'
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
`;
|
|
557
|
+
const context = createContext(code);
|
|
558
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
559
|
+
const classes = extractor.extract(callExpr, context);
|
|
560
|
+
expect(classes.map(c => c.className)).toContain('bg-blue-500');
|
|
561
|
+
expect(classes.map(c => c.className)).toContain('bg-gray-500');
|
|
562
|
+
});
|
|
563
|
+
it('should extract from binary expressions', () => {
|
|
564
|
+
const code = `
|
|
565
|
+
import { tv } from 'tailwind-variants';
|
|
566
|
+
const button = tv({
|
|
567
|
+
variants: {
|
|
568
|
+
active: {
|
|
569
|
+
true: active && 'bg-blue-500'
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
`;
|
|
574
|
+
const context = createContext(code);
|
|
575
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
576
|
+
const classes = extractor.extract(callExpr, context);
|
|
577
|
+
expect(classes.map(c => c.className)).toContain('bg-blue-500');
|
|
578
|
+
});
|
|
579
|
+
it('should extract from parenthesized expressions', () => {
|
|
580
|
+
const code = `
|
|
581
|
+
import { tv } from 'tailwind-variants';
|
|
582
|
+
const button = tv({
|
|
583
|
+
variants: {
|
|
584
|
+
size: {
|
|
585
|
+
sm: ('text-sm py-1')
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
`;
|
|
590
|
+
const context = createContext(code);
|
|
591
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
592
|
+
const classes = extractor.extract(callExpr, context);
|
|
593
|
+
expect(classes.map(c => c.className)).toContain('text-sm');
|
|
594
|
+
expect(classes.map(c => c.className)).toContain('py-1');
|
|
595
|
+
});
|
|
596
|
+
it('should extract from as expressions (type assertions)', () => {
|
|
597
|
+
const code = `
|
|
598
|
+
import { tv } from 'tailwind-variants';
|
|
599
|
+
const button = tv({
|
|
600
|
+
variants: {
|
|
601
|
+
size: {
|
|
602
|
+
sm: 'text-sm py-1' as string
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
`;
|
|
607
|
+
const context = createContext(code);
|
|
608
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
609
|
+
const classes = extractor.extract(callExpr, context);
|
|
610
|
+
expect(classes.map(c => c.className)).toContain('text-sm');
|
|
611
|
+
expect(classes.map(c => c.className)).toContain('py-1');
|
|
612
|
+
});
|
|
613
|
+
it('should extract from non-null assertions', () => {
|
|
614
|
+
const code = `
|
|
615
|
+
import { tv } from 'tailwind-variants';
|
|
616
|
+
const maybeClass = 'flex items-center';
|
|
617
|
+
const button = tv({
|
|
618
|
+
base: maybeClass!
|
|
619
|
+
});
|
|
620
|
+
`;
|
|
621
|
+
const context = createContext(code);
|
|
622
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
623
|
+
// Without typeChecker, this will not extract from the variable
|
|
624
|
+
// but should not crash
|
|
625
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
626
|
+
});
|
|
627
|
+
it('should handle angle bracket syntax (treated as JSX in TSX files)', () => {
|
|
628
|
+
// Note: In TSX files, angle bracket type assertions <T>value
|
|
629
|
+
// are interpreted as JSX elements, not type assertions.
|
|
630
|
+
// The isTypeAssertionExpression check won't match in TSX.
|
|
631
|
+
const code = `
|
|
632
|
+
import { tv } from 'tailwind-variants';
|
|
633
|
+
const button = tv({
|
|
634
|
+
variants: {
|
|
635
|
+
size: {
|
|
636
|
+
sm: 'text-sm py-1' as const
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
`;
|
|
641
|
+
const context = createContext(code);
|
|
642
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
643
|
+
const classes = extractor.extract(callExpr, context);
|
|
644
|
+
expect(classes.map(c => c.className)).toContain('text-sm');
|
|
645
|
+
expect(classes.map(c => c.className)).toContain('py-1');
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
describe('extract - array values with expressions', () => {
|
|
649
|
+
it('should extract from arrays with ternary expressions', () => {
|
|
650
|
+
const code = `
|
|
651
|
+
import { tv } from 'tailwind-variants';
|
|
652
|
+
const button = tv({
|
|
653
|
+
base: ['flex', condition ? 'visible' : 'hidden']
|
|
654
|
+
});
|
|
655
|
+
`;
|
|
656
|
+
const context = createContext(code);
|
|
657
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
658
|
+
const classes = extractor.extract(callExpr, context);
|
|
659
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
660
|
+
expect(classes.map(c => c.className)).toContain('visible');
|
|
661
|
+
expect(classes.map(c => c.className)).toContain('hidden');
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
describe('extract - property names', () => {
|
|
665
|
+
it('should handle string literal property names', () => {
|
|
666
|
+
const code = `
|
|
667
|
+
import { tv } from 'tailwind-variants';
|
|
668
|
+
const button = tv({
|
|
669
|
+
'base': 'flex',
|
|
670
|
+
'variants': {
|
|
671
|
+
'color': {
|
|
672
|
+
'primary': 'bg-blue-500'
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
`;
|
|
677
|
+
const context = createContext(code);
|
|
678
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
679
|
+
const classes = extractor.extract(callExpr, context);
|
|
680
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
681
|
+
expect(classes.map(c => c.className)).toContain('bg-blue-500');
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
describe('extract - slots with nested config', () => {
|
|
685
|
+
it('should extract from slots with nested base and variants', () => {
|
|
686
|
+
const code = `
|
|
687
|
+
import { tv } from 'tailwind-variants';
|
|
688
|
+
const card = tv({
|
|
689
|
+
slots: {
|
|
690
|
+
root: {
|
|
691
|
+
base: 'flex flex-col',
|
|
692
|
+
variants: {
|
|
693
|
+
size: {
|
|
694
|
+
sm: 'p-2',
|
|
695
|
+
lg: 'p-4'
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
`;
|
|
702
|
+
const context = createContext(code);
|
|
703
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
704
|
+
const classes = extractor.extract(callExpr, context);
|
|
705
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
706
|
+
expect(classes.map(c => c.className)).toContain('flex-col');
|
|
707
|
+
expect(classes.map(c => c.className)).toContain('p-2');
|
|
708
|
+
expect(classes.map(c => c.className)).toContain('p-4');
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
describe('extract - defaultVariants', () => {
|
|
712
|
+
it('should skip defaultVariants (no classes there)', () => {
|
|
713
|
+
const code = `
|
|
714
|
+
import { tv } from 'tailwind-variants';
|
|
715
|
+
const button = tv({
|
|
716
|
+
base: 'flex',
|
|
717
|
+
variants: {
|
|
718
|
+
color: {
|
|
719
|
+
primary: 'bg-blue-500'
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
defaultVariants: {
|
|
723
|
+
color: 'primary'
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
`;
|
|
727
|
+
const context = createContext(code);
|
|
728
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
729
|
+
const classes = extractor.extract(callExpr, context);
|
|
730
|
+
// defaultVariants contains 'primary' which is not a class
|
|
731
|
+
expect(classes.map(c => c.className)).not.toContain('primary');
|
|
732
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
733
|
+
expect(classes.map(c => c.className)).toContain('bg-blue-500');
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
describe('extract - unknown properties', () => {
|
|
737
|
+
it('should try to extract from unknown properties', () => {
|
|
738
|
+
const code = `
|
|
739
|
+
import { tv } from 'tailwind-variants';
|
|
740
|
+
const button = tv({
|
|
741
|
+
base: 'flex',
|
|
742
|
+
customProperty: 'custom-class'
|
|
743
|
+
});
|
|
744
|
+
`;
|
|
745
|
+
const context = createContext(code);
|
|
746
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
747
|
+
const classes = extractor.extract(callExpr, context);
|
|
748
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
749
|
+
expect(classes.map(c => c.className)).toContain('custom-class');
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
describe('extract - compoundVariants edge cases', () => {
|
|
753
|
+
it('should handle compoundVariants with array values', () => {
|
|
754
|
+
const code = `
|
|
755
|
+
import { tv } from 'tailwind-variants';
|
|
756
|
+
const button = tv({
|
|
757
|
+
compoundVariants: [
|
|
758
|
+
{
|
|
759
|
+
color: 'primary',
|
|
760
|
+
class: ['font-bold', 'uppercase']
|
|
761
|
+
}
|
|
762
|
+
]
|
|
763
|
+
});
|
|
764
|
+
`;
|
|
765
|
+
const context = createContext(code);
|
|
766
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
767
|
+
const classes = extractor.extract(callExpr, context);
|
|
768
|
+
expect(classes.map(c => c.className)).toContain('font-bold');
|
|
769
|
+
expect(classes.map(c => c.className)).toContain('uppercase');
|
|
770
|
+
});
|
|
771
|
+
it('should skip non-class properties in compoundVariants', () => {
|
|
772
|
+
const code = `
|
|
773
|
+
import { tv } from 'tailwind-variants';
|
|
774
|
+
const button = tv({
|
|
775
|
+
compoundVariants: [
|
|
776
|
+
{
|
|
777
|
+
color: 'primary',
|
|
778
|
+
size: 'sm',
|
|
779
|
+
class: 'font-bold'
|
|
780
|
+
}
|
|
781
|
+
]
|
|
782
|
+
});
|
|
783
|
+
`;
|
|
784
|
+
const context = createContext(code);
|
|
785
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
786
|
+
const classes = extractor.extract(callExpr, context);
|
|
787
|
+
// 'primary' and 'sm' should not be extracted as classes
|
|
788
|
+
expect(classes.map(c => c.className)).not.toContain('primary');
|
|
789
|
+
expect(classes.map(c => c.className)).not.toContain('sm');
|
|
790
|
+
expect(classes.map(c => c.className)).toContain('font-bold');
|
|
791
|
+
});
|
|
792
|
+
it('should return empty for non-array compoundVariants', () => {
|
|
793
|
+
const code = `
|
|
794
|
+
import { tv } from 'tailwind-variants';
|
|
795
|
+
const button = tv({
|
|
796
|
+
compoundVariants: 'not-an-array'
|
|
797
|
+
});
|
|
798
|
+
`;
|
|
799
|
+
const context = createContext(code);
|
|
800
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
801
|
+
// Should not crash
|
|
802
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
803
|
+
});
|
|
804
|
+
it('should handle empty compoundVariants array', () => {
|
|
805
|
+
const code = `
|
|
806
|
+
import { tv } from 'tailwind-variants';
|
|
807
|
+
const button = tv({
|
|
808
|
+
base: 'flex',
|
|
809
|
+
compoundVariants: []
|
|
810
|
+
});
|
|
811
|
+
`;
|
|
812
|
+
const context = createContext(code);
|
|
813
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
814
|
+
const classes = extractor.extract(callExpr, context);
|
|
815
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
816
|
+
});
|
|
817
|
+
it('should mark compoundVariants classes with isVariant', () => {
|
|
818
|
+
const code = `
|
|
819
|
+
import { tv } from 'tailwind-variants';
|
|
820
|
+
const button = tv({
|
|
821
|
+
base: 'base-class',
|
|
822
|
+
compoundVariants: [
|
|
823
|
+
{
|
|
824
|
+
class: 'compound-class'
|
|
825
|
+
}
|
|
826
|
+
]
|
|
827
|
+
});
|
|
828
|
+
`;
|
|
829
|
+
const context = createContext(code);
|
|
830
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
831
|
+
const classes = extractor.extract(callExpr, context);
|
|
832
|
+
const baseClass = classes.find(c => c.className === 'base-class');
|
|
833
|
+
const compoundClass = classes.find(c => c.className === 'compound-class');
|
|
834
|
+
expect(baseClass?.isVariant).toBeFalsy();
|
|
835
|
+
expect(compoundClass?.isVariant).toBe(true);
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
describe('extract - variants edge cases', () => {
|
|
839
|
+
it('should return empty for non-object variants', () => {
|
|
840
|
+
const code = `
|
|
841
|
+
import { tv } from 'tailwind-variants';
|
|
842
|
+
const button = tv({
|
|
843
|
+
variants: 'not-an-object'
|
|
844
|
+
});
|
|
845
|
+
`;
|
|
846
|
+
const context = createContext(code);
|
|
847
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
848
|
+
// Should not crash
|
|
849
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
850
|
+
});
|
|
851
|
+
it('should handle variant values that are not objects', () => {
|
|
852
|
+
const code = `
|
|
853
|
+
import { tv } from 'tailwind-variants';
|
|
854
|
+
const button = tv({
|
|
855
|
+
variants: {
|
|
856
|
+
color: 'not-an-object-value'
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
`;
|
|
860
|
+
const context = createContext(code);
|
|
861
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
862
|
+
// Should not crash
|
|
863
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
describe('extract - slots edge cases', () => {
|
|
867
|
+
it('should return empty for non-object slots', () => {
|
|
868
|
+
const code = `
|
|
869
|
+
import { tv } from 'tailwind-variants';
|
|
870
|
+
const card = tv({
|
|
871
|
+
slots: 'not-an-object'
|
|
872
|
+
});
|
|
873
|
+
`;
|
|
874
|
+
const context = createContext(code);
|
|
875
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
876
|
+
// Should not crash
|
|
877
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
878
|
+
});
|
|
879
|
+
it('should handle slot with array value', () => {
|
|
880
|
+
const code = `
|
|
881
|
+
import { tv } from 'tailwind-variants';
|
|
882
|
+
const card = tv({
|
|
883
|
+
slots: {
|
|
884
|
+
base: ['flex', 'flex-col']
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
`;
|
|
888
|
+
const context = createContext(code);
|
|
889
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
890
|
+
const classes = extractor.extract(callExpr, context);
|
|
891
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
892
|
+
expect(classes.map(c => c.className)).toContain('flex-col');
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
describe('import detection', () => {
|
|
896
|
+
it('should not detect non-tailwind-variants imports', () => {
|
|
897
|
+
const code = `
|
|
898
|
+
import { tv } from 'some-other-library';
|
|
899
|
+
const button = tv({ base: 'flex' });
|
|
900
|
+
`;
|
|
901
|
+
const context = createContext(code);
|
|
902
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
903
|
+
const classes = extractor.extract(callExpr, context);
|
|
904
|
+
expect(classes).toHaveLength(0);
|
|
905
|
+
});
|
|
906
|
+
it('should not detect default imports', () => {
|
|
907
|
+
const code = `
|
|
908
|
+
import tv from 'tailwind-variants';
|
|
909
|
+
const button = tv({ base: 'flex' });
|
|
910
|
+
`;
|
|
911
|
+
const context = createContext(code);
|
|
912
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
913
|
+
const classes = extractor.extract(callExpr, context);
|
|
914
|
+
// Default imports are not supported, only named imports
|
|
915
|
+
expect(classes).toHaveLength(0);
|
|
916
|
+
});
|
|
917
|
+
it('should skip imports without import clause', () => {
|
|
918
|
+
const code = `
|
|
919
|
+
import 'tailwind-variants';
|
|
920
|
+
import { tv } from 'tailwind-variants';
|
|
921
|
+
const button = tv({ base: 'flex' });
|
|
922
|
+
`;
|
|
923
|
+
const context = createContext(code);
|
|
924
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
925
|
+
const classes = extractor.extract(callExpr, context);
|
|
926
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
927
|
+
});
|
|
928
|
+
it('should skip namespace imports', () => {
|
|
929
|
+
const code = `
|
|
930
|
+
import * as tvLib from 'tailwind-variants';
|
|
931
|
+
const button = tvLib.tv({ base: 'flex' });
|
|
932
|
+
`;
|
|
933
|
+
const context = createContext(code);
|
|
934
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
935
|
+
const classes = extractor.extract(callExpr, context);
|
|
936
|
+
// Namespace imports are not supported in the same way
|
|
937
|
+
expect(classes).toHaveLength(0);
|
|
938
|
+
});
|
|
939
|
+
});
|
|
940
|
+
describe('non-property-assignment in config', () => {
|
|
941
|
+
it('should skip shorthand properties in config', () => {
|
|
942
|
+
const code = `
|
|
943
|
+
import { tv } from 'tailwind-variants';
|
|
944
|
+
const base = 'flex';
|
|
945
|
+
const button = tv({ base, other: 'items-center' });
|
|
946
|
+
`;
|
|
947
|
+
const context = createContext(code);
|
|
948
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
949
|
+
const classes = extractor.extract(callExpr, context);
|
|
950
|
+
// Shorthand is NOT a PropertyAssignment, so 'flex' won't be extracted
|
|
951
|
+
// But 'other' is a regular PropertyAssignment
|
|
952
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
953
|
+
});
|
|
954
|
+
it('should skip spread properties in config', () => {
|
|
955
|
+
const code = `
|
|
956
|
+
import { tv } from 'tailwind-variants';
|
|
957
|
+
const config = { base: 'flex' };
|
|
958
|
+
const button = tv({ ...config, other: 'items-center' });
|
|
959
|
+
`;
|
|
960
|
+
const context = createContext(code);
|
|
961
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
962
|
+
const classes = extractor.extract(callExpr, context);
|
|
963
|
+
// Spread assignment is not handled, but regular properties are
|
|
964
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
describe('property name edge cases', () => {
|
|
968
|
+
it('should return null for computed property names', () => {
|
|
969
|
+
const code = `
|
|
970
|
+
import { tv } from 'tailwind-variants';
|
|
971
|
+
const propName = 'base';
|
|
972
|
+
const button = tv({ [propName]: 'flex' });
|
|
973
|
+
`;
|
|
974
|
+
const context = createContext(code);
|
|
975
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
976
|
+
// Computed property names return null from getPropertyName
|
|
977
|
+
// so no classes should be extracted
|
|
978
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
describe('extract with TypeChecker - tv function calls', () => {
|
|
982
|
+
it('should extract class from tv-created function call with class property', () => {
|
|
983
|
+
const code = `
|
|
984
|
+
import { tv } from 'tailwind-variants';
|
|
985
|
+
const button = tv({
|
|
986
|
+
base: 'base-class',
|
|
987
|
+
variants: {
|
|
988
|
+
color: {
|
|
989
|
+
primary: 'bg-blue-500'
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
const result = button({ color: 'primary', class: 'extra-class' });
|
|
994
|
+
`;
|
|
995
|
+
const context = createContextWithTypeChecker(code);
|
|
996
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
997
|
+
const classes = extractor.extract(callExpr, context);
|
|
998
|
+
expect(classes.map(c => c.className)).toContain('extra-class');
|
|
999
|
+
});
|
|
1000
|
+
it('should extract class from tv-created function call with className property', () => {
|
|
1001
|
+
const code = `
|
|
1002
|
+
import { tv } from 'tailwind-variants';
|
|
1003
|
+
const button = tv({ base: 'base-class' });
|
|
1004
|
+
const result = button({ className: 'override-class' });
|
|
1005
|
+
`;
|
|
1006
|
+
const context = createContextWithTypeChecker(code);
|
|
1007
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1008
|
+
const classes = extractor.extract(callExpr, context);
|
|
1009
|
+
expect(classes.map(c => c.className)).toContain('override-class');
|
|
1010
|
+
});
|
|
1011
|
+
it('should extract multiple classes from tv function call', () => {
|
|
1012
|
+
const code = `
|
|
1013
|
+
import { tv } from 'tailwind-variants';
|
|
1014
|
+
const button = tv({ base: 'flex' });
|
|
1015
|
+
const result = button({ class: 'items-center gap-2' });
|
|
1016
|
+
`;
|
|
1017
|
+
const context = createContextWithTypeChecker(code);
|
|
1018
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1019
|
+
const classes = extractor.extract(callExpr, context);
|
|
1020
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
1021
|
+
expect(classes.map(c => c.className)).toContain('gap-2');
|
|
1022
|
+
});
|
|
1023
|
+
it('should return empty array when tv function call has no arguments', () => {
|
|
1024
|
+
const code = `
|
|
1025
|
+
import { tv } from 'tailwind-variants';
|
|
1026
|
+
const button = tv({ base: 'flex' });
|
|
1027
|
+
const result = button();
|
|
1028
|
+
`;
|
|
1029
|
+
const context = createContextWithTypeChecker(code);
|
|
1030
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1031
|
+
const classes = extractor.extract(callExpr, context);
|
|
1032
|
+
expect(classes).toHaveLength(0);
|
|
1033
|
+
});
|
|
1034
|
+
it('should return empty array when tv function call arg is not object literal', () => {
|
|
1035
|
+
const code = `
|
|
1036
|
+
import { tv } from 'tailwind-variants';
|
|
1037
|
+
const button = tv({ base: 'flex' });
|
|
1038
|
+
const options = { class: 'flex' };
|
|
1039
|
+
const result = button(options);
|
|
1040
|
+
`;
|
|
1041
|
+
const context = createContextWithTypeChecker(code);
|
|
1042
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1043
|
+
const classes = extractor.extract(callExpr, context);
|
|
1044
|
+
expect(classes).toHaveLength(0);
|
|
1045
|
+
});
|
|
1046
|
+
it('should skip utility functions even with TypeChecker', () => {
|
|
1047
|
+
const code = `
|
|
1048
|
+
import { tv } from 'tailwind-variants';
|
|
1049
|
+
const button = tv({ base: 'flex' });
|
|
1050
|
+
const result = button({ class: 'grid' });
|
|
1051
|
+
`;
|
|
1052
|
+
const context = createContextWithTypeChecker(code, {
|
|
1053
|
+
utilityFunctions: ['button']
|
|
1054
|
+
});
|
|
1055
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1056
|
+
const classes = extractor.extract(callExpr, context);
|
|
1057
|
+
expect(classes).toHaveLength(0);
|
|
1058
|
+
});
|
|
1059
|
+
it('should cache symbol lookup results', () => {
|
|
1060
|
+
const code = `
|
|
1061
|
+
import { tv } from 'tailwind-variants';
|
|
1062
|
+
const button = tv({ base: 'flex' });
|
|
1063
|
+
const result1 = button({ class: 'grid' });
|
|
1064
|
+
const result2 = button({ class: 'block' });
|
|
1065
|
+
`;
|
|
1066
|
+
const context = createContextWithTypeChecker(code);
|
|
1067
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
1068
|
+
const buttonCall1 = calls[1];
|
|
1069
|
+
const buttonCall2 = calls[2];
|
|
1070
|
+
const classes1 = extractor.extract(buttonCall1, context);
|
|
1071
|
+
const classes2 = extractor.extract(buttonCall2, context);
|
|
1072
|
+
expect(classes1.map(c => c.className)).toContain('grid');
|
|
1073
|
+
expect(classes2.map(c => c.className)).toContain('block');
|
|
1074
|
+
});
|
|
1075
|
+
it('should set attributeId for tv function call classes', () => {
|
|
1076
|
+
const code = `
|
|
1077
|
+
import { tv } from 'tailwind-variants';
|
|
1078
|
+
const button = tv({ base: 'flex' });
|
|
1079
|
+
const result = button({ class: 'items-center' });
|
|
1080
|
+
`;
|
|
1081
|
+
const context = createContextWithTypeChecker(code);
|
|
1082
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1083
|
+
const classes = extractor.extract(callExpr, context);
|
|
1084
|
+
expect(classes[0].attributeId).toBeDefined();
|
|
1085
|
+
expect(classes[0].attributeId).toMatch(/^tv-call:\d+-\d+$/);
|
|
1086
|
+
});
|
|
1087
|
+
it('should not extract from non-tv function calls', () => {
|
|
1088
|
+
const code = `
|
|
1089
|
+
import { tv } from 'tailwind-variants';
|
|
1090
|
+
const notTv = () => {};
|
|
1091
|
+
const result = notTv({ class: 'flex' });
|
|
1092
|
+
`;
|
|
1093
|
+
const context = createContextWithTypeChecker(code);
|
|
1094
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1095
|
+
const classes = extractor.extract(callExpr, context);
|
|
1096
|
+
expect(classes).toHaveLength(0);
|
|
1097
|
+
});
|
|
1098
|
+
it('should handle property access expression calls', () => {
|
|
1099
|
+
const code = `
|
|
1100
|
+
import { tv } from 'tailwind-variants';
|
|
1101
|
+
const variants = {
|
|
1102
|
+
button: tv({ base: 'base-class' })
|
|
1103
|
+
};
|
|
1104
|
+
const result = variants.button({ class: 'extra-class' });
|
|
1105
|
+
`;
|
|
1106
|
+
const context = createContextWithTypeChecker(code);
|
|
1107
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1108
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
1109
|
+
});
|
|
1110
|
+
it('should skip non-identifier and non-property-access expressions', () => {
|
|
1111
|
+
const code = `
|
|
1112
|
+
import { tv } from 'tailwind-variants';
|
|
1113
|
+
const getButton = () => tv({ base: 'flex' });
|
|
1114
|
+
const result = getButton()({ class: 'grid' });
|
|
1115
|
+
`;
|
|
1116
|
+
const context = createContextWithTypeChecker(code);
|
|
1117
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
1118
|
+
const outerCall = calls[calls.length - 1];
|
|
1119
|
+
const classes = extractor.extract(outerCall, context);
|
|
1120
|
+
expect(classes).toHaveLength(0);
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
describe('extract with TypeChecker - edge cases', () => {
|
|
1124
|
+
it('should handle class property with array value', () => {
|
|
1125
|
+
const code = `
|
|
1126
|
+
import { tv } from 'tailwind-variants';
|
|
1127
|
+
const button = tv({ base: 'flex' });
|
|
1128
|
+
const result = button({ class: ['grid', 'gap-4'] });
|
|
1129
|
+
`;
|
|
1130
|
+
const context = createContextWithTypeChecker(code);
|
|
1131
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1132
|
+
const classes = extractor.extract(callExpr, context);
|
|
1133
|
+
expect(classes.map(c => c.className)).toContain('grid');
|
|
1134
|
+
expect(classes.map(c => c.className)).toContain('gap-4');
|
|
1135
|
+
});
|
|
1136
|
+
it('should skip non-property-assignment in function call arg', () => {
|
|
1137
|
+
const code = `
|
|
1138
|
+
import { tv } from 'tailwind-variants';
|
|
1139
|
+
const button = tv({ base: 'flex' });
|
|
1140
|
+
const extra = 'items-center';
|
|
1141
|
+
const result = button({ class: 'grid', extra });
|
|
1142
|
+
`;
|
|
1143
|
+
const context = createContextWithTypeChecker(code);
|
|
1144
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1145
|
+
const classes = extractor.extract(callExpr, context);
|
|
1146
|
+
expect(classes.map(c => c.className)).toContain('grid');
|
|
1147
|
+
});
|
|
1148
|
+
it('should handle aliased tv import with TypeChecker', () => {
|
|
1149
|
+
const code = `
|
|
1150
|
+
import { tv as createVariants } from 'tailwind-variants';
|
|
1151
|
+
const button = createVariants({ base: 'base-class' });
|
|
1152
|
+
const result = button({ class: 'extra-class' });
|
|
1153
|
+
`;
|
|
1154
|
+
const context = createContextWithTypeChecker(code);
|
|
1155
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1156
|
+
const classes = extractor.extract(callExpr, context);
|
|
1157
|
+
expect(classes.map(c => c.className)).toContain('extra-class');
|
|
1158
|
+
});
|
|
1159
|
+
it('should handle slots with TypeChecker', () => {
|
|
1160
|
+
const code = `
|
|
1161
|
+
import { tv } from 'tailwind-variants';
|
|
1162
|
+
const card = tv({
|
|
1163
|
+
slots: {
|
|
1164
|
+
base: 'flex flex-col',
|
|
1165
|
+
header: 'font-bold'
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
const { base, header } = card();
|
|
1169
|
+
`;
|
|
1170
|
+
const context = createContextWithTypeChecker(code);
|
|
1171
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
1172
|
+
const tvCall = calls[0]; // The tv() call
|
|
1173
|
+
const classes = extractor.extract(tvCall, context);
|
|
1174
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
1175
|
+
expect(classes.map(c => c.className)).toContain('flex-col');
|
|
1176
|
+
expect(classes.map(c => c.className)).toContain('font-bold');
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
describe('100% coverage - edge cases', () => {
|
|
1180
|
+
it('should return false in isTvCall for element access expression (line 105)', () => {
|
|
1181
|
+
const code = `
|
|
1182
|
+
import { tv } from 'tailwind-variants';
|
|
1183
|
+
const tvFunctions = { default: tv };
|
|
1184
|
+
const button = tvFunctions['default']({ base: 'flex' });
|
|
1185
|
+
`;
|
|
1186
|
+
const context = createContext(code);
|
|
1187
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
1188
|
+
const lastCall = calls[calls.length - 1];
|
|
1189
|
+
const classes = extractor.extract(lastCall, context);
|
|
1190
|
+
expect(classes).toHaveLength(0);
|
|
1191
|
+
});
|
|
1192
|
+
it('should skip shorthand properties in variants (line 248)', () => {
|
|
1193
|
+
const code = `
|
|
1194
|
+
import { tv } from 'tailwind-variants';
|
|
1195
|
+
const size = { sm: 'text-sm' };
|
|
1196
|
+
const button = tv({ base: 'flex', variants: { size } });
|
|
1197
|
+
`;
|
|
1198
|
+
const context = createContext(code);
|
|
1199
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
1200
|
+
const classes = extractor.extract(callExpr, context);
|
|
1201
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
1202
|
+
});
|
|
1203
|
+
it('should skip spread in variant options (line 256)', () => {
|
|
1204
|
+
const code = `
|
|
1205
|
+
import { tv } from 'tailwind-variants';
|
|
1206
|
+
const options = { sm: 'text-sm' };
|
|
1207
|
+
const button = tv({
|
|
1208
|
+
base: 'flex',
|
|
1209
|
+
variants: {
|
|
1210
|
+
size: {
|
|
1211
|
+
...options,
|
|
1212
|
+
lg: 'text-lg'
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
`;
|
|
1217
|
+
const context = createContext(code);
|
|
1218
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
1219
|
+
const classes = extractor.extract(callExpr, context);
|
|
1220
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
1221
|
+
expect(classes.map(c => c.className)).toContain('text-lg');
|
|
1222
|
+
});
|
|
1223
|
+
it('should skip non-object elements in compoundVariants (line 288)', () => {
|
|
1224
|
+
const code = `
|
|
1225
|
+
import { tv } from 'tailwind-variants';
|
|
1226
|
+
const button = tv({
|
|
1227
|
+
base: 'flex',
|
|
1228
|
+
compoundVariants: [
|
|
1229
|
+
'not-an-object',
|
|
1230
|
+
{ color: 'primary', class: 'font-bold' }
|
|
1231
|
+
]
|
|
1232
|
+
});
|
|
1233
|
+
`;
|
|
1234
|
+
const context = createContext(code);
|
|
1235
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
1236
|
+
const classes = extractor.extract(callExpr, context);
|
|
1237
|
+
expect(classes.map(c => c.className)).toContain('font-bold');
|
|
1238
|
+
});
|
|
1239
|
+
it('should skip spread in compoundVariants properties (line 294)', () => {
|
|
1240
|
+
const code = `
|
|
1241
|
+
import { tv } from 'tailwind-variants';
|
|
1242
|
+
const conditions = { color: 'primary' };
|
|
1243
|
+
const button = tv({
|
|
1244
|
+
base: 'flex',
|
|
1245
|
+
compoundVariants: [
|
|
1246
|
+
{
|
|
1247
|
+
...conditions,
|
|
1248
|
+
class: 'font-bold'
|
|
1249
|
+
}
|
|
1250
|
+
]
|
|
1251
|
+
});
|
|
1252
|
+
`;
|
|
1253
|
+
const context = createContext(code);
|
|
1254
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
1255
|
+
const classes = extractor.extract(callExpr, context);
|
|
1256
|
+
expect(classes.map(c => c.className)).toContain('font-bold');
|
|
1257
|
+
});
|
|
1258
|
+
it('should skip shorthand properties in slots (line 326)', () => {
|
|
1259
|
+
const code = `
|
|
1260
|
+
import { tv } from 'tailwind-variants';
|
|
1261
|
+
const base = 'flex';
|
|
1262
|
+
const card = tv({ slots: { base, header: 'font-bold' } });
|
|
1263
|
+
`;
|
|
1264
|
+
const context = createContext(code);
|
|
1265
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
1266
|
+
const classes = extractor.extract(callExpr, context);
|
|
1267
|
+
expect(classes.map(c => c.className)).toContain('font-bold');
|
|
1268
|
+
});
|
|
1269
|
+
it('should handle identifier in extractFromValue (lines 371-372)', () => {
|
|
1270
|
+
const code = `
|
|
1271
|
+
import { tv } from 'tailwind-variants';
|
|
1272
|
+
const myBase = 'flex items-center';
|
|
1273
|
+
const button = tv({ base: myBase });
|
|
1274
|
+
`;
|
|
1275
|
+
const context = createContext(code);
|
|
1276
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
1277
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
1278
|
+
});
|
|
1279
|
+
it('should handle identifier in array (lines 386-388)', () => {
|
|
1280
|
+
const code = `
|
|
1281
|
+
import { tv } from 'tailwind-variants';
|
|
1282
|
+
const extraClass = 'extra';
|
|
1283
|
+
const button = tv({ base: ['flex', extraClass, 'items-center'] });
|
|
1284
|
+
`;
|
|
1285
|
+
const context = createContext(code);
|
|
1286
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
1287
|
+
const classes = extractor.extract(callExpr, context);
|
|
1288
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
1289
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
1290
|
+
});
|
|
1291
|
+
it('should handle type assertion in .ts file (lines 488-489)', () => {
|
|
1292
|
+
const fileName = '/test.ts';
|
|
1293
|
+
const code = `
|
|
1294
|
+
import { tv } from 'tailwind-variants';
|
|
1295
|
+
const button = tv({ base: <string>'base-class' });
|
|
1296
|
+
`;
|
|
1297
|
+
const files = {
|
|
1298
|
+
[fileName]: code,
|
|
1299
|
+
'/node_modules/tailwind-variants/index.d.ts': `
|
|
1300
|
+
export declare function tv<T>(config?: T): (...args: any[]) => string;
|
|
1301
|
+
`
|
|
1302
|
+
};
|
|
1303
|
+
const compilerHost = {
|
|
1304
|
+
getSourceFile: (name, languageVersion) => {
|
|
1305
|
+
const content = files[name];
|
|
1306
|
+
if (content !== undefined) {
|
|
1307
|
+
return ts.createSourceFile(name, content, languageVersion, true, name.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
1308
|
+
}
|
|
1309
|
+
return undefined;
|
|
1310
|
+
},
|
|
1311
|
+
getDefaultLibFileName: () => '/lib.d.ts',
|
|
1312
|
+
writeFile: () => { },
|
|
1313
|
+
getCurrentDirectory: () => '/',
|
|
1314
|
+
getCanonicalFileName: (f) => f,
|
|
1315
|
+
useCaseSensitiveFileNames: () => true,
|
|
1316
|
+
getNewLine: () => '\n',
|
|
1317
|
+
fileExists: (name) => name in files,
|
|
1318
|
+
readFile: (name) => files[name],
|
|
1319
|
+
directoryExists: () => true,
|
|
1320
|
+
getDirectories: () => []
|
|
1321
|
+
};
|
|
1322
|
+
const program = ts.createProgram([fileName], {
|
|
1323
|
+
target: ts.ScriptTarget.Latest,
|
|
1324
|
+
module: ts.ModuleKind.ESNext,
|
|
1325
|
+
strict: true,
|
|
1326
|
+
moduleResolution: ts.ModuleResolutionKind.NodeJs
|
|
1327
|
+
}, compilerHost);
|
|
1328
|
+
const sourceFile = program.getSourceFile(fileName);
|
|
1329
|
+
const context = {
|
|
1330
|
+
typescript: ts,
|
|
1331
|
+
sourceFile,
|
|
1332
|
+
typeChecker: program.getTypeChecker(),
|
|
1333
|
+
utilityFunctions: []
|
|
1334
|
+
};
|
|
1335
|
+
const callExpr = findCallExpression(sourceFile);
|
|
1336
|
+
const classes = extractor.extract(callExpr, context);
|
|
1337
|
+
expect(classes.map(c => c.className)).toContain('base-class');
|
|
1338
|
+
});
|
|
1339
|
+
it('should handle non-identifier/non-property-access in isTvCreatedFunctionCall (line 537)', () => {
|
|
1340
|
+
const code = `
|
|
1341
|
+
import { tv } from 'tailwind-variants';
|
|
1342
|
+
const getVariant = () => tv({ base: 'flex' });
|
|
1343
|
+
const result = getVariant()({ class: 'grid' });
|
|
1344
|
+
`;
|
|
1345
|
+
const context = createContextWithTypeChecker(code);
|
|
1346
|
+
const calls = findAllCallExpressions(context.sourceFile);
|
|
1347
|
+
const outerCall = calls[calls.length - 1];
|
|
1348
|
+
const classes = extractor.extract(outerCall, context);
|
|
1349
|
+
expect(classes).toHaveLength(0);
|
|
1350
|
+
});
|
|
1351
|
+
it('should return false when TypeChecker returns no symbol (line 554)', () => {
|
|
1352
|
+
const code = `
|
|
1353
|
+
import { tv } from 'tailwind-variants';
|
|
1354
|
+
const result = undefinedFunction({ class: 'flex' });
|
|
1355
|
+
`;
|
|
1356
|
+
const context = createContextWithTypeChecker(code);
|
|
1357
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1358
|
+
const classes = extractor.extract(callExpr, context);
|
|
1359
|
+
expect(classes).toHaveLength(0);
|
|
1360
|
+
});
|
|
1361
|
+
it('should return false when symbol has no declarations (line 576)', () => {
|
|
1362
|
+
const code = `
|
|
1363
|
+
import { tv } from 'tailwind-variants';
|
|
1364
|
+
declare const button: ReturnType<typeof tv>;
|
|
1365
|
+
const result = button({ class: 'flex' });
|
|
1366
|
+
`;
|
|
1367
|
+
const context = createContextWithTypeChecker(code);
|
|
1368
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1369
|
+
expect(() => extractor.extract(callExpr, context)).not.toThrow();
|
|
1370
|
+
});
|
|
1371
|
+
it('should handle export default tv pattern (lines 592-595)', () => {
|
|
1372
|
+
const code = `
|
|
1373
|
+
import { tv } from 'tailwind-variants';
|
|
1374
|
+
const button = tv({ base: 'flex' });
|
|
1375
|
+
export default button;
|
|
1376
|
+
const result = button({ class: 'grid' });
|
|
1377
|
+
`;
|
|
1378
|
+
const context = createContextWithTypeChecker(code);
|
|
1379
|
+
const callExpr = findLastCallExpression(context.sourceFile);
|
|
1380
|
+
const classes = extractor.extract(callExpr, context);
|
|
1381
|
+
expect(classes.map(c => c.className)).toContain('grid');
|
|
1382
|
+
});
|
|
1383
|
+
it('should handle identifier variable reference in array with TypeChecker', () => {
|
|
1384
|
+
const code = `
|
|
1385
|
+
import { tv } from 'tailwind-variants';
|
|
1386
|
+
const myClass = 'my-custom-class';
|
|
1387
|
+
const button = tv({ base: ['flex', myClass, 'items-center'] });
|
|
1388
|
+
`;
|
|
1389
|
+
const context = createContextWithTypeChecker(code);
|
|
1390
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
1391
|
+
const classes = extractor.extract(callExpr, context);
|
|
1392
|
+
expect(classes.map(c => c.className)).toContain('flex');
|
|
1393
|
+
expect(classes.map(c => c.className)).toContain('items-center');
|
|
1394
|
+
});
|
|
1395
|
+
it('should handle export default with direct tv call', () => {
|
|
1396
|
+
const code = `
|
|
1397
|
+
import { tv } from 'tailwind-variants';
|
|
1398
|
+
export default tv({ base: 'exported-base' });
|
|
1399
|
+
`;
|
|
1400
|
+
const context = createContextWithTypeChecker(code);
|
|
1401
|
+
const callExpr = findCallExpression(context.sourceFile);
|
|
1402
|
+
const classes = extractor.extract(callExpr, context);
|
|
1403
|
+
expect(classes.map(c => c.className)).toContain('exported-base');
|
|
1404
|
+
});
|
|
1405
|
+
});
|
|
1406
|
+
});
|
|
1407
|
+
//# sourceMappingURL=TailwindVariantsExtractor.spec.js.map
|