tailwind-unwind 0.1.1 → 0.3.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.
@@ -1,3 +1,357 @@
1
+ // src/config/validate.ts
2
+ var CLASS_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
3
+ var KNOWN_ROOT_KEYS = /* @__PURE__ */ new Set([
4
+ "include",
5
+ "exclude",
6
+ "names",
7
+ "prefix",
8
+ "output",
9
+ "minOccurrences",
10
+ "minSize",
11
+ "maxSize",
12
+ "top",
13
+ "dedupeSubsets",
14
+ "dryRun",
15
+ "prettier",
16
+ "fromReport",
17
+ "extractableOnly",
18
+ "analyze",
19
+ "generate",
20
+ "apply"
21
+ ]);
22
+ var KNOWN_COMMAND_KEYS = /* @__PURE__ */ new Set([
23
+ "minOccurrences",
24
+ "minSize",
25
+ "maxSize",
26
+ "top",
27
+ "prefix",
28
+ "output",
29
+ "dedupeSubsets",
30
+ "dryRun",
31
+ "prettier",
32
+ "fromReport",
33
+ "extractableOnly"
34
+ ]);
35
+ function isRecord(value) {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+ function assertPositiveNumber(value, path7, errors) {
39
+ if (value === void 0) {
40
+ return;
41
+ }
42
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
43
+ errors.push(`${path7} must be a positive number`);
44
+ }
45
+ }
46
+ function assertBoolean(value, path7, errors) {
47
+ if (value === void 0) {
48
+ return;
49
+ }
50
+ if (typeof value !== "boolean") {
51
+ errors.push(`${path7} must be a boolean`);
52
+ }
53
+ }
54
+ function assertStringArray(value, path7, errors) {
55
+ if (value === void 0) {
56
+ return;
57
+ }
58
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && item.length > 0)) {
59
+ errors.push(`${path7} must be an array of non-empty strings`);
60
+ }
61
+ }
62
+ function validateCommandSection(value, section, errors) {
63
+ if (value === void 0) {
64
+ return;
65
+ }
66
+ if (!isRecord(value)) {
67
+ errors.push(`${section} must be an object`);
68
+ return;
69
+ }
70
+ for (const key of Object.keys(value)) {
71
+ if (!KNOWN_COMMAND_KEYS.has(key)) {
72
+ errors.push(`Unknown key "${key}" in ${section}`);
73
+ }
74
+ }
75
+ assertPositiveNumber(value.minOccurrences, `${section}.minOccurrences`, errors);
76
+ assertPositiveNumber(value.minSize, `${section}.minSize`, errors);
77
+ assertPositiveNumber(value.maxSize, `${section}.maxSize`, errors);
78
+ assertPositiveNumber(value.top, `${section}.top`, errors);
79
+ assertBoolean(value.dedupeSubsets, `${section}.dedupeSubsets`, errors);
80
+ assertBoolean(value.dryRun, `${section}.dryRun`, errors);
81
+ assertBoolean(value.prettier, `${section}.prettier`, errors);
82
+ assertBoolean(value.extractableOnly, `${section}.extractableOnly`, errors);
83
+ if (value.fromReport !== void 0 && (typeof value.fromReport !== "string" || value.fromReport.length === 0)) {
84
+ errors.push(`${section}.fromReport must be a non-empty string`);
85
+ }
86
+ if (value.prefix !== void 0 && (typeof value.prefix !== "string" || value.prefix.length === 0)) {
87
+ errors.push(`${section}.prefix must be a non-empty string`);
88
+ }
89
+ if (value.output !== void 0 && (typeof value.output !== "string" || value.output.length === 0)) {
90
+ errors.push(`${section}.output must be a non-empty string`);
91
+ }
92
+ }
93
+ function validateNames(value, errors) {
94
+ if (value === void 0) {
95
+ return;
96
+ }
97
+ if (!isRecord(value)) {
98
+ errors.push('names must be an object of "utility string": "class-name" pairs');
99
+ return;
100
+ }
101
+ for (const [utilities, name] of Object.entries(value)) {
102
+ if (typeof utilities !== "string" || utilities.trim().length === 0) {
103
+ errors.push("names keys must be non-empty utility strings");
104
+ continue;
105
+ }
106
+ if (typeof name !== "string" || !CLASS_NAME_PATTERN.test(name)) {
107
+ errors.push(
108
+ `names["${utilities}"] must be a valid class name (letters, numbers, hyphens)`
109
+ );
110
+ }
111
+ }
112
+ }
113
+ function validateConfigFile(raw, configPath) {
114
+ if (!isRecord(raw)) {
115
+ throw new Error(`Invalid config in ${configPath}: root value must be an object`);
116
+ }
117
+ const source = isRecord(raw.default) ? raw.default : raw;
118
+ const errors = [];
119
+ for (const key of Object.keys(source)) {
120
+ if (!KNOWN_ROOT_KEYS.has(key)) {
121
+ errors.push(`Unknown config key "${key}"`);
122
+ }
123
+ }
124
+ assertStringArray(source.include, "include", errors);
125
+ assertStringArray(source.exclude, "exclude", errors);
126
+ assertPositiveNumber(source.minOccurrences, "minOccurrences", errors);
127
+ assertPositiveNumber(source.minSize, "minSize", errors);
128
+ assertPositiveNumber(source.maxSize, "maxSize", errors);
129
+ assertPositiveNumber(source.top, "top", errors);
130
+ assertBoolean(source.dedupeSubsets, "dedupeSubsets", errors);
131
+ assertBoolean(source.dryRun, "dryRun", errors);
132
+ assertBoolean(source.prettier, "prettier", errors);
133
+ assertBoolean(source.extractableOnly, "extractableOnly", errors);
134
+ if (source.fromReport !== void 0 && (typeof source.fromReport !== "string" || source.fromReport.length === 0)) {
135
+ errors.push("fromReport must be a non-empty string");
136
+ }
137
+ validateNames(source.names, errors);
138
+ validateCommandSection(source.analyze, "analyze", errors);
139
+ validateCommandSection(source.generate, "generate", errors);
140
+ validateCommandSection(source.apply, "apply", errors);
141
+ if (source.prefix !== void 0 && (typeof source.prefix !== "string" || source.prefix.length === 0)) {
142
+ errors.push("prefix must be a non-empty string");
143
+ }
144
+ if (source.output !== void 0 && (typeof source.output !== "string" || source.output.length === 0)) {
145
+ errors.push("output must be a non-empty string");
146
+ }
147
+ if (errors.length > 0) {
148
+ throw new Error(
149
+ `Invalid config in ${configPath}:
150
+ ${errors.map((error) => ` - ${error}`).join("\n")}`
151
+ );
152
+ }
153
+ }
154
+ function normalizeNamesConfig(names) {
155
+ const map = /* @__PURE__ */ new Map();
156
+ if (!names) {
157
+ return map;
158
+ }
159
+ for (const [utilities, baseName] of Object.entries(names)) {
160
+ const tokens = utilities.trim().split(/\s+/).filter((token) => token.length > 0);
161
+ const key = [...tokens].sort().join(" ");
162
+ map.set(key, baseName);
163
+ }
164
+ return map;
165
+ }
166
+
167
+ // src/config/loadConfig.ts
168
+ import fs from "fs/promises";
169
+ import path from "path";
170
+ import { pathToFileURL } from "url";
171
+ var CONFIG_FILENAMES = [
172
+ "tailwind-unwind.config.js",
173
+ "tailwind-unwind.config.mjs",
174
+ "tailwind-unwind.config.cjs",
175
+ "tailwind-unwind.config.json",
176
+ ".tailwind-unwindrc",
177
+ ".tailwind-unwindrc.json"
178
+ ];
179
+ function isRecord2(value) {
180
+ return typeof value === "object" && value !== null && !Array.isArray(value);
181
+ }
182
+ function pickNumber(source, key) {
183
+ const value = source[key];
184
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
185
+ }
186
+ function pickCommandConfig(source) {
187
+ const config = {};
188
+ const minOccurrences = pickNumber(source, "minOccurrences");
189
+ if (minOccurrences !== void 0) config.minOccurrences = minOccurrences;
190
+ const minSize = pickNumber(source, "minSize");
191
+ if (minSize !== void 0) config.minSize = minSize;
192
+ const maxSize = pickNumber(source, "maxSize");
193
+ if (maxSize !== void 0) config.maxSize = maxSize;
194
+ const top = pickNumber(source, "top");
195
+ if (top !== void 0) config.top = top;
196
+ if (typeof source.prefix === "string" && source.prefix.length > 0) {
197
+ config.prefix = source.prefix;
198
+ }
199
+ if (typeof source.output === "string" && source.output.length > 0) {
200
+ config.output = source.output;
201
+ }
202
+ if (typeof source.dedupeSubsets === "boolean") {
203
+ config.dedupeSubsets = source.dedupeSubsets;
204
+ }
205
+ if (typeof source.dryRun === "boolean") {
206
+ config.dryRun = source.dryRun;
207
+ }
208
+ if (typeof source.prettier === "boolean") {
209
+ config.prettier = source.prettier;
210
+ }
211
+ if (typeof source.fromReport === "string" && source.fromReport.length > 0) {
212
+ config.fromReport = source.fromReport;
213
+ }
214
+ if (typeof source.extractableOnly === "boolean") {
215
+ config.extractableOnly = source.extractableOnly;
216
+ }
217
+ return config;
218
+ }
219
+ function pickNames(source) {
220
+ if (!isRecord2(source.names)) {
221
+ return void 0;
222
+ }
223
+ const names = {};
224
+ for (const [utilities, name] of Object.entries(source.names)) {
225
+ if (typeof utilities === "string" && typeof name === "string") {
226
+ names[utilities] = name;
227
+ }
228
+ }
229
+ return Object.keys(names).length > 0 ? names : void 0;
230
+ }
231
+ function normalizeLoadedConfig(raw) {
232
+ if (!isRecord2(raw)) {
233
+ return {};
234
+ }
235
+ const source = isRecord2(raw.default) ? raw.default : raw;
236
+ const config = {
237
+ ...pickCommandConfig(source)
238
+ };
239
+ const names = pickNames(source);
240
+ if (names) {
241
+ config.names = names;
242
+ }
243
+ if (Array.isArray(source.include)) {
244
+ config.include = source.include.filter(
245
+ (item) => typeof item === "string" && item.length > 0
246
+ );
247
+ }
248
+ if (Array.isArray(source.exclude)) {
249
+ config.exclude = source.exclude.filter(
250
+ (item) => typeof item === "string" && item.length > 0
251
+ );
252
+ }
253
+ if (isRecord2(source.analyze)) {
254
+ config.analyze = pickCommandConfig(source.analyze);
255
+ }
256
+ if (isRecord2(source.generate)) {
257
+ config.generate = pickCommandConfig(source.generate);
258
+ }
259
+ if (isRecord2(source.apply)) {
260
+ config.apply = pickCommandConfig(source.apply);
261
+ }
262
+ return config;
263
+ }
264
+ function mergeCommandConfig(command, fileConfig) {
265
+ const { analyze, generate: generate2, apply, ...root } = fileConfig;
266
+ const commandSection = command === "analyze" ? analyze : command === "generate" ? generate2 : apply;
267
+ return {
268
+ ...root,
269
+ ...commandSection
270
+ };
271
+ }
272
+ async function pathExists(targetPath) {
273
+ try {
274
+ await fs.access(targetPath);
275
+ return true;
276
+ } catch {
277
+ return false;
278
+ }
279
+ }
280
+ async function collectSearchRoots(cwd, targetPath) {
281
+ const roots = [path.resolve(cwd)];
282
+ if (!targetPath) {
283
+ return roots;
284
+ }
285
+ let current = path.resolve(targetPath);
286
+ try {
287
+ const stat = await fs.stat(current);
288
+ if (stat.isFile()) {
289
+ current = path.dirname(current);
290
+ }
291
+ } catch {
292
+ return roots;
293
+ }
294
+ while (true) {
295
+ const resolved = path.resolve(current);
296
+ if (!roots.includes(resolved)) {
297
+ roots.push(resolved);
298
+ }
299
+ const parent = path.dirname(resolved);
300
+ if (parent === resolved) {
301
+ break;
302
+ }
303
+ current = parent;
304
+ }
305
+ return roots;
306
+ }
307
+ async function resolveConfigFile(explicitPath, searchRoots) {
308
+ if (explicitPath) {
309
+ const resolved = path.resolve(explicitPath);
310
+ if (!await pathExists(resolved)) {
311
+ throw new Error(`Config file not found: ${resolved}`);
312
+ }
313
+ return resolved;
314
+ }
315
+ for (const root of searchRoots) {
316
+ for (const filename of CONFIG_FILENAMES) {
317
+ const candidate = path.join(root, filename);
318
+ if (await pathExists(candidate)) {
319
+ return candidate;
320
+ }
321
+ }
322
+ }
323
+ return null;
324
+ }
325
+ async function importConfigModule(configPath) {
326
+ const moduleUrl = pathToFileURL(configPath).href;
327
+ const imported = await import(moduleUrl);
328
+ return imported;
329
+ }
330
+ async function readJsonConfig(configPath) {
331
+ const raw = await fs.readFile(configPath, "utf-8");
332
+ return JSON.parse(raw);
333
+ }
334
+ async function loadCommandOptions(command, cliOptions, options = {}) {
335
+ const cwd = options.cwd ?? process.cwd();
336
+ const searchRoots = await collectSearchRoots(cwd, options.targetPath);
337
+ const configPath = await resolveConfigFile(cliOptions.configPath, searchRoots);
338
+ if (!configPath) {
339
+ return cliOptions;
340
+ }
341
+ const isJson = configPath.endsWith(".json") || configPath.endsWith(".tailwind-unwindrc");
342
+ const loaded = isJson ? await readJsonConfig(configPath) : await importConfigModule(configPath);
343
+ validateConfigFile(loaded, configPath);
344
+ const fileConfig = mergeCommandConfig(command, normalizeLoadedConfig(loaded));
345
+ const { configPath: _ignored, ...cliOverrides } = cliOptions;
346
+ return {
347
+ ...fileConfig,
348
+ ...Object.fromEntries(
349
+ Object.entries(cliOverrides).filter(([, value]) => value !== void 0)
350
+ ),
351
+ configPath
352
+ };
353
+ }
354
+
1
355
  // src/analyzer/suggestions.ts
