tailwind-unwind 0.1.0 → 0.1.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/README.md +160 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,28 +1,57 @@
|
|
|
1
1
|
# tailwind-unwind
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI tool to analyze, extract, and refactor repeated Tailwind CSS utility patterns in React and Next.js projects.
|
|
4
4
|
|
|
5
5
|
**Repository:** [github.com/AVPletnev/tailwind-unwind](https://github.com/AVPletnev/tailwind-unwind)
|
|
6
6
|
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **analyze** — find frequent `className` patterns with file locations
|
|
10
|
+
- **generate** — create `@layer components` CSS with `@apply`
|
|
11
|
+
- **apply** — auto-replace repeated class strings in `.tsx`/`.jsx` source files
|
|
12
|
+
- Parses static strings, template literals, `cn()` / `clsx()` / `classnames()`
|
|
13
|
+
- Human-readable class names (`twu-page-header`, `twu-media-cover`) with `twu-` namespace prefix
|
|
14
|
+
|
|
7
15
|
## Installation
|
|
8
16
|
|
|
9
17
|
```bash
|
|
10
18
|
npm install -g tailwind-unwind
|
|
19
|
+
|
|
11
20
|
# or run without installing
|
|
12
21
|
npx tailwind-unwind analyze ./src
|
|
13
22
|
```
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
Requires **Node.js 18+**.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
16
27
|
|
|
17
28
|
```bash
|
|
18
|
-
|
|
29
|
+
# 1. See what repeats in your project
|
|
30
|
+
npx tailwind-unwind analyze ./src
|
|
31
|
+
|
|
32
|
+
# 2. Generate component CSS
|
|
33
|
+
npx tailwind-unwind generate ./src --output styles.css
|
|
34
|
+
|
|
35
|
+
# 3. Import styles.css in your global CSS (e.g. globals.css), then apply replacements
|
|
36
|
+
npx tailwind-unwind apply ./src --output styles.css --dry-run # preview
|
|
37
|
+
npx tailwind-unwind apply ./src --output styles.css # write changes
|
|
19
38
|
```
|
|
20
39
|
|
|
21
|
-
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
### `analyze`
|
|
45
|
+
|
|
46
|
+
Scan a directory and report the most frequent Tailwind class combinations.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx tailwind-unwind analyze <path>
|
|
50
|
+
```
|
|
22
51
|
|
|
23
52
|
| Flag | Default | Description |
|
|
24
53
|
|------|---------|-------------|
|
|
25
|
-
| `--min-occurrences <n>` | `5` | Minimum occurrences
|
|
54
|
+
| `--min-occurrences <n>` | `5` | Minimum occurrences (combinations must appear **more than** n times) |
|
|
26
55
|
| `--min-size <n>` | `2` | Minimum classes per combination |
|
|
27
56
|
| `--max-size <n>` | `5` | Maximum classes per combination |
|
|
28
57
|
| `--top <n>` | `10` | Number of top combinations to show |
|
|
@@ -37,121 +66,198 @@ npx tailwind-unwind analyze ./src --format json
|
|
|
37
66
|
npx tailwind-unwind analyze ./src --min-occurrences 10 --top 5
|
|
38
67
|
```
|
|
39
68
|
|
|
40
|
-
|
|
69
|
+
**Example output:**
|
|
41
70
|
|
|
42
|
-
|
|
71
|
+
```
|
|
72
|
+
📊 Tailwind Analysis Report
|
|
73
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
74
|
+
Files scanned: 47
|
|
75
|
+
Components with className: 312
|
|
76
|
+
Unique class combinations: 89
|
|
77
|
+
|
|
78
|
+
🏆 Top 10 most frequent combinations:
|
|
43
79
|
|
|
44
|
-
|
|
80
|
+
1. "flex items-center justify-between p-4"
|
|
81
|
+
Occurrences: 24
|
|
82
|
+
Suggestion: .page-header
|
|
83
|
+
Found in: src/components/Header.tsx:12, src/layout/Toolbar.tsx:5 (+18 more)
|
|
84
|
+
|
|
85
|
+
💡 Potential code reduction: 38%
|
|
86
|
+
💡 Generate CSS: npx tailwind-unwind generate <path> --output styles.css
|
|
87
|
+
💡 Apply classes: npx tailwind-unwind apply <path> --output styles.css
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`analyze` finds frequent **subsets** of classes (2–5 utilities) and deduplicates subsets by default.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### `generate`
|
|
95
|
+
|
|
96
|
+
Extract **exact duplicate `className` strings** into reusable component classes.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npx tailwind-unwind generate <path> --output <file.css>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
| Flag | Default | Description |
|
|
103
|
+
|------|---------|-------------|
|
|
104
|
+
| `--output <file>` | *(required)* | Output CSS file path |
|
|
105
|
+
| `--min-occurrences <n>` | `3` | Minimum occurrences (must appear **≥ n** times) |
|
|
106
|
+
| `--min-size <n>` | `2` | Minimum classes per set |
|
|
107
|
+
| `--max-size <n>` | `5` | Maximum classes per set |
|
|
108
|
+
| `--top <n>` | `10` | Max number of component classes to generate |
|
|
109
|
+
| `--prefix <name>` | `twu-` | Namespace prefix for generated classes |
|
|
45
110
|
|
|
46
111
|
```bash
|
|
47
112
|
npx tailwind-unwind generate ./src --output styles.css
|
|
48
|
-
# Custom namespace (default: twu-)
|
|
49
113
|
npx tailwind-unwind generate ./src --output styles.css --prefix app-
|
|
114
|
+
npx tailwind-unwind generate ./src --output styles.css --min-occurrences 2 --top 20
|
|
50
115
|
```
|
|
51
116
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Example output file:
|
|
117
|
+
**Example output (`styles.css`):**
|
|
55
118
|
|
|
56
119
|
```css
|
|
120
|
+
/**
|
|
121
|
+
* Generated by tailwind-unwind
|
|
122
|
+
* Source: ./src
|
|
123
|
+
* Class prefix: twu-
|
|
124
|
+
*/
|
|
57
125
|
@layer components {
|
|
58
|
-
.twu-
|
|
126
|
+
.twu-page-header {
|
|
59
127
|
@apply flex items-center justify-between p-4;
|
|
60
128
|
}
|
|
61
129
|
|
|
62
130
|
.twu-media-cover {
|
|
63
131
|
@apply w-full h-auto object-cover rounded-lg;
|
|
64
132
|
}
|
|
133
|
+
|
|
134
|
+
.twu-primary-button {
|
|
135
|
+
@apply bg-blue-500 text-white px-4 py-2 rounded-lg;
|
|
136
|
+
}
|
|
65
137
|
}
|
|
66
138
|
```
|
|
67
139
|
|
|
68
|
-
Import
|
|
140
|
+
Import `styles.css` in your global CSS, then use the generated classes in JSX.
|
|
69
141
|
|
|
70
|
-
|
|
142
|
+
---
|
|
71
143
|
|
|
72
|
-
###
|
|
144
|
+
### `apply`
|
|
73
145
|
|
|
74
|
-
Generate CSS **and** replace matching `className` strings in
|
|
146
|
+
Generate CSS **and** replace matching `className` strings in source files.
|
|
75
147
|
|
|
76
148
|
```bash
|
|
77
|
-
|
|
78
|
-
|
|
149
|
+
npx tailwind-unwind apply <path> --output <file.css>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Supports the same flags as `generate`, plus:
|
|
79
153
|
|
|
80
|
-
|
|
154
|
+
| Flag | Description |
|
|
155
|
+
|------|-------------|
|
|
156
|
+
| `--dry-run` | Preview replacements without writing files |
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npx tailwind-unwind apply ./src --output styles.css --dry-run
|
|
81
160
|
npx tailwind-unwind apply ./src --output styles.css
|
|
82
161
|
```
|
|
83
162
|
|
|
84
|
-
|
|
163
|
+
**What gets replaced:**
|
|
164
|
+
|
|
165
|
+
| Pattern | Replaced? |
|
|
166
|
+
|---------|-----------|
|
|
167
|
+
| `className="flex items-center p-4"` | ✅ |
|
|
168
|
+
| `className={cn('flex', 'items-center', 'p-4')}` | ✅ (static args only) |
|
|
169
|
+
| `className={getClasses()}` | ❌ skipped |
|
|
170
|
+
| `` className={`flex ${active ? 'p-4' : ''}`} `` | ❌ skipped (dynamic) |
|
|
171
|
+
|
|
172
|
+
**Before → After:**
|
|
85
173
|
|
|
86
|
-
|
|
174
|
+
```tsx
|
|
175
|
+
// before
|
|
176
|
+
<div className="flex items-center justify-between p-4">Header</div>
|
|
87
177
|
|
|
178
|
+
// after
|
|
179
|
+
<div className="twu-page-header">Header</div>
|
|
88
180
|
```
|
|
89
|
-
📊 Tailwind Analysis Report
|
|
90
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
91
|
-
Files scanned: 5
|
|
92
|
-
Components with className: 18
|
|
93
|
-
Unique class combinations: 4
|
|
94
181
|
|
|
95
|
-
|
|
182
|
+
---
|
|
96
183
|
|
|
97
|
-
|
|
98
|
-
Occurrences: 8
|
|
99
|
-
Suggestion: .flex-items-center-justify-between
|
|
184
|
+
## Class naming
|
|
100
185
|
|
|
101
|
-
|
|
102
|
-
Occurrences: 7
|
|
103
|
-
Suggestion: .w-full-h-auto-object-cover
|
|
186
|
+
Generated classes use a **`twu-` prefix** by default to avoid conflicts with existing project styles. Names are derived from semantic rules, not utility concatenation:
|
|
104
187
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
188
|
+
| Utilities | Generated class |
|
|
189
|
+
|-----------|-----------------|
|
|
190
|
+
| `flex items-center justify-between p-4` | `twu-page-header` |
|
|
191
|
+
| `w-full object-cover rounded-lg` | `twu-media-cover` |
|
|
192
|
+
| `bg-blue-500 px-4 py-2 rounded-lg` | `twu-primary-button` |
|
|
193
|
+
| `grid grid-cols-3 gap-4` | `twu-card-grid` |
|
|
194
|
+
| `fixed inset-0 bg-black/50` | `twu-backdrop` |
|
|
195
|
+
|
|
196
|
+
Customize with `--prefix app-` → `.app-page-header`, etc.
|
|
197
|
+
|
|
198
|
+
---
|
|
108
199
|
|
|
109
|
-
## What
|
|
200
|
+
## What gets parsed
|
|
110
201
|
|
|
111
|
-
|
|
112
|
-
- **Template literals:** `className={\`flex p-4 ${active ? 'bg-blue' : ''}\`}` — static segments extracted
|
|
113
|
-
- **Class merge utilities:** `cn()`, `clsx()`, `classnames()`, `twMerge()`, `cx()` — string arguments and conditionals extracted
|
|
114
|
-
- **Fully dynamic expressions:** `className={getClasses()}` — skipped with a warning
|
|
202
|
+
Scans `.tsx`, `.jsx`, `.ts`, `.js` recursively. Ignores `node_modules`, `.next`, `dist`, `build`, `.git`.
|
|
115
203
|
|
|
116
|
-
|
|
204
|
+
- **Static strings:** `className="flex p-4"` / `class="flex p-4"`
|
|
205
|
+
- **Template literals:** static segments extracted; `${...}` expressions flagged
|
|
206
|
+
- **Merge utilities:** `cn()`, `clsx()`, `classnames()`, `twMerge()`, `cx()`
|
|
207
|
+
- **Dynamic expressions:** `className={getClasses()}` — warning, skipped
|
|
117
208
|
|
|
118
|
-
|
|
209
|
+
Class order is normalized: `flex p-4` and `p-4 flex` are treated as the same set.
|
|
210
|
+
|
|
211
|
+
---
|
|
119
212
|
|
|
120
213
|
## Programmatic API
|
|
121
214
|
|
|
122
215
|
```typescript
|
|
123
216
|
import {
|
|
124
217
|
analyzeCommand,
|
|
218
|
+
generateCommand,
|
|
219
|
+
applyCommand,
|
|
125
220
|
walkSourceFiles,
|
|
126
221
|
parseFile,
|
|
127
222
|
findFrequentPatterns,
|
|
223
|
+
findRepeatedClassSets,
|
|
224
|
+
buildComponents,
|
|
128
225
|
normalizeClasses,
|
|
129
226
|
} from 'tailwind-unwind';
|
|
130
227
|
|
|
228
|
+
// Analyze
|
|
229
|
+
await analyzeCommand('./src', { format: 'json' });
|
|
230
|
+
|
|
231
|
+
// Build component map
|
|
131
232
|
const files = await walkSourceFiles('./src');
|
|
132
233
|
const result = await parseFile(files[0]);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
{
|
|
137
|
-
|
|
138
|
-
filePath: files[0],
|
|
139
|
-
line: extraction.line,
|
|
140
|
-
},
|
|
141
|
-
]);
|
|
234
|
+
|
|
235
|
+
const { components, css, replacementMap } = buildComponents(
|
|
236
|
+
[{ classes: result.extractions[0].classes, filePath: files[0] }],
|
|
237
|
+
{ sourcePath: './src', prefix: 'twu-' },
|
|
238
|
+
);
|
|
142
239
|
```
|
|
143
240
|
|
|
241
|
+
---
|
|
242
|
+
|
|
144
243
|
## Development
|
|
145
244
|
|
|
146
245
|
```bash
|
|
246
|
+
git clone https://github.com/AVPletnev/tailwind-unwind.git
|
|
247
|
+
cd tailwind-unwind
|
|
147
248
|
npm install
|
|
148
|
-
npm run build
|
|
249
|
+
npm run build # required before running CLI locally
|
|
149
250
|
npm test
|
|
251
|
+
|
|
150
252
|
node bin/index.js analyze ./test-project
|
|
151
253
|
node bin/index.js generate ./test-project --output styles.css
|
|
152
254
|
node bin/index.js apply ./test-project --output styles.css --dry-run
|
|
153
255
|
```
|
|
154
256
|
|
|
257
|
+
> **Note:** `bin/index.js` runs compiled code from `dist/`. Always run `npm run build` after changing source files.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
155
261
|
## License
|
|
156
262
|
|
|
157
263
|
MIT — see [LICENSE](LICENSE).
|