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 CHANGED
@@ -1,33 +1,113 @@
1
1
  # tailwind-unwind
2
2
 
3
- Analyze Tailwind CSS class usage patterns in React and Next.js projects. Find repeated utility combinations and opportunities to extract reusable component classes.
3
+ [![npm version](https://img.shields.io/npm/v/tailwind-unwind.svg)](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
- ## Usage
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
- npx tailwind-unwind analyze <path>
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
- ### CLI options
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 for a combination |
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
- Scan a directory recursively for `.tsx`, `.jsx`, `.ts`, and `.js` files. The tool ignores `node_modules`, `.next`, `dist`, `build`, and `.git`.
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
- ### Generate CSS
129
+ 🏆 Top 10 most frequent combinations:
43
130
 
44
- Extract **exact duplicate className strings** into reusable component classes:
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
- Unlike `analyze` (which finds frequent subsets), `generate` looks for identical full class lists on JSX elements. Default `--min-occurrences` is `3`.
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-toolbar {
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 the generated file in your global CSS (e.g. `globals.css`), then replace repeated `className` strings with the new classes.
192
+ Import `styles.css` in your global CSS, then use the generated classes in JSX.
69
193
 
70
- Supports filter flags: `--min-occurrences`, `--min-size`, `--max-size`, `--top`.
194
+ ---
71
195
 
72
- ### Apply (replace className in source)
196
+ ### `apply`
73
197
 
74
- Generate CSS **and** replace matching `className` strings in your `.tsx`/`.jsx` files:
198
+ Generate CSS **and** replace matching `className` strings in source files.
75
199
 
76
200
  ```bash
77
- # Preview changes without writing files
78
- npx tailwind-unwind apply ./src --output styles.css --dry-run
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
- # Apply replacements and write styles.css
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
- Only **exact static matches** are replaced (string literals and static `cn()`/`clsx()` calls). Dynamic expressions are left unchanged.
215
+ **What gets replaced:**
85
216
 
86
- ### Example output
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
- 🏆 Top 10 most frequent combinations:
234
+ ---
96
235
 
97
- 1. "flex items-center justify-between p-4"
98
- Occurrences: 8
99
- Suggestion: .flex-items-center-justify-between
236
+ ## Class naming
100
237
 
101
- 2. "w-full h-auto object-cover rounded-lg"
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
- 💡 Potential code reduction: 42%
106
- 💡 Generate CSS: npx tailwind-unwind generate <path> --output styles.css
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
- ## What it analyzes
259
+ `.app-page-header`
260
+
261
+ ---
262
+
263
+ ## What gets parsed
110
264
 
111
- - **Static strings:** `className="flex p-4"` and `class="flex p-4"`
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
- Class combinations are normalized (order-independent): `flex p-4` and `p-4 flex` count as the same pattern.
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
- Subset combinations are deduplicated by default (e.g. `flex p-4` is hidden when `flex items-center p-4` is present). Each result includes file locations.
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 patterns = findFrequentPatterns([
136
- {
137
- classes: extraction.classes,
138
- filePath: files[0],
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).