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.
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/bin/index.js +6 -0
- package/dist/chunk-N7HD4T2I.js +1469 -0
- package/dist/chunk-N7HD4T2I.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +63 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +265 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
|
@@ -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
|