tailwind-typescript-plugin 1.4.0-beta.21 → 1.4.0-beta.23
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 +12 -0
- package/lib/extractors/VueAttributeExtractor.d.ts +77 -0
- package/lib/extractors/VueAttributeExtractor.d.ts.map +1 -1
- package/lib/extractors/VueAttributeExtractor.js +854 -81
- 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"
|
|
@@ -278,6 +389,402 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
278
389
|
if (resolvedClasses.length > 0) {
|
|
279
390
|
return resolvedClasses;
|
|
280
391
|
}
|
|
392
|
+
// Handle props.propertyName patterns with default values from withDefaults
|
|
393
|
+
// Vue generates: class: (props.buttonClass) for :class="props.buttonClass"
|
|
394
|
+
const propsDefaultClasses = this.extractFromPropsWithDefaults(value, context, attributeId);
|
|
395
|
+
if (propsDefaultClasses.length > 0) {
|
|
396
|
+
return propsDefaultClasses;
|
|
397
|
+
}
|
|
398
|
+
return classNames;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Extract classes from an array expression, handling __VLS_ctx references.
|
|
402
|
+
* This method processes array elements directly to support variable references.
|
|
403
|
+
*/
|
|
404
|
+
extractFromArrayExpression(arrayExpr, context, attributeId) {
|
|
405
|
+
const { typescript } = context;
|
|
406
|
+
const classNames = [];
|
|
407
|
+
for (const element of arrayExpr.elements) {
|
|
408
|
+
if (element === undefined)
|
|
409
|
+
continue;
|
|
410
|
+
// String literal: 'flex'
|
|
411
|
+
if (typescript.isStringLiteral(element)) {
|
|
412
|
+
const fullText = element.text;
|
|
413
|
+
if (fullText.length > 0) {
|
|
414
|
+
const stringContentStart = element.getStart() + 1;
|
|
415
|
+
let offset = 0;
|
|
416
|
+
const parts = fullText.split(/(\s+)/);
|
|
417
|
+
for (const part of parts) {
|
|
418
|
+
if (part && !/^\s+$/.test(part)) {
|
|
419
|
+
classNames.push({
|
|
420
|
+
className: part,
|
|
421
|
+
absoluteStart: stringContentStart + offset,
|
|
422
|
+
length: part.length,
|
|
423
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(stringContentStart + offset)
|
|
424
|
+
.line + 1,
|
|
425
|
+
file: context.sourceFile.fileName,
|
|
426
|
+
attributeId
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
offset += part.length;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// Object literal: { 'bg-red-500': isActive }
|
|
434
|
+
else if (typescript.isObjectLiteralExpression(element)) {
|
|
435
|
+
classNames.push(...this.extractFromObjectExpression(element, context, attributeId));
|
|
436
|
+
}
|
|
437
|
+
// Spread element: ...classes
|
|
438
|
+
else if (typescript.isSpreadElement(element)) {
|
|
439
|
+
// Try to resolve __VLS_ctx reference
|
|
440
|
+
const vlsResults = this.extractFromVlsCtxReference(element.expression, context, attributeId);
|
|
441
|
+
if (vlsResults.length > 0) {
|
|
442
|
+
classNames.push(...vlsResults);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Handle __VLS_ctx.variable references: __VLS_ctx.myClass
|
|
446
|
+
else if (typescript.isPropertyAccessExpression(element)) {
|
|
447
|
+
const vlsResults = this.extractFromVlsCtxReference(element, context, attributeId);
|
|
448
|
+
if (vlsResults.length > 0) {
|
|
449
|
+
classNames.push(...vlsResults);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Handle parenthesized expressions: (__VLS_ctx.myVar)
|
|
453
|
+
else if (typescript.isParenthesizedExpression(element)) {
|
|
454
|
+
const inner = element.expression;
|
|
455
|
+
if (typescript.isPropertyAccessExpression(inner)) {
|
|
456
|
+
const vlsResults = this.extractFromVlsCtxReference(inner, context, attributeId);
|
|
457
|
+
if (vlsResults.length > 0) {
|
|
458
|
+
classNames.push(...vlsResults);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else if (typescript.isArrayLiteralExpression(inner)) {
|
|
462
|
+
// Nested array in parentheses
|
|
463
|
+
classNames.push(...this.extractFromArrayExpression(inner, context, attributeId));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// Handle nested arrays recursively
|
|
467
|
+
else if (typescript.isArrayLiteralExpression(element)) {
|
|
468
|
+
classNames.push(...this.extractFromArrayExpression(element, context, attributeId));
|
|
469
|
+
}
|
|
470
|
+
// Handle ternary/conditional expressions
|
|
471
|
+
else if (typescript.isConditionalExpression(element)) {
|
|
472
|
+
// Extract from both branches
|
|
473
|
+
classNames.push(...this.extractFromConditionalElement(element, context, attributeId));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return classNames;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Extract classes from an object expression, handling computed property names.
|
|
480
|
+
*/
|
|
481
|
+
extractFromObjectExpression(objExpr, context, attributeId) {
|
|
482
|
+
const { typescript } = context;
|
|
483
|
+
const classNames = [];
|
|
484
|
+
for (const prop of objExpr.properties) {
|
|
485
|
+
if (typescript.isPropertyAssignment(prop)) {
|
|
486
|
+
const propName = prop.name;
|
|
487
|
+
let className;
|
|
488
|
+
let start;
|
|
489
|
+
if (typescript.isStringLiteral(propName)) {
|
|
490
|
+
className = propName.text;
|
|
491
|
+
start = propName.getStart() + 1;
|
|
492
|
+
}
|
|
493
|
+
else if (typescript.isIdentifier(propName)) {
|
|
494
|
+
className = propName.text;
|
|
495
|
+
start = propName.getStart();
|
|
496
|
+
}
|
|
497
|
+
// Handle computed property names: { [__VLS_ctx.myVar]: true }
|
|
498
|
+
else if (typescript.isComputedPropertyName(propName)) {
|
|
499
|
+
let computedExpr = propName.expression;
|
|
500
|
+
// Unwrap parentheses
|
|
501
|
+
if (typescript.isParenthesizedExpression(computedExpr)) {
|
|
502
|
+
computedExpr = computedExpr.expression;
|
|
503
|
+
}
|
|
504
|
+
// Resolve __VLS_ctx.variable pattern
|
|
505
|
+
if (typescript.isPropertyAccessExpression(computedExpr)) {
|
|
506
|
+
const vlsResults = this.extractFromVlsCtxReference(computedExpr, context, attributeId);
|
|
507
|
+
if (vlsResults.length > 0) {
|
|
508
|
+
classNames.push(...vlsResults);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (className && start !== undefined) {
|
|
514
|
+
classNames.push({
|
|
515
|
+
className,
|
|
516
|
+
absoluteStart: start,
|
|
517
|
+
length: className.length,
|
|
518
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
519
|
+
file: context.sourceFile.fileName,
|
|
520
|
+
attributeId
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
else if (typescript.isShorthandPropertyAssignment(prop)) {
|
|
525
|
+
const className = prop.name.text;
|
|
526
|
+
const start = prop.name.getStart();
|
|
527
|
+
classNames.push({
|
|
528
|
+
className,
|
|
529
|
+
absoluteStart: start,
|
|
530
|
+
length: className.length,
|
|
531
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
532
|
+
file: context.sourceFile.fileName,
|
|
533
|
+
attributeId
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return classNames;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Extract classes from a conditional (ternary) expression in array context.
|
|
541
|
+
*/
|
|
542
|
+
extractFromConditionalElement(conditional, context, attributeId) {
|
|
543
|
+
return this.extractFromConditionalExpression(conditional, context, attributeId);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Extract classes from a conditional (ternary) expression.
|
|
547
|
+
* Handles: isActive ? 'flex' : 'hidden'
|
|
548
|
+
*/
|
|
549
|
+
extractFromConditionalExpression(conditional, context, attributeId) {
|
|
550
|
+
const classNames = [];
|
|
551
|
+
// Use the ternary's position as a unique identifier (like ExpressionExtractor)
|
|
552
|
+
const ternaryId = conditional.getStart();
|
|
553
|
+
// Extract from true branch with conditionalBranchId
|
|
554
|
+
const whenTrue = conditional.whenTrue;
|
|
555
|
+
classNames.push(...this.extractFromBranchExpression(whenTrue, context, attributeId, `ternary:true:${ternaryId}`));
|
|
556
|
+
// Extract from false branch with conditionalBranchId
|
|
557
|
+
const whenFalse = conditional.whenFalse;
|
|
558
|
+
classNames.push(...this.extractFromBranchExpression(whenFalse, context, attributeId, `ternary:false:${ternaryId}`));
|
|
559
|
+
return classNames;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Extract classes from a binary expression.
|
|
563
|
+
* Handles: isActive && 'flex', isDisabled || 'fallback'
|
|
564
|
+
*/
|
|
565
|
+
extractFromBinaryExpression(binary, context, attributeId) {
|
|
566
|
+
const classNames = [];
|
|
567
|
+
// Extract from left operand (for patterns like: 'flex' || fallback)
|
|
568
|
+
classNames.push(...this.extractFromBranchExpression(binary.left, context, attributeId));
|
|
569
|
+
// Extract from right operand (for patterns like: isActive && 'flex')
|
|
570
|
+
classNames.push(...this.extractFromBranchExpression(binary.right, context, attributeId));
|
|
571
|
+
return classNames;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Extract classes from a branch expression (ternary branch or binary operand).
|
|
575
|
+
* Handles string literals, nested ternaries, nested binaries, and VLS references.
|
|
576
|
+
*/
|
|
577
|
+
extractFromBranchExpression(expr, context, attributeId, conditionalBranchId) {
|
|
578
|
+
const { typescript } = context;
|
|
579
|
+
const classNames = [];
|
|
580
|
+
// Helper to add conditionalBranchId to extracted classes
|
|
581
|
+
const addBranchId = (classes) => conditionalBranchId ? classes.map(c => ({ ...c, conditionalBranchId })) : classes;
|
|
582
|
+
// String literal: 'flex items-center'
|
|
583
|
+
if (typescript.isStringLiteral(expr)) {
|
|
584
|
+
classNames.push(...addBranchId(this.extractClassesFromStringLiteral(expr, context, attributeId)));
|
|
585
|
+
}
|
|
586
|
+
// Nested ternary: condition ? 'a' : (nested ? 'b' : 'c')
|
|
587
|
+
else if (typescript.isConditionalExpression(expr)) {
|
|
588
|
+
classNames.push(...this.extractFromConditionalExpression(expr, context, attributeId));
|
|
589
|
+
}
|
|
590
|
+
// Nested binary: isA && isB && 'class'
|
|
591
|
+
else if (typescript.isBinaryExpression(expr)) {
|
|
592
|
+
classNames.push(...addBranchId(this.extractFromBinaryExpression(expr, context, attributeId)));
|
|
593
|
+
}
|
|
594
|
+
// Parenthesized expression
|
|
595
|
+
else if (typescript.isParenthesizedExpression(expr)) {
|
|
596
|
+
classNames.push(...this.extractFromBranchExpression(expr.expression, context, attributeId, conditionalBranchId));
|
|
597
|
+
}
|
|
598
|
+
// VLS ctx reference: __VLS_ctx.myClass
|
|
599
|
+
else if (typescript.isPropertyAccessExpression(expr)) {
|
|
600
|
+
const vlsResults = this.extractFromVlsCtxReference(expr, context, attributeId);
|
|
601
|
+
classNames.push(...addBranchId(vlsResults));
|
|
602
|
+
}
|
|
603
|
+
// Template literal: `flex ${something}`
|
|
604
|
+
else if (typescript.isTemplateExpression(expr) ||
|
|
605
|
+
typescript.isNoSubstitutionTemplateLiteral(expr)) {
|
|
606
|
+
const addAttributeId = (classes) => classes.map(c => ({ ...c, attributeId }));
|
|
607
|
+
classNames.push(...addBranchId(addAttributeId(this.expressionExtractor.extract(expr, context))));
|
|
608
|
+
}
|
|
609
|
+
return classNames;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Extract classes from CVA/TV function calls with class override.
|
|
613
|
+
* Handles patterns like: button({ color: 'primary', class: 'invalid-class' })
|
|
614
|
+
*
|
|
615
|
+
* Only extracts from functions that are defined using cva() or tv(),
|
|
616
|
+
* not from arbitrary custom functions.
|
|
617
|
+
*/
|
|
618
|
+
extractFromCvaTvClassOverride(callExpr, context, attributeId) {
|
|
619
|
+
const { typescript } = context;
|
|
620
|
+
const classNames = [];
|
|
621
|
+
// Check if this is a __VLS_ctx.functionName pattern
|
|
622
|
+
const calleeExpr = callExpr.expression;
|
|
623
|
+
if (!typescript.isPropertyAccessExpression(calleeExpr)) {
|
|
624
|
+
return classNames;
|
|
625
|
+
}
|
|
626
|
+
const objectExpr = calleeExpr.expression;
|
|
627
|
+
if (!typescript.isIdentifier(objectExpr) || objectExpr.text !== '__VLS_ctx') {
|
|
628
|
+
return classNames;
|
|
629
|
+
}
|
|
630
|
+
const functionName = calleeExpr.name;
|
|
631
|
+
if (!typescript.isIdentifier(functionName)) {
|
|
632
|
+
return classNames;
|
|
633
|
+
}
|
|
634
|
+
// Check if this function is defined using cva() or tv()
|
|
635
|
+
// by looking at its definition
|
|
636
|
+
if (!this.isCvaTvFunction(functionName, context)) {
|
|
637
|
+
return classNames;
|
|
638
|
+
}
|
|
639
|
+
// Look for object argument with class/className property
|
|
640
|
+
if (callExpr.arguments.length === 0) {
|
|
641
|
+
return classNames;
|
|
642
|
+
}
|
|
643
|
+
const firstArg = callExpr.arguments[0];
|
|
644
|
+
if (!typescript.isObjectLiteralExpression(firstArg)) {
|
|
645
|
+
return classNames;
|
|
646
|
+
}
|
|
647
|
+
// Find class or className property
|
|
648
|
+
for (const prop of firstArg.properties) {
|
|
649
|
+
if (!typescript.isPropertyAssignment(prop)) {
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const propName = prop.name;
|
|
653
|
+
if (!typescript.isIdentifier(propName)) {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (propName.text !== 'class' && propName.text !== 'className') {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
// Extract classes from the property value
|
|
660
|
+
const value = prop.initializer;
|
|
661
|
+
if (typescript.isStringLiteral(value)) {
|
|
662
|
+
classNames.push(...this.extractClassesFromStringLiteral(value, context, attributeId));
|
|
663
|
+
}
|
|
664
|
+
else if (typescript.isArrayLiteralExpression(value)) {
|
|
665
|
+
classNames.push(...this.extractFromArrayExpression(value, context, attributeId));
|
|
666
|
+
}
|
|
667
|
+
else if (typescript.isTemplateExpression(value) ||
|
|
668
|
+
typescript.isNoSubstitutionTemplateLiteral(value)) {
|
|
669
|
+
const addAttrId = (classes) => classes.map(c => ({ ...c, attributeId }));
|
|
670
|
+
classNames.push(...addAttrId(this.expressionExtractor.extract(value, context)));
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return classNames;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Check if a function is defined using cva() or tv().
|
|
677
|
+
* Resolves the function symbol and checks if its initializer is a cva/tv call.
|
|
678
|
+
*/
|
|
679
|
+
isCvaTvFunction(functionName, context) {
|
|
680
|
+
const { typescript, typeChecker } = context;
|
|
681
|
+
if (!typeChecker) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
// Get the symbol for the function name
|
|
685
|
+
const symbol = typeChecker.getSymbolAtLocation(functionName);
|
|
686
|
+
if (!symbol) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
const declarations = symbol.getDeclarations();
|
|
690
|
+
if (!declarations || declarations.length === 0) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
for (const declaration of declarations) {
|
|
694
|
+
// Check PropertySignature -> typeof reference in Volar 3.x
|
|
695
|
+
if (typescript.isPropertySignature(declaration) && declaration.type) {
|
|
696
|
+
if (typescript.isTypeQueryNode(declaration.type)) {
|
|
697
|
+
const exprName = declaration.type.exprName;
|
|
698
|
+
if (typescript.isIdentifier(exprName)) {
|
|
699
|
+
// Resolve the actual variable
|
|
700
|
+
const varSymbol = typeChecker.getSymbolAtLocation(exprName);
|
|
701
|
+
if (varSymbol) {
|
|
702
|
+
const varDeclarations = varSymbol.getDeclarations();
|
|
703
|
+
if (varDeclarations) {
|
|
704
|
+
for (const varDecl of varDeclarations) {
|
|
705
|
+
if (typescript.isVariableDeclaration(varDecl) && varDecl.initializer) {
|
|
706
|
+
if (this.isCallToCvaOrTv(varDecl.initializer, context)) {
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// Check direct variable declaration
|
|
717
|
+
else if (typescript.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
718
|
+
if (this.isCallToCvaOrTv(declaration.initializer, context)) {
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// Check property assignment in Vue's return
|
|
723
|
+
else if (typescript.isPropertyAssignment(declaration)) {
|
|
724
|
+
let expr = declaration.initializer;
|
|
725
|
+
if (typescript.isAsExpression(expr)) {
|
|
726
|
+
expr = expr.expression;
|
|
727
|
+
}
|
|
728
|
+
if (typescript.isIdentifier(expr)) {
|
|
729
|
+
const refSymbol = typeChecker.getSymbolAtLocation(expr);
|
|
730
|
+
if (refSymbol) {
|
|
731
|
+
const refDeclarations = refSymbol.getDeclarations();
|
|
732
|
+
if (refDeclarations) {
|
|
733
|
+
for (const refDecl of refDeclarations) {
|
|
734
|
+
if (typescript.isVariableDeclaration(refDecl) && refDecl.initializer) {
|
|
735
|
+
if (this.isCallToCvaOrTv(refDecl.initializer, context)) {
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Check if an expression is a call to cva() or tv().
|
|
749
|
+
*/
|
|
750
|
+
isCallToCvaOrTv(expr, context) {
|
|
751
|
+
const { typescript } = context;
|
|
752
|
+
if (!typescript.isCallExpression(expr)) {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
const callee = expr.expression;
|
|
756
|
+
// Direct call: cva(...) or tv(...)
|
|
757
|
+
if (typescript.isIdentifier(callee)) {
|
|
758
|
+
const name = callee.text;
|
|
759
|
+
return name === 'cva' || name === 'tv' || name === 'tvLite';
|
|
760
|
+
}
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Extract classes from a string literal with attributeId.
|
|
765
|
+
*/
|
|
766
|
+
extractClassesFromStringLiteral(literal, context, attributeId) {
|
|
767
|
+
const classNames = [];
|
|
768
|
+
const fullText = literal.text;
|
|
769
|
+
if (fullText.length === 0) {
|
|
770
|
+
return classNames;
|
|
771
|
+
}
|
|
772
|
+
const stringContentStart = literal.getStart() + 1;
|
|
773
|
+
let offset = 0;
|
|
774
|
+
const parts = fullText.split(/(\s+)/);
|
|
775
|
+
for (const part of parts) {
|
|
776
|
+
if (part && !/^\s+$/.test(part)) {
|
|
777
|
+
classNames.push({
|
|
778
|
+
className: part,
|
|
779
|
+
absoluteStart: stringContentStart + offset,
|
|
780
|
+
length: part.length,
|
|
781
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(stringContentStart + offset).line + 1,
|
|
782
|
+
file: context.sourceFile.fileName,
|
|
783
|
+
attributeId
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
offset += part.length;
|
|
787
|
+
}
|
|
281
788
|
return classNames;
|
|
282
789
|
}
|
|
283
790
|
/**
|
|
@@ -329,6 +836,84 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
329
836
|
return [];
|
|
330
837
|
}
|
|
331
838
|
const objectExpr = expr.expression;
|
|
839
|
+
// Handle nested property access: __VLS_ctx.obj.property
|
|
840
|
+
// e.g., __VLS_ctx.slotProps.buttonClass
|
|
841
|
+
if (typescript.isPropertyAccessExpression(objectExpr)) {
|
|
842
|
+
const nestedObject = objectExpr.expression;
|
|
843
|
+
if (typescript.isIdentifier(nestedObject) && nestedObject.text === '__VLS_ctx') {
|
|
844
|
+
// This is __VLS_ctx.something.somethingElse
|
|
845
|
+
const middlePropName = objectExpr.name;
|
|
846
|
+
const finalPropName = expr.name;
|
|
847
|
+
if (typescript.isIdentifier(middlePropName) && typescript.isIdentifier(finalPropName)) {
|
|
848
|
+
// Resolve the middle property (e.g., slotProps) to get its type/value
|
|
849
|
+
const middleSymbol = typeChecker.getSymbolAtLocation(middlePropName);
|
|
850
|
+
if (middleSymbol) {
|
|
851
|
+
const middleDeclarations = middleSymbol.getDeclarations();
|
|
852
|
+
if (middleDeclarations) {
|
|
853
|
+
for (const middleDecl of middleDeclarations) {
|
|
854
|
+
// Handle variable declaration: const slotProps = { buttonClass: '...' }
|
|
855
|
+
if (typescript.isVariableDeclaration(middleDecl) && middleDecl.initializer) {
|
|
856
|
+
if (typescript.isObjectLiteralExpression(middleDecl.initializer)) {
|
|
857
|
+
// Find the property in the object literal
|
|
858
|
+
for (const prop of middleDecl.initializer.properties) {
|
|
859
|
+
if (typescript.isPropertyAssignment(prop)) {
|
|
860
|
+
const propName = prop.name;
|
|
861
|
+
if (typescript.isIdentifier(propName) &&
|
|
862
|
+
propName.text === finalPropName.text) {
|
|
863
|
+
// Found the property, extract classes from its value
|
|
864
|
+
// Keep original positions from the class string
|
|
865
|
+
const classes = this.extractFromExpression(prop.initializer, context, attributeId);
|
|
866
|
+
return classes.map(c => ({
|
|
867
|
+
...c,
|
|
868
|
+
attributeId
|
|
869
|
+
}));
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// Handle property signature in Volar's generated types
|
|
876
|
+
else if (typescript.isPropertySignature(middleDecl) && middleDecl.type) {
|
|
877
|
+
if (typescript.isTypeQueryNode(middleDecl.type)) {
|
|
878
|
+
const exprName = middleDecl.type.exprName;
|
|
879
|
+
if (typescript.isIdentifier(exprName)) {
|
|
880
|
+
const varSymbol = typeChecker.getSymbolAtLocation(exprName);
|
|
881
|
+
if (varSymbol) {
|
|
882
|
+
const varDeclarations = varSymbol.getDeclarations();
|
|
883
|
+
if (varDeclarations) {
|
|
884
|
+
for (const varDecl of varDeclarations) {
|
|
885
|
+
if (typescript.isVariableDeclaration(varDecl) &&
|
|
886
|
+
varDecl.initializer &&
|
|
887
|
+
typescript.isObjectLiteralExpression(varDecl.initializer)) {
|
|
888
|
+
// Find the property
|
|
889
|
+
for (const prop of varDecl.initializer.properties) {
|
|
890
|
+
if (typescript.isPropertyAssignment(prop)) {
|
|
891
|
+
const pName = prop.name;
|
|
892
|
+
if (typescript.isIdentifier(pName) &&
|
|
893
|
+
pName.text === finalPropName.text) {
|
|
894
|
+
// Keep original positions from the class string
|
|
895
|
+
const classes = this.extractFromExpression(prop.initializer, context, attributeId);
|
|
896
|
+
return classes.map(c => ({
|
|
897
|
+
...c,
|
|
898
|
+
attributeId
|
|
899
|
+
}));
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return [];
|
|
916
|
+
}
|
|
332
917
|
if (!typescript.isIdentifier(objectExpr) || objectExpr.text !== '__VLS_ctx') {
|
|
333
918
|
return [];
|
|
334
919
|
}
|
|
@@ -362,13 +947,20 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
362
947
|
if (typescript.isVariableDeclaration(declaration)) {
|
|
363
948
|
const initializer = declaration.initializer;
|
|
364
949
|
if (initializer) {
|
|
365
|
-
// Check if this is a computed() call
|
|
950
|
+
// Check if this is a computed() or inject() call
|
|
366
951
|
if (typescript.isCallExpression(initializer)) {
|
|
952
|
+
// Handle computed() calls
|
|
367
953
|
const computedClasses = this.extractFromComputedCall(initializer, context, attributeId, templatePosition, templateLength);
|
|
368
954
|
if (computedClasses.length > 0) {
|
|
369
955
|
classNames.push(...computedClasses);
|
|
370
956
|
continue;
|
|
371
957
|
}
|
|
958
|
+
// Handle inject() calls with default value: inject('key', 'default-classes')
|
|
959
|
+
const injectClasses = this.extractFromInjectCall(initializer, context, attributeId);
|
|
960
|
+
if (injectClasses.length > 0) {
|
|
961
|
+
classNames.push(...injectClasses);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
372
964
|
}
|
|
373
965
|
// For regular variables, extract classes from the initializer
|
|
374
966
|
// Keep original position from string literal
|
|
@@ -443,7 +1035,7 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
443
1035
|
if (varDeclarations) {
|
|
444
1036
|
for (const varDecl of varDeclarations) {
|
|
445
1037
|
if (typescript.isVariableDeclaration(varDecl) && varDecl.initializer) {
|
|
446
|
-
// Check if it's a computed() call - use ORIGINAL position
|
|
1038
|
+
// Check if it's a computed() or inject() call - use ORIGINAL position
|
|
447
1039
|
// so errors point to actual class strings in script
|
|
448
1040
|
if (typescript.isCallExpression(varDecl.initializer)) {
|
|
449
1041
|
const computedClasses = this.extractFromComputedCall(varDecl.initializer, context, attributeId
|
|
@@ -452,6 +1044,11 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
452
1044
|
if (computedClasses.length > 0) {
|
|
453
1045
|
return computedClasses;
|
|
454
1046
|
}
|
|
1047
|
+
// Check for inject() calls with default value
|
|
1048
|
+
const injectClasses = this.extractFromInjectCall(varDecl.initializer, context, attributeId);
|
|
1049
|
+
if (injectClasses.length > 0) {
|
|
1050
|
+
return injectClasses;
|
|
1051
|
+
}
|
|
455
1052
|
}
|
|
456
1053
|
// For string literals, use ORIGINAL position from script
|
|
457
1054
|
// This makes errors point to the actual invalid class
|
|
@@ -502,6 +1099,11 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
502
1099
|
if (computedClasses.length > 0) {
|
|
503
1100
|
return computedClasses;
|
|
504
1101
|
}
|
|
1102
|
+
// Check for inject() calls with default value
|
|
1103
|
+
const injectClasses = this.extractFromInjectCall(decl.initializer, context, attributeId);
|
|
1104
|
+
if (injectClasses.length > 0) {
|
|
1105
|
+
return injectClasses;
|
|
1106
|
+
}
|
|
505
1107
|
}
|
|
506
1108
|
// Otherwise extract from the initializer directly
|
|
507
1109
|
const classes = this.extractFromExpression(decl.initializer, context);
|
|
@@ -726,13 +1328,19 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
726
1328
|
if (typescript.isVariableDeclaration(declaration)) {
|
|
727
1329
|
const init = declaration.initializer;
|
|
728
1330
|
if (init) {
|
|
729
|
-
// Check for computed() calls
|
|
1331
|
+
// Check for computed() or inject() calls
|
|
730
1332
|
if (typescript.isCallExpression(init)) {
|
|
731
1333
|
const computedClasses = this.extractFromComputedCall(init, context, attributeId, templatePosition, templateLength);
|
|
732
1334
|
if (computedClasses.length > 0) {
|
|
733
1335
|
classNames.push(...computedClasses);
|
|
734
1336
|
continue;
|
|
735
1337
|
}
|
|
1338
|
+
// Check for inject() calls with default value
|
|
1339
|
+
const injectClasses = this.extractFromInjectCall(init, context, attributeId);
|
|
1340
|
+
if (injectClasses.length > 0) {
|
|
1341
|
+
classNames.push(...injectClasses);
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
736
1344
|
}
|
|
737
1345
|
// Extract classes from the initializer - keep original position
|
|
738
1346
|
classNames.push(...addAttributeId(this.extractFromExpression(init, context)));
|
|
@@ -772,6 +1380,27 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
772
1380
|
}
|
|
773
1381
|
return [];
|
|
774
1382
|
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Extract classes from an inject() call with a default value.
|
|
1385
|
+
* Handles: const classes = inject('key', 'flex items-center')
|
|
1386
|
+
*/
|
|
1387
|
+
extractFromInjectCall(callExpr, context, attributeId) {
|
|
1388
|
+
const { typescript } = context;
|
|
1389
|
+
// Check if this is a call to 'inject'
|
|
1390
|
+
const calleeExpr = callExpr.expression;
|
|
1391
|
+
if (!typescript.isIdentifier(calleeExpr) || calleeExpr.text !== 'inject') {
|
|
1392
|
+
return [];
|
|
1393
|
+
}
|
|
1394
|
+
// inject() needs at least 2 arguments for us to extract the default value
|
|
1395
|
+
// inject(key, defaultValue) or inject(key, defaultValue, treatDefaultAsFactory)
|
|
1396
|
+
if (callExpr.arguments.length < 2) {
|
|
1397
|
+
return [];
|
|
1398
|
+
}
|
|
1399
|
+
const defaultValue = callExpr.arguments[1];
|
|
1400
|
+
// Extract classes from the default value
|
|
1401
|
+
const classes = this.extractFromExpression(defaultValue, context, attributeId);
|
|
1402
|
+
return classes.map(c => ({ ...c, attributeId }));
|
|
1403
|
+
}
|
|
775
1404
|
/**
|
|
776
1405
|
* Extract classes from a function declaration's return statements.
|
|
777
1406
|
* Handles: function getClasses() { return ['flex', 'items-center']; }
|
|
@@ -820,8 +1449,11 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
820
1449
|
}
|
|
821
1450
|
/**
|
|
822
1451
|
* Extract classes from an expression (array, object, string, etc.)
|
|
1452
|
+
* @param expr The expression to extract from
|
|
1453
|
+
* @param context The extraction context
|
|
1454
|
+
* @param attributeId Optional attribute ID for tracking
|
|
823
1455
|
*/
|
|
824
|
-
extractFromExpression(expr, context) {
|
|
1456
|
+
extractFromExpression(expr, context, attributeId) {
|
|
825
1457
|
const { typescript } = context;
|
|
826
1458
|
const classNames = [];
|
|
827
1459
|
// String literal: 'flex items-center'
|
|
@@ -854,13 +1486,36 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
854
1486
|
if (element === undefined)
|
|
855
1487
|
continue;
|
|
856
1488
|
if (typescript.isStringLiteral(element)) {
|
|
857
|
-
classNames.push(...this.extractFromExpression(element, context));
|
|
1489
|
+
classNames.push(...this.extractFromExpression(element, context, attributeId));
|
|
858
1490
|
}
|
|
859
1491
|
else if (typescript.isObjectLiteralExpression(element)) {
|
|
860
|
-
classNames.push(...this.extractFromExpression(element, context));
|
|
1492
|
+
classNames.push(...this.extractFromExpression(element, context, attributeId));
|
|
861
1493
|
}
|
|
862
1494
|
else if (typescript.isSpreadElement(element)) {
|
|
863
|
-
classNames.push(...this.extractFromExpression(element.expression, context));
|
|
1495
|
+
classNames.push(...this.extractFromExpression(element.expression, context, attributeId));
|
|
1496
|
+
}
|
|
1497
|
+
// Handle __VLS_ctx.variable references in arrays
|
|
1498
|
+
else if (typescript.isPropertyAccessExpression(element)) {
|
|
1499
|
+
if (attributeId) {
|
|
1500
|
+
const vlsResults = this.extractFromVlsCtxReference(element, context, attributeId);
|
|
1501
|
+
if (vlsResults.length > 0) {
|
|
1502
|
+
classNames.push(...vlsResults);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
// Handle parenthesized expressions: (__VLS_ctx.myVar)
|
|
1507
|
+
else if (typescript.isParenthesizedExpression(element)) {
|
|
1508
|
+
const inner = element.expression;
|
|
1509
|
+
if (typescript.isPropertyAccessExpression(inner) && attributeId) {
|
|
1510
|
+
const vlsResults = this.extractFromVlsCtxReference(inner, context, attributeId);
|
|
1511
|
+
if (vlsResults.length > 0) {
|
|
1512
|
+
classNames.push(...vlsResults);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
// Handle nested arrays recursively
|
|
1517
|
+
else if (typescript.isArrayLiteralExpression(element)) {
|
|
1518
|
+
classNames.push(...this.extractFromExpression(element, context, attributeId));
|
|
864
1519
|
}
|
|
865
1520
|
}
|
|
866
1521
|
return classNames;
|
|
@@ -880,13 +1535,30 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
880
1535
|
className = propName.text;
|
|
881
1536
|
start = propName.getStart();
|
|
882
1537
|
}
|
|
1538
|
+
// Handle computed property names: { [__VLS_ctx.myVar]: true }
|
|
1539
|
+
else if (typescript.isComputedPropertyName(propName)) {
|
|
1540
|
+
let computedExpr = propName.expression;
|
|
1541
|
+
// Unwrap parentheses: { [(__VLS_ctx.myVar)]: true }
|
|
1542
|
+
if (typescript.isParenthesizedExpression(computedExpr)) {
|
|
1543
|
+
computedExpr = computedExpr.expression;
|
|
1544
|
+
}
|
|
1545
|
+
// Resolve __VLS_ctx.variable pattern
|
|
1546
|
+
if (typescript.isPropertyAccessExpression(computedExpr) && attributeId) {
|
|
1547
|
+
const vlsResults = this.extractFromVlsCtxReference(computedExpr, context, attributeId);
|
|
1548
|
+
if (vlsResults.length > 0) {
|
|
1549
|
+
classNames.push(...vlsResults);
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
883
1554
|
if (className && start !== undefined) {
|
|
884
1555
|
classNames.push({
|
|
885
1556
|
className,
|
|
886
1557
|
absoluteStart: start,
|
|
887
1558
|
length: className.length,
|
|
888
1559
|
line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
889
|
-
file: context.sourceFile.fileName
|
|
1560
|
+
file: context.sourceFile.fileName,
|
|
1561
|
+
attributeId
|
|
890
1562
|
});
|
|
891
1563
|
}
|
|
892
1564
|
}
|
|
@@ -898,14 +1570,115 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
|
898
1570
|
absoluteStart: start,
|
|
899
1571
|
length: className.length,
|
|
900
1572
|
line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
901
|
-
file: context.sourceFile.fileName
|
|
1573
|
+
file: context.sourceFile.fileName,
|
|
1574
|
+
attributeId
|
|
902
1575
|
});
|
|
903
1576
|
}
|
|
904
1577
|
}
|
|
905
1578
|
return classNames;
|
|
906
1579
|
}
|
|
1580
|
+
// Handle conditional (ternary) expressions: isActive ? 'flex' : 'hidden'
|
|
1581
|
+
if (typescript.isConditionalExpression(expr)) {
|
|
1582
|
+
// Extract from both branches recursively
|
|
1583
|
+
classNames.push(...this.extractFromExpression(expr.whenTrue, context, attributeId));
|
|
1584
|
+
classNames.push(...this.extractFromExpression(expr.whenFalse, context, attributeId));
|
|
1585
|
+
return classNames;
|
|
1586
|
+
}
|
|
1587
|
+
// Handle binary expressions: isActive && 'flex', isDisabled || 'fallback'
|
|
1588
|
+
if (typescript.isBinaryExpression(expr)) {
|
|
1589
|
+
classNames.push(...this.extractFromExpression(expr.left, context, attributeId));
|
|
1590
|
+
classNames.push(...this.extractFromExpression(expr.right, context, attributeId));
|
|
1591
|
+
return classNames;
|
|
1592
|
+
}
|
|
1593
|
+
// Handle parenthesized expressions: ('flex items-center')
|
|
1594
|
+
if (typescript.isParenthesizedExpression(expr)) {
|
|
1595
|
+
return this.extractFromExpression(expr.expression, context, attributeId);
|
|
1596
|
+
}
|
|
1597
|
+
// Handle type assertions: 'flex' as string, 'flex' as const
|
|
1598
|
+
if (typescript.isAsExpression(expr)) {
|
|
1599
|
+
return this.extractFromExpression(expr.expression, context, attributeId);
|
|
1600
|
+
}
|
|
1601
|
+
// Handle non-null assertions: someValue!
|
|
1602
|
+
if (typescript.isNonNullExpression(expr)) {
|
|
1603
|
+
return this.extractFromExpression(expr.expression, context, attributeId);
|
|
1604
|
+
}
|
|
1605
|
+
// Handle template literals
|
|
1606
|
+
if (typescript.isTemplateExpression(expr) || typescript.isNoSubstitutionTemplateLiteral(expr)) {
|
|
1607
|
+
const addAttrId = (classes) => attributeId ? classes.map(c => ({ ...c, attributeId })) : classes;
|
|
1608
|
+
return addAttrId(this.expressionExtractor.extract(expr, context));
|
|
1609
|
+
}
|
|
907
1610
|
return classNames;
|
|
908
1611
|
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Extract classes from props.propertyName patterns with default values.
|
|
1614
|
+
* Vue generates __VLS_defaults for withDefaults() calls.
|
|
1615
|
+
*
|
|
1616
|
+
* Generated code pattern:
|
|
1617
|
+
* const __VLS_defaults = { buttonClass: 'flex items-center' };
|
|
1618
|
+
* ...{ class: (props.buttonClass) }
|
|
1619
|
+
*/
|
|
1620
|
+
extractFromPropsWithDefaults(value, context, attributeId) {
|
|
1621
|
+
const { typescript, typeChecker } = context;
|
|
1622
|
+
if (!typeChecker) {
|
|
1623
|
+
return [];
|
|
1624
|
+
}
|
|
1625
|
+
// Unwrap parentheses: (props.buttonClass) -> props.buttonClass
|
|
1626
|
+
let expr = value;
|
|
1627
|
+
if (typescript.isParenthesizedExpression(expr)) {
|
|
1628
|
+
expr = expr.expression;
|
|
1629
|
+
}
|
|
1630
|
+
// Check for props.propertyName pattern
|
|
1631
|
+
if (!typescript.isPropertyAccessExpression(expr)) {
|
|
1632
|
+
return [];
|
|
1633
|
+
}
|
|
1634
|
+
const objectExpr = expr.expression;
|
|
1635
|
+
if (!typescript.isIdentifier(objectExpr) || objectExpr.text !== 'props') {
|
|
1636
|
+
return [];
|
|
1637
|
+
}
|
|
1638
|
+
const propertyName = expr.name;
|
|
1639
|
+
if (!typescript.isIdentifier(propertyName)) {
|
|
1640
|
+
return [];
|
|
1641
|
+
}
|
|
1642
|
+
// Look for __VLS_defaults in the source file
|
|
1643
|
+
const defaultsValue = this.findVlsDefaultsProperty(propertyName.text, context);
|
|
1644
|
+
if (defaultsValue) {
|
|
1645
|
+
return this.extractFromExpression(defaultsValue, context, attributeId);
|
|
1646
|
+
}
|
|
1647
|
+
return [];
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Find a property value in __VLS_defaults object.
|
|
1651
|
+
*/
|
|
1652
|
+
findVlsDefaultsProperty(propertyName, context) {
|
|
1653
|
+
const { typescript, sourceFile } = context;
|
|
1654
|
+
// Walk through the source file to find __VLS_defaults
|
|
1655
|
+
let result;
|
|
1656
|
+
const visitor = (node) => {
|
|
1657
|
+
if (result)
|
|
1658
|
+
return;
|
|
1659
|
+
if (typescript.isVariableDeclaration(node)) {
|
|
1660
|
+
const name = node.name;
|
|
1661
|
+
if (typescript.isIdentifier(name) &&
|
|
1662
|
+
name.text === '__VLS_defaults' &&
|
|
1663
|
+
node.initializer &&
|
|
1664
|
+
typescript.isObjectLiteralExpression(node.initializer)) {
|
|
1665
|
+
// Found __VLS_defaults, look for the property
|
|
1666
|
+
for (const prop of node.initializer.properties) {
|
|
1667
|
+
if (typescript.isPropertyAssignment(prop)) {
|
|
1668
|
+
const propName = prop.name;
|
|
1669
|
+
if (typescript.isIdentifier(propName) && propName.text === propertyName) {
|
|
1670
|
+
result = prop.initializer;
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
typescript.forEachChild(node, visitor);
|
|
1678
|
+
};
|
|
1679
|
+
typescript.forEachChild(sourceFile, visitor);
|
|
1680
|
+
return result;
|
|
1681
|
+
}
|
|
909
1682
|
}
|
|
910
1683
|
exports.VueAttributeExtractor = VueAttributeExtractor;
|
|
911
1684
|
//# sourceMappingURL=VueAttributeExtractor.js.map
|