tailwind-unwind 0.1.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.
@@ -0,0 +1,1469 @@
1
+ // src/analyzer/suggestions.ts
2
+ var BREAKPOINT_PREFIX = /^(sm|md|lg|xl|2xl):/;
3
+ function baseClass(cls) {
4
+ return cls.replace(BREAKPOINT_PREFIX, "");
5
+ }
6
+ function has(classes, name) {
7
+ return classes.some((cls) => baseClass(cls) === name);
8
+ }
9
+ function hasPrefix(classes, prefix) {
10
+ return classes.some((cls) => baseClass(cls).startsWith(prefix));
11
+ }
12
+ function hasAll(classes, names) {
13
+ return names.every((name) => has(classes, name));
14
+ }
15
+ function hasAny(classes, names) {
16
+ return names.some((name) => has(classes, name));
17
+ }
18
+ function hasAnyPrefix(classes, prefixes) {
19
+ return prefixes.some((prefix) => hasPrefix(classes, prefix));
20
+ }
21
+ function hasPadding(classes) {
22
+ return hasAnyPrefix(classes, ["p-", "px-", "py-", "pt-", "pb-", "pl-", "pr-"]);
23
+ }
24
+ function hasMargin(classes) {
25
+ return hasAnyPrefix(classes, ["m-", "mx-", "my-", "mt-", "mb-", "ml-", "mr-"]);
26
+ }
27
+ function hasGap(classes) {
28
+ return hasPrefix(classes, "gap-") || hasPrefix(classes, "gap-x-") || hasPrefix(classes, "gap-y-");
29
+ }
30
+ function hasShadow(classes) {
31
+ return hasPrefix(classes, "shadow-");
32
+ }
33
+ function hasRing(classes) {
34
+ return hasPrefix(classes, "ring-");
35
+ }
36
+ function isFullWidth(classes) {
37
+ return has(classes, "w-full");
38
+ }
39
+ function gridColumnCount(classes) {
40
+ const colClass = classes.find((cls) => baseClass(cls).startsWith("grid-cols-"));
41
+ if (!colClass) return null;
42
+ return baseClass(colClass).replace("grid-cols-", "");
43
+ }
44
+ function iconSize(classes) {
45
+ return hasAny(classes, ["w-4", "h-4", "w-5", "h-5", "w-6", "h-6", "size-4", "size-5", "size-6"]);
46
+ }
47
+ var SEMANTIC_RULES = [
48
+ // ── Navigation & chrome ──────────────────────────────────────────
49
+ {
50
+ name: "page-header",
51
+ match: (c) => hasAll(c, ["flex", "items-center", "justify-between"]) && (hasPadding(c) || hasPrefix(c, "border-b"))
52
+ },
53
+ {
54
+ name: "toolbar",
55
+ match: (c) => hasAll(c, ["flex", "items-center", "justify-between"])
56
+ },
57
+ {
58
+ name: "footer-bar",
59
+ match: (c) => has(c, "flex") && hasPrefix(c, "border-t") && hasPadding(c)
60
+ },
61
+ {
62
+ name: "action-bar",
63
+ match: (c) => hasAll(c, ["flex", "items-center", "justify-end"]) && hasPadding(c)
64
+ },
65
+ {
66
+ name: "breadcrumb",
67
+ match: (c) => hasAll(c, ["flex", "items-center"]) && hasGap(c) && hasAny(c, ["text-sm", "text-xs"])
68
+ },
69
+ {
70
+ name: "nav-item",
71
+ match: (c) => hasAll(c, ["flex", "items-center"]) && hasGap(c) && hasPadding(c)
72
+ },
73
+ {
74
+ name: "sidebar",
75
+ match: (c) => hasAll(c, ["flex", "flex-col"]) && (has(c, "h-full") || has(c, "h-screen") || hasPrefix(c, "w-"))
76
+ },
77
+ {
78
+ name: "tab-bar",
79
+ match: (c) => has(c, "flex") && hasPrefix(c, "border-b") && hasGap(c)
80
+ },
81
+ // ── Flex layout ──────────────────────────────────────────────────
82
+ {
83
+ name: "centered-row",
84
+ match: (c) => hasAll(c, ["flex", "items-center", "justify-center"])
85
+ },
86
+ {
87
+ name: "spread-row",
88
+ match: (c) => has(c, "flex") && has(c, "justify-between")
89
+ },
90
+ {
91
+ name: "aligned-row",
92
+ match: (c) => hasAll(c, ["flex", "items-center"])
93
+ },
94
+ {
95
+ name: "inline-actions",
96
+ match: (c) => has(c, "inline-flex") && has(c, "items-center")
97
+ },
98
+ {
99
+ name: "wrap-row",
100
+ match: (c) => has(c, "flex") && has(c, "flex-wrap")
101
+ },
102
+ {
103
+ name: "form-row",
104
+ match: (c) => hasAll(c, ["flex", "flex-col"]) && hasGap(c)
105
+ },
106
+ {
107
+ name: "stack",
108
+ match: (c) => hasAll(c, ["flex", "flex-col"])
109
+ },
110
+ {
111
+ name: "row",
112
+ match: (c) => has(c, "flex")
113
+ },
114
+ // ── Grid layout ──────────────────────────────────────────────────
115
+ {
116
+ name: "photo-grid",
117
+ match: (c) => has(c, "grid") && hasPrefix(c, "grid-cols-") && has(c, "object-cover")
118
+ },
119
+ {
120
+ name: "card-grid",
121
+ match: (c) => has(c, "grid") && hasGap(c) && hasPrefix(c, "grid-cols-")
122
+ },
123
+ {
124
+ name: "grid",
125
+ match: (c) => has(c, "grid")
126
+ },
127
+ // ── Media & images ───────────────────────────────────────────────
128
+ {
129
+ name: "media-cover",
130
+ match: (c) => has(c, "object-cover") && isFullWidth(c)
131
+ },
132
+ {
133
+ name: "media-contain",
134
+ match: (c) => has(c, "object-contain") && isFullWidth(c)
135
+ },
136
+ {
137
+ name: "aspect-video",
138
+ match: (c) => has(c, "aspect-video")
139
+ },
140
+ {
141
+ name: "aspect-square",
142
+ match: (c) => has(c, "aspect-square")
143
+ },
144
+ {
145
+ name: "avatar",
146
+ match: (c) => has(c, "rounded-full") && (hasPrefix(c, "w-") || hasPrefix(c, "h-") || hasPrefix(c, "size-")) && hasPrefix(c, "object-")
147
+ },
148
+ {
149
+ name: "thumbnail",
150
+ match: (c) => has(c, "object-cover") && hasAnyPrefix(c, ["w-16", "w-12", "w-20", "h-16", "h-12", "h-20"])
151
+ },
152
+ {
153
+ name: "icon",
154
+ match: (c) => iconSize(c) && !has(c, "flex")
155
+ },
156
+ {
157
+ name: "logo",
158
+ match: (c) => hasAny(c, ["h-8", "h-6", "h-10"]) && has(c, "w-auto")
159
+ },
160
+ {
161
+ name: "media-frame",
162
+ match: (c) => isFullWidth(c) && has(c, "h-auto")
163
+ },
164
+ // ── Interactive (before surfaces — shares bg-/rounded-/px-) ─────
165
+ {
166
+ name: "badge",
167
+ match: (c) => has(c, "rounded-full") && hasAnyPrefix(c, ["px-", "py-"]) && hasAny(c, ["text-xs", "text-sm"])
168
+ },
169
+ {
170
+ name: "chip",
171
+ match: (c) => hasAnyPrefix(c, ["px-", "py-"]) && !has(c, "rounded-full") && hasPrefix(c, "rounded-") && hasAny(c, ["text-xs", "text-sm"])
172
+ },
173
+ {
174
+ name: "tag",
175
+ match: (c) => hasAnyPrefix(c, ["px-2", "px-3"]) && hasAnyPrefix(c, ["py-0", "py-1"]) && hasPrefix(c, "rounded-md")
176
+ },
177
+ {
178
+ name: "icon-button",
179
+ match: (c) => hasAnyPrefix(c, ["p-1", "p-1.5", "p-2", "p-3"]) && hasPrefix(c, "rounded-") && !hasAnyPrefix(c, ["px-4", "px-5", "px-6", "px-8"])
180
+ },
181
+ {
182
+ name: "primary-button",
183
+ match: (c) => hasAnyPrefix(c, ["px-", "py-"]) && hasPrefix(c, "rounded-") && hasAnyPrefix(c, ["bg-blue", "bg-indigo", "bg-primary", "bg-violet"])
184
+ },
185
+ {
186
+ name: "danger-button",
187
+ match: (c) => hasAnyPrefix(c, ["px-", "py-"]) && hasPrefix(c, "rounded-") && hasAnyPrefix(c, ["bg-red", "bg-rose", "bg-destructive"])
188
+ },
189
+ {
190
+ name: "ghost-button",
191
+ match: (c) => hasAnyPrefix(c, ["px-", "py-"]) && hasPrefix(c, "rounded-") && hasAnyPrefix(c, ["hover:bg-", "bg-transparent"])
192
+ },
193
+ {
194
+ name: "input",
195
+ match: (c) => hasAnyPrefix(c, ["px-", "py-"]) && hasPrefix(c, "border") && hasPrefix(c, "rounded-")
196
+ },
197
+ {
198
+ name: "textarea",
199
+ match: (c) => hasPrefix(c, "border") && hasPrefix(c, "rounded-") && hasAny(c, ["resize-none", "min-h-"])
200
+ },
201
+ {
202
+ name: "select",
203
+ match: (c) => hasPrefix(c, "border") && hasPrefix(c, "rounded-") && has(c, "appearance-none")
204
+ },
205
+ {
206
+ name: "checkbox-row",
207
+ match: (c) => hasAll(c, ["flex", "items-center"]) && hasGap(c) && hasAny(c, ["text-sm", "text-base"])
208
+ },
209
+ {
210
+ name: "button",
211
+ match: (c) => hasAnyPrefix(c, ["px-", "py-"]) && hasPrefix(c, "rounded-")
212
+ },
213
+ // ── Surfaces & containers ────────────────────────────────────────
214
+ {
215
+ name: "page-container",
216
+ match: (c) => has(c, "mx-auto") && hasPrefix(c, "max-w-")
217
+ },
218
+ {
219
+ name: "section",
220
+ match: (c) => hasPadding(c) && hasMargin(c) && !has(c, "flex") && !has(c, "grid")
221
+ },
222
+ {
223
+ name: "hero",
224
+ match: (c) => hasPadding(c) && hasAny(c, ["text-center", "justify-center"]) && hasAnyPrefix(c, ["text-3xl", "text-4xl", "text-5xl"])
225
+ },
226
+ {
227
+ name: "alert",
228
+ match: (c) => hasAnyPrefix(c, ["border-l-4", "border-l-2"]) && hasPadding(c) && hasPrefix(c, "bg-")
229
+ },
230
+ {
231
+ name: "callout",
232
+ match: (c) => hasPrefix(c, "bg-") && hasPadding(c) && hasPrefix(c, "border") && hasPrefix(c, "rounded-")
233
+ },
234
+ {
235
+ name: "panel",
236
+ match: (c) => hasPrefix(c, "rounded-") && hasPadding(c) && hasPrefix(c, "border")
237
+ },
238
+ {
239
+ name: "card",
240
+ match: (c) => hasPrefix(c, "rounded-") && hasPadding(c) && hasPrefix(c, "bg-")
241
+ },
242
+ {
243
+ name: "elevated-card",
244
+ match: (c) => hasShadow(c) && hasPrefix(c, "rounded-") && hasPadding(c)
245
+ },
246
+ {
247
+ name: "surface",
248
+ match: (c) => hasPrefix(c, "bg-") && hasPadding(c)
249
+ },
250
+ // ── Overlays & positioning ───────────────────────────────────────
251
+ {
252
+ name: "backdrop",
253
+ match: (c) => has(c, "fixed") && has(c, "inset-0") && hasAnyPrefix(c, ["bg-black", "bg-white", "bg-gray"])
254
+ },
255
+ {
256
+ name: "overlay",
257
+ match: (c) => has(c, "fixed") && has(c, "inset-0")
258
+ },
259
+ {
260
+ name: "modal-shell",
261
+ match: (c) => has(c, "fixed") && hasAll(c, ["flex", "items-center", "justify-center"])
262
+ },
263
+ {
264
+ name: "drawer",
265
+ match: (c) => has(c, "fixed") && hasAny(c, ["inset-y-0", "top-0", "bottom-0"]) && hasPrefix(c, "w-")
266
+ },
267
+ {
268
+ name: "sticky-header",
269
+ match: (c) => has(c, "sticky") && hasPrefix(c, "top-") && hasPrefix(c, "z-")
270
+ },
271
+ {
272
+ name: "dropdown",
273
+ match: (c) => has(c, "absolute") && hasPrefix(c, "z-") && hasPrefix(c, "rounded-") && hasShadow(c)
274
+ },
275
+ // ── Typography ───────────────────────────────────────────────────
276
+ {
277
+ name: "prose",
278
+ match: (c) => hasPrefix(c, "prose")
279
+ },
280
+ {
281
+ name: "heading",
282
+ match: (c) => hasAnyPrefix(c, ["text-xl", "text-2xl", "text-3xl", "text-4xl", "text-5xl"]) && hasPrefix(c, "font-")
283
+ },
284
+ {
285
+ name: "title",
286
+ match: (c) => hasAnyPrefix(c, ["text-lg", "text-xl", "text-2xl"]) && hasPrefix(c, "font-semibold")
287
+ },
288
+ {
289
+ name: "subtitle",
290
+ match: (c) => hasAnyPrefix(c, ["text-base", "text-lg"]) && hasPrefix(c, "text-gray")
291
+ },
292
+ {
293
+ name: "label",
294
+ match: (c) => hasAny(c, ["text-xs", "text-sm"]) && hasAnyPrefix(c, ["font-medium", "font-semibold"])
295
+ },
296
+ {
297
+ name: "caption",
298
+ match: (c) => hasAny(c, ["text-sm", "text-xs"]) && hasPrefix(c, "font-") && !hasAnyPrefix(c, ["font-medium", "font-semibold"])
299
+ },
300
+ {
301
+ name: "muted",
302
+ match: (c) => hasAnyPrefix(c, ["text-gray", "text-slate", "text-zinc", "text-neutral"])
303
+ },
304
+ {
305
+ name: "error-text",
306
+ match: (c) => hasAnyPrefix(c, ["text-red", "text-rose", "text-destructive"])
307
+ },
308
+ {
309
+ name: "link",
310
+ match: (c) => has(c, "underline") || hasAnyPrefix(c, ["text-blue", "text-indigo", "text-primary"])
311
+ },
312
+ {
313
+ name: "truncate",
314
+ match: (c) => has(c, "truncate") || has(c, "line-clamp-1")
315
+ },
316
+ // ── Lists & tables ───────────────────────────────────────────────
317
+ {
318
+ name: "table-header",
319
+ match: (c) => hasAnyPrefix(c, ["text-xs", "uppercase"]) && hasAnyPrefix(c, ["font-medium", "font-semibold", "tracking-"])
320
+ },
321
+ {
322
+ name: "table-row",
323
+ match: (c) => hasPrefix(c, "border-b") && hasAll(c, ["flex", "items-center"])
324
+ },
325
+ {
326
+ name: "list-item",
327
+ match: (c) => hasAll(c, ["flex", "items-center"]) && hasGap(c) && hasPadding(c)
328
+ },
329
+ {
330
+ name: "menu-item",
331
+ match: (c) => hasAll(c, ["flex", "items-center"]) && hasGap(c) && hasAnyPrefix(c, ["px-", "py-"]) && hasAnyPrefix(c, ["hover:bg-", "rounded-"])
332
+ },
333
+ {
334
+ name: "divider",
335
+ match: (c) => hasPrefix(c, "border-") && (isFullWidth(c) || hasMargin(c))
336
+ },
337
+ // ── States & effects ─────────────────────────────────────────────
338
+ {
339
+ name: "skeleton",
340
+ match: (c) => has(c, "animate-pulse") && hasPrefix(c, "bg-")
341
+ },
342
+ {
343
+ name: "loading",
344
+ match: (c) => has(c, "animate-spin") || has(c, "animate-pulse")
345
+ },
346
+ {
347
+ name: "disabled",
348
+ match: (c) => has(c, "opacity-50") || has(c, "cursor-not-allowed")
349
+ },
350
+ {
351
+ name: "focus-ring",
352
+ match: (c) => hasRing(c) || hasAnyPrefix(c, ["focus:ring-", "focus-visible:ring-"])
353
+ },
354
+ {
355
+ name: "hover-lift",
356
+ match: (c) => hasAnyPrefix(c, ["hover:shadow-", "hover:-translate-y-"]) && hasPrefix(c, "transition")
357
+ },
358
+ // ── Scroll & overflow ────────────────────────────────────────────
359
+ {
360
+ name: "scroll-panel",
361
+ match: (c) => hasAny(c, ["overflow-y-auto", "overflow-auto", "overflow-x-auto", "overflow-hidden"])
362
+ },
363
+ // ── Visual fallbacks ─────────────────────────────────────────────
364
+ {
365
+ name: "shadow-box",
366
+ match: (c) => hasShadow(c) && hasPrefix(c, "rounded-")
367
+ },
368
+ {
369
+ name: "bordered",
370
+ match: (c) => hasPrefix(c, "border") && hasPadding(c)
371
+ },
372
+ {
373
+ name: "rounded-box",
374
+ match: (c) => hasPrefix(c, "rounded-")
375
+ },
376
+ {
377
+ name: "padded",
378
+ match: (c) => hasPadding(c)
379
+ }
380
+ ];
381
+ function buildCompositionalName(classes) {
382
+ const parts = [];
383
+ if (has(classes, "flex")) {
384
+ if (has(classes, "flex-col")) parts.push("stack");
385
+ else if (has(classes, "flex-wrap")) parts.push("wrap");
386
+ else if (has(classes, "justify-between")) parts.push("between");
387
+ else if (has(classes, "justify-center")) parts.push("centered");
388
+ else if (has(classes, "items-center")) parts.push("aligned");
389
+ else parts.push("row");
390
+ } else if (has(classes, "grid")) {
391
+ const cols = gridColumnCount(classes);
392
+ parts.push(cols ? `grid-${cols}` : "grid");
393
+ } else if (has(classes, "inline-flex")) {
394
+ parts.push("inline-row");
395
+ } else if (has(classes, "inline-block")) {
396
+ parts.push("inline");
397
+ } else if (has(classes, "block")) {
398
+ parts.push("block");
399
+ }
400
+ if (has(classes, "fixed")) parts.push("fixed");
401
+ else if (has(classes, "absolute")) parts.push("absolute");
402
+ else if (has(classes, "sticky")) parts.push("sticky");
403
+ else if (has(classes, "relative")) parts.push("relative");
404
+ if (has(classes, "object-cover")) parts.push("cover");
405
+ else if (has(classes, "object-contain")) parts.push("contain");
406
+ else if (has(classes, "aspect-video")) parts.push("video");
407
+ else if (has(classes, "rounded-full")) parts.push("circle");
408
+ else if (hasPrefix(classes, "rounded-")) parts.push("rounded");
409
+ if (hasShadow(classes)) parts.push("shadow");
410
+ else if (hasRing(classes)) parts.push("ring");
411
+ else if (hasPrefix(classes, "border")) parts.push("bordered");
412
+ if (hasPrefix(classes, "bg-")) parts.push("surface");
413
+ else if (hasPrefix(classes, "text-")) parts.push("text");
414
+ if (has(classes, "truncate")) parts.push("truncate");
415
+ if (has(classes, "animate-pulse")) parts.push("pulse");
416
+ if (has(classes, "transition")) parts.push("transition");
417
+ if (parts.length === 0 && hasPadding(classes)) parts.push("padded");
418
+ if (parts.length === 0 && hasMargin(classes)) parts.push("spaced");
419
+ const unique = [...new Set(parts)];
420
+ return unique.slice(0, 2).join("-") || "component";
421
+ }
422
+ function suggestClassName(classes) {
423
+ if (classes.length === 0) {
424
+ return "component";
425
+ }
426
+ for (const rule of SEMANTIC_RULES) {
427
+ if (rule.match(classes)) {
428
+ return rule.name;
429
+ }
430
+ }
431
+ return buildCompositionalName(classes);
432
+ }
433
+
434
+ // src/analyzer/dedupe.ts
435
+ function isStrictSubset(smaller, larger) {
436
+ if (smaller.length >= larger.length) {
437
+ return false;
438
+ }
439
+ const largerSet = new Set(larger);
440
+ return smaller.every((cls) => largerSet.has(cls));
441
+ }
442
+ function dedupeSubsetCombinations(combinations) {
443
+ return combinations.filter((combo) => {
444
+ return !combinations.some(
445
+ (other) => other.normalized !== combo.normalized && isStrictSubset(combo.classes, other.classes)
446
+ );
447
+ });
448
+ }
449
+
450
+ // src/analyzer/combiner.ts
451
+ function normalizeClasses(classes) {
452
+ return [...classes].sort().join(" ");
453
+ }
454
+ function splitClassString(classString) {
455
+ return classString.trim().split(/\s+/).filter((token) => token.length > 0);
456
+ }
457
+ function generateCombinations(classes, size) {
458
+ if (size < 1 || size > classes.length) {
459
+ return [];
460
+ }
461
+ const results = [];
462
+ const backtrack = (start, current) => {
463
+ if (current.length === size) {
464
+ results.push([...current]);
465
+ return;
466
+ }
467
+ for (let i = start; i <= classes.length - (size - current.length); i += 1) {
468
+ const item = classes[i];
469
+ if (item !== void 0) {
470
+ current.push(item);
471
+ backtrack(i + 1, current);
472
+ current.pop();
473
+ }
474
+ }
475
+ };
476
+ backtrack(0, []);
477
+ return results;
478
+ }
479
+
480
+ // src/analyzer/patternFinder.ts
481
+ var DEFAULT_MIN_COMBINATION_SIZE = 2;
482
+ var DEFAULT_MAX_COMBINATION_SIZE = 5;
483
+ var DEFAULT_MIN_OCCURRENCES = 5;
484
+ var DEFAULT_TOP_LIMIT = 10;
485
+ function locationKey(location) {
486
+ return `${location.filePath}:${location.line ?? 0}`;
487
+ }
488
+ function findFrequentPatterns(occurrences, options = {}) {
489
+ const minOccurrences = options.minOccurrences ?? DEFAULT_MIN_OCCURRENCES;
490
+ const minSize = options.minSize ?? DEFAULT_MIN_COMBINATION_SIZE;
491
+ const maxSize = options.maxSize ?? DEFAULT_MAX_COMBINATION_SIZE;
492
+ const topLimit = options.topLimit ?? DEFAULT_TOP_LIMIT;
493
+ const dedupeSubsets = options.dedupeSubsets ?? true;
494
+ const frequency = /* @__PURE__ */ new Map();
495
+ for (const occurrence of occurrences) {
496
+ const uniqueInElement = [...new Set(occurrence.classes)];
497
+ if (uniqueInElement.length < minSize) {
498
+ continue;
499
+ }
500
+ const location = {
501
+ filePath: occurrence.filePath,
502
+ line: occurrence.line
503
+ };
504
+ const cappedMaxSize = Math.min(maxSize, uniqueInElement.length);
505
+ for (let size = minSize; size <= cappedMaxSize; size += 1) {
506
+ const combos = generateCombinations(uniqueInElement, size);
507
+ for (const combo of combos) {
508
+ const normalized = normalizeClasses(combo);
509
+ const existing = frequency.get(normalized);
510
+ if (existing) {
511
+ existing.count += 1;
512
+ const key = locationKey(location);
513
+ if (!existing.locations.some((loc) => locationKey(loc) === key)) {
514
+ existing.locations.push(location);
515
+ }
516
+ } else {
517
+ frequency.set(normalized, {
518
+ classes: [...combo].sort(),
519
+ count: 1,
520
+ locations: [location]
521
+ });
522
+ }
523
+ }
524
+ }
525
+ }
526
+ let frequent = [...frequency.entries()].filter(([, value]) => value.count > minOccurrences).map(([normalized, value]) => ({
527
+ normalized,
528
+ classes: value.classes,
529
+ occurrences: value.count,
530
+ suggestion: `.${suggestClassName(value.classes)}`,
531
+ locations: value.locations
532
+ })).sort((a, b) => {
533
+ if (b.occurrences !== a.occurrences) {
534
+ return b.occurrences - a.occurrences;
535
+ }
536
+ return b.classes.length - a.classes.length;
537
+ });
538
+ if (dedupeSubsets) {
539
+ frequent = dedupeSubsetCombinations(frequent);
540
+ }
541
+ return frequent.slice(0, topLimit);
542
+ }
543
+ function findRepeatedClassSets(occurrences, options = {}) {
544
+ const minOccurrences = options.minOccurrences ?? 3;
545
+ const minSize = options.minSize ?? 2;
546
+ const maxSize = options.maxSize ?? DEFAULT_MAX_COMBINATION_SIZE;
547
+ const topLimit = options.topLimit ?? DEFAULT_TOP_LIMIT;
548
+ const frequency = /* @__PURE__ */ new Map();
549
+ for (const occurrence of occurrences) {
550
+ const uniqueInElement = [...new Set(occurrence.classes)];
551
+ if (uniqueInElement.length < minSize || uniqueInElement.length > maxSize) {
552
+ continue;
553
+ }
554
+ const normalized = normalizeClasses(uniqueInElement);
555
+ const location = {
556
+ filePath: occurrence.filePath,
557
+ line: occurrence.line
558
+ };
559
+ const existing = frequency.get(normalized);
560
+ if (existing) {
561
+ existing.count += 1;
562
+ const key = locationKey(location);
563
+ if (!existing.locations.some((loc) => locationKey(loc) === key)) {
564
+ existing.locations.push(location);
565
+ }
566
+ } else {
567
+ frequency.set(normalized, {
568
+ classes: [...uniqueInElement].sort(),
569
+ count: 1,
570
+ locations: [location]
571
+ });
572
+ }
573
+ }
574
+ return [...frequency.entries()].filter(([, value]) => value.count >= minOccurrences).map(([normalized, value]) => ({
575
+ normalized,
576
+ classes: value.classes,
577
+ occurrences: value.count,
578
+ suggestion: `.${suggestClassName(value.classes)}`,
579
+ locations: value.locations
580
+ })).sort((a, b) => {
581
+ if (b.occurrences !== a.occurrences) {
582
+ return b.occurrences - a.occurrences;
583
+ }
584
+ return b.classes.length - a.classes.length;
585
+ }).slice(0, topLimit);
586
+ }
587
+ function calculatePotentialReduction(occurrences, topCombinations) {
588
+ const totalClassUsages = occurrences.reduce(
589
+ (sum, occurrence) => sum + occurrence.classes.length,
590
+ 0
591
+ );
592
+ if (totalClassUsages === 0 || topCombinations.length === 0) {
593
+ return 0;
594
+ }
595
+ const best = [...topCombinations].sort((a, b) => {
596
+ if (b.classes.length !== a.classes.length) {
597
+ return b.classes.length - a.classes.length;
598
+ }
599
+ return b.occurrences - a.occurrences;
600
+ })[0];
601
+ if (!best || best.occurrences <= 1 || best.classes.length < 2) {
602
+ return 0;
603
+ }
604
+ const redundantInstances = best.occurrences - 1;
605
+ const utilitiesSavedPerInstance = best.classes.length - 1;
606
+ const savable = redundantInstances * utilitiesSavedPerInstance;
607
+ return Math.min(100, Math.round(savable / totalClassUsages * 100));
608
+ }
609
+
610
+ // src/parser/classHelpers.ts
611
+ var CLASS_MERGE_CALLEES = /* @__PURE__ */ new Set([
612
+ "cn",
613
+ "clsx",
614
+ "classnames",
615
+ "classNames",
616
+ "twMerge",
617
+ "cx"
618
+ ]);
619
+ function mergeExtractions(parts) {
620
+ const classes = [...new Set(parts.flatMap((part) => part.classes))];
621
+ const isDynamic = parts.some((part) => part.isDynamic);
622
+ return { classes, isDynamic };
623
+ }
624
+ function extractFromStringLiteral(value) {
625
+ return {
626
+ classes: splitClassString(value),
627
+ isDynamic: false
628
+ };
629
+ }
630
+ function extractFromTemplateLiteral(node) {
631
+ const parts = [];
632
+ for (const quasi of node.quasis) {
633
+ if (quasi.value.cooked) {
634
+ parts.push(quasi.value.cooked);
635
+ }
636
+ }
637
+ const combined = parts.join(" ").trim();
638
+ return {
639
+ classes: combined ? splitClassString(combined) : [],
640
+ isDynamic: node.expressions.length > 0
641
+ };
642
+ }
643
+ function isClassMergeCallee(expression) {
644
+ if (expression.type === "Identifier") {
645
+ return CLASS_MERGE_CALLEES.has(expression.name);
646
+ }
647
+ if (expression.type === "MemberExpression" && expression.property.type === "Identifier") {
648
+ return CLASS_MERGE_CALLEES.has(expression.property.name);
649
+ }
650
+ return false;
651
+ }
652
+ function extractFromCallArguments(args) {
653
+ const parts = [];
654
+ for (const arg of args) {
655
+ if (arg.type === "SpreadElement" || arg.type === "ArgumentPlaceholder") {
656
+ parts.push({ classes: [], isDynamic: true });
657
+ continue;
658
+ }
659
+ parts.push(extractClassesFromExpression(arg));
660
+ }
661
+ return mergeExtractions(parts);
662
+ }
663
+ function extractClassesFromExpression(expression) {
664
+ switch (expression.type) {
665
+ case "StringLiteral":
666
+ return extractFromStringLiteral(expression.value);
667
+ case "TemplateLiteral":
668
+ return extractFromTemplateLiteral(expression);
669
+ case "CallExpression": {
670
+ const { callee } = expression;
671
+ if (callee.type !== "V8IntrinsicIdentifier" && isClassMergeCallee(callee)) {
672
+ return extractFromCallArguments(expression.arguments);
673
+ }
674
+ return { classes: [], isDynamic: true };
675
+ }
676
+ case "ConditionalExpression": {
677
+ const merged = mergeExtractions([
678
+ extractClassesFromExpression(expression.consequent),
679
+ extractClassesFromExpression(expression.alternate)
680
+ ]);
681
+ return { ...merged, isDynamic: true };
682
+ }
683
+ case "LogicalExpression": {
684
+ const merged = mergeExtractions([
685
+ extractClassesFromExpression(expression.left),
686
+ extractClassesFromExpression(expression.right)
687
+ ]);
688
+ return { ...merged, isDynamic: true };
689
+ }
690
+ case "ArrayExpression":
691
+ return extractFromArrayExpression(expression);
692
+ case "ObjectExpression":
693
+ return extractFromObjectExpression(expression);
694
+ default:
695
+ return { classes: [], isDynamic: true };
696
+ }
697
+ }
698
+ function extractFromArrayExpression(node) {
699
+ const parts = [];
700
+ for (const element of node.elements) {
701
+ if (element === null || element.type === "SpreadElement") {
702
+ parts.push({ classes: [], isDynamic: true });
703
+ continue;
704
+ }
705
+ parts.push(extractClassesFromExpression(element));
706
+ }
707
+ return mergeExtractions(parts);
708
+ }
709
+ function extractFromObjectExpression(node) {
710
+ const classes = [];
711
+ let isDynamic = false;
712
+ for (const prop of node.properties) {
713
+ if (prop.type === "SpreadElement") {
714
+ isDynamic = true;
715
+ continue;
716
+ }
717
+ if (prop.type !== "ObjectProperty") {
718
+ isDynamic = true;
719
+ continue;
720
+ }
721
+ const key = prop.key;
722
+ if (key.type === "StringLiteral") {
723
+ classes.push(...splitClassString(key.value));
724
+ } else if (key.type === "Identifier") {
725
+ classes.push(...splitClassString(key.name));
726
+ }
727
+ if (prop.value.type !== "BooleanLiteral" || prop.value.value !== true) {
728
+ isDynamic = true;
729
+ }
730
+ }
731
+ return {
732
+ classes: [...new Set(classes)],
733
+ isDynamic
734
+ };
735
+ }
736
+
737
+ // src/parser/ast.ts
738
+ import { parse } from "@babel/parser";
739
+ var PARSER_PLUGINS = [
740
+ "jsx",
741
+ "typescript",
742
+ "decorators-legacy",
743
+ "classProperties",
744
+ "classPrivateProperties",
745
+ "classPrivateMethods",
746
+ "dynamicImport",
747
+ "importMeta"
748
+ ];
749
+ var CLASS_ATTRIBUTES = /* @__PURE__ */ new Set(["className", "class"]);
750
+ function getAttributeName(attr) {
751
+ if (attr.name.type === "JSXIdentifier") {
752
+ return attr.name.name;
753
+ }
754
+ return null;
755
+ }
756
+ function isClassAttribute(attr) {
757
+ const name = getAttributeName(attr);
758
+ return name !== null && CLASS_ATTRIBUTES.has(name);
759
+ }
760
+ function extractFromJSXAttribute(attr) {
761
+ if (!isClassAttribute(attr)) {
762
+ return null;
763
+ }
764
+ const line = attr.loc?.start.line;
765
+ const value = attr.value;
766
+ if (value == null) {
767
+ return { classes: [], isDynamic: true, line };
768
+ }
769
+ if (value.type === "StringLiteral") {
770
+ const result = extractClassesFromExpression(value);
771
+ return { classes: result.classes, isDynamic: result.isDynamic, line };
772
+ }
773
+ if (value.type === "JSXExpressionContainer") {
774
+ const expr = value.expression;
775
+ if (expr.type === "JSXEmptyExpression") {
776
+ return { classes: [], isDynamic: true, line };
777
+ }
778
+ const result = extractClassesFromExpression(expr);
779
+ return { classes: result.classes, isDynamic: result.isDynamic, line };
780
+ }
781
+ return { classes: [], isDynamic: true, line };
782
+ }
783
+ function parseSourceToAst(source) {
784
+ return parse(source, {
785
+ sourceType: "module",
786
+ plugins: [...PARSER_PLUGINS],
787
+ errorRecovery: true,
788
+ allowReturnOutsideFunction: true,
789
+ ranges: false,
790
+ tokens: false
791
+ });
792
+ }
793
+
794
+ // src/parser/jsxParser.ts
795
+ import babelTraverse from "@babel/traverse";
796
+ import fs from "fs/promises";
797
+ function resolveTraverse(module) {
798
+ if (typeof module === "function") {
799
+ return module;
800
+ }
801
+ const withDefault = module;
802
+ if (typeof withDefault.default === "function") {
803
+ return withDefault.default;
804
+ }
805
+ throw new Error("Failed to load @babel/traverse");
806
+ }
807
+ var traverse = resolveTraverse(babelTraverse);
808
+ function isJSXElementWithClassAttribute(path5) {
809
+ const opening = path5.node.openingElement;
810
+ return opening.attributes.some(
811
+ (attr) => attr.type === "JSXAttribute" && isClassAttribute(attr)
812
+ );
813
+ }
814
+ function collectExtractionsFromAst(ast, filePath) {
815
+ const extractions = [];
816
+ const warnings = [];
817
+ traverse(ast, {
818
+ JSXElement(path5) {
819
+ if (!isJSXElementWithClassAttribute(path5)) {
820
+ return;
821
+ }
822
+ const opening = path5.node.openingElement;
823
+ for (const attr of opening.attributes) {
824
+ if (attr.type !== "JSXAttribute") continue;
825
+ const extraction = extractFromJSXAttribute(attr);
826
+ if (!extraction) continue;
827
+ if (extraction.isDynamic && extraction.classes.length === 0) {
828
+ const lineInfo = extraction.line ? `:${extraction.line}` : "";
829
+ warnings.push(
830
+ `Dynamic className skipped in ${filePath}${lineInfo}`
831
+ );
832
+ continue;
833
+ }
834
+ if (extraction.classes.length > 0) {
835
+ extractions.push(extraction);
836
+ }
837
+ }
838
+ }
839
+ });
840
+ return { extractions, warnings };
841
+ }
842
+ function parseSource(source, filePath = "unknown") {
843
+ const extractions = [];
844
+ const warnings = [];
845
+ try {
846
+ const ast = parseSourceToAst(source);
847
+ const collected = collectExtractionsFromAst(ast, filePath);
848
+ extractions.push(...collected.extractions);
849
+ warnings.push(...collected.warnings);
850
+ } catch (error) {
851
+ const message = error instanceof Error ? error.message : String(error);
852
+ warnings.push(`Failed to parse ${filePath}: ${message}`);
853
+ }
854
+ return { filePath, extractions, warnings };
855
+ }
856
+ async function parseFile(filePath) {
857
+ const source = await fs.readFile(filePath, "utf-8");
858
+ return parseSource(source, filePath);
859
+ }
860
+
861
+ // src/scanner/ignore.ts
862
+ var IGNORED_DIRECTORIES = [
863
+ "node_modules",
864
+ ".next",
865
+ "dist",
866
+ "build",
867
+ ".git"
868
+ ];
869
+ var IGNORE_PATTERNS = IGNORED_DIRECTORIES.map(
870
+ (dir) => `**/${dir}/**`
871
+ );
872
+
873
+ // src/scanner/fileWalker.ts
874
+ import fg from "fast-glob";
875
+ import path from "path";
876
+ var SOURCE_EXTENSIONS = ["tsx", "jsx", "ts", "js"];
877
+ async function walkSourceFiles(targetPath) {
878
+ const absolutePath = path.resolve(targetPath);
879
+ const patterns = SOURCE_EXTENSIONS.map(
880
+ (ext) => path.join(absolutePath, `**/*.${ext}`).replace(/\\/g, "/")
881
+ );
882
+ const files = await fg(patterns, {
883
+ absolute: true,
884
+ onlyFiles: true,
885
+ unique: true,
886
+ ignore: IGNORE_PATTERNS,
887
+ dot: false
888
+ });
889
+ return files.sort();
890
+ }
891
+
892
+ // src/core/scanProject.ts
893
+ import fs2 from "fs/promises";
894
+ import path2 from "path";
895
+ async function pathExists(targetPath) {
896
+ try {
897
+ await fs2.access(targetPath);
898
+ return true;
899
+ } catch {
900
+ return false;
901
+ }
902
+ }
903
+ async function scanProject(options) {
904
+ const resolvedPath = path2.resolve(options.targetPath);
905
+ if (!await pathExists(resolvedPath)) {
906
+ throw new Error(`Path does not exist: ${resolvedPath}`);
907
+ }
908
+ const files = await walkSourceFiles(resolvedPath);
909
+ if (files.length === 0) {
910
+ throw new Error(
911
+ `No source files (.tsx, .jsx, .ts, .js) found in: ${resolvedPath}`
912
+ );
913
+ }
914
+ const occurrences = [];
915
+ const warnings = [];
916
+ for (const file of files) {
917
+ const result = await parseFile(file);
918
+ warnings.push(...result.warnings);
919
+ for (const extraction of result.extractions) {
920
+ if (extraction.classes.length > 0) {
921
+ occurrences.push({
922
+ classes: extraction.classes,
923
+ filePath: file,
924
+ line: extraction.line
925
+ });
926
+ }
927
+ }
928
+ }
929
+ const uniqueCombinationKeys = new Set(
930
+ occurrences.map(
931
+ (occurrence) => normalizeClasses([...new Set(occurrence.classes)])
932
+ )
933
+ );
934
+ const topCombinations = findFrequentPatterns(occurrences, {
935
+ minOccurrences: options.minOccurrences,
936
+ minSize: options.minSize,
937
+ maxSize: options.maxSize,
938
+ topLimit: options.topLimit,
939
+ dedupeSubsets: options.dedupeSubsets
940
+ });
941
+ const potentialReductionPercent = calculatePotentialReduction(
942
+ occurrences,
943
+ topCombinations
944
+ );
945
+ const report = {
946
+ targetPath: resolvedPath,
947
+ stats: {
948
+ filesScanned: files.length,
949
+ componentsWithClassName: occurrences.length,
950
+ uniqueCombinations: uniqueCombinationKeys.size,
951
+ totalClassUsages: occurrences.reduce(
952
+ (sum, occurrence) => sum + occurrence.classes.length,
953
+ 0
954
+ ),
955
+ topCombinations,
956
+ potentialReductionPercent
957
+ },
958
+ parseWarnings: warnings
959
+ };
960
+ return {
961
+ resolvedPath,
962
+ files,
963
+ occurrences,
964
+ warnings,
965
+ report
966
+ };
967
+ }
968
+
969
+ // src/reporters/consoleReporter.ts
970
+ import chalk from "chalk";
971
+ function formatNumber(value) {
972
+ return value.toLocaleString("en-US");
973
+ }
974
+ function formatLocations(locations) {
975
+ const preview = locations.slice(0, 3).map((loc) => {
976
+ const line = loc.line ? `:${loc.line}` : "";
977
+ return `${loc.filePath}${line}`;
978
+ });
979
+ const suffix = locations.length > 3 ? ` (+${locations.length - 3} more)` : "";
980
+ return preview.join(", ") + suffix;
981
+ }
982
+ function printConsoleReport(report, options = {}) {
983
+ const { stats } = report;
984
+ const topLimit = options.topLimit ?? 10;
985
+ console.log("");
986
+ console.log(chalk.bold.cyan("\u{1F4CA} Tailwind Analysis Report"));
987
+ console.log(chalk.cyan("\u2501".repeat(41)));
988
+ console.log(`Files scanned: ${chalk.white(formatNumber(stats.filesScanned))}`);
989
+ console.log(
990
+ `Components with className: ${chalk.white(formatNumber(stats.componentsWithClassName))}`
991
+ );
992
+ console.log(
993
+ `Unique class combinations: ${chalk.white(formatNumber(stats.uniqueCombinations))}`
994
+ );
995
+ console.log("");
996
+ if (stats.topCombinations.length === 0) {
997
+ console.log(
998
+ chalk.yellow(
999
+ "No frequent class combinations found matching the current filters."
1000
+ )
1001
+ );
1002
+ } else {
1003
+ console.log(
1004
+ chalk.bold.green(`\u{1F3C6} Top ${Math.min(topLimit, stats.topCombinations.length)} most frequent combinations:`)
1005
+ );
1006
+ console.log("");
1007
+ stats.topCombinations.forEach((combo, index) => {
1008
+ const displayClasses = normalizeClasses(combo.classes);
1009
+ console.log(
1010
+ chalk.white(`${index + 1}. `) + chalk.yellow(`"${displayClasses}"`)
1011
+ );
1012
+ console.log(
1013
+ chalk.gray(` Occurrences: `) + chalk.white(String(combo.occurrences))
1014
+ );
1015
+ console.log(
1016
+ chalk.gray(` Suggestion: `) + chalk.green(combo.suggestion)
1017
+ );
1018
+ console.log(
1019
+ chalk.gray(` Found in: `) + chalk.dim(formatLocations(combo.locations))
1020
+ );
1021
+ console.log("");
1022
+ });
1023
+ }
1024
+ console.log(
1025
+ chalk.magenta(
1026
+ `\u{1F4A1} Potential code reduction: ${stats.potentialReductionPercent}%`
1027
+ )
1028
+ );
1029
+ console.log(
1030
+ chalk.magenta(
1031
+ "\u{1F4A1} Generate CSS: npx tailwind-unwind generate <path> --output styles.css"
1032
+ )
1033
+ );
1034
+ console.log(
1035
+ chalk.magenta(
1036
+ "\u{1F4A1} Apply classes: npx tailwind-unwind apply <path> --output styles.css"
1037
+ )
1038
+ );
1039
+ console.log("");
1040
+ }
1041
+
1042
+ // src/reporters/jsonReporter.ts
1043
+ function printJsonReport(report) {
1044
+ console.log(JSON.stringify(report, null, 2));
1045
+ }
1046
+
1047
+ // src/commands/analyze.ts
1048
+ import chalk2 from "chalk";
1049
+ async function analyzeCommand(targetPath, options = {}) {
1050
+ let scanResult;
1051
+ try {
1052
+ scanResult = await scanProject({
1053
+ targetPath,
1054
+ minOccurrences: options.minOccurrences,
1055
+ minSize: options.minSize,
1056
+ maxSize: options.maxSize,
1057
+ topLimit: options.top,
1058
+ dedupeSubsets: options.dedupeSubsets
1059
+ });
1060
+ } catch (error) {
1061
+ const message = error instanceof Error ? error.message : String(error);
1062
+ console.error(chalk2.red(`Error: ${message}`));
1063
+ process.exit(1);
1064
+ }
1065
+ if (options.format !== "json") {
1066
+ for (const warning of scanResult.warnings) {
1067
+ console.warn(chalk2.yellow(`\u26A0 ${warning}`));
1068
+ }
1069
+ }
1070
+ const report = scanResult.report;
1071
+ if (options.format === "json") {
1072
+ printJsonReport(report);
1073
+ } else {
1074
+ printConsoleReport(report, { topLimit: options.top });
1075
+ }
1076
+ return report;
1077
+ }
1078
+
1079
+ // src/codemod/replaceClassNames.ts
1080
+ import babelGenerate from "@babel/generator";
1081
+ import babelTraverse2 from "@babel/traverse";
1082
+ import * as t from "@babel/types";
1083
+ function resolveTraverse2(module) {
1084
+ if (typeof module === "function") {
1085
+ return module;
1086
+ }
1087
+ const withDefault = module;
1088
+ if (typeof withDefault.default === "function") {
1089
+ return withDefault.default;
1090
+ }
1091
+ throw new Error("Failed to load @babel/traverse");
1092
+ }
1093
+ function resolveGenerator(module) {
1094
+ if (typeof module === "function") {
1095
+ return module;
1096
+ }
1097
+ const withDefault = module;
1098
+ if (typeof withDefault.default === "function") {
1099
+ return withDefault.default;
1100
+ }
1101
+ throw new Error("Failed to load @babel/generator");
1102
+ }
1103
+ var traverse2 = resolveTraverse2(babelTraverse2);
1104
+ var generate = resolveGenerator(babelGenerate);
1105
+ function lookupReplacement(extraction, replacementMap) {
1106
+ if (extraction.isDynamic || extraction.classes.length === 0) {
1107
+ return null;
1108
+ }
1109
+ const key = normalizeClasses([...new Set(extraction.classes)]);
1110
+ return replacementMap.get(key) ?? null;
1111
+ }
1112
+ function setStringClassAttribute(attr, className) {
1113
+ attr.value = t.stringLiteral(className);
1114
+ }
1115
+ function replaceClassNamesInSource(source, replacementMap, filePath) {
1116
+ const replacements = [];
1117
+ const skipped = [];
1118
+ if (replacementMap.size === 0) {
1119
+ return { source, replacements, skipped, changed: false };
1120
+ }
1121
+ let ast;
1122
+ try {
1123
+ ast = parseSourceToAst(source);
1124
+ } catch (error) {
1125
+ const message = error instanceof Error ? error.message : String(error);
1126
+ throw new Error(`Failed to parse ${filePath}: ${message}`);
1127
+ }
1128
+ traverse2(ast, {
1129
+ JSXElement(path5) {
1130
+ const opening = path5.node.openingElement;
1131
+ for (const attr of opening.attributes) {
1132
+ if (attr.type !== "JSXAttribute" || !isClassAttribute(attr)) {
1133
+ continue;
1134
+ }
1135
+ const extraction = extractFromJSXAttribute(attr);
1136
+ if (!extraction) continue;
1137
+ const replacement = lookupReplacement(extraction, replacementMap);
1138
+ if (!replacement) {
1139
+ if (extraction.classes.length > 0) {
1140
+ const key = normalizeClasses([...new Set(extraction.classes)]);
1141
+ if (replacementMap.size > 0 && !replacementMap.has(key)) {
1142
+ if (extraction.isDynamic) {
1143
+ skipped.push({
1144
+ filePath,
1145
+ line: extraction.line,
1146
+ reason: "dynamic className",
1147
+ classes: extraction.classes
1148
+ });
1149
+ }
1150
+ }
1151
+ }
1152
+ continue;
1153
+ }
1154
+ const from = normalizeClasses([...new Set(extraction.classes)]);
1155
+ setStringClassAttribute(attr, replacement);
1156
+ replacements.push({
1157
+ filePath,
1158
+ line: extraction.line,
1159
+ from,
1160
+ to: replacement
1161
+ });
1162
+ }
1163
+ }
1164
+ });
1165
+ if (replacements.length === 0) {
1166
+ return { source, replacements, skipped, changed: false };
1167
+ }
1168
+ const output = generate(ast, { retainLines: true }, source);
1169
+ return {
1170
+ source: output.code,
1171
+ replacements,
1172
+ skipped,
1173
+ changed: true
1174
+ };
1175
+ }
1176
+
1177
+ // src/generator/classPrefix.ts
1178
+ var DEFAULT_CLASS_PREFIX = "twu-";
1179
+ function normalizeClassPrefix(prefix) {
1180
+ if (!prefix || prefix.trim().length === 0) {
1181
+ return DEFAULT_CLASS_PREFIX;
1182
+ }
1183
+ const trimmed = prefix.trim();
1184
+ return trimmed.endsWith("-") ? trimmed : `${trimmed}-`;
1185
+ }
1186
+ function withClassPrefix(baseName, prefix) {
1187
+ const normalizedPrefix = normalizeClassPrefix(prefix);
1188
+ if (baseName.startsWith(normalizedPrefix)) {
1189
+ return baseName;
1190
+ }
1191
+ return `${normalizedPrefix}${baseName}`;
1192
+ }
1193
+
1194
+ // src/generator/cssGenerator.ts
1195
+ function assignComponentClassNames(combinations, options = {}) {
1196
+ const used = /* @__PURE__ */ new Set();
1197
+ return combinations.map((combo) => {
1198
+ const base = suggestClassName(combo.classes);
1199
+ let className = withClassPrefix(base, options.prefix);
1200
+ let suffix = 2;
1201
+ while (used.has(className)) {
1202
+ className = withClassPrefix(`${base}-${suffix}`, options.prefix);
1203
+ suffix += 1;
1204
+ }
1205
+ used.add(className);
1206
+ return {
1207
+ className,
1208
+ classes: combo.classes,
1209
+ occurrences: combo.occurrences
1210
+ };
1211
+ });
1212
+ }
1213
+ function generateComponentCss(options) {
1214
+ const components = assignComponentClassNames(options.combinations, {
1215
+ prefix: options.prefix
1216
+ });
1217
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1218
+ const classPrefix = normalizeClassPrefix(options.prefix);
1219
+ const header = [
1220
+ "/**",
1221
+ " * Generated by tailwind-unwind",
1222
+ ` * Source: ${options.sourcePath}`,
1223
+ ` * Generated at: ${timestamp}`,
1224
+ ` * Class prefix: ${classPrefix}`,
1225
+ " */",
1226
+ ""
1227
+ ].join("\n");
1228
+ if (components.length === 0) {
1229
+ return {
1230
+ css: `${header}
1231
+ /* No frequent class combinations found. Try lowering --min-occurrences. */
1232
+ `,
1233
+ components: []
1234
+ };
1235
+ }
1236
+ const rules = components.map((component) => {
1237
+ const applyClasses = component.classes.join(" ");
1238
+ return ` .${component.className} {
1239
+ @apply ${applyClasses};
1240
+ }`;
1241
+ }).join("\n\n");
1242
+ const css = `${header}
1243
+ @layer components {
1244
+ ${rules}
1245
+ }
1246
+ `;
1247
+ return { css, components };
1248
+ }
1249
+
1250
+ // src/core/buildComponents.ts
1251
+ function buildComponents(occurrences, options) {
1252
+ const combinations = findRepeatedClassSets(occurrences, {
1253
+ minOccurrences: options.minOccurrences,
1254
+ minSize: options.minSize,
1255
+ maxSize: options.maxSize,
1256
+ topLimit: options.topLimit
1257
+ });
1258
+ const { css, components } = generateComponentCss({
1259
+ sourcePath: options.sourcePath,
1260
+ combinations,
1261
+ prefix: options.prefix
1262
+ });
1263
+ const replacementMap = /* @__PURE__ */ new Map();
1264
+ for (const component of components) {
1265
+ const key = [...component.classes].sort().join(" ");
1266
+ replacementMap.set(key, component.className);
1267
+ }
1268
+ return { components, css, replacementMap };
1269
+ }
1270
+
1271
+ // src/commands/apply.ts
1272
+ import fs3 from "fs/promises";
1273
+ import path3 from "path";
1274
+ import chalk3 from "chalk";
1275
+ async function applyCommand(targetPath, options) {
1276
+ let scanResult;
1277
+ try {
1278
+ scanResult = await scanProject({ targetPath });
1279
+ } catch (error) {
1280
+ const message = error instanceof Error ? error.message : String(error);
1281
+ console.error(chalk3.red(`Error: ${message}`));
1282
+ process.exit(1);
1283
+ }
1284
+ for (const warning of scanResult.warnings) {
1285
+ console.warn(chalk3.yellow(`\u26A0 ${warning}`));
1286
+ }
1287
+ const { components, css, replacementMap } = buildComponents(
1288
+ scanResult.occurrences,
1289
+ {
1290
+ sourcePath: scanResult.resolvedPath,
1291
+ minOccurrences: options.minOccurrences ?? 3,
1292
+ minSize: options.minSize,
1293
+ maxSize: options.maxSize,
1294
+ topLimit: options.top,
1295
+ prefix: options.prefix
1296
+ }
1297
+ );
1298
+ if (components.length === 0) {
1299
+ console.error(
1300
+ chalk3.yellow(
1301
+ "No repeated className sets found. Try lowering --min-occurrences."
1302
+ )
1303
+ );
1304
+ process.exit(1);
1305
+ }
1306
+ const outputPath = path3.resolve(options.output);
1307
+ let filesModified = 0;
1308
+ let replacementsTotal = 0;
1309
+ const allReplacements = [];
1310
+ for (const file of scanResult.files) {
1311
+ const original = await fs3.readFile(file, "utf-8");
1312
+ const result = replaceClassNamesInSource(
1313
+ original,
1314
+ replacementMap,
1315
+ file
1316
+ );
1317
+ replacementsTotal += result.replacements.length;
1318
+ allReplacements.push(...result.replacements);
1319
+ if (result.changed) {
1320
+ filesModified += 1;
1321
+ if (!options.dryRun) {
1322
+ await fs3.writeFile(file, result.source, "utf-8");
1323
+ }
1324
+ }
1325
+ }
1326
+ if (!options.dryRun) {
1327
+ await fs3.mkdir(path3.dirname(outputPath), { recursive: true });
1328
+ await fs3.writeFile(outputPath, css, "utf-8");
1329
+ }
1330
+ console.log("");
1331
+ if (options.dryRun) {
1332
+ console.log(chalk3.bold.yellow("\u{1F50D} Dry run \u2014 no files were modified"));
1333
+ } else {
1334
+ console.log(chalk3.bold.green("\u2705 Classes applied successfully"));
1335
+ }
1336
+ console.log(chalk3.gray(` CSS output: `) + chalk3.white(outputPath));
1337
+ console.log(
1338
+ chalk3.gray(` Component classes: `) + chalk3.white(String(components.length))
1339
+ );
1340
+ console.log(
1341
+ chalk3.gray(` Files modified: `) + chalk3.white(String(filesModified))
1342
+ );
1343
+ console.log(
1344
+ chalk3.gray(` Replacements: `) + chalk3.white(String(replacementsTotal))
1345
+ );
1346
+ if (allReplacements.length > 0) {
1347
+ console.log("");
1348
+ console.log(chalk3.bold("Replacements:"));
1349
+ for (const item of allReplacements) {
1350
+ const line = item.line ? `:${item.line}` : "";
1351
+ console.log(
1352
+ chalk3.gray(` ${item.filePath}${line}`) + chalk3.white(` "${item.from}" `) + chalk3.cyan("\u2192") + chalk3.green(` "${item.to}"`)
1353
+ );
1354
+ }
1355
+ }
1356
+ console.log("");
1357
+ if (!options.dryRun) {
1358
+ console.log(
1359
+ chalk3.cyan(
1360
+ `Import ${path3.basename(outputPath)} in your global CSS if you haven't already.`
1361
+ )
1362
+ );
1363
+ console.log("");
1364
+ }
1365
+ return {
1366
+ filesModified,
1367
+ replacementsTotal,
1368
+ outputPath,
1369
+ componentsGenerated: components.length
1370
+ };
1371
+ }
1372
+
1373
+ // src/commands/generate.ts
1374
+ import fs4 from "fs/promises";
1375
+ import path4 from "path";
1376
+ import chalk4 from "chalk";
1377
+ async function generateCommand(targetPath, options) {
1378
+ let scanResult;
1379
+ try {
1380
+ scanResult = await scanProject({ targetPath });
1381
+ } catch (error) {
1382
+ const message = error instanceof Error ? error.message : String(error);
1383
+ console.error(chalk4.red(`Error: ${message}`));
1384
+ process.exit(1);
1385
+ }
1386
+ for (const warning of scanResult.warnings) {
1387
+ console.warn(chalk4.yellow(`\u26A0 ${warning}`));
1388
+ }
1389
+ const { css, components } = buildComponents(scanResult.occurrences, {
1390
+ sourcePath: scanResult.resolvedPath,
1391
+ minOccurrences: options.minOccurrences ?? 3,
1392
+ minSize: options.minSize,
1393
+ maxSize: options.maxSize,
1394
+ topLimit: options.top,
1395
+ prefix: options.prefix
1396
+ });
1397
+ const outputPath = path4.resolve(options.output);
1398
+ await fs4.mkdir(path4.dirname(outputPath), { recursive: true });
1399
+ await fs4.writeFile(outputPath, css, "utf-8");
1400
+ console.log("");
1401
+ console.log(chalk4.bold.green("\u2705 CSS generated successfully"));
1402
+ console.log(chalk4.gray(` Output: `) + chalk4.white(outputPath));
1403
+ console.log(
1404
+ chalk4.gray(` Components: `) + chalk4.white(String(components.length))
1405
+ );
1406
+ if (components.length > 0) {
1407
+ console.log("");
1408
+ console.log(chalk4.bold("Generated classes:"));
1409
+ for (const component of components) {
1410
+ console.log(
1411
+ chalk4.green(` .${component.className}`) + chalk4.gray(` \u2014 ${component.occurrences} occurrences, `) + chalk4.dim(component.classes.join(" "))
1412
+ );
1413
+ }
1414
+ console.log("");
1415
+ console.log(
1416
+ chalk4.cyan(
1417
+ "Run apply to replace className strings: npx tailwind-unwind apply <path> --output styles.css"
1418
+ )
1419
+ );
1420
+ } else {
1421
+ console.log(
1422
+ chalk4.yellow(
1423
+ "\nNo repeated className sets matched the filters. Try lowering --min-occurrences."
1424
+ )
1425
+ );
1426
+ }
1427
+ console.log("");
1428
+ return {
1429
+ outputPath,
1430
+ componentsGenerated: components.length,
1431
+ report: scanResult.report
1432
+ };
1433
+ }
1434
+
1435
+ export {
1436
+ suggestClassName,
1437
+ isStrictSubset,
1438
+ dedupeSubsetCombinations,
1439
+ normalizeClasses,
1440
+ splitClassString,
1441
+ generateCombinations,
1442
+ findFrequentPatterns,
1443
+ findRepeatedClassSets,
1444
+ calculatePotentialReduction,
1445
+ CLASS_MERGE_CALLEES,
1446
+ extractClassesFromExpression,
1447
+ isClassAttribute,
1448
+ extractFromJSXAttribute,
1449
+ parseSourceToAst,
1450
+ parseSource,
1451
+ parseFile,
1452
+ IGNORED_DIRECTORIES,
1453
+ IGNORE_PATTERNS,
1454
+ walkSourceFiles,
1455
+ scanProject,
1456
+ printConsoleReport,
1457
+ printJsonReport,
1458
+ analyzeCommand,
1459
+ replaceClassNamesInSource,
1460
+ DEFAULT_CLASS_PREFIX,
1461
+ normalizeClassPrefix,
1462
+ withClassPrefix,
1463
+ assignComponentClassNames,
1464
+ generateComponentCss,
1465
+ buildComponents,
1466
+ applyCommand,
1467
+ generateCommand
1468
+ };
1469
+ //# sourceMappingURL=chunk-N7HD4T2I.js.map