2
356
  var BREAKPOINT_PREFIX = /^(sm|md|lg|xl|2xl):/;
3
357
  function baseClass(cls) {
@@ -607,6 +961,113 @@ function calculatePotentialReduction(occurrences, topCombinations) {
607
961
  return Math.min(100, Math.round(savable / totalClassUsages * 100));
608
962
  }
609
963
 
964
+ // src/parser/variantHelpers.ts
965
+ import babelTraverse from "@babel/traverse";
966
+ var VARIANT_CALLEES = /* @__PURE__ */ new Set(["tv", "cva"]);
967
+ function isVariantCallee(expression) {
968
+ if (expression.type === "Identifier") {
969
+ return VARIANT_CALLEES.has(expression.name);
970
+ }
971
+ if (expression.type === "MemberExpression" && expression.property.type === "Identifier") {
972
+ return VARIANT_CALLEES.has(expression.property.name);
973
+ }
974
+ return false;
975
+ }
976
+ function collectStringsFromObject(node) {
977
+ const classes = [];
978
+ for (const prop of node.properties) {
979
+ if (prop.type === "SpreadElement") {
980
+ continue;
981
+ }
982
+ if (prop.type !== "ObjectProperty") {
983
+ continue;
984
+ }
985
+ collectStringsFromPropertyValue(prop, classes);
986
+ }
987
+ return classes;
988
+ }
989
+ function collectStringsFromPropertyValue(prop, classes) {
990
+ const keyName = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "StringLiteral" ? prop.key.value : null;
991
+ const { value } = prop;
992
+ if (value.type === "StringLiteral") {
993
+ classes.push(...splitClassString(value.value));
994
+ return;
995
+ }
996
+ if (value.type === "ObjectExpression") {
997
+ if (keyName === "variants" || keyName === "compoundVariants") {
998
+ for (const nested of value.properties) {
999
+ if (nested.type === "ObjectProperty") {
1000
+ collectStringsFromPropertyValue(nested, classes);
1001
+ }
1002
+ }
1003
+ return;
1004
+ }
1005
+ classes.push(...collectStringsFromObject(value));
1006
+ return;
1007
+ }
1008
+ if (value.type === "ArrayExpression") {
1009
+ for (const element of value.elements) {
1010
+ if (element === null || element.type === "SpreadElement") {
1011
+ continue;
1012
+ }
1013
+ if (element.type === "StringLiteral") {
1014
+ classes.push(...splitClassString(element.value));
1015
+ } else if (element.type === "ObjectExpression") {
1016
+ classes.push(...collectStringsFromObject(element));
1017
+ }
1018
+ }
1019
+ }
1020
+ }
1021
+ function extractClassesFromVariantCall(call) {
1022
+ const classes = [];
1023
+ for (const arg of call.arguments) {
1024
+ if (arg.type === "SpreadElement" || arg.type === "ArgumentPlaceholder") {
1025
+ continue;
1026
+ }
1027
+ if (arg.type === "StringLiteral") {
1028
+ classes.push(...splitClassString(arg.value));
1029
+ continue;
1030
+ }
1031
+ if (arg.type === "ObjectExpression") {
1032
+ classes.push(...collectStringsFromObject(arg));
1033
+ }
1034
+ }
1035
+ return [...new Set(classes)];
1036
+ }
1037
+ function resolveTraverse(module) {
1038
+ if (typeof module === "function") {
1039
+ return module;
1040
+ }
1041
+ const withDefault = module;
1042
+ if (typeof withDefault.default === "function") {
1043
+ return withDefault.default;
1044
+ }
1045
+ throw new Error("Failed to load @babel/traverse");
1046
+ }
1047
+ var traverse = resolveTraverse(babelTraverse);
1048
+ function collectVariantRegistry(ast) {
1049
+ const registry = /* @__PURE__ */ new Map();
1050
+ traverse(ast, {
1051
+ VariableDeclarator(path7) {
1052
+ registerVariantDeclarator(path7.node, registry);
1053
+ }
1054
+ });
1055
+ return registry;
1056
+ }
1057
+ function registerVariantDeclarator(declarator, registry) {
1058
+ if (declarator.id.type !== "Identifier" || declarator.init?.type !== "CallExpression") {
1059
+ return;
1060
+ }
1061
+ const { init } = declarator;
1062
+ if (init.callee.type === "V8IntrinsicIdentifier" || !isVariantCallee(init.callee)) {
1063
+ return;
1064
+ }
1065
+ const classes = extractClassesFromVariantCall(init);
1066
+ if (classes.length > 0) {
1067
+ registry.set(declarator.id.name, classes);
1068
+ }
1069
+ }
1070
+
610
1071
  // src/parser/classHelpers.ts
611
1072
  var CLASS_MERGE_CALLEES = /* @__PURE__ */ new Set([
612
1073
  "cn",
@@ -649,18 +1110,34 @@ function isClassMergeCallee(expression) {
649
1110
  }
650
1111
  return false;
651
1112
  }
652
- function extractFromCallArguments(args) {
1113
+ function extractFromCallArguments(args, registry) {
653
1114
  const parts = [];
654
1115
  for (const arg of args) {
655
1116
  if (arg.type === "SpreadElement" || arg.type === "ArgumentPlaceholder") {
656
1117
  parts.push({ classes: [], isDynamic: true });
657
1118
  continue;
658
1119
  }
659
- parts.push(extractClassesFromExpression(arg));
1120
+ parts.push(extractClassesFromExpression(arg, registry));
660
1121
  }
661
1122
  return mergeExtractions(parts);
662
1123
  }
663
- function extractClassesFromExpression(expression) {
1124
+ function extractFromVariantCall(call, registry) {
1125
+ const { callee } = call;
1126
+ if (callee.type !== "V8IntrinsicIdentifier" && isVariantCallee(callee)) {
1127
+ const classes = extractClassesFromVariantCall(call);
1128
+ const hasDynamicArgs = call.arguments.some(
1129
+ (arg) => arg.type === "SpreadElement" || arg.type === "ArgumentPlaceholder"
1130
+ );
1131
+ return { classes, isDynamic: hasDynamicArgs };
1132
+ }
1133
+ if (callee.type === "Identifier" && registry?.has(callee.name)) {
1134
+ const classes = registry.get(callee.name) ?? [];
1135
+ const hasArgs = call.arguments.length > 0;
1136
+ return { classes, isDynamic: hasArgs };
1137
+ }
1138
+ return { classes: [], isDynamic: true };
1139
+ }
1140
+ function extractClassesFromExpression(expression, registry) {
664
1141
  switch (expression.type) {
665
1142
  case "StringLiteral":
666
1143
  return extractFromStringLiteral(expression.value);
@@ -669,40 +1146,44 @@ function extractClassesFromExpression(expression) {
669
1146
  case "CallExpression": {
670
1147
  const { callee } = expression;
671
1148
  if (callee.type !== "V8IntrinsicIdentifier" && isClassMergeCallee(callee)) {
672
- return extractFromCallArguments(expression.arguments);
1149
+ return extractFromCallArguments(expression.arguments, registry);
1150
+ }
1151
+ const variantExtraction = extractFromVariantCall(expression, registry);
1152
+ if (variantExtraction.classes.length > 0 || !variantExtraction.isDynamic) {
1153
+ return variantExtraction;
673
1154
  }
674
1155
  return { classes: [], isDynamic: true };
675
1156
  }
676
1157
  case "ConditionalExpression": {
677
1158
  const merged = mergeExtractions([
678
- extractClassesFromExpression(expression.consequent),
679
- extractClassesFromExpression(expression.alternate)
1159
+ extractClassesFromExpression(expression.consequent, registry),
1160
+ extractClassesFromExpression(expression.alternate, registry)
680
1161
  ]);
681
1162
  return { ...merged, isDynamic: true };
682
1163
  }
683
1164
  case "LogicalExpression": {
684
1165
  const merged = mergeExtractions([
685
- extractClassesFromExpression(expression.left),
686
- extractClassesFromExpression(expression.right)
1166
+ extractClassesFromExpression(expression.left, registry),
1167
+ extractClassesFromExpression(expression.right, registry)
687
1168
  ]);
688
1169
  return { ...merged, isDynamic: true };
689
1170
  }
690
1171
  case "ArrayExpression":
691
- return extractFromArrayExpression(expression);
1172
+ return extractFromArrayExpression(expression, registry);
692
1173
  case "ObjectExpression":
693
1174
  return extractFromObjectExpression(expression);
694
1175
  default:
695
1176
  return { classes: [], isDynamic: true };
696
1177
  }
697
1178
  }
698
- function extractFromArrayExpression(node) {
1179
+ function extractFromArrayExpression(node, registry) {
699
1180
  const parts = [];
700
1181
  for (const element of node.elements) {
701
1182
  if (element === null || element.type === "SpreadElement") {
702
1183
  parts.push({ classes: [], isDynamic: true });
703
1184
  continue;
704
1185
  }
705
- parts.push(extractClassesFromExpression(element));
1186
+ parts.push(extractClassesFromExpression(element, registry));
706
1187
  }
707
1188
  return mergeExtractions(parts);
708
1189
  }
@@ -757,7 +1238,7 @@ function isClassAttribute(attr) {
757
1238
  const name = getAttributeName(attr);
758
1239
  return name !== null && CLASS_ATTRIBUTES.has(name);
759
1240
  }
760
- function extractFromJSXAttribute(attr) {
1241
+ function extractFromJSXAttribute(attr, registry) {
761
1242
  if (!isClassAttribute(attr)) {
762
1243
  return null;
763
1244
  }
@@ -767,7 +1248,7 @@ function extractFromJSXAttribute(attr) {
767
1248
  return { classes: [], isDynamic: true, line };
768
1249
  }
769
1250
  if (value.type === "StringLiteral") {
770
- const result = extractClassesFromExpression(value);
1251
+ const result = extractClassesFromExpression(value, registry);
771
1252
  return { classes: result.classes, isDynamic: result.isDynamic, line };
772
1253
  }
773
1254
  if (value.type === "JSXExpressionContainer") {
@@ -775,7 +1256,7 @@ function extractFromJSXAttribute(attr) {
775
1256
  if (expr.type === "JSXEmptyExpression") {
776
1257
  return { classes: [], isDynamic: true, line };
777
1258
  }
778
- const result = extractClassesFromExpression(expr);
1259
+ const result = extractClassesFromExpression(expr, registry);
779
1260
  return { classes: result.classes, isDynamic: result.isDynamic, line };
780
1261
  }
781
1262
  return { classes: [], isDynamic: true, line };
@@ -792,9 +1273,9 @@ function parseSourceToAst(source) {
792
1273
  }
793
1274
 
794
1275
  // src/parser/jsxParser.ts
795
- import babelTraverse from "@babel/traverse";
796
- import fs from "fs/promises";
797
- function resolveTraverse(module) {
1276
+ import babelTraverse2 from "@babel/traverse";
1277
+ import fs2 from "fs/promises";
1278
+ function resolveTraverse2(module) {
798
1279
  if (typeof module === "function") {
799
1280
  return module;
800
1281
  }
@@ -804,9 +1285,9 @@ function resolveTraverse(module) {
804
1285
  }
805
1286
  throw new Error("Failed to load @babel/traverse");
806
1287
  }
807
- var traverse = resolveTraverse(babelTraverse);
808
- function isJSXElementWithClassAttribute(path5) {
809
- const opening = path5.node.openingElement;
1288
+ var traverse2 = resolveTraverse2(babelTraverse2);
1289
+ function isJSXElementWithClassAttribute(path7) {
1290
+ const opening = path7.node.openingElement;
810
1291
  return opening.attributes.some(
811
1292
  (attr) => attr.type === "JSXAttribute" && isClassAttribute(attr)
812
1293
  );
@@ -814,15 +1295,16 @@ function isJSXElementWithClassAttribute(path5) {
814
1295
  function collectExtractionsFromAst(ast, filePath) {
815
1296
  const extractions = [];
816
1297
  const warnings = [];
817
- traverse(ast, {
818
- JSXElement(path5) {
819
- if (!isJSXElementWithClassAttribute(path5)) {
1298
+ const variantRegistry = collectVariantRegistry(ast);
1299
+ traverse2(ast, {
1300
+ JSXElement(path7) {
1301
+ if (!isJSXElementWithClassAttribute(path7)) {
820
1302
  return;
821
1303
  }
822
- const opening = path5.node.openingElement;
1304
+ const opening = path7.node.openingElement;
823
1305
  for (const attr of opening.attributes) {
824
1306
  if (attr.type !== "JSXAttribute") continue;
825
- const extraction = extractFromJSXAttribute(attr);
1307
+ const extraction = extractFromJSXAttribute(attr, variantRegistry);
826
1308
  if (!extraction) continue;
827
1309
  if (extraction.isDynamic && extraction.classes.length === 0) {
828
1310
  const lineInfo = extraction.line ? `:${extraction.line}` : "";
@@ -854,7 +1336,7 @@ function parseSource(source, filePath = "unknown") {
854
1336
  return { filePath, extractions, warnings };
855
1337
  }
856
1338
  async function parseFile(filePath) {
857
- const source = await fs.readFile(filePath, "utf-8");
1339
+ const source = await fs2.readFile(filePath, "utf-8");
858
1340
  return parseSource(source, filePath);
859
1341
  }
860
1342
 
@@ -872,40 +1354,67 @@ var IGNORE_PATTERNS = IGNORED_DIRECTORIES.map(
872
1354
 
873
1355
  // src/scanner/fileWalker.ts
874
1356
  import fg from "fast-glob";
875
- import path from "path";
1357
+ import path2 from "path";
876
1358
  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, "/")
1359
+ function toAbsolutePattern(basePath, pattern) {
1360
+ const normalized = pattern.replace(/\\/g, "/");
1361
+ if (path2.isAbsolute(normalized)) {
1362
+ return normalized;
1363
+ }
1364
+ return path2.join(basePath, normalized).replace(/\\/g, "/");
1365
+ }
1366
+ function buildIncludePatterns(basePath, include) {
1367
+ if (include && include.length > 0) {
1368
+ return include.map((pattern) => toAbsolutePattern(basePath, pattern));
1369
+ }
1370
+ return SOURCE_EXTENSIONS.map(
1371
+ (ext) => path2.join(basePath, `**/*.${ext}`).replace(/\\/g, "/")
881
1372
  );
1373
+ }
1374
+ function buildIgnorePatterns(exclude) {
1375
+ const userExcludes = (exclude ?? []).map((pattern) => {
1376
+ const normalized = pattern.replace(/\\/g, "/");
1377
+ if (normalized.startsWith("**")) {
1378
+ return normalized;
1379
+ }
1380
+ return `**/${normalized}`;
1381
+ });
1382
+ return [...IGNORE_PATTERNS, ...userExcludes];
1383
+ }
1384
+ async function walkSourceFiles(targetPath, options = {}) {
1385
+ const absolutePath = path2.resolve(targetPath);
1386
+ const patterns = buildIncludePatterns(absolutePath, options.include);
1387
+ const ignore = buildIgnorePatterns(options.exclude);
882
1388
  const files = await fg(patterns, {
883
1389
  absolute: true,
884
1390
  onlyFiles: true,
885
1391
  unique: true,
886
- ignore: IGNORE_PATTERNS,
1392
+ ignore,
887
1393
  dot: false
888
1394
  });
889
1395
  return files.sort();
890
1396
  }
891
1397
 
892
1398
  // src/core/scanProject.ts
893
- import fs2 from "fs/promises";
894
- import path2 from "path";
895
- async function pathExists(targetPath) {
1399
+ import fs3 from "fs/promises";
1400
+ import path3 from "path";
1401
+ async function pathExists2(targetPath) {
896
1402
  try {
897
- await fs2.access(targetPath);
1403
+ await fs3.access(targetPath);
898
1404
  return true;
899
1405
  } catch {
900
1406
  return false;
901
1407
  }
902
1408
  }
903
1409
  async function scanProject(options) {
904
- const resolvedPath = path2.resolve(options.targetPath);
905
- if (!await pathExists(resolvedPath)) {
1410
+ const resolvedPath = path3.resolve(options.targetPath);
1411
+ if (!await pathExists2(resolvedPath)) {
906
1412
  throw new Error(`Path does not exist: ${resolvedPath}`);
907
1413
  }
908
- const files = await walkSourceFiles(resolvedPath);
1414
+ const files = await walkSourceFiles(resolvedPath, {
1415
+ include: options.include,
1416
+ exclude: options.exclude
1417
+ });
909
1418
  if (files.length === 0) {
910
1419
  throw new Error(
911
1420
  `No source files (.tsx, .jsx, .ts, .js) found in: ${resolvedPath}`
@@ -931,13 +1440,26 @@ async function scanProject(options) {
931
1440
  (occurrence) => normalizeClasses([...new Set(occurrence.classes)])
932
1441
  )
933
1442
  );
934
- const topCombinations = findFrequentPatterns(occurrences, {
1443
+ const frequentCombinations = findFrequentPatterns(occurrences, {
935
1444
  minOccurrences: options.minOccurrences,
936
1445
  minSize: options.minSize,
937
1446
  maxSize: options.maxSize,
938
1447
  topLimit: options.topLimit,
939
1448
  dedupeSubsets: options.dedupeSubsets
940
1449
  });
1450
+ const extractableSets = findRepeatedClassSets(occurrences, {
1451
+ minOccurrences: options.extractableMinOccurrences ?? 3,
1452
+ minSize: options.minSize,
1453
+ maxSize: options.maxSize,
1454
+ topLimit: Number.POSITIVE_INFINITY
1455
+ });
1456
+ const extractableKeys = new Set(
1457
+ extractableSets.map((combo) => combo.normalized)
1458
+ );
1459
+ const topCombinations = frequentCombinations.map((combo) => ({
1460
+ ...combo,
1461
+ extractable: extractableKeys.has(combo.normalized)
1462
+ }));
941
1463
  const potentialReductionPercent = calculatePotentialReduction(
942
1464
  occurrences,
943
1465
  topCombinations
@@ -1015,6 +1537,15 @@ function printConsoleReport(report, options = {}) {
1015
1537
  console.log(
1016
1538
  chalk.gray(` Suggestion: `) + chalk.green(combo.suggestion)
1017
1539
  );
1540
+ if (combo.extractable) {
1541
+ console.log(
1542
+ chalk.gray(` Extractable: `) + chalk.green("yes \u2014 use generate/apply")
1543
+ );
1544
+ } else {
1545
+ console.log(
1546
+ chalk.gray(` Extractable: `) + chalk.yellow("subset only \u2014 analyze hint")
1547
+ );
1548
+ }
1018
1549
  console.log(
1019
1550
  chalk.gray(` Found in: `) + chalk.dim(formatLocations(combo.locations))
1020
1551
  );
@@ -1026,6 +1557,16 @@ function printConsoleReport(report, options = {}) {
1026
1557
  `\u{1F4A1} Potential code reduction: ${stats.potentialReductionPercent}%`
1027
1558
  )
1028
1559
  );
1560
+ const extractableCount = stats.topCombinations.filter(
1561
+ (combo) => combo.extractable
1562
+ ).length;
1563
+ if (extractableCount > 0) {
1564
+ console.log(
1565
+ chalk.magenta(
1566
+ `\u{1F4A1} ${extractableCount} pattern(s) ready for generate/apply`
1567
+ )
1568
+ );
1569
+ }
1029
1570
  console.log(
1030
1571
  chalk.magenta(
1031
1572
  "\u{1F4A1} Generate CSS: npx tailwind-unwind generate <path> --output styles.css"
@@ -1055,7 +1596,10 @@ async function analyzeCommand(targetPath, options = {}) {
1055
1596
  minSize: options.minSize,
1056
1597
  maxSize: options.maxSize,
1057
1598
  topLimit: options.top,
1058
- dedupeSubsets: options.dedupeSubsets
1599
+ dedupeSubsets: options.dedupeSubsets,
1600
+ include: options.include,
1601
+ exclude: options.exclude,
1602
+ extractableMinOccurrences: 3
1059
1603
  });
1060
1604
  } catch (error) {
1061
1605
  const message = error instanceof Error ? error.message : String(error);
@@ -1076,11 +1620,66 @@ async function analyzeCommand(targetPath, options = {}) {
1076
1620
  return report;
1077
1621
  }
1078
1622
 
1623
+ // src/codemod/formatSource.ts
1624
+ import { createRequire } from "module";
1625
+ import path4 from "path";
1626
+ var require2 = createRequire(import.meta.url);
1627
+ async function loadPrettier() {
1628
+ try {
1629
+ const prettier = await import("prettier");
1630
+ return prettier;
1631
+ } catch {
1632
+ try {
1633
+ return require2("prettier");
1634
+ } catch {
1635
+ return null;
1636
+ }
1637
+ }
1638
+ }
1639
+ async function formatSource(source, options) {
1640
+ const prettier = await loadPrettier();
1641
+ if (!prettier) {
1642
+ return { source, formatted: false };
1643
+ }
1644
+ try {
1645
+ const config = await prettier.resolveConfig(options.filePath);
1646
+ const formatted = await prettier.format(source, {
1647
+ ...config,
1648
+ filepath: options.filePath
1649
+ });
1650
+ return { source: formatted, formatted: true };
1651
+ } catch {
1652
+ return { source, formatted: false };
1653
+ }
1654
+ }
1655
+ async function formatModifiedFiles(files, sources, cwd = process.cwd()) {
1656
+ const formatted = [];
1657
+ const skipped = [];
1658
+ for (const file of files) {
1659
+ const source = sources.get(file);
1660
+ if (!source) {
1661
+ skipped.push(file);
1662
+ continue;
1663
+ }
1664
+ const result = await formatSource(source, {
1665
+ filePath: path4.resolve(cwd, file),
1666
+ cwd
1667
+ });
1668
+ if (result.formatted) {
1669
+ sources.set(file, result.source);
1670
+ formatted.push(file);
1671
+ } else {
1672
+ skipped.push(file);
1673
+ }
1674
+ }
1675
+ return { formatted, skipped };
1676
+ }
1677
+
1079
1678
  // src/codemod/replaceClassNames.ts
1080
1679
  import babelGenerate from "@babel/generator";
1081
- import babelTraverse2 from "@babel/traverse";
1680
+ import babelTraverse3 from "@babel/traverse";
1082
1681
  import * as t from "@babel/types";
1083
- function resolveTraverse2(module) {
1682
+ function resolveTraverse3(module) {
1084
1683
  if (typeof module === "function") {
1085
1684
  return module;
1086
1685
  }
@@ -1100,8 +1699,20 @@ function resolveGenerator(module) {
1100
1699
  }
1101
1700
  throw new Error("Failed to load @babel/generator");
1102
1701
  }
1103
- var traverse2 = resolveTraverse2(babelTraverse2);
1702
+ var traverse3 = resolveTraverse3(babelTraverse3);
1104
1703
  var generate = resolveGenerator(babelGenerate);
1704
+ function isClassMergeCallee2(expression) {
1705
+ if (expression.type === "Identifier") {
1706
+ return CLASS_MERGE_CALLEES.has(expression.name);
1707
+ }
1708
+ if (expression.type === "MemberExpression" && expression.property.type === "Identifier") {
1709
+ return CLASS_MERGE_CALLEES.has(expression.property.name);
1710
+ }
1711
+ return false;
1712
+ }
1713
+ function isClassMergeCall(expression) {
1714
+ return expression.type === "CallExpression" && expression.callee.type !== "V8IntrinsicIdentifier" && isClassMergeCallee2(expression.callee);
1715
+ }
1105
1716
  function lookupReplacement(extraction, replacementMap) {
1106
1717
  if (extraction.isDynamic || extraction.classes.length === 0) {
1107
1718
  return null;
@@ -1112,6 +1723,126 @@ function lookupReplacement(extraction, replacementMap) {
1112
1723
  function setStringClassAttribute(attr, className) {
1113
1724
  attr.value = t.stringLiteral(className);
1114
1725
  }
1726
+ function tryReplaceMergeCall(call, replacementMap) {
1727
+ const staticClasses = [];
1728
+ const dynamicArgs = [];
1729
+ for (const arg of call.arguments) {
1730
+ if (arg.type === "SpreadElement" || arg.type === "ArgumentPlaceholder") {
1731
+ dynamicArgs.push(arg);
1732
+ continue;
1733
+ }
1734
+ const extracted = extractClassesFromExpression(arg);
1735
+ if (extracted.isDynamic) {
1736
+ dynamicArgs.push(arg);
1737
+ continue;
1738
+ }
1739
+ staticClasses.push(...extracted.classes);
1740
+ }
1741
+ const combinedKey = normalizeClasses([...new Set(staticClasses)]);
1742
+ const combinedReplacement = replacementMap.get(combinedKey);
1743
+ if (combinedReplacement && staticClasses.length > 0) {
1744
+ if (dynamicArgs.length === 0) {
1745
+ return {
1746
+ expression: t.stringLiteral(combinedReplacement),
1747
+ from: combinedKey,
1748
+ to: combinedReplacement,
1749
+ partial: false
1750
+ };
1751
+ }
1752
+ return {
1753
+ expression: t.callExpression(call.callee, [
1754
+ t.stringLiteral(combinedReplacement),
1755
+ ...dynamicArgs
1756
+ ]),
1757
+ from: combinedKey,
1758
+ to: combinedReplacement,
1759
+ partial: true
1760
+ };
1761
+ }
1762
+ let replaced = false;
1763
+ let replacedTo = "";
1764
+ let replacedFrom = "";
1765
+ const newArgs = [...call.arguments];
1766
+ for (let index = 0; index < call.arguments.length; index += 1) {
1767
+ const arg = call.arguments[index];
1768
+ if (arg === void 0 || arg.type === "SpreadElement" || arg.type === "ArgumentPlaceholder") {
1769
+ continue;
1770
+ }
1771
+ const extracted = extractClassesFromExpression(arg);
1772
+ if (extracted.isDynamic || extracted.classes.length === 0) {
1773
+ continue;
1774
+ }
1775
+ const argKey = normalizeClasses(extracted.classes);
1776
+ const replacement = replacementMap.get(argKey);
1777
+ if (!replacement) {
1778
+ continue;
1779
+ }
1780
+ newArgs[index] = t.stringLiteral(replacement);
1781
+ if (!replaced) {
1782
+ replacedFrom = argKey;
1783
+ replacedTo = replacement;
1784
+ }
1785
+ replaced = true;
1786
+ }
1787
+ if (!replaced) {
1788
+ return null;
1789
+ }
1790
+ return {
1791
+ expression: t.callExpression(call.callee, newArgs),
1792
+ from: replacedFrom,
1793
+ to: replacedTo,
1794
+ partial: true
1795
+ };
1796
+ }
1797
+ function tryReplaceTemplateLiteral(attr, replacementMap, registry) {
1798
+ const value = attr.value;
1799
+ if (value?.type !== "JSXExpressionContainer") {
1800
+ return null;
1801
+ }
1802
+ const expression = value.expression;
1803
+ if (expression.type !== "TemplateLiteral" || expression.expressions.length === 0) {
1804
+ return null;
1805
+ }
1806
+ const extracted = extractClassesFromExpression(expression, registry);
1807
+ if (extracted.classes.length === 0) {
1808
+ return null;
1809
+ }
1810
+ const key = normalizeClasses(extracted.classes);
1811
+ const replacement = replacementMap.get(key);
1812
+ if (!replacement) {
1813
+ return null;
1814
+ }
1815
+ const newQuasis = expression.quasis.map((quasi, index) => {
1816
+ if (index !== 0) {
1817
+ return quasi;
1818
+ }
1819
+ const prefix = expression.expressions.length > 0 ? `${replacement} ` : replacement;
1820
+ return t.templateElement(
1821
+ { raw: prefix, cooked: prefix },
1822
+ quasi.tail
1823
+ );
1824
+ });
1825
+ return {
1826
+ expression: t.templateLiteral(newQuasis, [...expression.expressions]),
1827
+ from: key,
1828
+ to: replacement,
1829
+ partial: true
1830
+ };
1831
+ }
1832
+ function tryReplaceClassAttribute(attr, replacementMap, registry) {
1833
+ const value = attr.value;
1834
+ if (value?.type !== "JSXExpressionContainer") {
1835
+ return null;
1836
+ }
1837
+ const expression = value.expression;
1838
+ if (expression.type === "JSXEmptyExpression") {
1839
+ return null;
1840
+ }
1841
+ if (isClassMergeCall(expression)) {
1842
+ return tryReplaceMergeCall(expression, replacementMap);
1843
+ }
1844
+ return tryReplaceTemplateLiteral(attr, replacementMap, registry);
1845
+ }
1115
1846
  function replaceClassNamesInSource(source, replacementMap, filePath) {
1116
1847
  const replacements = [];
1117
1848
  const skipped = [];
@@ -1125,15 +1856,36 @@ function replaceClassNamesInSource(source, replacementMap, filePath) {
1125
1856
  const message = error instanceof Error ? error.message : String(error);
1126
1857
  throw new Error(`Failed to parse ${filePath}: ${message}`);
1127
1858
  }
1128
- traverse2(ast, {
1129
- JSXElement(path5) {
1130
- const opening = path5.node.openingElement;
1859
+ const variantRegistry = collectVariantRegistry(ast);
1860
+ traverse3(ast, {
1861
+ JSXElement(path7) {
1862
+ const opening = path7.node.openingElement;
1131
1863
  for (const attr of opening.attributes) {
1132
1864
  if (attr.type !== "JSXAttribute" || !isClassAttribute(attr)) {
1133
1865
  continue;
1134
1866
  }
1135
- const extraction = extractFromJSXAttribute(attr);
1867
+ const extraction = extractFromJSXAttribute(attr, variantRegistry);
1136
1868
  if (!extraction) continue;
1869
+ const mergeReplacement = tryReplaceClassAttribute(
1870
+ attr,
1871
+ replacementMap,
1872
+ variantRegistry
1873
+ );
1874
+ if (mergeReplacement) {
1875
+ if (mergeReplacement.expression.type === "StringLiteral") {
1876
+ setStringClassAttribute(attr, mergeReplacement.expression.value);
1877
+ } else {
1878
+ attr.value = t.jsxExpressionContainer(mergeReplacement.expression);
1879
+ }
1880
+ replacements.push({
1881
+ filePath,
1882
+ line: extraction.line,
1883
+ from: mergeReplacement.from,
1884
+ to: mergeReplacement.to,
1885
+ partial: mergeReplacement.partial
1886
+ });
1887
+ continue;
1888
+ }
1137
1889
  const replacement = lookupReplacement(extraction, replacementMap);
1138
1890
  if (!replacement) {
1139
1891
  if (extraction.classes.length > 0) {
@@ -1192,10 +1944,15 @@ function withClassPrefix(baseName, prefix) {
1192
1944
  }
1193
1945
 
1194
1946
  // src/generator/cssGenerator.ts
1947
+ function resolveBaseClassName(classes, customNames) {
1948
+ const key = normalizeClasses(classes);
1949
+ return customNames.get(key) ?? suggestClassName(classes);
1950
+ }
1195
1951
  function assignComponentClassNames(combinations, options = {}) {
1196
1952
  const used = /* @__PURE__ */ new Set();
1953
+ const customNames = normalizeNamesConfig(options.names);
1197
1954
  return combinations.map((combo) => {
1198
- const base = suggestClassName(combo.classes);
1955
+ const base = resolveBaseClassName(combo.classes, customNames);
1199
1956
  let className = withClassPrefix(base, options.prefix);
1200
1957
  let suffix = 2;
1201
1958
  while (used.has(className)) {
@@ -1212,7 +1969,8 @@ function assignComponentClassNames(combinations, options = {}) {
1212
1969
  }
1213
1970
  function generateComponentCss(options) {
1214
1971
  const components = assignComponentClassNames(options.combinations, {
1215
- prefix: options.prefix
1972
+ prefix: options.prefix,
1973
+ names: options.names
1216
1974
  });
1217
1975
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1218
1976
  const classPrefix = normalizeClassPrefix(options.prefix);
@@ -1258,7 +2016,22 @@ function buildComponents(occurrences, options) {
1258
2016
  const { css, components } = generateComponentCss({
1259
2017
  sourcePath: options.sourcePath,
1260
2018
  combinations,
1261
- prefix: options.prefix
2019
+ prefix: options.prefix,
2020
+ names: options.names
2021
+ });
2022
+ const replacementMap = /* @__PURE__ */ new Map();
2023
+ for (const component of components) {
2024
+ const key = [...component.classes].sort().join(" ");
2025
+ replacementMap.set(key, component.className);
2026
+ }
2027
+ return { components, css, replacementMap };
2028
+ }
2029
+ function buildComponentsFromCombinations(combinations, options) {
2030
+ const { css, components } = generateComponentCss({
2031
+ sourcePath: options.sourcePath,
2032
+ combinations,
2033
+ prefix: options.prefix,
2034
+ names: options.names
1262
2035
  });
1263
2036
  const replacementMap = /* @__PURE__ */ new Map();
1264
2037
  for (const component of components) {
@@ -1268,33 +2041,113 @@ function buildComponents(occurrences, options) {
1268
2041
  return { components, css, replacementMap };
1269
2042
  }
1270
2043
 
2044
+ // src/core/loadAnalyzeReport.ts
2045
+ import fs4 from "fs/promises";
2046
+ function isAnalysisReport(value) {
2047
+ if (typeof value !== "object" || value === null) {
2048
+ return false;
2049
+ }
2050
+ const report = value;
2051
+ return typeof report.targetPath === "string" && typeof report.stats === "object" && Array.isArray(report.stats.topCombinations);
2052
+ }
2053
+ async function loadExtractableCombinations(reportPath, options = {}) {
2054
+ const raw = await fs4.readFile(reportPath, "utf-8");
2055
+ const parsed = JSON.parse(raw);
2056
+ if (!isAnalysisReport(parsed)) {
2057
+ throw new Error(`Invalid analyze report: ${reportPath}`);
2058
+ }
2059
+ const combinations = parsed.stats.topCombinations.filter(
2060
+ (combo) => options.extractableOnly === false ? true : combo.extractable === true
2061
+ );
2062
+ if (combinations.length === 0) {
2063
+ throw new Error(
2064
+ "No extractable combinations found in report. Re-run analyze or use --extractable-only=false."
2065
+ );
2066
+ }
2067
+ return {
2068
+ targetPath: parsed.targetPath,
2069
+ combinations
2070
+ };
2071
+ }
2072
+
2073
+ // src/reporters/operationJsonReporter.ts
2074
+ function printGenerateJsonReport(report) {
2075
+ console.log(JSON.stringify(report, null, 2));
2076
+ }
2077
+ function printApplyJsonReport(report) {
2078
+ console.log(JSON.stringify(report, null, 2));
2079
+ }
2080
+
1271
2081
  // src/commands/apply.ts
1272
- import fs3 from "fs/promises";
1273
- import path3 from "path";
2082
+ import fs5 from "fs/promises";
2083
+ import path5 from "path";
1274
2084
  import chalk3 from "chalk";
1275
2085
  async function applyCommand(targetPath, options) {
1276
2086
  let scanResult;
1277
2087
  try {
1278
- scanResult = await scanProject({ targetPath });
2088
+ scanResult = await scanProject({
2089
+ targetPath,
2090
+ include: options.include,
2091
+ exclude: options.exclude,
2092
+ extractableMinOccurrences: options.minOccurrences ?? 3
2093
+ });
1279
2094
  } catch (error) {
1280
2095
  const message = error instanceof Error ? error.message : String(error);
1281
2096
  console.error(chalk3.red(`Error: ${message}`));
1282
2097
  process.exit(1);
1283
2098
  }
1284
2099
  for (const warning of scanResult.warnings) {
1285
- console.warn(chalk3.yellow(`\u26A0 ${warning}`));
2100
+ if (options.format !== "json") {
2101
+ console.warn(chalk3.yellow(`\u26A0 ${warning}`));
2102
+ }
1286
2103
  }
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
2104
+ let components;
2105
+ let css;
2106
+ let replacementMap;
2107
+ try {
2108
+ if (options.fromReport) {
2109
+ const loadedReport = await loadExtractableCombinations(options.fromReport, {
2110
+ extractableOnly: options.extractableOnly ?? true
2111
+ });
2112
+ const built = buildComponentsFromCombinations(loadedReport.combinations, {
2113
+ sourcePath: scanResult.resolvedPath,
2114
+ prefix: options.prefix,
2115
+ names: options.names
2116
+ });
2117
+ components = built.components;
2118
+ css = built.css;
2119
+ replacementMap = built.replacementMap;
2120
+ } else if (options.extractableOnly) {
2121
+ const combinations = scanResult.report.stats.topCombinations.filter(
2122
+ (combo) => combo.extractable
2123
+ );
2124
+ const built = buildComponentsFromCombinations(combinations, {
2125
+ sourcePath: scanResult.resolvedPath,
2126
+ prefix: options.prefix,
2127
+ names: options.names
2128
+ });
2129
+ components = built.components;
2130
+ css = built.css;
2131
+ replacementMap = built.replacementMap;
2132
+ } else {
2133
+ const built = buildComponents(scanResult.occurrences, {
2134
+ sourcePath: scanResult.resolvedPath,
2135
+ minOccurrences: options.minOccurrences ?? 3,
2136
+ minSize: options.minSize,
2137
+ maxSize: options.maxSize,
2138
+ topLimit: options.top,
2139
+ prefix: options.prefix,
2140
+ names: options.names
2141
+ });
2142
+ components = built.components;
2143
+ css = built.css;
2144
+ replacementMap = built.replacementMap;
1296
2145
  }
1297
- );
2146
+ } catch (error) {
2147
+ const message = error instanceof Error ? error.message : String(error);
2148
+ console.error(chalk3.red(`Error: ${message}`));
2149
+ process.exit(1);
2150
+ }
1298
2151
  if (components.length === 0) {
1299
2152
  console.error(
1300
2153
  chalk3.yellow(
@@ -1303,29 +2156,71 @@ async function applyCommand(targetPath, options) {
1303
2156
  );
1304
2157
  process.exit(1);
1305
2158
  }
1306
- const outputPath = path3.resolve(options.output);
2159
+ const outputPath = path5.resolve(options.output);
1307
2160
  let filesModified = 0;
1308
2161
  let replacementsTotal = 0;
1309
2162
  const allReplacements = [];
2163
+ const allSkipped = [];
2164
+ const modifiedSources = /* @__PURE__ */ new Map();
2165
+ const modifiedFiles = [];
1310
2166
  for (const file of scanResult.files) {
1311
- const original = await fs3.readFile(file, "utf-8");
1312
- const result = replaceClassNamesInSource(
2167
+ const original = await fs5.readFile(file, "utf-8");
2168
+ const result2 = replaceClassNamesInSource(
1313
2169
  original,
1314
2170
  replacementMap,
1315
2171
  file
1316
2172
  );
1317
- replacementsTotal += result.replacements.length;
1318
- allReplacements.push(...result.replacements);
1319
- if (result.changed) {
2173
+ replacementsTotal += result2.replacements.length;
2174
+ allReplacements.push(...result2.replacements);
2175
+ allSkipped.push(...result2.skipped);
2176
+ if (result2.changed) {
1320
2177
  filesModified += 1;
1321
- if (!options.dryRun) {
1322
- await fs3.writeFile(file, result.source, "utf-8");
1323
- }
2178
+ modifiedSources.set(file, result2.source);
2179
+ modifiedFiles.push(file);
1324
2180
  }
1325
2181
  }
2182
+ let prettierFormatted = [];
2183
+ if (!options.dryRun && options.prettier && modifiedFiles.length > 0) {
2184
+ const formatResult = await formatModifiedFiles(
2185
+ modifiedFiles,
2186
+ modifiedSources,
2187
+ process.cwd()
2188
+ );
2189
+ prettierFormatted = formatResult.formatted;
2190
+ }
1326
2191
  if (!options.dryRun) {
1327
- await fs3.mkdir(path3.dirname(outputPath), { recursive: true });
1328
- await fs3.writeFile(outputPath, css, "utf-8");
2192
+ for (const file of modifiedFiles) {
2193
+ const source = modifiedSources.get(file);
2194
+ if (source) {
2195
+ await fs5.writeFile(file, source, "utf-8");
2196
+ }
2197
+ }
2198
+ await fs5.mkdir(path5.dirname(outputPath), { recursive: true });
2199
+ await fs5.writeFile(outputPath, css, "utf-8");
2200
+ }
2201
+ const result = {
2202
+ filesModified,
2203
+ replacementsTotal,
2204
+ outputPath,
2205
+ componentsGenerated: components.length,
2206
+ components,
2207
+ replacements: allReplacements,
2208
+ skipped: allSkipped,
2209
+ prettierFormatted
2210
+ };
2211
+ if (options.format === "json") {
2212
+ printApplyJsonReport({
2213
+ command: "apply",
2214
+ dryRun: Boolean(options.dryRun),
2215
+ outputPath,
2216
+ filesModified,
2217
+ replacementsTotal,
2218
+ componentsGenerated: components.length,
2219
+ components,
2220
+ replacements: allReplacements,
2221
+ skipped: allSkipped
2222
+ });
2223
+ return result;
1329
2224
  }
1330
2225
  console.log("");
1331
2226
  if (options.dryRun) {
@@ -1343,13 +2238,30 @@ async function applyCommand(targetPath, options) {
1343
2238
  console.log(
1344
2239
  chalk3.gray(` Replacements: `) + chalk3.white(String(replacementsTotal))
1345
2240
  );
2241
+ if (prettierFormatted.length > 0) {
2242
+ console.log(
2243
+ chalk3.gray(` Prettier formatted: `) + chalk3.white(String(prettierFormatted.length))
2244
+ );
2245
+ }
1346
2246
  if (allReplacements.length > 0) {
1347
2247
  console.log("");
1348
2248
  console.log(chalk3.bold("Replacements:"));
1349
2249
  for (const item of allReplacements) {
1350
2250
  const line = item.line ? `:${item.line}` : "";
2251
+ const partialTag = item.partial ? chalk3.dim(" (partial)") : "";
2252
+ console.log(
2253
+ chalk3.gray(` ${item.filePath}${line}`) + chalk3.white(` "${item.from}" `) + chalk3.cyan("\u2192") + chalk3.green(` "${item.to}"`) + partialTag
2254
+ );
2255
+ }
2256
+ }
2257
+ if (allSkipped.length > 0) {
2258
+ console.log("");
2259
+ console.log(chalk3.bold.yellow(`Skipped (${allSkipped.length}):`));
2260
+ for (const item of allSkipped) {
2261
+ const line = item.line ? `:${item.line}` : "";
2262
+ const classes = item.classes.join(" ");
1351
2263
  console.log(
1352
- chalk3.gray(` ${item.filePath}${line}`) + chalk3.white(` "${item.from}" `) + chalk3.cyan("\u2192") + chalk3.green(` "${item.to}"`)
2264
+ chalk3.gray(` ${item.filePath}${line}`) + chalk3.yellow(` [${item.reason}]`) + chalk3.dim(` "${classes}"`)
1353
2265
  );
1354
2266
  }
1355
2267
  }
@@ -1357,46 +2269,97 @@ async function applyCommand(targetPath, options) {
1357
2269
  if (!options.dryRun) {
1358
2270
  console.log(
1359
2271
  chalk3.cyan(
1360
- `Import ${path3.basename(outputPath)} in your global CSS if you haven't already.`
2272
+ `Import ${path5.basename(outputPath)} in your global CSS if you haven't already.`
1361
2273
  )
1362
2274
  );
1363
2275
  console.log("");
1364
2276
  }
1365
- return {
1366
- filesModified,
1367
- replacementsTotal,
1368
- outputPath,
1369
- componentsGenerated: components.length
1370
- };
2277
+ return result;
1371
2278
  }
1372
2279
 
1373
2280
  // src/commands/generate.ts
1374
- import fs4 from "fs/promises";
1375
- import path4 from "path";
2281
+ import fs6 from "fs/promises";
2282
+ import path6 from "path";
1376
2283
  import chalk4 from "chalk";
1377
2284
  async function generateCommand(targetPath, options) {
1378
- let scanResult;
2285
+ let scanResult = null;
2286
+ let components;
2287
+ let css;
1379
2288
  try {
1380
- scanResult = await scanProject({ targetPath });
2289
+ if (options.fromReport) {
2290
+ const loadedReport = await loadExtractableCombinations(options.fromReport, {
2291
+ extractableOnly: options.extractableOnly ?? true
2292
+ });
2293
+ const built = buildComponentsFromCombinations(loadedReport.combinations, {
2294
+ sourcePath: loadedReport.targetPath || targetPath,
2295
+ prefix: options.prefix,
2296
+ names: options.names
2297
+ });
2298
+ components = built.components;
2299
+ css = built.css;
2300
+ } else {
2301
+ scanResult = await scanProject({
2302
+ targetPath,
2303
+ include: options.include,
2304
+ exclude: options.exclude,
2305
+ extractableMinOccurrences: options.minOccurrences ?? 3
2306
+ });
2307
+ if (options.extractableOnly) {
2308
+ const combinations = scanResult.report.stats.topCombinations.filter(
2309
+ (combo) => combo.extractable
2310
+ );
2311
+ const built = buildComponentsFromCombinations(combinations, {
2312
+ sourcePath: scanResult.resolvedPath,
2313
+ prefix: options.prefix,
2314
+ names: options.names
2315
+ });
2316
+ components = built.components;
2317
+ css = built.css;
2318
+ } else {
2319
+ const built = buildComponents(scanResult.occurrences, {
2320
+ sourcePath: scanResult.resolvedPath,
2321
+ minOccurrences: options.minOccurrences ?? 3,
2322
+ minSize: options.minSize,
2323
+ maxSize: options.maxSize,
2324
+ topLimit: options.top,
2325
+ prefix: options.prefix,
2326
+ names: options.names
2327
+ });
2328
+ components = built.components;
2329
+ css = built.css;
2330
+ }
2331
+ }
1381
2332
  } catch (error) {
1382
2333
  const message = error instanceof Error ? error.message : String(error);
1383
2334
  console.error(chalk4.red(`Error: ${message}`));
1384
2335
  process.exit(1);
1385
2336
  }
1386
- for (const warning of scanResult.warnings) {
1387
- console.warn(chalk4.yellow(`\u26A0 ${warning}`));
2337
+ if (scanResult) {
2338
+ for (const warning of scanResult.warnings) {
2339
+ if (options.format !== "json") {
2340
+ console.warn(chalk4.yellow(`\u26A0 ${warning}`));
2341
+ }
2342
+ }
2343
+ }
2344
+ const outputPath = path6.resolve(options.output);
2345
+ await fs6.mkdir(path6.dirname(outputPath), { recursive: true });
2346
+ await fs6.writeFile(outputPath, css, "utf-8");
2347
+ const result = {
2348
+ outputPath,
2349
+ componentsGenerated: components.length,
2350
+ components,
2351
+ report: scanResult?.report ?? null
2352
+ };
2353
+ if (options.format === "json") {
2354
+ printGenerateJsonReport({
2355
+ command: "generate",
2356
+ outputPath,
2357
+ componentsGenerated: components.length,
2358
+ components,
2359
+ cssWritten: true
2360
+ });
2361
+ return result;
1388
2362
  }
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
2363
  console.log("");
1401
2364
  console.log(chalk4.bold.green("\u2705 CSS generated successfully"));
1402
2365
  console.log(chalk4.gray(` Output: `) + chalk4.white(outputPath));
@@ -1425,14 +2388,13 @@ async function generateCommand(targetPath, options) {
1425
2388
  );
1426
2389
  }
1427
2390
  console.log("");
1428
- return {
1429
- outputPath,
1430
- componentsGenerated: components.length,
1431
- report: scanResult.report
1432
- };
2391
+ return result;
1433
2392
  }
1434
2393
 
1435
2394
  export {
2395
+ validateConfigFile,
2396
+ normalizeNamesConfig,
2397
+ loadCommandOptions,
1436
2398
  suggestClassName,
1437
2399
  isStrictSubset,
1438
2400
  dedupeSubsetCombinations,
@@ -1442,6 +2404,10 @@ export {
1442
2404
  findFrequentPatterns,
1443
2405
  findRepeatedClassSets,
1444
2406
  calculatePotentialReduction,
2407
+ VARIANT_CALLEES,
2408
+ isVariantCallee,
2409
+ extractClassesFromVariantCall,
2410
+ collectVariantRegistry,
1445
2411
  CLASS_MERGE_CALLEES,
1446
2412
  extractClassesFromExpression,
1447
2413
  isClassAttribute,
@@ -1456,6 +2422,8 @@ export {
1456
2422
  printConsoleReport,
1457
2423
  printJsonReport,
1458
2424
  analyzeCommand,
2425
+ formatSource,
2426
+ formatModifiedFiles,
1459
2427
  replaceClassNamesInSource,
1460
2428
  DEFAULT_CLASS_PREFIX,
1461
2429
  normalizeClassPrefix,
@@ -1463,7 +2431,11 @@ export {
1463
2431
  assignComponentClassNames,
1464
2432
  generateComponentCss,
1465
2433
  buildComponents,
2434
+ buildComponentsFromCombinations,
2435
+ loadExtractableCombinations,
2436
+ printGenerateJsonReport,
2437
+ printApplyJsonReport,
1466
2438
  applyCommand,
1467
2439
  generateCommand
1468
2440
  };
1469
- //# sourceMappingURL=chunk-N7HD4T2I.js.map
2441
+ //# sourceMappingURL=chunk-4GXMK3NB.js.map