tailwind-unwind 0.2.0 → 0.4.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.
@@ -12,6 +12,9 @@ var KNOWN_ROOT_KEYS = /* @__PURE__ */ new Set([
12
12
  "top",
13
13
  "dedupeSubsets",
14
14
  "dryRun",
15
+ "prettier",
16
+ "fromReport",
17
+ "extractableOnly",
15
18
  "analyze",
16
19
  "generate",
17
20
  "apply"
@@ -24,33 +27,36 @@ var KNOWN_COMMAND_KEYS = /* @__PURE__ */ new Set([
24
27
  "prefix",
25
28
  "output",
26
29
  "dedupeSubsets",
27
- "dryRun"
30
+ "dryRun",
31
+ "prettier",
32
+ "fromReport",
33
+ "extractableOnly"
28
34
  ]);
29
35
  function isRecord(value) {
30
36
  return typeof value === "object" && value !== null && !Array.isArray(value);
31
37
  }
32
- function assertPositiveNumber(value, path6, errors) {
38
+ function assertPositiveNumber(value, path9, errors) {
33
39
  if (value === void 0) {
34
40
  return;
35
41
  }
36
42
  if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
37
- errors.push(`${path6} must be a positive number`);
43
+ errors.push(`${path9} must be a positive number`);
38
44
  }
39
45
  }
40
- function assertBoolean(value, path6, errors) {
46
+ function assertBoolean(value, path9, errors) {
41
47
  if (value === void 0) {
42
48
  return;
43
49
  }
44
50
  if (typeof value !== "boolean") {
45
- errors.push(`${path6} must be a boolean`);
51
+ errors.push(`${path9} must be a boolean`);
46
52
  }
47
53
  }
48
- function assertStringArray(value, path6, errors) {
54
+ function assertStringArray(value, path9, errors) {
49
55
  if (value === void 0) {
50
56
  return;
51
57
  }
52
58
  if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && item.length > 0)) {
53
- errors.push(`${path6} must be an array of non-empty strings`);
59
+ errors.push(`${path9} must be an array of non-empty strings`);
54
60
  }
55
61
  }
56
62
  function validateCommandSection(value, section, errors) {
@@ -72,6 +78,11 @@ function validateCommandSection(value, section, errors) {
72
78
  assertPositiveNumber(value.top, `${section}.top`, errors);
73
79
  assertBoolean(value.dedupeSubsets, `${section}.dedupeSubsets`, errors);
74
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
+ }
75
86
  if (value.prefix !== void 0 && (typeof value.prefix !== "string" || value.prefix.length === 0)) {
76
87
  errors.push(`${section}.prefix must be a non-empty string`);
77
88
  }
@@ -118,6 +129,11 @@ function validateConfigFile(raw, configPath) {
118
129
  assertPositiveNumber(source.top, "top", errors);
119
130
  assertBoolean(source.dedupeSubsets, "dedupeSubsets", errors);
120
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
+ }
121
137
  validateNames(source.names, errors);
122
138
  validateCommandSection(source.analyze, "analyze", errors);
123
139
  validateCommandSection(source.generate, "generate", errors);
@@ -153,6 +169,7 @@ import fs from "fs/promises";
153
169
  import path from "path";
154
170
  import { pathToFileURL } from "url";
155
171
  var CONFIG_FILENAMES = [
172
+ "tailwind-unwind.config.ts",
156
173
  "tailwind-unwind.config.js",
157
174
  "tailwind-unwind.config.mjs",
158
175
  "tailwind-unwind.config.cjs",
@@ -189,6 +206,15 @@ function pickCommandConfig(source) {
189
206
  if (typeof source.dryRun === "boolean") {
190
207
  config.dryRun = source.dryRun;
191
208
  }
209
+ if (typeof source.prettier === "boolean") {
210
+ config.prettier = source.prettier;
211
+ }
212
+ if (typeof source.fromReport === "string" && source.fromReport.length > 0) {
213
+ config.fromReport = source.fromReport;
214
+ }
215
+ if (typeof source.extractableOnly === "boolean") {
216
+ config.extractableOnly = source.extractableOnly;
217
+ }
192
218
  return config;
193
219
  }
194
220
  function pickNames(source) {
@@ -238,7 +264,7 @@ function normalizeLoadedConfig(raw) {
238
264
  }
239
265
  function mergeCommandConfig(command, fileConfig) {
240
266
  const { analyze, generate: generate2, apply, ...root } = fileConfig;
241
- const commandSection = command === "analyze" ? analyze : command === "generate" ? generate2 : apply;
267
+ const commandSection = command === "analyze" ? analyze : command === "generate" ? generate2 : command === "apply" ? apply : void 0;
242
268
  return {
243
269
  ...root,
244
270
  ...commandSection
@@ -298,6 +324,11 @@ async function resolveConfigFile(explicitPath, searchRoots) {
298
324
  return null;
299
325
  }
300
326
  async function importConfigModule(configPath) {
327
+ if (configPath.endsWith(".ts")) {
328
+ const { createJiti } = await import("jiti");
329
+ const jiti = createJiti(import.meta.url, { interopDefault: true });
330
+ return jiti(configPath);
331
+ }
301
332
  const moduleUrl = pathToFileURL(configPath).href;
302
333
  const imported = await import(moduleUrl);
303
334
  return imported;
@@ -936,6 +967,113 @@ function calculatePotentialReduction(occurrences, topCombinations) {
936
967
  return Math.min(100, Math.round(savable / totalClassUsages * 100));
937
968
  }
938
969
 
970
+ // src/parser/variantHelpers.ts
971
+ import babelTraverse from "@babel/traverse";
972
+ var VARIANT_CALLEES = /* @__PURE__ */ new Set(["tv", "cva"]);
973
+ function isVariantCallee(expression) {
974
+ if (expression.type === "Identifier") {
975
+ return VARIANT_CALLEES.has(expression.name);
976
+ }
977
+ if (expression.type === "MemberExpression" && expression.property.type === "Identifier") {
978
+ return VARIANT_CALLEES.has(expression.property.name);
979
+ }
980
+ return false;
981
+ }
982
+ function collectStringsFromObject(node) {
983
+ const classes = [];
984
+ for (const prop of node.properties) {
985
+ if (prop.type === "SpreadElement") {
986
+ continue;
987
+ }
988
+ if (prop.type !== "ObjectProperty") {
989
+ continue;
990
+ }
991
+ collectStringsFromPropertyValue(prop, classes);
992
+ }
993
+ return classes;
994
+ }
995
+ function collectStringsFromPropertyValue(prop, classes) {
996
+ const keyName = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "StringLiteral" ? prop.key.value : null;
997
+ const { value } = prop;
998
+ if (value.type === "StringLiteral") {
999
+ classes.push(...splitClassString(value.value));
1000
+ return;
1001
+ }
1002
+ if (value.type === "ObjectExpression") {
1003
+ if (keyName === "variants" || keyName === "compoundVariants") {
1004
+ for (const nested of value.properties) {
1005
+ if (nested.type === "ObjectProperty") {
1006
+ collectStringsFromPropertyValue(nested, classes);
1007
+ }
1008
+ }
1009
+ return;
1010
+ }
1011
+ classes.push(...collectStringsFromObject(value));
1012
+ return;
1013
+ }
1014
+ if (value.type === "ArrayExpression") {
1015
+ for (const element of value.elements) {
1016
+ if (element === null || element.type === "SpreadElement") {
1017
+ continue;
1018
+ }
1019
+ if (element.type === "StringLiteral") {
1020
+ classes.push(...splitClassString(element.value));
1021
+ } else if (element.type === "ObjectExpression") {
1022
+ classes.push(...collectStringsFromObject(element));
1023
+ }
1024
+ }
1025
+ }
1026
+ }
1027
+ function extractClassesFromVariantCall(call) {
1028
+ const classes = [];
1029
+ for (const arg of call.arguments) {
1030
+ if (arg.type === "SpreadElement" || arg.type === "ArgumentPlaceholder") {
1031
+ continue;
1032
+ }
1033
+ if (arg.type === "StringLiteral") {
1034
+ classes.push(...splitClassString(arg.value));
1035
+ continue;
1036
+ }
1037
+ if (arg.type === "ObjectExpression") {
1038
+ classes.push(...collectStringsFromObject(arg));
1039
+ }
1040
+ }
1041
+ return [...new Set(classes)];
1042
+ }
1043
+ function resolveTraverse(module) {
1044
+ if (typeof module === "function") {
1045
+ return module;
1046
+ }
1047
+ const withDefault = module;
1048
+ if (typeof withDefault.default === "function") {
1049
+ return withDefault.default;
1050
+ }
1051
+ throw new Error("Failed to load @babel/traverse");
1052
+ }
1053
+ var traverse = resolveTraverse(babelTraverse);
1054
+ function collectVariantRegistry(ast) {
1055
+ const registry = /* @__PURE__ */ new Map();
1056
+ traverse(ast, {
1057
+ VariableDeclarator(path9) {
1058
+ registerVariantDeclarator(path9.node, registry);
1059
+ }
1060
+ });
1061
+ return registry;
1062
+ }
1063
+ function registerVariantDeclarator(declarator, registry) {
1064
+ if (declarator.id.type !== "Identifier" || declarator.init?.type !== "CallExpression") {
1065
+ return;
1066
+ }
1067
+ const { init } = declarator;
1068
+ if (init.callee.type === "V8IntrinsicIdentifier" || !isVariantCallee(init.callee)) {
1069
+ return;
1070
+ }
1071
+ const classes = extractClassesFromVariantCall(init);
1072
+ if (classes.length > 0) {
1073
+ registry.set(declarator.id.name, classes);
1074
+ }
1075
+ }
1076
+
939
1077
  // src/parser/classHelpers.ts
940
1078
  var CLASS_MERGE_CALLEES = /* @__PURE__ */ new Set([
941
1079
  "cn",
@@ -978,18 +1116,34 @@ function isClassMergeCallee(expression) {
978
1116
  }
979
1117
  return false;
980
1118
  }
981
- function extractFromCallArguments(args) {
1119
+ function extractFromCallArguments(args, registry) {
982
1120
  const parts = [];
983
1121
  for (const arg of args) {
984
1122
  if (arg.type === "SpreadElement" || arg.type === "ArgumentPlaceholder") {
985
1123
  parts.push({ classes: [], isDynamic: true });
986
1124
  continue;
987
1125
  }
988
- parts.push(extractClassesFromExpression(arg));
1126
+ parts.push(extractClassesFromExpression(arg, registry));
989
1127
  }
990
1128
  return mergeExtractions(parts);
991
1129
  }
992
- function extractClassesFromExpression(expression) {
1130
+ function extractFromVariantCall(call, registry) {
1131
+ const { callee } = call;
1132
+ if (callee.type !== "V8IntrinsicIdentifier" && isVariantCallee(callee)) {
1133
+ const classes = extractClassesFromVariantCall(call);
1134
+ const hasDynamicArgs = call.arguments.some(
1135
+ (arg) => arg.type === "SpreadElement" || arg.type === "ArgumentPlaceholder"
1136
+ );
1137
+ return { classes, isDynamic: hasDynamicArgs };
1138
+ }
1139
+ if (callee.type === "Identifier" && registry?.has(callee.name)) {
1140
+ const classes = registry.get(callee.name) ?? [];
1141
+ const hasArgs = call.arguments.length > 0;
1142
+ return { classes, isDynamic: hasArgs };
1143
+ }
1144
+ return { classes: [], isDynamic: true };
1145
+ }
1146
+ function extractClassesFromExpression(expression, registry) {
993
1147
  switch (expression.type) {
994
1148
  case "StringLiteral":
995
1149
  return extractFromStringLiteral(expression.value);
@@ -998,40 +1152,44 @@ function extractClassesFromExpression(expression) {
998
1152
  case "CallExpression": {
999
1153
  const { callee } = expression;
1000
1154
  if (callee.type !== "V8IntrinsicIdentifier" && isClassMergeCallee(callee)) {
1001
- return extractFromCallArguments(expression.arguments);
1155
+ return extractFromCallArguments(expression.arguments, registry);
1156
+ }
1157
+ const variantExtraction = extractFromVariantCall(expression, registry);
1158
+ if (variantExtraction.classes.length > 0 || !variantExtraction.isDynamic) {
1159
+ return variantExtraction;
1002
1160
  }
1003
1161
  return { classes: [], isDynamic: true };
1004
1162
  }
1005
1163
  case "ConditionalExpression": {
1006
1164
  const merged = mergeExtractions([
1007
- extractClassesFromExpression(expression.consequent),
1008
- extractClassesFromExpression(expression.alternate)
1165
+ extractClassesFromExpression(expression.consequent, registry),
1166
+ extractClassesFromExpression(expression.alternate, registry)
1009
1167
  ]);
1010
1168
  return { ...merged, isDynamic: true };
1011
1169
  }
1012
1170
  case "LogicalExpression": {
1013
1171
  const merged = mergeExtractions([
1014
- extractClassesFromExpression(expression.left),
1015
- extractClassesFromExpression(expression.right)
1172
+ extractClassesFromExpression(expression.left, registry),
1173
+ extractClassesFromExpression(expression.right, registry)
1016
1174
  ]);
1017
1175
  return { ...merged, isDynamic: true };
1018
1176
  }
1019
1177
  case "ArrayExpression":
1020
- return extractFromArrayExpression(expression);
1178
+ return extractFromArrayExpression(expression, registry);
1021
1179
  case "ObjectExpression":
1022
1180
  return extractFromObjectExpression(expression);
1023
1181
  default:
1024
1182
  return { classes: [], isDynamic: true };
1025
1183
  }
1026
1184
  }
1027
- function extractFromArrayExpression(node) {
1185
+ function extractFromArrayExpression(node, registry) {
1028
1186
  const parts = [];
1029
1187
  for (const element of node.elements) {
1030
1188
  if (element === null || element.type === "SpreadElement") {
1031
1189
  parts.push({ classes: [], isDynamic: true });
1032
1190
  continue;
1033
1191
  }
1034
- parts.push(extractClassesFromExpression(element));
1192
+ parts.push(extractClassesFromExpression(element, registry));
1035
1193
  }
1036
1194
  return mergeExtractions(parts);
1037
1195
  }
@@ -1086,7 +1244,7 @@ function isClassAttribute(attr) {
1086
1244
  const name = getAttributeName(attr);
1087
1245
  return name !== null && CLASS_ATTRIBUTES.has(name);
1088
1246
  }
1089
- function extractFromJSXAttribute(attr) {
1247
+ function extractFromJSXAttribute(attr, registry) {
1090
1248
  if (!isClassAttribute(attr)) {
1091
1249
  return null;
1092
1250
  }
@@ -1096,7 +1254,7 @@ function extractFromJSXAttribute(attr) {
1096
1254
  return { classes: [], isDynamic: true, line };
1097
1255
  }
1098
1256
  if (value.type === "StringLiteral") {
1099
- const result = extractClassesFromExpression(value);
1257
+ const result = extractClassesFromExpression(value, registry);
1100
1258
  return { classes: result.classes, isDynamic: result.isDynamic, line };
1101
1259
  }
1102
1260
  if (value.type === "JSXExpressionContainer") {
@@ -1104,7 +1262,7 @@ function extractFromJSXAttribute(attr) {
1104
1262
  if (expr.type === "JSXEmptyExpression") {
1105
1263
  return { classes: [], isDynamic: true, line };
1106
1264
  }
1107
- const result = extractClassesFromExpression(expr);
1265
+ const result = extractClassesFromExpression(expr, registry);
1108
1266
  return { classes: result.classes, isDynamic: result.isDynamic, line };
1109
1267
  }
1110
1268
  return { classes: [], isDynamic: true, line };
@@ -1121,9 +1279,9 @@ function parseSourceToAst(source) {
1121
1279
  }
1122
1280
 
1123
1281
  // src/parser/jsxParser.ts
1124
- import babelTraverse from "@babel/traverse";
1282
+ import babelTraverse2 from "@babel/traverse";
1125
1283
  import fs2 from "fs/promises";
1126
- function resolveTraverse(module) {
1284
+ function resolveTraverse2(module) {
1127
1285
  if (typeof module === "function") {
1128
1286
  return module;
1129
1287
  }
@@ -1133,9 +1291,9 @@ function resolveTraverse(module) {
1133
1291
  }
1134
1292
  throw new Error("Failed to load @babel/traverse");
1135
1293
  }
1136
- var traverse = resolveTraverse(babelTraverse);
1137
- function isJSXElementWithClassAttribute(path6) {
1138
- const opening = path6.node.openingElement;
1294
+ var traverse2 = resolveTraverse2(babelTraverse2);
1295
+ function isJSXElementWithClassAttribute(path9) {
1296
+ const opening = path9.node.openingElement;
1139
1297
  return opening.attributes.some(
1140
1298
  (attr) => attr.type === "JSXAttribute" && isClassAttribute(attr)
1141
1299
  );
@@ -1143,15 +1301,16 @@ function isJSXElementWithClassAttribute(path6) {
1143
1301
  function collectExtractionsFromAst(ast, filePath) {
1144
1302
  const extractions = [];
1145
1303
  const warnings = [];
1146
- traverse(ast, {
1147
- JSXElement(path6) {
1148
- if (!isJSXElementWithClassAttribute(path6)) {
1304
+ const variantRegistry = collectVariantRegistry(ast);
1305
+ traverse2(ast, {
1306
+ JSXElement(path9) {
1307
+ if (!isJSXElementWithClassAttribute(path9)) {
1149
1308
  return;
1150
1309
  }
1151
- const opening = path6.node.openingElement;
1310
+ const opening = path9.node.openingElement;
1152
1311
  for (const attr of opening.attributes) {
1153
1312
  if (attr.type !== "JSXAttribute") continue;
1154
- const extraction = extractFromJSXAttribute(attr);
1313
+ const extraction = extractFromJSXAttribute(attr, variantRegistry);
1155
1314
  if (!extraction) continue;
1156
1315
  if (extraction.isDynamic && extraction.classes.length === 0) {
1157
1316
  const lineInfo = extraction.line ? `:${extraction.line}` : "";
@@ -1199,23 +1358,95 @@ var IGNORE_PATTERNS = IGNORED_DIRECTORIES.map(
1199
1358
  (dir) => `**/${dir}/**`
1200
1359
  );
1201
1360
 
1361
+ // src/scanner/gitChanged.ts
1362
+ import { execFile } from "child_process";
1363
+ import path2 from "path";
1364
+ import { promisify } from "util";
1365
+ var execFileAsync = promisify(execFile);
1366
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js"]);
1367
+ function isSourceFile(filePath) {
1368
+ const ext = path2.extname(filePath).toLowerCase();
1369
+ return SOURCE_EXTENSIONS.has(ext);
1370
+ }
1371
+ function isIgnoredPath(filePath) {
1372
+ const normalized = filePath.replace(/\\/g, "/");
1373
+ return IGNORE_PATTERNS.some((pattern) => {
1374
+ const dir = pattern.replace("/**", "").replace("**/", "");
1375
+ return normalized.includes(`/${dir}/`);
1376
+ });
1377
+ }
1378
+ async function runGit(cwd, args) {
1379
+ try {
1380
+ const { stdout } = await execFileAsync("git", args, { cwd });
1381
+ return stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
1382
+ } catch {
1383
+ return [];
1384
+ }
1385
+ }
1386
+ function resolveAbsoluteFiles(files, rootPath) {
1387
+ const absoluteRoot = path2.resolve(rootPath);
1388
+ return [...new Set(
1389
+ files.map((file) => path2.resolve(absoluteRoot, file)).filter((file) => file.startsWith(absoluteRoot)).filter(isSourceFile).filter((file) => !isIgnoredPath(path2.relative(absoluteRoot, file)))
1390
+ )].sort();
1391
+ }
1392
+ async function getChangedSourceFiles(rootPath, ref = "HEAD") {
1393
+ const cwd = path2.resolve(rootPath);
1394
+ const unstaged = await runGit(cwd, ["diff", "--name-only", ref]);
1395
+ const staged = await runGit(cwd, ["diff", "--cached", "--name-only", ref]);
1396
+ const untracked = await runGit(cwd, [
1397
+ "ls-files",
1398
+ "--others",
1399
+ "--exclude-standard"
1400
+ ]);
1401
+ return resolveAbsoluteFiles(
1402
+ [...unstaged, ...staged, ...untracked],
1403
+ cwd
1404
+ );
1405
+ }
1406
+ async function findGitRoot(startPath) {
1407
+ try {
1408
+ const { stdout } = await execFileAsync(
1409
+ "git",
1410
+ ["rev-parse", "--show-toplevel"],
1411
+ { cwd: path2.resolve(startPath) }
1412
+ );
1413
+ return stdout.trim();
1414
+ } catch {
1415
+ return null;
1416
+ }
1417
+ }
1418
+ async function isGitRepository(rootPath) {
1419
+ return await findGitRoot(rootPath) !== null;
1420
+ }
1421
+ async function getChangedFilesInScope(scopePath, ref = "HEAD") {
1422
+ const gitRoot = await findGitRoot(scopePath);
1423
+ if (!gitRoot) {
1424
+ throw new Error("Not a git repository. Remove --changed or run inside a git repo.");
1425
+ }
1426
+ const absoluteScope = path2.resolve(scopePath);
1427
+ const changed = await getChangedSourceFiles(gitRoot, ref);
1428
+ return changed.filter(
1429
+ (file) => file === absoluteScope || file.startsWith(`${absoluteScope}${path2.sep}`)
1430
+ );
1431
+ }
1432
+
1202
1433
  // src/scanner/fileWalker.ts
1203
1434
  import fg from "fast-glob";
1204
- import path2 from "path";
1205
- var SOURCE_EXTENSIONS = ["tsx", "jsx", "ts", "js"];
1435
+ import path3 from "path";
1436
+ var SOURCE_EXTENSIONS2 = ["tsx", "jsx", "ts", "js"];
1206
1437
  function toAbsolutePattern(basePath, pattern) {
1207
1438
  const normalized = pattern.replace(/\\/g, "/");
1208
- if (path2.isAbsolute(normalized)) {
1439
+ if (path3.isAbsolute(normalized)) {
1209
1440
  return normalized;
1210
1441
  }
1211
- return path2.join(basePath, normalized).replace(/\\/g, "/");
1442
+ return path3.join(basePath, normalized).replace(/\\/g, "/");
1212
1443
  }
1213
1444
  function buildIncludePatterns(basePath, include) {
1214
1445
  if (include && include.length > 0) {
1215
1446
  return include.map((pattern) => toAbsolutePattern(basePath, pattern));
1216
1447
  }
1217
- return SOURCE_EXTENSIONS.map(
1218
- (ext) => path2.join(basePath, `**/*.${ext}`).replace(/\\/g, "/")
1448
+ return SOURCE_EXTENSIONS2.map(
1449
+ (ext) => path3.join(basePath, `**/*.${ext}`).replace(/\\/g, "/")
1219
1450
  );
1220
1451
  }
1221
1452
  function buildIgnorePatterns(exclude) {
@@ -1229,7 +1460,7 @@ function buildIgnorePatterns(exclude) {
1229
1460
  return [...IGNORE_PATTERNS, ...userExcludes];
1230
1461
  }
1231
1462
  async function walkSourceFiles(targetPath, options = {}) {
1232
- const absolutePath = path2.resolve(targetPath);
1463
+ const absolutePath = path3.resolve(targetPath);
1233
1464
  const patterns = buildIncludePatterns(absolutePath, options.include);
1234
1465
  const ignore = buildIgnorePatterns(options.exclude);
1235
1466
  const files = await fg(patterns, {
@@ -1244,7 +1475,7 @@ async function walkSourceFiles(targetPath, options = {}) {
1244
1475
 
1245
1476
  // src/core/scanProject.ts
1246
1477
  import fs3 from "fs/promises";
1247
- import path3 from "path";
1478
+ import path4 from "path";
1248
1479
  async function pathExists2(targetPath) {
1249
1480
  try {
1250
1481
  await fs3.access(targetPath);
@@ -1254,18 +1485,23 @@ async function pathExists2(targetPath) {
1254
1485
  }
1255
1486
  }
1256
1487
  async function scanProject(options) {
1257
- const resolvedPath = path3.resolve(options.targetPath);
1488
+ const resolvedPath = path4.resolve(options.targetPath);
1258
1489
  if (!await pathExists2(resolvedPath)) {
1259
1490
  throw new Error(`Path does not exist: ${resolvedPath}`);
1260
1491
  }
1261
- const files = await walkSourceFiles(resolvedPath, {
1262
- include: options.include,
1263
- exclude: options.exclude
1264
- });
1492
+ let files;
1493
+ if (options.changed !== void 0) {
1494
+ const ref = typeof options.changed === "string" ? options.changed : "HEAD";
1495
+ files = await getChangedFilesInScope(resolvedPath, ref);
1496
+ } else {
1497
+ files = await walkSourceFiles(resolvedPath, {
1498
+ include: options.include,
1499
+ exclude: options.exclude
1500
+ });
1501
+ }
1265
1502
  if (files.length === 0) {
1266
- throw new Error(
1267
- `No source files (.tsx, .jsx, .ts, .js) found in: ${resolvedPath}`
1268
- );
1503
+ const hint = options.changed !== void 0 ? "No changed source files found for the current git diff." : `No source files (.tsx, .jsx, .ts, .js) found in: ${resolvedPath}`;
1504
+ throw new Error(hint);
1269
1505
  }
1270
1506
  const occurrences = [];
1271
1507
  const warnings = [];
@@ -1335,8 +1571,121 @@ async function scanProject(options) {
1335
1571
  };
1336
1572
  }
1337
1573
 
1338
- // src/reporters/consoleReporter.ts
1574
+ // src/commands/init.ts
1575
+ import fs4 from "fs/promises";
1576
+ import path5 from "path";
1339
1577
  import chalk from "chalk";
1578
+ function suggestionToName(suggestion) {
1579
+ return suggestion.replace(/^\./, "");
1580
+ }
1581
+ function detectIncludePattern(targetPath) {
1582
+ const normalized = targetPath.replace(/\\/g, "/");
1583
+ if (normalized.endsWith("/src") || normalized === "src") {
1584
+ return ["src/**/*.tsx", "src/**/*.jsx"];
1585
+ }
1586
+ return ["**/*.tsx", "**/*.jsx"];
1587
+ }
1588
+ function buildConfigFromScan(scanResult, targetPath, options) {
1589
+ const extractable = scanResult.report.stats.topCombinations.filter(
1590
+ (combo) => combo.extractable
1591
+ );
1592
+ const names = {};
1593
+ for (const combo of extractable.slice(0, options.top ?? 10)) {
1594
+ const utilities = [...combo.classes].sort().join(" ");
1595
+ names[utilities] = suggestionToName(combo.suggestion);
1596
+ }
1597
+ return {
1598
+ include: detectIncludePattern(targetPath),
1599
+ exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
1600
+ names: Object.keys(names).length > 0 ? names : void 0,
1601
+ analyze: {
1602
+ minOccurrences: options.minOccurrences ?? 5,
1603
+ top: options.top ?? 10,
1604
+ dedupeSubsets: options.dedupeSubsets ?? true
1605
+ },
1606
+ generate: {
1607
+ minOccurrences: 3,
1608
+ prefix: options.prefix ?? "twu-",
1609
+ output: "src/styles/components.css",
1610
+ top: 20,
1611
+ extractableOnly: true
1612
+ },
1613
+ apply: {
1614
+ minOccurrences: 3,
1615
+ prefix: options.prefix ?? "twu-",
1616
+ output: "src/styles/components.css",
1617
+ prettier: true,
1618
+ extractableOnly: true
1619
+ }
1620
+ };
1621
+ }
1622
+ async function initCommand(targetPath, options = {}) {
1623
+ const resolvedPath = path5.resolve(targetPath);
1624
+ const outputPath = path5.resolve(
1625
+ options.output ?? path5.join(resolvedPath, "tailwind-unwind.config.json")
1626
+ );
1627
+ if (!options.force && await fileExists(outputPath)) {
1628
+ throw new Error(
1629
+ `Config already exists: ${outputPath}. Use --force to overwrite.`
1630
+ );
1631
+ }
1632
+ const scanResult = await scanProject({
1633
+ targetPath: resolvedPath,
1634
+ minOccurrences: options.minOccurrences ?? 5,
1635
+ minSize: options.minSize,
1636
+ maxSize: options.maxSize,
1637
+ topLimit: options.top ?? 10,
1638
+ dedupeSubsets: options.dedupeSubsets ?? true,
1639
+ include: options.include,
1640
+ exclude: options.exclude,
1641
+ extractableMinOccurrences: 3
1642
+ });
1643
+ const config = buildConfigFromScan(scanResult, resolvedPath, options);
1644
+ const json = `${JSON.stringify(config, null, 2)}
1645
+ `;
1646
+ await fs4.mkdir(path5.dirname(outputPath), { recursive: true });
1647
+ await fs4.writeFile(outputPath, json, "utf-8");
1648
+ const extractableCount = scanResult.report.stats.topCombinations.filter(
1649
+ (combo) => combo.extractable
1650
+ ).length;
1651
+ console.log("");
1652
+ console.log(chalk.bold.green("\u2705 Config created"));
1653
+ console.log(chalk.gray(" Output: ") + chalk.white(outputPath));
1654
+ console.log(
1655
+ chalk.gray(" Extractable patterns: ") + chalk.white(String(extractableCount))
1656
+ );
1657
+ console.log(
1658
+ chalk.gray(" Custom names: ") + chalk.white(String(Object.keys(config.names ?? {}).length))
1659
+ );
1660
+ console.log("");
1661
+ console.log(chalk.cyan("Next steps:"));
1662
+ console.log(
1663
+ chalk.white(
1664
+ ` npx tailwind-unwind analyze ${targetPath} --config ${path5.basename(outputPath)}`
1665
+ )
1666
+ );
1667
+ console.log(
1668
+ chalk.white(
1669
+ ` npx tailwind-unwind generate ${targetPath} --config ${path5.basename(outputPath)}`
1670
+ )
1671
+ );
1672
+ console.log("");
1673
+ return {
1674
+ configPath: outputPath,
1675
+ extractablePatterns: extractableCount
1676
+ };
1677
+ }
1678
+ async function fileExists(targetPath) {
1679
+ try {
1680
+ await fs4.access(targetPath);
1681
+ return true;
1682
+ } catch {
1683
+ return false;
1684
+ }
1685
+ }
1686
+
1687
+ // src/reporters/consoleReporter.ts
1688
+ import chalk2 from "chalk";
1340
1689
  function formatNumber(value) {
1341
1690
  return value.toLocaleString("en-US");
1342
1691
  }
@@ -1352,55 +1701,55 @@ function printConsoleReport(report, options = {}) {
1352
1701
  const { stats } = report;
1353
1702
  const topLimit = options.topLimit ?? 10;
1354
1703
  console.log("");
1355
- console.log(chalk.bold.cyan("\u{1F4CA} Tailwind Analysis Report"));
1356
- console.log(chalk.cyan("\u2501".repeat(41)));
1357
- console.log(`Files scanned: ${chalk.white(formatNumber(stats.filesScanned))}`);
1704
+ console.log(chalk2.bold.cyan("\u{1F4CA} Tailwind Analysis Report"));
1705
+ console.log(chalk2.cyan("\u2501".repeat(41)));
1706
+ console.log(`Files scanned: ${chalk2.white(formatNumber(stats.filesScanned))}`);
1358
1707
  console.log(
1359
- `Components with className: ${chalk.white(formatNumber(stats.componentsWithClassName))}`
1708
+ `Components with className: ${chalk2.white(formatNumber(stats.componentsWithClassName))}`
1360
1709
  );
1361
1710
  console.log(
1362
- `Unique class combinations: ${chalk.white(formatNumber(stats.uniqueCombinations))}`
1711
+ `Unique class combinations: ${chalk2.white(formatNumber(stats.uniqueCombinations))}`
1363
1712
  );
1364
1713
  console.log("");
1365
1714
  if (stats.topCombinations.length === 0) {
1366
1715
  console.log(
1367
- chalk.yellow(
1716
+ chalk2.yellow(
1368
1717
  "No frequent class combinations found matching the current filters."
1369
1718
  )
1370
1719
  );
1371
1720
  } else {
1372
1721
  console.log(
1373
- chalk.bold.green(`\u{1F3C6} Top ${Math.min(topLimit, stats.topCombinations.length)} most frequent combinations:`)
1722
+ chalk2.bold.green(`\u{1F3C6} Top ${Math.min(topLimit, stats.topCombinations.length)} most frequent combinations:`)
1374
1723
  );
1375
1724
  console.log("");
1376
1725
  stats.topCombinations.forEach((combo, index) => {
1377
1726
  const displayClasses = normalizeClasses(combo.classes);
1378
1727
  console.log(
1379
- chalk.white(`${index + 1}. `) + chalk.yellow(`"${displayClasses}"`)
1728
+ chalk2.white(`${index + 1}. `) + chalk2.yellow(`"${displayClasses}"`)
1380
1729
  );
1381
1730
  console.log(
1382
- chalk.gray(` Occurrences: `) + chalk.white(String(combo.occurrences))
1731
+ chalk2.gray(` Occurrences: `) + chalk2.white(String(combo.occurrences))
1383
1732
  );
1384
1733
  console.log(
1385
- chalk.gray(` Suggestion: `) + chalk.green(combo.suggestion)
1734
+ chalk2.gray(` Suggestion: `) + chalk2.green(combo.suggestion)
1386
1735
  );
1387
1736
  if (combo.extractable) {
1388
1737
  console.log(
1389
- chalk.gray(` Extractable: `) + chalk.green("yes \u2014 use generate/apply")
1738
+ chalk2.gray(` Extractable: `) + chalk2.green("yes \u2014 use generate/apply")
1390
1739
  );
1391
1740
  } else {
1392
1741
  console.log(
1393
- chalk.gray(` Extractable: `) + chalk.yellow("subset only \u2014 analyze hint")
1742
+ chalk2.gray(` Extractable: `) + chalk2.yellow("subset only \u2014 analyze hint")
1394
1743
  );
1395
1744
  }
1396
1745
  console.log(
1397
- chalk.gray(` Found in: `) + chalk.dim(formatLocations(combo.locations))
1746
+ chalk2.gray(` Found in: `) + chalk2.dim(formatLocations(combo.locations))
1398
1747
  );
1399
1748
  console.log("");
1400
1749
  });
1401
1750
  }
1402
1751
  console.log(
1403
- chalk.magenta(
1752
+ chalk2.magenta(
1404
1753
  `\u{1F4A1} Potential code reduction: ${stats.potentialReductionPercent}%`
1405
1754
  )
1406
1755
  );
@@ -1409,18 +1758,18 @@ function printConsoleReport(report, options = {}) {
1409
1758
  ).length;
1410
1759
  if (extractableCount > 0) {
1411
1760
  console.log(
1412
- chalk.magenta(
1761
+ chalk2.magenta(
1413
1762
  `\u{1F4A1} ${extractableCount} pattern(s) ready for generate/apply`
1414
1763
  )
1415
1764
  );
1416
1765
  }
1417
1766
  console.log(
1418
- chalk.magenta(
1767
+ chalk2.magenta(
1419
1768
  "\u{1F4A1} Generate CSS: npx tailwind-unwind generate <path> --output styles.css"
1420
1769
  )
1421
1770
  );
1422
1771
  console.log(
1423
- chalk.magenta(
1772
+ chalk2.magenta(
1424
1773
  "\u{1F4A1} Apply classes: npx tailwind-unwind apply <path> --output styles.css"
1425
1774
  )
1426
1775
  );
@@ -1433,7 +1782,7 @@ function printJsonReport(report) {
1433
1782
  }
1434
1783
 
1435
1784
  // src/commands/analyze.ts
1436
- import chalk2 from "chalk";
1785
+ import chalk3 from "chalk";
1437
1786
  async function analyzeCommand(targetPath, options = {}) {
1438
1787
  let scanResult;
1439
1788
  try {
@@ -1446,16 +1795,17 @@ async function analyzeCommand(targetPath, options = {}) {
1446
1795
  dedupeSubsets: options.dedupeSubsets,
1447
1796
  include: options.include,
1448
1797
  exclude: options.exclude,
1798
+ changed: options.changed,
1449
1799
  extractableMinOccurrences: 3
1450
1800
  });
1451
1801
  } catch (error) {
1452
1802
  const message = error instanceof Error ? error.message : String(error);
1453
- console.error(chalk2.red(`Error: ${message}`));
1803
+ console.error(chalk3.red(`Error: ${message}`));
1454
1804
  process.exit(1);
1455
1805
  }
1456
1806
  if (options.format !== "json") {
1457
1807
  for (const warning of scanResult.warnings) {
1458
- console.warn(chalk2.yellow(`\u26A0 ${warning}`));
1808
+ console.warn(chalk3.yellow(`\u26A0 ${warning}`));
1459
1809
  }
1460
1810
  }
1461
1811
  const report = scanResult.report;
@@ -1467,11 +1817,93 @@ async function analyzeCommand(targetPath, options = {}) {
1467
1817
  return report;
1468
1818
  }
1469
1819
 
1820
+ // src/analyzer/savings.ts
1821
+ function calculateSavings(replacements) {
1822
+ if (replacements.length === 0) {
1823
+ return {
1824
+ replacementCount: 0,
1825
+ utilityTokensBefore: 0,
1826
+ utilityTokensAfter: 0,
1827
+ tokensSaved: 0,
1828
+ percentReduction: 0
1829
+ };
1830
+ }
1831
+ let utilityTokensBefore = 0;
1832
+ for (const replacement of replacements) {
1833
+ utilityTokensBefore += replacement.from.split(/\s+/).filter(Boolean).length;
1834
+ }
1835
+ const utilityTokensAfter = replacements.length;
1836
+ const tokensSaved = Math.max(0, utilityTokensBefore - utilityTokensAfter);
1837
+ const percentReduction = utilityTokensBefore === 0 ? 0 : Math.round(tokensSaved / utilityTokensBefore * 100);
1838
+ return {
1839
+ replacementCount: replacements.length,
1840
+ utilityTokensBefore,
1841
+ utilityTokensAfter,
1842
+ tokensSaved,
1843
+ percentReduction
1844
+ };
1845
+ }
1846
+
1847
+ // src/codemod/formatSource.ts
1848
+ import { createRequire } from "module";
1849
+ import path6 from "path";
1850
+ var require2 = createRequire(import.meta.url);
1851
+ async function loadPrettier() {
1852
+ try {
1853
+ const prettier = await import("prettier");
1854
+ return prettier;
1855
+ } catch {
1856
+ try {
1857
+ return require2("prettier");
1858
+ } catch {
1859
+ return null;
1860
+ }
1861
+ }
1862
+ }
1863
+ async function formatSource(source, options) {
1864
+ const prettier = await loadPrettier();
1865
+ if (!prettier) {
1866
+ return { source, formatted: false };
1867
+ }
1868
+ try {
1869
+ const config = await prettier.resolveConfig(options.filePath);
1870
+ const formatted = await prettier.format(source, {
1871
+ ...config,
1872
+ filepath: options.filePath
1873
+ });
1874
+ return { source: formatted, formatted: true };
1875
+ } catch {
1876
+ return { source, formatted: false };
1877
+ }
1878
+ }
1879
+ async function formatModifiedFiles(files, sources, cwd = process.cwd()) {
1880
+ const formatted = [];
1881
+ const skipped = [];
1882
+ for (const file of files) {
1883
+ const source = sources.get(file);
1884
+ if (!source) {
1885
+ skipped.push(file);
1886
+ continue;
1887
+ }
1888
+ const result = await formatSource(source, {
1889
+ filePath: path6.resolve(cwd, file),
1890
+ cwd
1891
+ });
1892
+ if (result.formatted) {
1893
+ sources.set(file, result.source);
1894
+ formatted.push(file);
1895
+ } else {
1896
+ skipped.push(file);
1897
+ }
1898
+ }
1899
+ return { formatted, skipped };
1900
+ }
1901
+
1470
1902
  // src/codemod/replaceClassNames.ts
1471
1903
  import babelGenerate from "@babel/generator";
1472
- import babelTraverse2 from "@babel/traverse";
1904
+ import babelTraverse3 from "@babel/traverse";
1473
1905
  import * as t from "@babel/types";
1474
- function resolveTraverse2(module) {
1906
+ function resolveTraverse3(module) {
1475
1907
  if (typeof module === "function") {
1476
1908
  return module;
1477
1909
  }
@@ -1491,7 +1923,7 @@ function resolveGenerator(module) {
1491
1923
  }
1492
1924
  throw new Error("Failed to load @babel/generator");
1493
1925
  }
1494
- var traverse2 = resolveTraverse2(babelTraverse2);
1926
+ var traverse3 = resolveTraverse3(babelTraverse3);
1495
1927
  var generate = resolveGenerator(babelGenerate);
1496
1928
  function isClassMergeCallee2(expression) {
1497
1929
  if (expression.type === "Identifier") {
@@ -1586,16 +2018,54 @@ function tryReplaceMergeCall(call, replacementMap) {
1586
2018
  partial: true
1587
2019
  };
1588
2020
  }
1589
- function tryReplaceClassAttribute(attr, replacementMap) {
2021
+ function tryReplaceTemplateLiteral(attr, replacementMap, registry) {
2022
+ const value = attr.value;
2023
+ if (value?.type !== "JSXExpressionContainer") {
2024
+ return null;
2025
+ }
2026
+ const expression = value.expression;
2027
+ if (expression.type !== "TemplateLiteral" || expression.expressions.length === 0) {
2028
+ return null;
2029
+ }
2030
+ const extracted = extractClassesFromExpression(expression, registry);
2031
+ if (extracted.classes.length === 0) {
2032
+ return null;
2033
+ }
2034
+ const key = normalizeClasses(extracted.classes);
2035
+ const replacement = replacementMap.get(key);
2036
+ if (!replacement) {
2037
+ return null;
2038
+ }
2039
+ const newQuasis = expression.quasis.map((quasi, index) => {
2040
+ if (index !== 0) {
2041
+ return quasi;
2042
+ }
2043
+ const prefix = expression.expressions.length > 0 ? `${replacement} ` : replacement;
2044
+ return t.templateElement(
2045
+ { raw: prefix, cooked: prefix },
2046
+ quasi.tail
2047
+ );
2048
+ });
2049
+ return {
2050
+ expression: t.templateLiteral(newQuasis, [...expression.expressions]),
2051
+ from: key,
2052
+ to: replacement,
2053
+ partial: true
2054
+ };
2055
+ }
2056
+ function tryReplaceClassAttribute(attr, replacementMap, registry) {
1590
2057
  const value = attr.value;
1591
2058
  if (value?.type !== "JSXExpressionContainer") {
1592
2059
  return null;
1593
2060
  }
1594
2061
  const expression = value.expression;
1595
- if (expression.type === "JSXEmptyExpression" || !isClassMergeCall(expression)) {
2062
+ if (expression.type === "JSXEmptyExpression") {
1596
2063
  return null;
1597
2064
  }
1598
- return tryReplaceMergeCall(expression, replacementMap);
2065
+ if (isClassMergeCall(expression)) {
2066
+ return tryReplaceMergeCall(expression, replacementMap);
2067
+ }
2068
+ return tryReplaceTemplateLiteral(attr, replacementMap, registry);
1599
2069
  }
1600
2070
  function replaceClassNamesInSource(source, replacementMap, filePath) {
1601
2071
  const replacements = [];
@@ -1610,16 +2080,21 @@ function replaceClassNamesInSource(source, replacementMap, filePath) {
1610
2080
  const message = error instanceof Error ? error.message : String(error);
1611
2081
  throw new Error(`Failed to parse ${filePath}: ${message}`);
1612
2082
  }
1613
- traverse2(ast, {
1614
- JSXElement(path6) {
1615
- const opening = path6.node.openingElement;
2083
+ const variantRegistry = collectVariantRegistry(ast);
2084
+ traverse3(ast, {
2085
+ JSXElement(path9) {
2086
+ const opening = path9.node.openingElement;
1616
2087
  for (const attr of opening.attributes) {
1617
2088
  if (attr.type !== "JSXAttribute" || !isClassAttribute(attr)) {
1618
2089
  continue;
1619
2090
  }
1620
- const extraction = extractFromJSXAttribute(attr);
2091
+ const extraction = extractFromJSXAttribute(attr, variantRegistry);
1621
2092
  if (!extraction) continue;
1622
- const mergeReplacement = tryReplaceClassAttribute(attr, replacementMap);
2093
+ const mergeReplacement = tryReplaceClassAttribute(
2094
+ attr,
2095
+ replacementMap,
2096
+ variantRegistry
2097
+ );
1623
2098
  if (mergeReplacement) {
1624
2099
  if (mergeReplacement.expression.type === "StringLiteral") {
1625
2100
  setStringClassAttribute(attr, mergeReplacement.expression.value);
@@ -1775,195 +2250,390 @@ function buildComponents(occurrences, options) {
1775
2250
  }
1776
2251
  return { components, css, replacementMap };
1777
2252
  }
2253
+ function buildComponentsFromCombinations(combinations, options) {
2254
+ const { css, components } = generateComponentCss({
2255
+ sourcePath: options.sourcePath,
2256
+ combinations,
2257
+ prefix: options.prefix,
2258
+ names: options.names
2259
+ });
2260
+ const replacementMap = /* @__PURE__ */ new Map();
2261
+ for (const component of components) {
2262
+ const key = [...component.classes].sort().join(" ");
2263
+ replacementMap.set(key, component.className);
2264
+ }
2265
+ return { components, css, replacementMap };
2266
+ }
2267
+
2268
+ // src/core/loadAnalyzeReport.ts
2269
+ import fs5 from "fs/promises";
2270
+ function isAnalysisReport(value) {
2271
+ if (typeof value !== "object" || value === null) {
2272
+ return false;
2273
+ }
2274
+ const report = value;
2275
+ return typeof report.targetPath === "string" && typeof report.stats === "object" && Array.isArray(report.stats.topCombinations);
2276
+ }
2277
+ async function loadExtractableCombinations(reportPath, options = {}) {
2278
+ const raw = await fs5.readFile(reportPath, "utf-8");
2279
+ const parsed = JSON.parse(raw);
2280
+ if (!isAnalysisReport(parsed)) {
2281
+ throw new Error(`Invalid analyze report: ${reportPath}`);
2282
+ }
2283
+ const combinations = parsed.stats.topCombinations.filter(
2284
+ (combo) => options.extractableOnly === false ? true : combo.extractable === true
2285
+ );
2286
+ if (combinations.length === 0) {
2287
+ throw new Error(
2288
+ "No extractable combinations found in report. Re-run analyze or use --extractable-only=false."
2289
+ );
2290
+ }
2291
+ return {
2292
+ targetPath: parsed.targetPath,
2293
+ combinations
2294
+ };
2295
+ }
2296
+
2297
+ // src/reporters/operationJsonReporter.ts
2298
+ function printGenerateJsonReport(report) {
2299
+ console.log(JSON.stringify(report, null, 2));
2300
+ }
2301
+ function printApplyJsonReport(report) {
2302
+ console.log(JSON.stringify(report, null, 2));
2303
+ }
1778
2304
 
1779
2305
  // src/commands/apply.ts
1780
- import fs4 from "fs/promises";
1781
- import path4 from "path";
1782
- import chalk3 from "chalk";
2306
+ import fs6 from "fs/promises";
2307
+ import path7 from "path";
2308
+ import chalk4 from "chalk";
1783
2309
  async function applyCommand(targetPath, options) {
1784
2310
  let scanResult;
1785
2311
  try {
1786
2312
  scanResult = await scanProject({
1787
2313
  targetPath,
1788
2314
  include: options.include,
1789
- exclude: options.exclude
2315
+ exclude: options.exclude,
2316
+ changed: options.changed,
2317
+ extractableMinOccurrences: options.minOccurrences ?? 3
1790
2318
  });
1791
2319
  } catch (error) {
1792
2320
  const message = error instanceof Error ? error.message : String(error);
1793
- console.error(chalk3.red(`Error: ${message}`));
2321
+ console.error(chalk4.red(`Error: ${message}`));
1794
2322
  process.exit(1);
1795
2323
  }
1796
2324
  for (const warning of scanResult.warnings) {
1797
- console.warn(chalk3.yellow(`\u26A0 ${warning}`));
2325
+ if (options.format !== "json") {
2326
+ console.warn(chalk4.yellow(`\u26A0 ${warning}`));
2327
+ }
1798
2328
  }
1799
- const { components, css, replacementMap } = buildComponents(
1800
- scanResult.occurrences,
1801
- {
1802
- sourcePath: scanResult.resolvedPath,
1803
- minOccurrences: options.minOccurrences ?? 3,
1804
- minSize: options.minSize,
1805
- maxSize: options.maxSize,
1806
- topLimit: options.top,
1807
- prefix: options.prefix,
1808
- names: options.names
2329
+ let components;
2330
+ let css;
2331
+ let replacementMap;
2332
+ try {
2333
+ if (options.fromReport) {
2334
+ const loadedReport = await loadExtractableCombinations(options.fromReport, {
2335
+ extractableOnly: options.extractableOnly ?? true
2336
+ });
2337
+ const built = buildComponentsFromCombinations(loadedReport.combinations, {
2338
+ sourcePath: scanResult.resolvedPath,
2339
+ prefix: options.prefix,
2340
+ names: options.names
2341
+ });
2342
+ components = built.components;
2343
+ css = built.css;
2344
+ replacementMap = built.replacementMap;
2345
+ } else if (options.extractableOnly) {
2346
+ const combinations = scanResult.report.stats.topCombinations.filter(
2347
+ (combo) => combo.extractable
2348
+ );
2349
+ const built = buildComponentsFromCombinations(combinations, {
2350
+ sourcePath: scanResult.resolvedPath,
2351
+ prefix: options.prefix,
2352
+ names: options.names
2353
+ });
2354
+ components = built.components;
2355
+ css = built.css;
2356
+ replacementMap = built.replacementMap;
2357
+ } else {
2358
+ const built = buildComponents(scanResult.occurrences, {
2359
+ sourcePath: scanResult.resolvedPath,
2360
+ minOccurrences: options.minOccurrences ?? 3,
2361
+ minSize: options.minSize,
2362
+ maxSize: options.maxSize,
2363
+ topLimit: options.top,
2364
+ prefix: options.prefix,
2365
+ names: options.names
2366
+ });
2367
+ components = built.components;
2368
+ css = built.css;
2369
+ replacementMap = built.replacementMap;
1809
2370
  }
1810
- );
2371
+ } catch (error) {
2372
+ const message = error instanceof Error ? error.message : String(error);
2373
+ console.error(chalk4.red(`Error: ${message}`));
2374
+ process.exit(1);
2375
+ }
1811
2376
  if (components.length === 0) {
1812
2377
  console.error(
1813
- chalk3.yellow(
2378
+ chalk4.yellow(
1814
2379
  "No repeated className sets found. Try lowering --min-occurrences."
1815
2380
  )
1816
2381
  );
1817
2382
  process.exit(1);
1818
2383
  }
1819
- const outputPath = path4.resolve(options.output);
2384
+ const outputPath = path7.resolve(options.output);
1820
2385
  let filesModified = 0;
1821
2386
  let replacementsTotal = 0;
1822
2387
  const allReplacements = [];
1823
2388
  const allSkipped = [];
2389
+ const modifiedSources = /* @__PURE__ */ new Map();
2390
+ const modifiedFiles = [];
1824
2391
  for (const file of scanResult.files) {
1825
- const original = await fs4.readFile(file, "utf-8");
1826
- const result = replaceClassNamesInSource(
2392
+ const original = await fs6.readFile(file, "utf-8");
2393
+ const result2 = replaceClassNamesInSource(
1827
2394
  original,
1828
2395
  replacementMap,
1829
2396
  file
1830
2397
  );
1831
- replacementsTotal += result.replacements.length;
1832
- allReplacements.push(...result.replacements);
1833
- allSkipped.push(...result.skipped);
1834
- if (result.changed) {
2398
+ replacementsTotal += result2.replacements.length;
2399
+ allReplacements.push(...result2.replacements);
2400
+ allSkipped.push(...result2.skipped);
2401
+ if (result2.changed) {
1835
2402
  filesModified += 1;
1836
- if (!options.dryRun) {
1837
- await fs4.writeFile(file, result.source, "utf-8");
1838
- }
2403
+ modifiedSources.set(file, result2.source);
2404
+ modifiedFiles.push(file);
1839
2405
  }
1840
2406
  }
2407
+ let prettierFormatted = [];
2408
+ if (!options.dryRun && options.prettier && modifiedFiles.length > 0) {
2409
+ const formatResult = await formatModifiedFiles(
2410
+ modifiedFiles,
2411
+ modifiedSources,
2412
+ process.cwd()
2413
+ );
2414
+ prettierFormatted = formatResult.formatted;
2415
+ }
1841
2416
  if (!options.dryRun) {
1842
- await fs4.mkdir(path4.dirname(outputPath), { recursive: true });
1843
- await fs4.writeFile(outputPath, css, "utf-8");
2417
+ for (const file of modifiedFiles) {
2418
+ const source = modifiedSources.get(file);
2419
+ if (source) {
2420
+ await fs6.writeFile(file, source, "utf-8");
2421
+ }
2422
+ }
2423
+ await fs6.mkdir(path7.dirname(outputPath), { recursive: true });
2424
+ await fs6.writeFile(outputPath, css, "utf-8");
2425
+ }
2426
+ const savings = calculateSavings(allReplacements);
2427
+ const result = {
2428
+ filesModified,
2429
+ replacementsTotal,
2430
+ outputPath,
2431
+ componentsGenerated: components.length,
2432
+ components,
2433
+ replacements: allReplacements,
2434
+ skipped: allSkipped,
2435
+ prettierFormatted,
2436
+ savings
2437
+ };
2438
+ if (options.format === "json") {
2439
+ printApplyJsonReport({
2440
+ command: "apply",
2441
+ dryRun: Boolean(options.dryRun),
2442
+ outputPath,
2443
+ filesModified,
2444
+ replacementsTotal,
2445
+ componentsGenerated: components.length,
2446
+ components,
2447
+ replacements: allReplacements,
2448
+ skipped: allSkipped,
2449
+ savings
2450
+ });
2451
+ return result;
1844
2452
  }
1845
2453
  console.log("");
1846
2454
  if (options.dryRun) {
1847
- console.log(chalk3.bold.yellow("\u{1F50D} Dry run \u2014 no files were modified"));
2455
+ console.log(chalk4.bold.yellow("\u{1F50D} Dry run \u2014 no files were modified"));
1848
2456
  } else {
1849
- console.log(chalk3.bold.green("\u2705 Classes applied successfully"));
2457
+ console.log(chalk4.bold.green("\u2705 Classes applied successfully"));
1850
2458
  }
1851
- console.log(chalk3.gray(` CSS output: `) + chalk3.white(outputPath));
2459
+ console.log(chalk4.gray(` CSS output: `) + chalk4.white(outputPath));
1852
2460
  console.log(
1853
- chalk3.gray(` Component classes: `) + chalk3.white(String(components.length))
2461
+ chalk4.gray(` Component classes: `) + chalk4.white(String(components.length))
1854
2462
  );
1855
2463
  console.log(
1856
- chalk3.gray(` Files modified: `) + chalk3.white(String(filesModified))
2464
+ chalk4.gray(` Files modified: `) + chalk4.white(String(filesModified))
1857
2465
  );
1858
2466
  console.log(
1859
- chalk3.gray(` Replacements: `) + chalk3.white(String(replacementsTotal))
2467
+ chalk4.gray(` Replacements: `) + chalk4.white(String(replacementsTotal))
1860
2468
  );
2469
+ if (prettierFormatted.length > 0) {
2470
+ console.log(
2471
+ chalk4.gray(` Prettier formatted: `) + chalk4.white(String(prettierFormatted.length))
2472
+ );
2473
+ }
2474
+ if (savings.replacementCount > 0) {
2475
+ console.log("");
2476
+ console.log(chalk4.bold("Savings:"));
2477
+ console.log(
2478
+ chalk4.gray(" Utility tokens before: ") + chalk4.white(String(savings.utilityTokensBefore))
2479
+ );
2480
+ console.log(
2481
+ chalk4.gray(" Utility tokens after: ") + chalk4.white(String(savings.utilityTokensAfter))
2482
+ );
2483
+ console.log(
2484
+ chalk4.gray(" Tokens saved: ") + chalk4.green(String(savings.tokensSaved))
2485
+ );
2486
+ console.log(
2487
+ chalk4.gray(" Reduction: ") + chalk4.green(`${savings.percentReduction}%`)
2488
+ );
2489
+ }
1861
2490
  if (allReplacements.length > 0) {
1862
2491
  console.log("");
1863
- console.log(chalk3.bold("Replacements:"));
2492
+ console.log(chalk4.bold("Replacements:"));
1864
2493
  for (const item of allReplacements) {
1865
2494
  const line = item.line ? `:${item.line}` : "";
1866
- const partialTag = item.partial ? chalk3.dim(" (partial)") : "";
2495
+ const partialTag = item.partial ? chalk4.dim(" (partial)") : "";
1867
2496
  console.log(
1868
- chalk3.gray(` ${item.filePath}${line}`) + chalk3.white(` "${item.from}" `) + chalk3.cyan("\u2192") + chalk3.green(` "${item.to}"`) + partialTag
2497
+ chalk4.gray(` ${item.filePath}${line}`) + chalk4.white(` "${item.from}" `) + chalk4.cyan("\u2192") + chalk4.green(` "${item.to}"`) + partialTag
1869
2498
  );
1870
2499
  }
1871
2500
  }
1872
2501
  if (allSkipped.length > 0) {
1873
2502
  console.log("");
1874
- console.log(
1875
- chalk3.bold.yellow(`Skipped (${allSkipped.length}):`)
1876
- );
2503
+ console.log(chalk4.bold.yellow(`Skipped (${allSkipped.length}):`));
1877
2504
  for (const item of allSkipped) {
1878
2505
  const line = item.line ? `:${item.line}` : "";
1879
2506
  const classes = item.classes.join(" ");
1880
2507
  console.log(
1881
- chalk3.gray(` ${item.filePath}${line}`) + chalk3.yellow(` [${item.reason}]`) + chalk3.dim(` "${classes}"`)
2508
+ chalk4.gray(` ${item.filePath}${line}`) + chalk4.yellow(` [${item.reason}]`) + chalk4.dim(` "${classes}"`)
1882
2509
  );
1883
2510
  }
1884
2511
  }
1885
2512
  console.log("");
1886
2513
  if (!options.dryRun) {
1887
2514
  console.log(
1888
- chalk3.cyan(
1889
- `Import ${path4.basename(outputPath)} in your global CSS if you haven't already.`
2515
+ chalk4.cyan(
2516
+ `Import ${path7.basename(outputPath)} in your global CSS if you haven't already.`
1890
2517
  )
1891
2518
  );
1892
2519
  console.log("");
1893
2520
  }
1894
- return {
1895
- filesModified,
1896
- replacementsTotal,
1897
- outputPath,
1898
- componentsGenerated: components.length
1899
- };
2521
+ return result;
1900
2522
  }
1901
2523
 
1902
2524
  // src/commands/generate.ts
1903
- import fs5 from "fs/promises";
1904
- import path5 from "path";
1905
- import chalk4 from "chalk";
2525
+ import fs7 from "fs/promises";
2526
+ import path8 from "path";
2527
+ import chalk5 from "chalk";
1906
2528
  async function generateCommand(targetPath, options) {
1907
- let scanResult;
2529
+ let scanResult = null;
2530
+ let components;
2531
+ let css;
1908
2532
  try {
1909
- scanResult = await scanProject({
1910
- targetPath,
1911
- include: options.include,
1912
- exclude: options.exclude
1913
- });
2533
+ if (options.fromReport) {
2534
+ const loadedReport = await loadExtractableCombinations(options.fromReport, {
2535
+ extractableOnly: options.extractableOnly ?? true
2536
+ });
2537
+ const built = buildComponentsFromCombinations(loadedReport.combinations, {
2538
+ sourcePath: loadedReport.targetPath || targetPath,
2539
+ prefix: options.prefix,
2540
+ names: options.names
2541
+ });
2542
+ components = built.components;
2543
+ css = built.css;
2544
+ } else {
2545
+ scanResult = await scanProject({
2546
+ targetPath,
2547
+ include: options.include,
2548
+ exclude: options.exclude,
2549
+ changed: options.changed,
2550
+ extractableMinOccurrences: options.minOccurrences ?? 3
2551
+ });
2552
+ if (options.extractableOnly) {
2553
+ const combinations = scanResult.report.stats.topCombinations.filter(
2554
+ (combo) => combo.extractable
2555
+ );
2556
+ const built = buildComponentsFromCombinations(combinations, {
2557
+ sourcePath: scanResult.resolvedPath,
2558
+ prefix: options.prefix,
2559
+ names: options.names
2560
+ });
2561
+ components = built.components;
2562
+ css = built.css;
2563
+ } else {
2564
+ const built = buildComponents(scanResult.occurrences, {
2565
+ sourcePath: scanResult.resolvedPath,
2566
+ minOccurrences: options.minOccurrences ?? 3,
2567
+ minSize: options.minSize,
2568
+ maxSize: options.maxSize,
2569
+ topLimit: options.top,
2570
+ prefix: options.prefix,
2571
+ names: options.names
2572
+ });
2573
+ components = built.components;
2574
+ css = built.css;
2575
+ }
2576
+ }
1914
2577
  } catch (error) {
1915
2578
  const message = error instanceof Error ? error.message : String(error);
1916
- console.error(chalk4.red(`Error: ${message}`));
2579
+ console.error(chalk5.red(`Error: ${message}`));
1917
2580
  process.exit(1);
1918
2581
  }
1919
- for (const warning of scanResult.warnings) {
1920
- console.warn(chalk4.yellow(`\u26A0 ${warning}`));
2582
+ if (scanResult) {
2583
+ for (const warning of scanResult.warnings) {
2584
+ if (options.format !== "json") {
2585
+ console.warn(chalk5.yellow(`\u26A0 ${warning}`));
2586
+ }
2587
+ }
2588
+ }
2589
+ const outputPath = path8.resolve(options.output);
2590
+ await fs7.mkdir(path8.dirname(outputPath), { recursive: true });
2591
+ await fs7.writeFile(outputPath, css, "utf-8");
2592
+ const result = {
2593
+ outputPath,
2594
+ componentsGenerated: components.length,
2595
+ components,
2596
+ report: scanResult?.report ?? null
2597
+ };
2598
+ if (options.format === "json") {
2599
+ printGenerateJsonReport({
2600
+ command: "generate",
2601
+ outputPath,
2602
+ componentsGenerated: components.length,
2603
+ components,
2604
+ cssWritten: true
2605
+ });
2606
+ return result;
1921
2607
  }
1922
- const { css, components } = buildComponents(scanResult.occurrences, {
1923
- sourcePath: scanResult.resolvedPath,
1924
- minOccurrences: options.minOccurrences ?? 3,
1925
- minSize: options.minSize,
1926
- maxSize: options.maxSize,
1927
- topLimit: options.top,
1928
- prefix: options.prefix,
1929
- names: options.names
1930
- });
1931
- const outputPath = path5.resolve(options.output);
1932
- await fs5.mkdir(path5.dirname(outputPath), { recursive: true });
1933
- await fs5.writeFile(outputPath, css, "utf-8");
1934
2608
  console.log("");
1935
- console.log(chalk4.bold.green("\u2705 CSS generated successfully"));
1936
- console.log(chalk4.gray(` Output: `) + chalk4.white(outputPath));
2609
+ console.log(chalk5.bold.green("\u2705 CSS generated successfully"));
2610
+ console.log(chalk5.gray(` Output: `) + chalk5.white(outputPath));
1937
2611
  console.log(
1938
- chalk4.gray(` Components: `) + chalk4.white(String(components.length))
2612
+ chalk5.gray(` Components: `) + chalk5.white(String(components.length))
1939
2613
  );
1940
2614
  if (components.length > 0) {
1941
2615
  console.log("");
1942
- console.log(chalk4.bold("Generated classes:"));
2616
+ console.log(chalk5.bold("Generated classes:"));
1943
2617
  for (const component of components) {
1944
2618
  console.log(
1945
- chalk4.green(` .${component.className}`) + chalk4.gray(` \u2014 ${component.occurrences} occurrences, `) + chalk4.dim(component.classes.join(" "))
2619
+ chalk5.green(` .${component.className}`) + chalk5.gray(` \u2014 ${component.occurrences} occurrences, `) + chalk5.dim(component.classes.join(" "))
1946
2620
  );
1947
2621
  }
1948
2622
  console.log("");
1949
2623
  console.log(
1950
- chalk4.cyan(
2624
+ chalk5.cyan(
1951
2625
  "Run apply to replace className strings: npx tailwind-unwind apply <path> --output styles.css"
1952
2626
  )
1953
2627
  );
1954
2628
  } else {
1955
2629
  console.log(
1956
- chalk4.yellow(
2630
+ chalk5.yellow(
1957
2631
  "\nNo repeated className sets matched the filters. Try lowering --min-occurrences."
1958
2632
  )
1959
2633
  );
1960
2634
  }
1961
2635
  console.log("");
1962
- return {
1963
- outputPath,
1964
- componentsGenerated: components.length,
1965
- report: scanResult.report
1966
- };
2636
+ return result;
1967
2637
  }
1968
2638
 
1969
2639
  export {
@@ -1979,6 +2649,10 @@ export {
1979
2649
  findFrequentPatterns,
1980
2650
  findRepeatedClassSets,
1981
2651
  calculatePotentialReduction,
2652
+ VARIANT_CALLEES,
2653
+ isVariantCallee,
2654
+ extractClassesFromVariantCall,
2655
+ collectVariantRegistry,
1982
2656
  CLASS_MERGE_CALLEES,
1983
2657
  extractClassesFromExpression,
1984
2658
  isClassAttribute,
@@ -1988,11 +2662,18 @@ export {
1988
2662
  parseFile,
1989
2663
  IGNORED_DIRECTORIES,
1990
2664
  IGNORE_PATTERNS,
2665
+ getChangedSourceFiles,
2666
+ isGitRepository,
2667
+ getChangedFilesInScope,
1991
2668
  walkSourceFiles,
1992
2669
  scanProject,
2670
+ initCommand,
1993
2671
  printConsoleReport,
1994
2672
  printJsonReport,
1995
2673
  analyzeCommand,
2674
+ calculateSavings,
2675
+ formatSource,
2676
+ formatModifiedFiles,
1996
2677
  replaceClassNamesInSource,
1997
2678
  DEFAULT_CLASS_PREFIX,
1998
2679
  normalizeClassPrefix,
@@ -2000,7 +2681,11 @@ export {
2000
2681
  assignComponentClassNames,
2001
2682
  generateComponentCss,
2002
2683
  buildComponents,
2684
+ buildComponentsFromCombinations,
2685
+ loadExtractableCombinations,
2686
+ printGenerateJsonReport,
2687
+ printApplyJsonReport,
2003
2688
  applyCommand,
2004
2689
  generateCommand
2005
2690
  };
2006
- //# sourceMappingURL=chunk-FASYIEVZ.js.map
2691
+ //# sourceMappingURL=chunk-UXXIEFP4.js.map