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