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.
- package/CHANGELOG.md +42 -0
- package/LICENSE +21 -0
- package/README.md +538 -0
- package/lib/core/interfaces.d.ts +45 -0
- package/lib/core/interfaces.d.ts.map +1 -0
- package/lib/core/interfaces.js +3 -0
- package/lib/core/interfaces.js.map +1 -0
- package/lib/core/types.d.ts +27 -0
- package/lib/core/types.d.ts.map +1 -0
- package/lib/core/types.js +3 -0
- package/lib/core/types.js.map +1 -0
- package/lib/extractors/BaseExtractor.d.ts +20 -0
- package/lib/extractors/BaseExtractor.d.ts.map +1 -0
- package/lib/extractors/BaseExtractor.js +83 -0
- package/lib/extractors/BaseExtractor.js.map +1 -0
- package/lib/extractors/CvaExtractor.d.ts +88 -0
- package/lib/extractors/CvaExtractor.d.ts.map +1 -0
- package/lib/extractors/CvaExtractor.js +425 -0
- package/lib/extractors/CvaExtractor.js.map +1 -0
- package/lib/extractors/ExpressionExtractor.d.ts +16 -0
- package/lib/extractors/ExpressionExtractor.d.ts.map +1 -0
- package/lib/extractors/ExpressionExtractor.js +132 -0
- package/lib/extractors/ExpressionExtractor.js.map +1 -0
- package/lib/extractors/JsxAttributeExtractor.d.ts +20 -0
- package/lib/extractors/JsxAttributeExtractor.d.ts.map +1 -0
- package/lib/extractors/JsxAttributeExtractor.js +107 -0
- package/lib/extractors/JsxAttributeExtractor.js.map +1 -0
- package/lib/extractors/JsxAttributeExtractor.original.d.ts +15 -0
- package/lib/extractors/JsxAttributeExtractor.original.d.ts.map +1 -0
- package/lib/extractors/JsxAttributeExtractor.original.js +84 -0
- package/lib/extractors/JsxAttributeExtractor.original.js.map +1 -0
- package/lib/extractors/StringLiteralExtractor.d.ts +12 -0
- package/lib/extractors/StringLiteralExtractor.d.ts.map +1 -0
- package/lib/extractors/StringLiteralExtractor.js +21 -0
- package/lib/extractors/StringLiteralExtractor.js.map +1 -0
- package/lib/extractors/TailwindVariantsExtractor.d.ts +87 -0
- package/lib/extractors/TailwindVariantsExtractor.d.ts.map +1 -0
- package/lib/extractors/TailwindVariantsExtractor.js +447 -0
- package/lib/extractors/TailwindVariantsExtractor.js.map +1 -0
- package/lib/extractors/TemplateExpressionExtractor.d.ts +14 -0
- package/lib/extractors/TemplateExpressionExtractor.d.ts.map +1 -0
- package/lib/extractors/TemplateExpressionExtractor.js +66 -0
- package/lib/extractors/TemplateExpressionExtractor.js.map +1 -0
- package/lib/index.d.ts +65 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -0
- package/lib/infrastructure/TailwindValidator.d.ts +42 -0
- package/lib/infrastructure/TailwindValidator.d.ts.map +1 -0
- package/lib/infrastructure/TailwindValidator.js +152 -0
- package/lib/infrastructure/TailwindValidator.js.map +1 -0
- package/lib/infrastructure/TailwindValidator.spec.d.ts +2 -0
- package/lib/infrastructure/TailwindValidator.spec.d.ts.map +1 -0
- package/lib/infrastructure/TailwindValidator.spec.js +219 -0
- package/lib/infrastructure/TailwindValidator.spec.js.map +1 -0
- package/lib/plugin/TailwindTypescriptPlugin.d.ts +52 -0
- package/lib/plugin/TailwindTypescriptPlugin.d.ts.map +1 -0
- package/lib/plugin/TailwindTypescriptPlugin.js +142 -0
- package/lib/plugin/TailwindTypescriptPlugin.js.map +1 -0
- package/lib/services/ClassNameExtractionService.d.ts +37 -0
- package/lib/services/ClassNameExtractionService.d.ts.map +1 -0
- package/lib/services/ClassNameExtractionService.js +98 -0
- package/lib/services/ClassNameExtractionService.js.map +1 -0
- package/lib/services/ClassNameExtractionService.original.d.ts +20 -0
- package/lib/services/ClassNameExtractionService.original.d.ts.map +1 -0
- package/lib/services/ClassNameExtractionService.original.js +48 -0
- package/lib/services/ClassNameExtractionService.original.js.map +1 -0
- package/lib/services/DiagnosticService.d.ts +14 -0
- package/lib/services/DiagnosticService.d.ts.map +1 -0
- package/lib/services/DiagnosticService.js +61 -0
- package/lib/services/DiagnosticService.js.map +1 -0
- package/lib/services/PerformanceCache.d.ts +15 -0
- package/lib/services/PerformanceCache.d.ts.map +1 -0
- package/lib/services/PerformanceCache.js +44 -0
- package/lib/services/PerformanceCache.js.map +1 -0
- package/lib/services/PluginConfigService.d.ts +22 -0
- package/lib/services/PluginConfigService.d.ts.map +1 -0
- package/lib/services/PluginConfigService.js +86 -0
- package/lib/services/PluginConfigService.js.map +1 -0
- package/lib/services/ValidationService.d.ts +25 -0
- package/lib/services/ValidationService.d.ts.map +1 -0
- package/lib/services/ValidationService.js +50 -0
- package/lib/services/ValidationService.js.map +1 -0
- package/lib/utils/Logger.d.ts +10 -0
- package/lib/utils/Logger.d.ts.map +1 -0
- package/lib/utils/Logger.js +13 -0
- package/lib/utils/Logger.js.map +1 -0
- 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 @@
|
|
|
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"}
|