tailwind-typescript-plugin 0.0.2-beta.1

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +21 -0
  3. package/README.md +538 -0
  4. package/lib/core/interfaces.d.ts +45 -0
  5. package/lib/core/interfaces.d.ts.map +1 -0
  6. package/lib/core/interfaces.js +3 -0
  7. package/lib/core/interfaces.js.map +1 -0
  8. package/lib/core/types.d.ts +27 -0
  9. package/lib/core/types.d.ts.map +1 -0
  10. package/lib/core/types.js +3 -0
  11. package/lib/core/types.js.map +1 -0
  12. package/lib/extractors/BaseExtractor.d.ts +20 -0
  13. package/lib/extractors/BaseExtractor.d.ts.map +1 -0
  14. package/lib/extractors/BaseExtractor.js +83 -0
  15. package/lib/extractors/BaseExtractor.js.map +1 -0
  16. package/lib/extractors/CvaExtractor.d.ts +88 -0
  17. package/lib/extractors/CvaExtractor.d.ts.map +1 -0
  18. package/lib/extractors/CvaExtractor.js +425 -0
  19. package/lib/extractors/CvaExtractor.js.map +1 -0
  20. package/lib/extractors/ExpressionExtractor.d.ts +16 -0
  21. package/lib/extractors/ExpressionExtractor.d.ts.map +1 -0
  22. package/lib/extractors/ExpressionExtractor.js +132 -0
  23. package/lib/extractors/ExpressionExtractor.js.map +1 -0
  24. package/lib/extractors/JsxAttributeExtractor.d.ts +20 -0
  25. package/lib/extractors/JsxAttributeExtractor.d.ts.map +1 -0
  26. package/lib/extractors/JsxAttributeExtractor.js +107 -0
  27. package/lib/extractors/JsxAttributeExtractor.js.map +1 -0
  28. package/lib/extractors/JsxAttributeExtractor.original.d.ts +15 -0
  29. package/lib/extractors/JsxAttributeExtractor.original.d.ts.map +1 -0
  30. package/lib/extractors/JsxAttributeExtractor.original.js +84 -0
  31. package/lib/extractors/JsxAttributeExtractor.original.js.map +1 -0
  32. package/lib/extractors/StringLiteralExtractor.d.ts +12 -0
  33. package/lib/extractors/StringLiteralExtractor.d.ts.map +1 -0
  34. package/lib/extractors/StringLiteralExtractor.js +21 -0
  35. package/lib/extractors/StringLiteralExtractor.js.map +1 -0
  36. package/lib/extractors/TailwindVariantsExtractor.d.ts +87 -0
  37. package/lib/extractors/TailwindVariantsExtractor.d.ts.map +1 -0
  38. package/lib/extractors/TailwindVariantsExtractor.js +447 -0
  39. package/lib/extractors/TailwindVariantsExtractor.js.map +1 -0
  40. package/lib/extractors/TemplateExpressionExtractor.d.ts +14 -0
  41. package/lib/extractors/TemplateExpressionExtractor.d.ts.map +1 -0
  42. package/lib/extractors/TemplateExpressionExtractor.js +66 -0
  43. package/lib/extractors/TemplateExpressionExtractor.js.map +1 -0
  44. package/lib/index.d.ts +65 -0
  45. package/lib/index.d.ts.map +1 -0
  46. package/lib/index.js +4 -0
  47. package/lib/index.js.map +1 -0
  48. package/lib/infrastructure/TailwindValidator.d.ts +42 -0
  49. package/lib/infrastructure/TailwindValidator.d.ts.map +1 -0
  50. package/lib/infrastructure/TailwindValidator.js +152 -0
  51. package/lib/infrastructure/TailwindValidator.js.map +1 -0
  52. package/lib/infrastructure/TailwindValidator.spec.d.ts +2 -0
  53. package/lib/infrastructure/TailwindValidator.spec.d.ts.map +1 -0
  54. package/lib/infrastructure/TailwindValidator.spec.js +219 -0
  55. package/lib/infrastructure/TailwindValidator.spec.js.map +1 -0
  56. package/lib/plugin/TailwindTypescriptPlugin.d.ts +52 -0
  57. package/lib/plugin/TailwindTypescriptPlugin.d.ts.map +1 -0
  58. package/lib/plugin/TailwindTypescriptPlugin.js +142 -0
  59. package/lib/plugin/TailwindTypescriptPlugin.js.map +1 -0
  60. package/lib/services/ClassNameExtractionService.d.ts +37 -0
  61. package/lib/services/ClassNameExtractionService.d.ts.map +1 -0
  62. package/lib/services/ClassNameExtractionService.js +98 -0
  63. package/lib/services/ClassNameExtractionService.js.map +1 -0
  64. package/lib/services/ClassNameExtractionService.original.d.ts +20 -0
  65. package/lib/services/ClassNameExtractionService.original.d.ts.map +1 -0
  66. package/lib/services/ClassNameExtractionService.original.js +48 -0
  67. package/lib/services/ClassNameExtractionService.original.js.map +1 -0
  68. package/lib/services/DiagnosticService.d.ts +14 -0
  69. package/lib/services/DiagnosticService.d.ts.map +1 -0
  70. package/lib/services/DiagnosticService.js +61 -0
  71. package/lib/services/DiagnosticService.js.map +1 -0
  72. package/lib/services/PerformanceCache.d.ts +15 -0
  73. package/lib/services/PerformanceCache.d.ts.map +1 -0
  74. package/lib/services/PerformanceCache.js +44 -0
  75. package/lib/services/PerformanceCache.js.map +1 -0
  76. package/lib/services/PluginConfigService.d.ts +22 -0
  77. package/lib/services/PluginConfigService.d.ts.map +1 -0
  78. package/lib/services/PluginConfigService.js +86 -0
  79. package/lib/services/PluginConfigService.js.map +1 -0
  80. package/lib/services/ValidationService.d.ts +25 -0
  81. package/lib/services/ValidationService.d.ts.map +1 -0
  82. package/lib/services/ValidationService.js +50 -0
  83. package/lib/services/ValidationService.js.map +1 -0
  84. package/lib/utils/Logger.d.ts +10 -0
  85. package/lib/utils/Logger.d.ts.map +1 -0
  86. package/lib/utils/Logger.js +13 -0
  87. package/lib/utils/Logger.js.map +1 -0
  88. package/package.json +84 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ - Initial release with Tailwind CSS class validation
