tailwind-typescript-plugin 1.4.0-beta.21 → 1.4.0-beta.22
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 +6 -0
- package/lib/extractors/VueAttributeExtractor.d.ts +59 -0
- package/lib/extractors/VueAttributeExtractor.d.ts.map +1 -1
- package/lib/extractors/VueAttributeExtractor.js +653 -78
- package/lib/extractors/VueAttributeExtractor.js.map +1 -1
- package/lib/extractors/VueExpressionExtractor.d.ts +14 -1
- package/lib/extractors/VueExpressionExtractor.d.ts.map +1 -1
- package/lib/extractors/VueExpressionExtractor.js +121 -1
- package/lib/extractors/VueExpressionExtractor.js.map +1 -1
- package/package.json +1 -1
|
@@ -35,13 +35,16 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
35
35
|
* Vue generates code like __VLS_ctx.clsx(...) for template expressions
|
|
36
36
|
* where clsx is imported in the script section. We need to check if the
|
|
37
37
|
* function name (not __VLS_ctx) is directly imported.
|
|
38
|
+
*
|
|
39
|
+
* Also handles namespace imports: __VLS_ctx.utils.clsx(...) for `import * as utils from 'clsx'`
|
|
38
40
|
*/
|
|
39
41
|
shouldValidateFunctionCall(callExpression, utilityFunctions, context) {
|
|
40
|
-
// First, check if this is a __VLS_ctx.functionName() pattern
|
|
42
|
+
// First, check if this is a __VLS_ctx.functionName() or __VLS_ctx.namespace.functionName() pattern
|
|
41
43
|
if (context) {
|
|
42
44
|
const expr = callExpression.expression;
|
|
43
45
|
if (context.typescript.isPropertyAccessExpression(expr)) {
|
|
44
46
|
const objectExpr = expr.expression;
|
|
47
|
+
// Pattern 1: __VLS_ctx.functionName() - direct import
|
|
45
48
|
if (context.typescript.isIdentifier(objectExpr) && objectExpr.text === '__VLS_ctx') {
|
|
46
49
|
const functionName = expr.name.text;
|
|
47
50
|
// Check each utility function configuration
|
|
@@ -60,6 +63,31 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
60
63
|
}
|
|
61
64
|
return false;
|
|
62
65
|
}
|
|
66
|
+
// Pattern 2: __VLS_ctx.namespace.functionName() - namespace import
|
|
67
|
+
// e.g., import * as utils from 'clsx' -> __VLS_ctx.utils.clsx()
|
|
68
|
+
if (context.typescript.isPropertyAccessExpression(objectExpr)) {
|
|
69
|
+
const namespaceRoot = objectExpr.expression;
|
|
70
|
+
if (context.typescript.isIdentifier(namespaceRoot) &&
|
|
71
|
+
namespaceRoot.text === '__VLS_ctx') {
|
|
72
|
+
const namespaceName = objectExpr.name.text; // e.g., 'utils'
|
|
73
|
+
const functionName = expr.name.text; // e.g., 'clsx'
|
|
74
|
+
// Check each utility function configuration
|
|
75
|
+
for (const utilityFunc of utilityFunctions) {
|
|
76
|
+
if (typeof utilityFunc === 'string') {
|
|
77
|
+
if (utilityFunc === functionName) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (utilityFunc.name === functionName) {
|
|
82
|
+
// Check if namespace is imported from expected module
|
|
83
|
+
if (this.isNamespaceImportedFrom(namespaceName, utilityFunc.from, context)) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
63
91
|
}
|
|
64
92
|
}
|
|
65
93
|
// Fall back to base implementation for non-Vue patterns
|
|
@@ -67,17 +95,12 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
67
95
|
}
|
|
68
96
|
canHandle(node, context) {
|
|
69
97
|
// We handle call expressions that look like Vue's generated element calls
|
|
70
|
-
// Pattern: __VLS_asFunctionalElement(...)({ ...{ class: ... } })
|
|
98
|
+
// Pattern 1 (intrinsic elements): __VLS_asFunctionalElement(...)({ ...{ class: ... } })
|
|
99
|
+
// Pattern 2 (custom components): __VLS_1({ colorStyles: ... }, ...)
|
|
71
100
|
if (!context.typescript.isCallExpression(node)) {
|
|
72
101
|
return false;
|
|
73
102
|
}
|
|
74
|
-
// Check if
|
|
75
|
-
// The pattern is: func(...)({...}) where the result of func(...) is called again
|
|
76
|
-
const expression = node.expression;
|
|
77
|
-
if (!context.typescript.isCallExpression(expression)) {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
// Check if the arguments contain an object with spread that has a 'class' property
|
|
103
|
+
// Check if the arguments contain an object with class properties
|
|
81
104
|
if (node.arguments.length === 0) {
|
|
82
105
|
return false;
|
|
83
106
|
}
|
|
@@ -85,10 +108,28 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
85
108
|
if (!context.typescript.isObjectLiteralExpression(firstArg)) {
|
|
86
109
|
return false;
|
|
87
110
|
}
|
|
88
|
-
|
|
89
|
-
|
|
111
|
+
const expression = node.expression;
|
|
112
|
+
// Pattern 1: Chained call (intrinsic elements)
|
|
113
|
+
// func(...)({...}) where the result of func(...) is called again
|
|
114
|
+
if (context.typescript.isCallExpression(expression)) {
|
|
115
|
+
// Look for spread assignments with class property
|
|
116
|
+
return this.hasClassSpreadProperty(firstArg, context);
|
|
117
|
+
}
|
|
118
|
+
// Pattern 2: Identifier call (custom components)
|
|
119
|
+
// __VLS_N({ classAttribute: ... }, ...)
|
|
120
|
+
if (context.typescript.isIdentifier(expression)) {
|
|
121
|
+
const name = expression.text;
|
|
122
|
+
// Vue generates __VLS_0, __VLS_1, etc. for component instances
|
|
123
|
+
if (name.startsWith('__VLS_')) {
|
|
124
|
+
// Check for direct class attribute properties
|
|
125
|
+
return this.hasClassDirectProperty(firstArg, context);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
90
129
|
}
|
|
91
130
|
hasClassSpreadProperty(obj, context) {
|
|
131
|
+
// Build set of class attribute names to check
|
|
132
|
+
const classAttributeNames = new Set(['class', ...(context.classAttributes || [])]);
|
|
92
133
|
for (const prop of obj.properties) {
|
|
93
134
|
if (context.typescript.isSpreadAssignment(prop)) {
|
|
94
135
|
const spreadExpr = prop.expression;
|
|
@@ -96,7 +137,7 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
96
137
|
for (const innerProp of spreadExpr.properties) {
|
|
97
138
|
if (context.typescript.isPropertyAssignment(innerProp)) {
|
|
98
139
|
const name = innerProp.name;
|
|
99
|
-
if (context.typescript.isIdentifier(name) && name.text
|
|
140
|
+
if (context.typescript.isIdentifier(name) && classAttributeNames.has(name.text)) {
|
|
100
141
|
return true;
|
|
101
142
|
}
|
|
102
143
|
}
|
|
@@ -106,6 +147,22 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
106
147
|
}
|
|
107
148
|
return false;
|
|
108
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Check if an object has direct class attribute properties (for custom components).
|
|
152
|
+
* Vue generates direct properties like: { colorStyles: "bg-blue-500" }
|
|
153
|
+
*/
|
|
154
|
+
hasClassDirectProperty(obj, context) {
|
|
155
|
+
const classAttributeNames = new Set(['class', ...(context.classAttributes || [])]);
|
|
156
|
+
for (const prop of obj.properties) {
|
|
157
|
+
if (context.typescript.isPropertyAssignment(prop)) {
|
|
158
|
+
const name = prop.name;
|
|
159
|
+
if (context.typescript.isIdentifier(name) && classAttributeNames.has(name.text)) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
109
166
|
extract(node, context) {
|
|
110
167
|
const classNames = [];
|
|
111
168
|
if (!context.typescript.isCallExpression(node)) {
|
|
@@ -115,26 +172,44 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
115
172
|
if (!firstArg || !context.typescript.isObjectLiteralExpression(firstArg)) {
|
|
116
173
|
return classNames;
|
|
117
174
|
}
|
|
118
|
-
//
|
|
175
|
+
// Build set of class attribute names to check
|
|
176
|
+
const classAttributeNames = new Set(['class', ...(context.classAttributes || [])]);
|
|
177
|
+
// Process all properties in the object literal
|
|
119
178
|
for (const prop of firstArg.properties) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
179
|
+
// Handle spread assignments: ...{ class: "..." }
|
|
180
|
+
if (context.typescript.isSpreadAssignment(prop)) {
|
|
181
|
+
const spreadExpr = prop.expression;
|
|
182
|
+
if (context.typescript.isObjectLiteralExpression(spreadExpr)) {
|
|
183
|
+
for (const innerProp of spreadExpr.properties) {
|
|
184
|
+
if (!context.typescript.isPropertyAssignment(innerProp)) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const name = innerProp.name;
|
|
188
|
+
if (!context.typescript.isIdentifier(name)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
// Check if this is a class attribute
|
|
192
|
+
if (!classAttributeNames.has(name.text)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const value = innerProp.initializer;
|
|
196
|
+
const attributeId = `${innerProp.getStart()}-${innerProp.getEnd()}`;
|
|
197
|
+
classNames.push(...this.extractClassesFromValue(value, context, attributeId));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
126
200
|
}
|
|
127
|
-
|
|
128
|
-
|
|
201
|
+
// Handle direct property assignments: colorStyles: "..."
|
|
202
|
+
else if (context.typescript.isPropertyAssignment(prop)) {
|
|
203
|
+
const name = prop.name;
|
|
204
|
+
if (!context.typescript.isIdentifier(name)) {
|
|
129
205
|
continue;
|
|
130
206
|
}
|
|
131
|
-
|
|
132
|
-
if (!
|
|
207
|
+
// Check if this is a class attribute (custom attributes like colorStyles)
|
|
208
|
+
if (!classAttributeNames.has(name.text)) {
|
|
133
209
|
continue;
|
|
134
210
|
}
|
|
135
|
-
const value =
|
|
136
|
-
const attributeId = `${
|
|
137
|
-
// Handle different class value types
|
|
211
|
+
const value = prop.initializer;
|
|
212
|
+
const attributeId = `${prop.getStart()}-${prop.getEnd()}`;
|
|
138
213
|
classNames.push(...this.extractClassesFromValue(value, context, attributeId));
|
|
139
214
|
}
|
|
140
215
|
}
|
|
@@ -180,47 +255,8 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
180
255
|
}
|
|
181
256
|
}
|
|
182
257
|
if (objectExpr) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const propName = prop.name;
|
|
186
|
-
let className;
|
|
187
|
-
let start;
|
|
188
|
-
// Handle string literal keys: { 'bg-red-500': true }
|
|
189
|
-
if (context.typescript.isStringLiteral(propName)) {
|
|
190
|
-
className = propName.text;
|
|
191
|
-
start = propName.getStart() + 1; // Skip opening quote
|
|
192
|
-
}
|
|
193
|
-
// Handle identifier keys: { flex: true }
|
|
194
|
-
else if (context.typescript.isIdentifier(propName)) {
|
|
195
|
-
className = propName.text;
|
|
196
|
-
start = propName.getStart();
|
|
197
|
-
}
|
|
198
|
-
if (className && start !== undefined) {
|
|
199
|
-
classNames.push({
|
|
200
|
-
className,
|
|
201
|
-
absoluteStart: start,
|
|
202
|
-
length: className.length,
|
|
203
|
-
line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
204
|
-
file: context.sourceFile.fileName,
|
|
205
|
-
attributeId
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
// Handle shorthand properties: { flex }
|
|
210
|
-
else if (context.typescript.isShorthandPropertyAssignment(prop)) {
|
|
211
|
-
const className = prop.name.text;
|
|
212
|
-
const start = prop.name.getStart();
|
|
213
|
-
classNames.push({
|
|
214
|
-
className,
|
|
215
|
-
absoluteStart: start,
|
|
216
|
-
length: className.length,
|
|
217
|
-
line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
218
|
-
file: context.sourceFile.fileName,
|
|
219
|
-
attributeId
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return classNames;
|
|
258
|
+
// Use extractFromObjectExpression which handles computed property names
|
|
259
|
+
return this.extractFromObjectExpression(objectExpr, context, attributeId);
|
|
224
260
|
}
|
|
225
261
|
// Array literal: class: ['flex', 'items-center']
|
|
226
262
|
// Vue wraps expressions in parentheses: class: (['flex', 'items-center'])
|
|
@@ -235,8 +271,8 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
235
271
|
}
|
|
236
272
|
}
|
|
237
273
|
if (arrayExpr) {
|
|
238
|
-
|
|
239
|
-
return
|
|
274
|
+
// Process array elements directly to handle __VLS_ctx references
|
|
275
|
+
return this.extractFromArrayExpression(arrayExpr, context, attributeId);
|
|
240
276
|
}
|
|
241
277
|
// Template literal or other expressions - delegate to expression extractor
|
|
242
278
|
// Vue wraps expressions in parentheses: class: (`flex items-center`)
|
|
@@ -268,9 +304,84 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
268
304
|
callExpr = inner;
|
|
269
305
|
}
|
|
270
306
|
}
|
|
271
|
-
if (callExpr
|
|
272
|
-
|
|
273
|
-
|
|
307
|
+
if (callExpr) {
|
|
308
|
+
// Check if it's a utility function (clsx, cn, etc.) - extract all arguments
|
|
309
|
+
if (this.shouldValidateFunctionCall(callExpr, context.utilityFunctions, context)) {
|
|
310
|
+
const addAttributeId = (classes) => classes.map(c => ({ ...c, attributeId }));
|
|
311
|
+
return addAttributeId(this.expressionExtractor.extract(callExpr, context));
|
|
312
|
+
}
|
|
313
|
+
// Check for CVA/TV function calls with class override: button({ class: '...' })
|
|
314
|
+
// These are __VLS_ctx.functionName({ class: '...' }) patterns
|
|
315
|
+
const classOverrideClasses = this.extractFromCvaTvClassOverride(callExpr, context, attributeId);
|
|
316
|
+
if (classOverrideClasses.length > 0) {
|
|
317
|
+
return classOverrideClasses;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Handle conditional (ternary) expressions: class: (isActive ? 'flex' : 'hidden')
|
|
321
|
+
// Vue wraps expressions in parentheses: class: (__VLS_ctx.isActive ? 'active' : 'inactive')
|
|
322
|
+
let conditionalExpr;
|
|
323
|
+
if (context.typescript.isConditionalExpression(value)) {
|
|
324
|
+
conditionalExpr = value;
|
|
325
|
+
}
|
|
326
|
+
else if (context.typescript.isParenthesizedExpression(value)) {
|
|
327
|
+
const inner = value.expression;
|
|
328
|
+
if (context.typescript.isConditionalExpression(inner)) {
|
|
329
|
+
conditionalExpr = inner;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (conditionalExpr) {
|
|
333
|
+
return this.extractFromConditionalExpression(conditionalExpr, context, attributeId);
|
|
334
|
+
}
|
|
335
|
+
// Handle binary expressions: class: (isActive && 'flex')
|
|
336
|
+
// Vue wraps expressions in parentheses: class: (__VLS_ctx.isActive && 'active')
|
|
337
|
+
let binaryExpr;
|
|
338
|
+
if (context.typescript.isBinaryExpression(value)) {
|
|
339
|
+
binaryExpr = value;
|
|
340
|
+
}
|
|
341
|
+
else if (context.typescript.isParenthesizedExpression(value)) {
|
|
342
|
+
const inner = value.expression;
|
|
343
|
+
if (context.typescript.isBinaryExpression(inner)) {
|
|
344
|
+
binaryExpr = inner;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (binaryExpr) {
|
|
348
|
+
return this.extractFromBinaryExpression(binaryExpr, context, attributeId);
|
|
349
|
+
}
|
|
350
|
+
// Handle type assertions: class: ('invalid-class' as string)
|
|
351
|
+
// Vue wraps expressions: class: (('invalid-class' as string))
|
|
352
|
+
let asExpr;
|
|
353
|
+
if (context.typescript.isAsExpression(value)) {
|
|
354
|
+
asExpr = value;
|
|
355
|
+
}
|
|
356
|
+
else if (context.typescript.isParenthesizedExpression(value)) {
|
|
357
|
+
let inner = value.expression;
|
|
358
|
+
// Double unwrap for nested parentheses
|
|
359
|
+
if (context.typescript.isParenthesizedExpression(inner)) {
|
|
360
|
+
inner = inner.expression;
|
|
361
|
+
}
|
|
362
|
+
if (context.typescript.isAsExpression(inner)) {
|
|
363
|
+
asExpr = inner;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (asExpr) {
|
|
367
|
+
return this.extractClassesFromValue(asExpr.expression, context, attributeId);
|
|
368
|
+
}
|
|
369
|
+
// Handle non-null assertions: class: (someClass!)
|
|
370
|
+
let nonNullExpr;
|
|
371
|
+
if (context.typescript.isNonNullExpression(value)) {
|
|
372
|
+
nonNullExpr = value;
|
|
373
|
+
}
|
|
374
|
+
else if (context.typescript.isParenthesizedExpression(value)) {
|
|
375
|
+
let inner = value.expression;
|
|
376
|
+
if (context.typescript.isParenthesizedExpression(inner)) {
|
|
377
|
+
inner = inner.expression;
|
|
378
|
+
}
|
|
379
|
+
if (context.typescript.isNonNullExpression(inner)) {
|
|
380
|
+
nonNullExpr = inner;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (nonNullExpr) {
|
|
384
|
+
return this.extractClassesFromValue(nonNullExpr.expression, context, attributeId);
|
|
274
385
|
}
|
|
275
386
|
// Handle __VLS_ctx.propertyName patterns for variable/computed/function references
|
|
276
387
|
// Vue generates: class: (__VLS_ctx.myClass) for :class="myClass"
|
|
@@ -280,6 +391,396 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
280
391
|
}
|
|
281
392
|
return classNames;
|
|
282
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* Extract classes from an array expression, handling __VLS_ctx references.
|
|
396
|
+
* This method processes array elements directly to support variable references.
|
|
397
|
+
*/
|
|
398
|
+
extractFromArrayExpression(arrayExpr, context, attributeId) {
|
|
399
|
+
const { typescript } = context;
|
|
400
|
+
const classNames = [];
|
|
401
|
+
for (const element of arrayExpr.elements) {
|
|
402
|
+
if (element === undefined)
|
|
403
|
+
continue;
|
|
404
|
+
// String literal: 'flex'
|
|
405
|
+
if (typescript.isStringLiteral(element)) {
|
|
406
|
+
const fullText = element.text;
|
|
407
|
+
if (fullText.length > 0) {
|
|
408
|
+
const stringContentStart = element.getStart() + 1;
|
|
409
|
+
let offset = 0;
|
|
410
|
+
const parts = fullText.split(/(\s+)/);
|
|
411
|
+
for (const part of parts) {
|
|
412
|
+
if (part && !/^\s+$/.test(part)) {
|
|
413
|
+
classNames.push({
|
|
414
|
+
className: part,
|
|
415
|
+
absoluteStart: stringContentStart + offset,
|
|
416
|
+
length: part.length,
|
|
417
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(stringContentStart + offset)
|
|
418
|
+
.line + 1,
|
|
419
|
+
file: context.sourceFile.fileName,
|
|
420
|
+
attributeId
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
offset += part.length;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Object literal: { 'bg-red-500': isActive }
|
|
428
|
+
else if (typescript.isObjectLiteralExpression(element)) {
|
|
429
|
+
classNames.push(...this.extractFromObjectExpression(element, context, attributeId));
|
|
430
|
+
}
|
|
431
|
+
// Spread element: ...classes
|
|
432
|
+
else if (typescript.isSpreadElement(element)) {
|
|
433
|
+
// Try to resolve __VLS_ctx reference
|
|
434
|
+
const vlsResults = this.extractFromVlsCtxReference(element.expression, context, attributeId);
|
|
435
|
+
if (vlsResults.length > 0) {
|
|
436
|
+
classNames.push(...vlsResults);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Handle __VLS_ctx.variable references: __VLS_ctx.myClass
|
|
440
|
+
else if (typescript.isPropertyAccessExpression(element)) {
|
|
441
|
+
const vlsResults = this.extractFromVlsCtxReference(element, context, attributeId);
|
|
442
|
+
if (vlsResults.length > 0) {
|
|
443
|
+
classNames.push(...vlsResults);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Handle parenthesized expressions: (__VLS_ctx.myVar)
|
|
447
|
+
else if (typescript.isParenthesizedExpression(element)) {
|
|
448
|
+
const inner = element.expression;
|
|
449
|
+
if (typescript.isPropertyAccessExpression(inner)) {
|
|
450
|
+
const vlsResults = this.extractFromVlsCtxReference(inner, context, attributeId);
|
|
451
|
+
if (vlsResults.length > 0) {
|
|
452
|
+
classNames.push(...vlsResults);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
else if (typescript.isArrayLiteralExpression(inner)) {
|
|
456
|
+
// Nested array in parentheses
|
|
457
|
+
classNames.push(...this.extractFromArrayExpression(inner, context, attributeId));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Handle nested arrays recursively
|
|
461
|
+
else if (typescript.isArrayLiteralExpression(element)) {
|
|
462
|
+
classNames.push(...this.extractFromArrayExpression(element, context, attributeId));
|
|
463
|
+
}
|
|
464
|
+
// Handle ternary/conditional expressions
|
|
465
|
+
else if (typescript.isConditionalExpression(element)) {
|
|
466
|
+
// Extract from both branches
|
|
467
|
+
classNames.push(...this.extractFromConditionalElement(element, context, attributeId));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return classNames;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Extract classes from an object expression, handling computed property names.
|
|
474
|
+
*/
|
|
475
|
+
extractFromObjectExpression(objExpr, context, attributeId) {
|
|
476
|
+
const { typescript } = context;
|
|
477
|
+
const classNames = [];
|
|
478
|
+
for (const prop of objExpr.properties) {
|
|
479
|
+
if (typescript.isPropertyAssignment(prop)) {
|
|
480
|
+
const propName = prop.name;
|
|
481
|
+
let className;
|
|
482
|
+
let start;
|
|
483
|
+
if (typescript.isStringLiteral(propName)) {
|
|
484
|
+
className = propName.text;
|
|
485
|
+
start = propName.getStart() + 1;
|
|
486
|
+
}
|
|
487
|
+
else if (typescript.isIdentifier(propName)) {
|
|
488
|
+
className = propName.text;
|
|
489
|
+
start = propName.getStart();
|
|
490
|
+
}
|
|
491
|
+
// Handle computed property names: { [__VLS_ctx.myVar]: true }
|
|
492
|
+
else if (typescript.isComputedPropertyName(propName)) {
|
|
493
|
+
let computedExpr = propName.expression;
|
|
494
|
+
// Unwrap parentheses
|
|
495
|
+
if (typescript.isParenthesizedExpression(computedExpr)) {
|
|
496
|
+
computedExpr = computedExpr.expression;
|
|
497
|
+
}
|
|
498
|
+
// Resolve __VLS_ctx.variable pattern
|
|
499
|
+
if (typescript.isPropertyAccessExpression(computedExpr)) {
|
|
500
|
+
const vlsResults = this.extractFromVlsCtxReference(computedExpr, context, attributeId);
|
|
501
|
+
if (vlsResults.length > 0) {
|
|
502
|
+
classNames.push(...vlsResults);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (className && start !== undefined) {
|
|
508
|
+
classNames.push({
|
|
509
|
+
className,
|
|
510
|
+
absoluteStart: start,
|
|
511
|
+
length: className.length,
|
|
512
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
513
|
+
file: context.sourceFile.fileName,
|
|
514
|
+
attributeId
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
else if (typescript.isShorthandPropertyAssignment(prop)) {
|
|
519
|
+
const className = prop.name.text;
|
|
520
|
+
const start = prop.name.getStart();
|
|
521
|
+
classNames.push({
|
|
522
|
+
className,
|
|
523
|
+
absoluteStart: start,
|
|
524
|
+
length: className.length,
|
|
525
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
526
|
+
file: context.sourceFile.fileName,
|
|
527
|
+
attributeId
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return classNames;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Extract classes from a conditional (ternary) expression in array context.
|
|
535
|
+
*/
|
|
536
|
+
extractFromConditionalElement(conditional, context, attributeId) {
|
|
537
|
+
return this.extractFromConditionalExpression(conditional, context, attributeId);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Extract classes from a conditional (ternary) expression.
|
|
541
|
+
* Handles: isActive ? 'flex' : 'hidden'
|
|
542
|
+
*/
|
|
543
|
+
extractFromConditionalExpression(conditional, context, attributeId) {
|
|
544
|
+
const classNames = [];
|
|
545
|
+
// Use the ternary's position as a unique identifier (like ExpressionExtractor)
|
|
546
|
+
const ternaryId = conditional.getStart();
|
|
547
|
+
// Extract from true branch with conditionalBranchId
|
|
548
|
+
const whenTrue = conditional.whenTrue;
|
|
549
|
+
classNames.push(...this.extractFromBranchExpression(whenTrue, context, attributeId, `ternary:true:${ternaryId}`));
|
|
550
|
+
// Extract from false branch with conditionalBranchId
|
|
551
|
+
const whenFalse = conditional.whenFalse;
|
|
552
|
+
classNames.push(...this.extractFromBranchExpression(whenFalse, context, attributeId, `ternary:false:${ternaryId}`));
|
|
553
|
+
return classNames;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Extract classes from a binary expression.
|
|
557
|
+
* Handles: isActive && 'flex', isDisabled || 'fallback'
|
|
558
|
+
*/
|
|
559
|
+
extractFromBinaryExpression(binary, context, attributeId) {
|
|
560
|
+
const classNames = [];
|
|
561
|
+
// Extract from left operand (for patterns like: 'flex' || fallback)
|
|
562
|
+
classNames.push(...this.extractFromBranchExpression(binary.left, context, attributeId));
|
|
563
|
+
// Extract from right operand (for patterns like: isActive && 'flex')
|
|
564
|
+
classNames.push(...this.extractFromBranchExpression(binary.right, context, attributeId));
|
|
565
|
+
return classNames;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Extract classes from a branch expression (ternary branch or binary operand).
|
|
569
|
+
* Handles string literals, nested ternaries, nested binaries, and VLS references.
|
|
570
|
+
*/
|
|
571
|
+
extractFromBranchExpression(expr, context, attributeId, conditionalBranchId) {
|
|
572
|
+
const { typescript } = context;
|
|
573
|
+
const classNames = [];
|
|
574
|
+
// Helper to add conditionalBranchId to extracted classes
|
|
575
|
+
const addBranchId = (classes) => conditionalBranchId ? classes.map(c => ({ ...c, conditionalBranchId })) : classes;
|
|
576
|
+
// String literal: 'flex items-center'
|
|
577
|
+
if (typescript.isStringLiteral(expr)) {
|
|
578
|
+
classNames.push(...addBranchId(this.extractClassesFromStringLiteral(expr, context, attributeId)));
|
|
579
|
+
}
|
|
580
|
+
// Nested ternary: condition ? 'a' : (nested ? 'b' : 'c')
|
|
581
|
+
else if (typescript.isConditionalExpression(expr)) {
|
|
582
|
+
classNames.push(...this.extractFromConditionalExpression(expr, context, attributeId));
|
|
583
|
+
}
|
|
584
|
+
// Nested binary: isA && isB && 'class'
|
|
585
|
+
else if (typescript.isBinaryExpression(expr)) {
|
|
586
|
+
classNames.push(...addBranchId(this.extractFromBinaryExpression(expr, context, attributeId)));
|
|
587
|
+
}
|
|
588
|
+
// Parenthesized expression
|
|
589
|
+
else if (typescript.isParenthesizedExpression(expr)) {
|
|
590
|
+
classNames.push(...this.extractFromBranchExpression(expr.expression, context, attributeId, conditionalBranchId));
|
|
591
|
+
}
|
|
592
|
+
// VLS ctx reference: __VLS_ctx.myClass
|
|
593
|
+
else if (typescript.isPropertyAccessExpression(expr)) {
|
|
594
|
+
const vlsResults = this.extractFromVlsCtxReference(expr, context, attributeId);
|
|
595
|
+
classNames.push(...addBranchId(vlsResults));
|
|
596
|
+
}
|
|
597
|
+
// Template literal: `flex ${something}`
|
|
598
|
+
else if (typescript.isTemplateExpression(expr) ||
|
|
599
|
+
typescript.isNoSubstitutionTemplateLiteral(expr)) {
|
|
600
|
+
const addAttributeId = (classes) => classes.map(c => ({ ...c, attributeId }));
|
|
601
|
+
classNames.push(...addBranchId(addAttributeId(this.expressionExtractor.extract(expr, context))));
|
|
602
|
+
}
|
|
603
|
+
return classNames;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Extract classes from CVA/TV function calls with class override.
|
|
607
|
+
* Handles patterns like: button({ color: 'primary', class: 'invalid-class' })
|
|
608
|
+
*
|
|
609
|
+
* Only extracts from functions that are defined using cva() or tv(),
|
|
610
|
+
* not from arbitrary custom functions.
|
|
611
|
+
*/
|
|
612
|
+
extractFromCvaTvClassOverride(callExpr, context, attributeId) {
|
|
613
|
+
const { typescript } = context;
|
|
614
|
+
const classNames = [];
|
|
615
|
+
// Check if this is a __VLS_ctx.functionName pattern
|
|
616
|
+
const calleeExpr = callExpr.expression;
|
|
617
|
+
if (!typescript.isPropertyAccessExpression(calleeExpr)) {
|
|
618
|
+
return classNames;
|
|
619
|
+
}
|
|
620
|
+
const objectExpr = calleeExpr.expression;
|
|
621
|
+
if (!typescript.isIdentifier(objectExpr) || objectExpr.text !== '__VLS_ctx') {
|
|
622
|
+
return classNames;
|
|
623
|
+
}
|
|
624
|
+
const functionName = calleeExpr.name;
|
|
625
|
+
if (!typescript.isIdentifier(functionName)) {
|
|
626
|
+
return classNames;
|
|
627
|
+
}
|
|
628
|
+
// Check if this function is defined using cva() or tv()
|
|
629
|
+
// by looking at its definition
|
|
630
|
+
if (!this.isCvaTvFunction(functionName, context)) {
|
|
631
|
+
return classNames;
|
|
632
|
+
}
|
|
633
|
+
// Look for object argument with class/className property
|
|
634
|
+
if (callExpr.arguments.length === 0) {
|
|
635
|
+
return classNames;
|
|
636
|
+
}
|
|
637
|
+
const firstArg = callExpr.arguments[0];
|
|
638
|
+
if (!typescript.isObjectLiteralExpression(firstArg)) {
|
|
639
|
+
return classNames;
|
|
640
|
+
}
|
|
641
|
+
// Find class or className property
|
|
642
|
+
for (const prop of firstArg.properties) {
|
|
643
|
+
if (!typescript.isPropertyAssignment(prop)) {
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const propName = prop.name;
|
|
647
|
+
if (!typescript.isIdentifier(propName)) {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (propName.text !== 'class' && propName.text !== 'className') {
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
// Extract classes from the property value
|
|
654
|
+
const value = prop.initializer;
|
|
655
|
+
if (typescript.isStringLiteral(value)) {
|
|
656
|
+
classNames.push(...this.extractClassesFromStringLiteral(value, context, attributeId));
|
|
657
|
+
}
|
|
658
|
+
else if (typescript.isArrayLiteralExpression(value)) {
|
|
659
|
+
classNames.push(...this.extractFromArrayExpression(value, context, attributeId));
|
|
660
|
+
}
|
|
661
|
+
else if (typescript.isTemplateExpression(value) ||
|
|
662
|
+
typescript.isNoSubstitutionTemplateLiteral(value)) {
|
|
663
|
+
const addAttrId = (classes) => classes.map(c => ({ ...c, attributeId }));
|
|
664
|
+
classNames.push(...addAttrId(this.expressionExtractor.extract(value, context)));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return classNames;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Check if a function is defined using cva() or tv().
|
|
671
|
+
* Resolves the function symbol and checks if its initializer is a cva/tv call.
|
|
672
|
+
*/
|
|
673
|
+
isCvaTvFunction(functionName, context) {
|
|
674
|
+
const { typescript, typeChecker } = context;
|
|
675
|
+
if (!typeChecker) {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
// Get the symbol for the function name
|
|
679
|
+
const symbol = typeChecker.getSymbolAtLocation(functionName);
|
|
680
|
+
if (!symbol) {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
const declarations = symbol.getDeclarations();
|
|
684
|
+
if (!declarations || declarations.length === 0) {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
for (const declaration of declarations) {
|
|
688
|
+
// Check PropertySignature -> typeof reference in Volar 3.x
|
|
689
|
+
if (typescript.isPropertySignature(declaration) && declaration.type) {
|
|
690
|
+
if (typescript.isTypeQueryNode(declaration.type)) {
|
|
691
|
+
const exprName = declaration.type.exprName;
|
|
692
|
+
if (typescript.isIdentifier(exprName)) {
|
|
693
|
+
// Resolve the actual variable
|
|
694
|
+
const varSymbol = typeChecker.getSymbolAtLocation(exprName);
|
|
695
|
+
if (varSymbol) {
|
|
696
|
+
const varDeclarations = varSymbol.getDeclarations();
|
|
697
|
+
if (varDeclarations) {
|
|
698
|
+
for (const varDecl of varDeclarations) {
|
|
699
|
+
if (typescript.isVariableDeclaration(varDecl) && varDecl.initializer) {
|
|
700
|
+
if (this.isCallToCvaOrTv(varDecl.initializer, context)) {
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
// Check direct variable declaration
|
|
711
|
+
else if (typescript.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
712
|
+
if (this.isCallToCvaOrTv(declaration.initializer, context)) {
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// Check property assignment in Vue's return
|
|
717
|
+
else if (typescript.isPropertyAssignment(declaration)) {
|
|
718
|
+
let expr = declaration.initializer;
|
|
719
|
+
if (typescript.isAsExpression(expr)) {
|
|
720
|
+
expr = expr.expression;
|
|
721
|
+
}
|
|
722
|
+
if (typescript.isIdentifier(expr)) {
|
|
723
|
+
const refSymbol = typeChecker.getSymbolAtLocation(expr);
|
|
724
|
+
if (refSymbol) {
|
|
725
|
+
const refDeclarations = refSymbol.getDeclarations();
|
|
726
|
+
if (refDeclarations) {
|
|
727
|
+
for (const refDecl of refDeclarations) {
|
|
728
|
+
if (typescript.isVariableDeclaration(refDecl) && refDecl.initializer) {
|
|
729
|
+
if (this.isCallToCvaOrTv(refDecl.initializer, context)) {
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Check if an expression is a call to cva() or tv().
|
|
743
|
+
*/
|
|
744
|
+
isCallToCvaOrTv(expr, context) {
|
|
745
|
+
const { typescript } = context;
|
|
746
|
+
if (!typescript.isCallExpression(expr)) {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
const callee = expr.expression;
|
|
750
|
+
// Direct call: cva(...) or tv(...)
|
|
751
|
+
if (typescript.isIdentifier(callee)) {
|
|
752
|
+
const name = callee.text;
|
|
753
|
+
return name === 'cva' || name === 'tv' || name === 'tvLite';
|
|
754
|
+
}
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Extract classes from a string literal with attributeId.
|
|
759
|
+
*/
|
|
760
|
+
extractClassesFromStringLiteral(literal, context, attributeId) {
|
|
761
|
+
const classNames = [];
|
|
762
|
+
const fullText = literal.text;
|
|
763
|
+
if (fullText.length === 0) {
|
|
764
|
+
return classNames;
|
|
765
|
+
}
|
|
766
|
+
const stringContentStart = literal.getStart() + 1;
|
|
767
|
+
let offset = 0;
|
|
768
|
+
const parts = fullText.split(/(\s+)/);
|
|
769
|
+
for (const part of parts) {
|
|
770
|
+
if (part && !/^\s+$/.test(part)) {
|
|
771
|
+
classNames.push({
|
|
772
|
+
className: part,
|
|
773
|
+
absoluteStart: stringContentStart + offset,
|
|
774
|
+
length: part.length,
|
|
775
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(stringContentStart + offset).line + 1,
|
|
776
|
+
file: context.sourceFile.fileName,
|
|
777
|
+
attributeId
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
offset += part.length;
|
|
781
|
+
}
|
|
782
|
+
return classNames;
|
|
783
|
+
}
|
|
283
784
|
/**
|
|
284
785
|
* Extract classes from __VLS_ctx patterns by resolving variable/function references.
|
|
285
786
|
*
|
|
@@ -820,8 +1321,11 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
820
1321
|
}
|
|
821
1322
|
/**
|
|
822
1323
|
* Extract classes from an expression (array, object, string, etc.)
|
|
1324
|
+
* @param expr The expression to extract from
|
|
1325
|
+
* @param context The extraction context
|
|
1326
|
+
* @param attributeId Optional attribute ID for tracking
|
|
823
1327
|
*/
|
|
824
|
-
extractFromExpression(expr, context) {
|
|
1328
|
+
extractFromExpression(expr, context, attributeId) {
|
|
825
1329
|
const { typescript } = context;
|
|
826
1330
|
const classNames = [];
|
|
827
1331
|
// String literal: 'flex items-center'
|
|
@@ -854,13 +1358,36 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
854
1358
|
if (element === undefined)
|
|
855
1359
|
continue;
|
|
856
1360
|
if (typescript.isStringLiteral(element)) {
|
|
857
|
-
classNames.push(...this.extractFromExpression(element, context));
|
|
1361
|
+
classNames.push(...this.extractFromExpression(element, context, attributeId));
|
|
858
1362
|
}
|
|
859
1363
|
else if (typescript.isObjectLiteralExpression(element)) {
|
|
860
|
-
classNames.push(...this.extractFromExpression(element, context));
|
|
1364
|
+
classNames.push(...this.extractFromExpression(element, context, attributeId));
|
|
861
1365
|
}
|
|
862
1366
|
else if (typescript.isSpreadElement(element)) {
|
|
863
|
-
classNames.push(...this.extractFromExpression(element.expression, context));
|
|
1367
|
+
classNames.push(...this.extractFromExpression(element.expression, context, attributeId));
|
|
1368
|
+
}
|
|
1369
|
+
// Handle __VLS_ctx.variable references in arrays
|
|
1370
|
+
else if (typescript.isPropertyAccessExpression(element)) {
|
|
1371
|
+
if (attributeId) {
|
|
1372
|
+
const vlsResults = this.extractFromVlsCtxReference(element, context, attributeId);
|
|
1373
|
+
if (vlsResults.length > 0) {
|
|
1374
|
+
classNames.push(...vlsResults);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
// Handle parenthesized expressions: (__VLS_ctx.myVar)
|
|
1379
|
+
else if (typescript.isParenthesizedExpression(element)) {
|
|
1380
|
+
const inner = element.expression;
|
|
1381
|
+
if (typescript.isPropertyAccessExpression(inner) && attributeId) {
|
|
1382
|
+
const vlsResults = this.extractFromVlsCtxReference(inner, context, attributeId);
|
|
1383
|
+
if (vlsResults.length > 0) {
|
|
1384
|
+
classNames.push(...vlsResults);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
// Handle nested arrays recursively
|
|
1389
|
+
else if (typescript.isArrayLiteralExpression(element)) {
|
|
1390
|
+
classNames.push(...this.extractFromExpression(element, context, attributeId));
|
|
864
1391
|
}
|
|
865
1392
|
}
|
|
866
1393
|
return classNames;
|
|
@@ -880,13 +1407,30 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
880
1407
|
className = propName.text;
|
|
881
1408
|
start = propName.getStart();
|
|
882
1409
|
}
|
|
1410
|
+
// Handle computed property names: { [__VLS_ctx.myVar]: true }
|
|
1411
|
+
else if (typescript.isComputedPropertyName(propName)) {
|
|
1412
|
+
let computedExpr = propName.expression;
|
|
1413
|
+
// Unwrap parentheses: { [(__VLS_ctx.myVar)]: true }
|
|
1414
|
+
if (typescript.isParenthesizedExpression(computedExpr)) {
|
|
1415
|
+
computedExpr = computedExpr.expression;
|
|
1416
|
+
}
|
|
1417
|
+
// Resolve __VLS_ctx.variable pattern
|
|
1418
|
+
if (typescript.isPropertyAccessExpression(computedExpr) && attributeId) {
|
|
1419
|
+
const vlsResults = this.extractFromVlsCtxReference(computedExpr, context, attributeId);
|
|
1420
|
+
if (vlsResults.length > 0) {
|
|
1421
|
+
classNames.push(...vlsResults);
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
883
1426
|
if (className && start !== undefined) {
|
|
884
1427
|
classNames.push({
|
|
885
1428
|
className,
|
|
886
1429
|
absoluteStart: start,
|
|
887
1430
|
length: className.length,
|
|
888
1431
|
line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
889
|
-
file: context.sourceFile.fileName
|
|
1432
|
+
file: context.sourceFile.fileName,
|
|
1433
|
+
attributeId
|
|
890
1434
|
});
|
|
891
1435
|
}
|
|
892
1436
|
}
|
|
@@ -898,12 +1442,43 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
898
1442
|
absoluteStart: start,
|
|
899
1443
|
length: className.length,
|
|
900
1444
|
line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
901
|
-
file: context.sourceFile.fileName
|
|
1445
|
+
file: context.sourceFile.fileName,
|
|
1446
|
+
attributeId
|
|
902
1447
|
});
|
|
903
1448
|
}
|
|
904
1449
|
}
|
|
905
1450
|
return classNames;
|
|
906
1451
|
}
|
|
1452
|
+
// Handle conditional (ternary) expressions: isActive ? 'flex' : 'hidden'
|
|
1453
|
+
if (typescript.isConditionalExpression(expr)) {
|
|
1454
|
+
// Extract from both branches recursively
|
|
1455
|
+
classNames.push(...this.extractFromExpression(expr.whenTrue, context, attributeId));
|
|
1456
|
+
classNames.push(...this.extractFromExpression(expr.whenFalse, context, attributeId));
|
|
1457
|
+
return classNames;
|
|
1458
|
+
}
|
|
1459
|
+
// Handle binary expressions: isActive && 'flex', isDisabled || 'fallback'
|
|
1460
|
+
if (typescript.isBinaryExpression(expr)) {
|
|
1461
|
+
classNames.push(...this.extractFromExpression(expr.left, context, attributeId));
|
|
1462
|
+
classNames.push(...this.extractFromExpression(expr.right, context, attributeId));
|
|
1463
|
+
return classNames;
|
|
1464
|
+
}
|
|
1465
|
+
// Handle parenthesized expressions: ('flex items-center')
|
|
1466
|
+
if (typescript.isParenthesizedExpression(expr)) {
|
|
1467
|
+
return this.extractFromExpression(expr.expression, context, attributeId);
|
|
1468
|
+
}
|
|
1469
|
+
// Handle type assertions: 'flex' as string, 'flex' as const
|
|
1470
|
+
if (typescript.isAsExpression(expr)) {
|
|
1471
|
+
return this.extractFromExpression(expr.expression, context, attributeId);
|
|
1472
|
+
}
|
|
1473
|
+
// Handle non-null assertions: someValue!
|
|
1474
|
+
if (typescript.isNonNullExpression(expr)) {
|
|
1475
|
+
return this.extractFromExpression(expr.expression, context, attributeId);
|
|
1476
|
+
}
|
|
1477
|
+
// Handle template literals
|
|
1478
|
+
if (typescript.isTemplateExpression(expr) || typescript.isNoSubstitutionTemplateLiteral(expr)) {
|
|
1479
|
+
const addAttrId = (classes) => attributeId ? classes.map(c => ({ ...c, attributeId })) : classes;
|
|
1480
|
+
return addAttrId(this.expressionExtractor.extract(expr, context));
|
|
1481
|
+
}
|
|
907
1482
|
return classNames;
|
|
908
1483
|
}
|
|
909
1484
|
}
|