12
+ - Support for `tailwind-variants` tv() function validation
13
+ - Support for `class-variance-authority` cva() function validation
14
+ - Configurable variant library extractors via `variants` config
15
+ - Real-time validation in TypeScript Language Service
16
+ - Support for arbitrary values, responsive variants, and state variants
17
+
18
+ ### Changed
19
+
20
+ ### Fixed
21
+
22
+ ### Removed
23
+
24
+ ---
25
+
26
+ ## How to maintain this file
27
+
28
+ When releasing a new version:
29
+ 1. Move items from `[Unreleased]` to a new version section
30
+ 2. Add the version number and date
31
+ 3. Keep `[Unreleased]` section for future changes
32
+
33
+ Example:
34
+ ```markdown
35
+ ## [1.0.33] - 2024-01-15
36
+
37
+ ### Added
38
+ - New feature description
39
+
40
+ ### Fixed
41
+ - Bug fix description
42
+ ```
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Ivan Rodriguez Calleja
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,538 @@
1
+ # Tailwind TypeScript Plugin
2
+
3
+ A TypeScript Language Service plugin that catches **typos and invalid Tailwind CSS class names** in your JSX/TSX files. When you write a class name that doesn't exist in Tailwind, it won't apply any styles—this plugin detects those mistakes and shows errors directly in your editor before you ship broken styles.
4
+
5
+ ## What does this plugin do?
6
+
7
+ Ever written `className="flex itms-center"` instead of `"flex items-center"`? That typo silently fails—Tailwind ignores invalid classes and your component looks broken. This plugin prevents that by analyzing your JSX/TSX code and validating that all Tailwind classes used in `className` attributes actually exist in your Tailwind CSS configuration. It provides real-time feedback by showing TypeScript errors for invalid or misspelled Tailwind classes, catching styling mistakes before they reach production.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Features](#features)
12
+ - [Installation](#installation)
13
+ - [Configuration](#configuration)
14
+ - [Add the plugin to your tsconfig.json](#1-add-the-plugin-to-your-tsconfigjson)
15
+ - [Ensure your CSS file imports Tailwind](#2-ensure-your-css-file-imports-tailwind)
16
+ - [Enable the plugin in your editor](#3-enable-the-plugin-in-your-editor)
17
+ - [What it validates](#what-it-validates)
18
+ - [Implemented features](#implemented-features)
19
+ - [How It Works](#how-it-works)
20
+ - [Performance Optimizations](#performance-optimizations)
21
+ - [Development](#development)
22
+ - [Publishing](#publishing)
23
+ - [Contributing](#contributing)
24
+ - [License](#license)
25
+
26
+ ## Features
27
+
28
+ - **Real-time validation**: Get instant feedback on invalid Tailwind classes while you code
29
+ - **Editor integration**: Works with any editor that supports TypeScript Language Service (VS Code, WebStorm, etc.)
30
+ - **Supports Tailwind variants**: Validates responsive (`md:`, `lg:`), state (`hover:`, `focus:`), and other variants
31
+ - **Arbitrary values**: Correctly handles Tailwind arbitrary values like `h-[50vh]` or `bg-[#ff0000]`
32
+ - **Variant library support**:
33
+ - **tailwind-variants**: Validates classes in `tv()` function calls including `base`, `variants`, `compoundVariants`, `slots`, and `class`/`className` override properties
34
+ - **class-variance-authority**: Validates classes in `cva()` function calls including base classes, `variants`, `compoundVariants`, and `class`/`className` override properties
35
+
36
+ ## Installation
37
+
38
+ Install the plugin as a dependency:
39
+
40
+ ```bash
41
+ npm install tailwind-typescript-plugin
42
+ # or
43
+ yarn add tailwind-typescript-plugin
44
+ # or
45
+ pnpm add tailwind-typescript-plugin
46
+ ```
47
+
48
+ ## Configuration
49
+
50
+ ### 1. Add the plugin to your `tsconfig.json`
51
+
52
+ Add the plugin to the `compilerOptions.plugins` array in your `tsconfig.json`:
53
+
54
+ ```json
55
+ {
56
+ "compilerOptions": {
57
+ "plugins": [
58
+ {
59
+ "name": "tailwind-typescript-plugin",
60
+ "globalCss": "./src/global.css",
61
+ "utilityFunctions": ["clsx", "cn", "classnames"],
62
+ "variants": {
63
+ "tailwindVariants": true,
64
+ "classVarianceAuthority": true
65
+ }
66
+ }
67
+ ]
68
+ }
69
+ }
70
+ ```
71
+
72
+ **Configuration options:**
73
+
74
+ - `globalCss` (required): Path to your global CSS file that imports Tailwind CSS. This can be relative to your project root.
75
+
76
+ - `variants` (optional): Configure which variant library extractors to enable. This is useful for performance optimization when you only use one library.
77
+ - **Default behavior (no config)**: Both `tailwind-variants` and `class-variance-authority` are enabled
78
+ - **Selective enabling**: If you specify ANY variant config, only those explicitly set to `true` are enabled
79
+ - **Example configurations**:
80
+ ```json
81
+ // Enable only tailwind-variants
82
+ {
83
+ "variants": {
84
+ "tailwindVariants": true
85
+ }
86
+ }
87
+
88
+ // Enable only class-variance-authority
89
+ {
90
+ "variants": {
91
+ "classVarianceAuthority": true
92
+ }
93
+ }
94
+
95
+ // Enable both explicitly
96
+ {
97
+ "variants": {
98
+ "tailwindVariants": true,
99
+ "classVarianceAuthority": true
100
+ }
101
+ }
102
+
103
+ // No config = both enabled by default
104
+ {
105
+ // variants not specified - both libraries validated
106
+ }
107
+ ```
108
+ - **Performance impact**: Disabling unused extractors skips TypeChecker operations and symbol resolution for that library, providing faster validation
109
+
110
+ - `utilityFunctions` (optional): Array of additional function names to validate. These will be **merged with the defaults**, so you don't lose the common ones.
111
+ - **Defaults (always included)**: `['clsx', 'cn', 'classnames', 'classNames', 'cx', 'cva', 'twMerge', 'tv']`
112
+ - **Add your own**: Provide custom function names that will be added to the defaults
113
+ - **Example config**:
114
+ ```json
115
+ {
116
+ "utilityFunctions": ["myCustomFn", "buildClasses"]
117
+ }
118
+ ```
119
+ This will validate: `clsx`, `cn`, `classnames`, `classNames`, `cx`, `cva`, `twMerge`, `tv`, **`myCustomFn`**, **`buildClasses`**
120
+
121
+ - **Supported patterns**:
122
+ ```typescript
123
+ // Simple calls (validated by default):
124
+ className={clsx('flex', 'items-center')}
125
+ className={cn('flex', 'items-center')}
126
+
127
+ // Member expressions (nested property access):
128
+ className={utils.cn('flex', 'items-center')}
129
+ className={lib.clsx('flex', 'items-center')}
130
+
131
+ // Custom functions (add via config):
132
+ className={myCustomFn('flex', 'items-center')}
133
+ className={buildClasses('flex', 'items-center')}
134
+
135
+ // Dynamic calls (ignored, won't throw errors):
136
+ className={functions['cn']('flex', 'items-center')}
137
+ ```
138
+
139
+ ### 2. Ensure your CSS file imports Tailwind
140
+
141
+ Your global CSS file (referenced in `globalCss`) should import Tailwind CSS:
142
+
143
+ ```css
144
+ @import "tailwindcss";
145
+ ```
146
+
147
+ Or using the traditional approach:
148
+
149
+ ```css
150
+ @tailwind base;
151
+ @tailwind components;
152
+ @tailwind utilities;
153
+ ```
154
+
155
+ ### 3. Enable the plugin in your editor
156
+
157
+ #### VS Code
158
+
159
+ The plugin should work automatically if you have the TypeScript version from your workspace selected. To ensure this:
160
+
161
+ 1. Open a TypeScript or TSX file
162
+ 2. Open the command palette (Cmd+Shift+P on Mac, Ctrl+Shift+P on Windows/Linux)
163
+ 3. Type "TypeScript: Select TypeScript Version"
164
+ 4. Choose "Use Workspace Version"
165
+
166
+ You may need to restart the TypeScript server:
167
+ - Open command palette
168
+ - Type "TypeScript: Restart TS Server"
169
+
170
+ #### Other Editors
171
+
172
+ Most editors that support TypeScript Language Service plugins should work automatically. Refer to your editor's documentation for TypeScript plugin configuration.
173
+
174
+ ## What it validates
175
+
176
+ **Valid classes**:
177
+ ```tsx
178
+ // ✅ Standard Tailwind classes
179
+ <div className="flex items-center justify-center">
180
+ <p className="text-lg font-bold text-blue-500">Hello World</p>
181
+ </div>
182
+
183
+ // ✅ Arbitrary values
184
+ <div className="h-[50vh] w-[100px] bg-[#ff0000]">
185
+ <p className="p-[20px] text-[14px]">Custom values</p>
186
+ </div>
187
+
188
+ // ✅ Variants (responsive, state, etc.)
189
+ <div className="hover:bg-blue-500 md:flex lg:grid-cols-3 dark:text-white">
190
+ Responsive and state variants
191
+ </div>
192
+ ```
193
+
194
+ **Invalid classes are flagged**:
195
+ ```tsx
196
+ // ❌ Invalid class name
197
+ <div className="random-class">Invalid class</div>
198
+ // Error: The class "holii" is not a valid Tailwind class
199
+
200
+ // ❌ Mix of valid and invalid
201
+ <div className="random-class container mx-auto">Mixed classes</div>
202
+ // Error: The class "holii" is not a valid Tailwind class
203
+
204
+ // ❌ Invalid variant
205
+ <div className="invalid-variant:bg-blue-500">Bad variant</div>
206
+ // Error: The class "invalidvariant:bg-blue-500" is not a valid Tailwind class
207
+ ```
208
+
209
+ **tailwind-variants validation**:
210
+ ```tsx
211
+ import { tv } from 'tailwind-variants';
212
+ import { tv as myTv } from 'tailwind-variants'; // Import aliasing supported!
213
+
214
+ // ✅ Valid tv() usage
215
+ const button = tv({
216
+ base: 'font-semibold text-white text-sm py-1 px-4 rounded-full',
217
+ variants: {
218
+ color: {
219
+ primary: 'bg-blue-500 hover:bg-blue-700',
220
+ secondary: 'bg-purple-500 hover:bg-purple-700'
221
+ }
222
+ }
223
+ });
224
+
225
+ // ✅ Valid: Array syntax
226
+ const buttonArray = tv({
227
+ base: ['font-semibold', 'text-white', 'px-4', 'py-2'],
228
+ variants: {
229
+ color: {
230
+ primary: ['bg-blue-500', 'hover:bg-blue-700']
231
+ }
232
+ }
233
+ });
234
+
235
+ // ✅ Valid: Import aliasing
236
+ const buttonAliased = myTv({
237
+ base: 'flex items-center gap-2'
238
+ });
239
+
240
+ // ❌ Invalid class in base
241
+ const invalid = tv({
242
+ base: 'font-semibold invalid-class text-white'
243
+ // Error: The class "invalid-class" is not a valid Tailwind class
244
+ });
245
+
246
+ // ❌ Invalid class in variant
247
+ const invalidVariant = tv({
248
+ base: 'font-semibold',
249
+ variants: {
250
+ color: {
251
+ primary: 'bg-blue-500 wrong-class'
252
+ // Error: The class "wrong-class" is not a valid Tailwind class
253
+ }
254
+ }
255
+ });
256
+
257
+ // ❌ Invalid class in array
258
+ const invalidArray = tv({
259
+ base: ['font-semibold', 'invalid-array-class', 'text-white']
260
+ // Error: The class "invalid-array-class" is not a valid Tailwind class
261
+ });
262
+
263
+ // ✅ Valid: class override at call site
264
+ <button className={button({ color: 'primary', class: 'bg-pink-500 hover:bg-pink-700' })}>
265
+ Override
266
+ </button>
267
+
268
+ // ❌ Invalid: class override with invalid class
269
+ <button className={button({ color: 'primary', class: 'invalid-override-class' })}>
270
+ // Error: The class "invalid-override-class" is not a valid Tailwind class
271
+ </button>
272
+ ```
273
+
274
+ **class-variance-authority validation**:
275
+ ```tsx
276
+ import { cva } from 'class-variance-authority';
277
+ import { cva as myCva } from 'class-variance-authority'; // Import aliasing supported!
278
+
279
+ // ✅ Valid cva() usage
280
+ const button = cva(['font-semibold', 'border', 'rounded'], {
281
+ variants: {
282
+ intent: {
283
+ primary: ['bg-blue-500', 'text-white', 'border-transparent'],
284
+ secondary: ['bg-white', 'text-gray-800', 'border-gray-400']
285
+ },
286
+ size: {
287
+ small: ['text-sm', 'py-1', 'px-2'],
288
+ medium: ['text-base', 'py-2', 'px-4']
289
+ }
290
+ }
291
+ });
292
+
293
+ // ✅ Valid: String syntax for base
294
+ const buttonString = cva('font-semibold border rounded', {
295
+ variants: {
296
+ intent: {
297
+ primary: 'bg-blue-500 text-white'
298
+ }
299
+ }
300
+ });
301
+
302
+ // ✅ Valid: Import aliasing
303
+ const buttonAliased = myCva(['flex', 'items-center', 'gap-2']);
304
+
305
+ // ❌ Invalid class in base array
306
+ const invalid = cva(['font-semibold', 'invalid-class', 'border']);
307
+ // Error: The class "invalid-class" is not a valid Tailwind class
308
+
309
+ // ❌ Invalid class in variant
310
+ const invalidVariant = cva(['font-semibold'], {
311
+ variants: {
312
+ intent: {
313
+ primary: 'bg-blue-500 wrong-class'
314
+ // Error: The class "wrong-class" is not a valid Tailwind class
315
+ }
316
+ }
317
+ });
318
+
319
+ // ✅ Valid: class override at call site
320
+ <button className={button({ intent: 'primary', class: 'bg-pink-500 hover:bg-pink-700' })}>
321
+ Override
322
+ </button>
323
+
324
+ // ❌ Invalid: class override with invalid class
325
+ <button className={button({ intent: 'primary', class: 'invalid-override-class' })}>
326
+ // Error: The class "invalid-override-class" is not a valid Tailwind class
327
+ </button>
328
+ ```
329
+
330
+ ## Implemented features
331
+
332
+ > **Note on examples:** Each feature has a corresponding test file in `example/src/` following the naming pattern `[context]-[pattern].tsx` where:
333
+ > - **Context** = the container (literal, expression, template, function, array, object, tv)
334
+ > - **Pattern** = what's inside (static, variable, binary, ternary, mixed)
335
+
336
+ - [X] **Literal Static** → [`literal-static.tsx`](./example/src/literal-static.tsx)
337
+ Validates string literal `className` attributes
338
+ Example: `className="flex invalid-class"`
339
+
340
+ - [X] **Expression Static** → [`expression-static.tsx`](./example/src/expression-static.tsx)
341
+ Validates JSX expressions with string literals
342
+ Example: `className={'flex invalid-class'}`
343
+
344
+ - [X] **Template Variable** → [`template-variable.tsx`](./example/src/template-variable.tsx)
345
+ Validates template literals with variable interpolation
346
+ Example: `className={`flex ${someClass} invalid-class`}`
347
+
348
+ - [X] **Template Ternary** → [`template-ternary.tsx`](./example/src/template-ternary.tsx)
349
+ Validates template literals with conditional expressions
350
+ Example: `className={`flex ${isActive ? 'invalid-class' : ''}`}`
351
+
352
+ - [X] **Template Binary** → [`template-binary.tsx`](./example/src/template-binary.tsx)
353
+ Validates template literals with binary expressions
354
+ Example: `className={`flex ${isError && 'invalid-class'}`}`
355
+
356
+ - [X] **Function Static** → [`function-static.tsx`](./example/src/function-static.tsx)
357
+ Validates function calls with static arguments
358
+ Example: `className={clsx('flex', 'invalid-class')}`
359
+
360
+ - [X] **Function Binary** → [`function-binary.tsx`](./example/src/function-binary.tsx)
361
+ Validates function calls with binary expressions
362
+ Example: `className={clsx('flex', isError && 'invalid-class')}`
363
+
364
+ - [X] **Function Ternary** → [`function-ternary.tsx`](./example/src/function-ternary.tsx)
365
+ Validates function calls with conditional expressions
366
+ Example: `className={clsx('flex', isActive ? 'invalid-class' : 'bg-gray-500')}`
367
+
368
+ - [X] **Expression Binary** → [`expression-binary.tsx`](./example/src/expression-binary.tsx)
369
+ Validates direct binary expressions
370
+ Example: `className={isError && 'invalid-class'}`
371
+
372
+ - [X] **Expression Ternary** → [`expression-ternary.tsx`](./example/src/expression-ternary.tsx)
373
+ Validates direct conditional expressions
374
+ Example: `className={isActive ? 'invalid-class' : 'bg-gray-500'}`
375
+
376
+ - [X] **Array Static** → [`array-static.tsx`](./example/src/array-static.tsx)
377
+ Validates array literals
378
+ Example: `className={cn(['flex', 'invalid-class'])}`
379
+
380
+ - [X] **Array Binary** → [`array-binary.tsx`](./example/src/array-binary.tsx)
381
+ Validates array literals with binary expressions
382
+ Example: `className={cn(['flex', isError && 'invalid-class'])}`
383
+
384
+ - [X] **Array Ternary** → [`array-ternary.tsx`](./example/src/array-ternary.tsx)
385
+ Validates array literals with conditional expressions
386
+ Example: `className={cn(['flex', isActive ? 'invalid-class' : 'bg-gray-500'])}`
387
+
388
+ - [X] **Object Static** → [`object-static.tsx`](./example/src/object-static.tsx)
389
+ Validates object literal keys
390
+ Example: `className={clsx({ 'invalid-class': true })}` or `className={clsx({ 'invalid-class': isActive })}`
391
+
392
+ - [X] **Array Nested** → [`array-nested.tsx`](./example/src/array-nested.tsx)
393
+ Validates nested arrays
394
+ Example: `className={cn([['flex', 'invalid-class']])}` or `className={cn([['flex'], [['items-center'], 'invalid-class']])}`
395
+
396
+ - [X] **Object Array Values** → [`object-array-values.tsx`](./example/src/object-array-values.tsx)
397
+ Validates arrays as object property values
398
+ Example: `className={clsx({ flex: ['items-center', 'invalid-class'] })}`
399
+
400
+ - [X] **Mixed Complex** → [`mixed-complex.tsx`](./example/src/mixed-complex.tsx)
401
+ Validates kitchen sink complex nesting with all patterns combined
402
+ Example: `className={clsx('flex', [1 && 'bar', { baz: ['invalid-class'] }])}`
403
+
404
+ - [X] **TV Static** → [`tv-static.tsx`](./example/src/tv-static.tsx)
405
+ Validates `tailwind-variants` tv() function definitions
406
+ Example: `const styles = tv({ base: 'invalid-class', variants: { size: { sm: 'invalid-class' } } })`
407
+
408
+ - [X] **TV Class Override** → [`tv-class-override.tsx`](./example/src/tv-class-override.tsx)
409
+ Validates `tailwind-variants` class/className property overrides at call site
410
+ Example: `button({ color: 'primary', class: 'invalid-class' })`
411
+
412
+ - [X] **CVA Static** → [`cva-static.tsx`](./example/src/cva-static.tsx)
413
+ Validates `class-variance-authority` cva() function definitions
414
+ Example: `const button = cva(['invalid-class'], { variants: { intent: { primary: 'invalid-class' } } })`
415
+
416
+ - [X] **CVA Class Override** → [`cva-class-override.tsx`](./example/src/cva-class-override.tsx)
417
+ Validates `class-variance-authority` class/className property overrides at call site
418
+ Example: `button({ intent: 'primary', class: 'invalid-class' })`
419
+
420
+ - [ ] **Expression Variable**
421
+ Validates variable references
422
+ Example: `const dynamicClass = isActive ? 'bg-blue-500' : 'bg-gray-500'; <div className={dynamicClass}>Dynamic</div>`
423
+
424
+ ## How It Works
425
+
426
+ The plugin hooks into the TypeScript Language Service and:
427
+
428
+ 1. Parses your TSX/JSX files to find `className` attributes, `tv()` calls, and `cva()` calls
429
+ 2. Extracts individual class names from className strings, tv() configurations, and cva() configurations
430
+ 3. Validates each class against your Tailwind CSS configuration
431
+ 4. Reports invalid classes as TypeScript errors in your editor
432
+
433
+ ## Performance Optimizations
434
+
435
+ The plugin is designed for minimal performance impact:
436
+
437
+ - **Import caching**: Detects tailwind-variants and class-variance-authority imports once per file
438
+ - **Early bailout**: Skips tv()/cva() validation for files without respective library imports
439
+ - **Configurable extractors**: Disable unused variant libraries via `variants` config for better performance
440
+ - **Smart traversal**: Only processes JSX elements and call expressions
441
+ - **Fast paths**: Optimized hot paths for common patterns (string literals)
442
+ - **Lazy validation**: Tailwind design system loaded on-demand
443
+ - **Symbol caching**: TypeChecker results cached to avoid redundant type resolution
444
+
445
+ **Typical overhead**: <1ms per file for most files, ~2-3ms for files with many tv()/cva() calls
446
+
447
+ ## Development
448
+
449
+ This is a monorepo using Yarn workspaces:
450
+
451
+ ```bash
452
+ # Install dependencies
453
+ yarn install
454
+
455
+ # Build the plugin
456
+ yarn build
457
+
458
+ # Build all packages
459
+ yarn build:all
460
+
461
+ # Set up e2e tests
462
+ yarn setup-e2e
463
+ ```
464
+
465
+ ### Project Structure
466
+
467
+ ```
468
+ ├── packages/
469
+ │ ├── plugin/ # The TypeScript plugin package
470
+ │ └── e2e/ # End-to-end test examples
471
+ ```
472
+
473
+ ## Publishing
474
+
475
+ This project uses an automated publishing workflow with beta releases on every commit and manual stable releases.
476
+
477
+ ### Beta Releases (Automatic)
478
+
479
+ Every commit to `main` automatically publishes a beta version to npm:
480
+
481
+ ```
482
+ Commit to main → Auto-publishes 1.0.33-beta.1
483
+ Commit to main → Auto-publishes 1.0.33-beta.2
484
+ Commit to main → Auto-publishes 1.0.33-beta.3
485
+ ```
486
+
487
+ Users can install beta versions:
488
+ ```bash
489
+ npm install typescript-custom-plugin@beta
490
+ ```
491
+
492
+ ### Stable Releases (Manual)
493
+
494
+ To publish a stable release:
495
+
496
+ 1. Go to **Actions** tab on GitHub
497
+ 2. Click **"Stable Release"** workflow
498
+ 3. Click **"Run workflow"** button
499
+ 4. Select version bump type:
500
+ - **patch**: Bug fixes (1.0.32 → 1.0.33)
501
+ - **minor**: New features (1.0.32 → 1.1.0)
502
+ - **major**: Breaking changes (1.0.32 → 2.0.0)
503
+ 5. Click **"Run workflow"**
504
+
505
+ The workflow will:
506
+ - Run tests and build
507
+ - Bump version in `package.json`
508
+ - Create git tag
509
+ - Publish to npm as `@latest`
510
+ - Create GitHub Release
511
+
512
+ ### Version Timeline Example
513
+
514
+ ```
515
+ package.json: 1.0.32
516
+
517
+ Day 1: Commit → Publishes 1.0.33-beta.1
518
+ Day 2: Commit → Publishes 1.0.33-beta.2
519
+ Day 3: Click "Stable Release" (patch) → Publishes 1.0.33
520
+ Day 4: Commit → Publishes 1.0.34-beta.1 (starts over)
521
+ ```
522
+
523
+ ### Requirements
524
+
525
+ - **NPM_TOKEN**: Set in GitHub repository secrets (Settings → Secrets → Actions)
526
+ - Create at [npmjs.com](https://www.npmjs.com/) → Access Tokens → Generate New Token (Automation)
527
+
528
+ ## Contributing
529
+
530
+ Contributions are welcome! Please feel free to submit issues or pull requests.
531
+
532
+ ## License
533
+
534
+ MIT
535
+
536
+ ## Author
537
+
538
+ Ivan Rodriguez Calleja
@@ -0,0 +1,45 @@
1
+ import * as ts from 'typescript/lib/tsserverlibrary';
2
+ import { ClassNameInfo, ExtractionContext } from './types';
3
+ /**
4
+ * Base interface for class name extractors
5
+ * Follows the Strategy pattern for extensibility
6
+ */
7
+ export interface IClassNameExtractor {
8
+ /**
9
+ * Determines if this extractor can handle the given node
10
+ */
11
+ canHandle(node: ts.Node, context: ExtractionContext): boolean;
12
+ /**
13
+ * Extracts class names from the given node
14
+ */
15
+ extract(node: ts.Node, context: ExtractionContext): ClassNameInfo[];
16
+ }
17
+ /**
18
+ * Interface for class name validators
19
+ */
20
+ export interface IClassNameValidator {
21
+ isValidClass(className: string): boolean;
22
+ isInitialized(): boolean;
23
+ }
24
+ /**
25
+ * Interface for variant library configuration
26
+ */
27
+ export interface IVariantsConfig {
28
+ tailwindVariants?: boolean;
29
+ classVarianceAuthority?: boolean;
30
+ }
31
+ /**
32
+ * Interface for configuration management
33
+ */
34
+ export interface IPluginConfig {
35
+ globalCss?: string;
36
+ utilityFunctions?: string[];
37
+ variants?: IVariantsConfig;
38
+ }
39
+ /**
40
+ * Interface for diagnostic creation
41
+ */
42
+ export interface IDiagnosticService {
43
+ createDiagnostic(classInfo: ClassNameInfo, sourceFile: ts.SourceFile): ts.Diagnostic;
44
+ }
45
+ //# sourceMappingURL=interfaces.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../../src/core/interfaces.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,gCAAgC,CAAC;AAErD,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAE3D;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IACnC;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC;IAE9D;;OAEG;IACH,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,iBAAiB,GAAG,aAAa,EAAE,CAAC;CACpE;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IACzC,aAAa,IAAI,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sBAAsB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC,gBAAgB,CAAC,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,EAAE,CAAC,UAAU,GAAG,EAAE,CAAC,UAAU,CAAC;CACrF"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=interfaces.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interfaces.js","sourceRoot":"","sources":["../../src/core/interfaces.ts"],"names":[],"mappings":""}
@@ -0,0 +1,27 @@
1
+ import * as ts from 'typescript/lib/tsserverlibrary';
2
+ /**
3
+ * Represents information about a class name found in source code
4
+ */
5
+ export interface ClassNameInfo {
6
+ className: string;
7
+ absoluteStart: number;
8
+ length: number;
9
+ line: number;
10
+ file: string;
11
+ }
12
+ /**
13
+ * Context provided to extractors for class name extraction
14
+ */
15
+ export interface ExtractionContext {
16
+ readonly typescript: typeof ts;
17
+ readonly sourceFile: ts.SourceFile;
18
+ readonly utilityFunctions: string[];
19
+ readonly typeChecker?: ts.TypeChecker;
20
+ }
21
+ /**
22
+ * Result of extraction operation
23
+ */
24
+ export interface ExtractionResult {
25
+ classNames: ClassNameInfo[];
26
+ }
27
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,gCAAgC,CAAC;AAErD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,UAAU,EAAE,OAAO,EAAE,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC;IACnC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,WAAW,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC;CACtC;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,UAAU,EAAE,aAAa,EAAE,CAAC;CAC5B"}