tailwind-unwind 0.3.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.
@@ -35,28 +35,28 @@ var KNOWN_COMMAND_KEYS = /* @__PURE__ */ new Set([
35
35
  function isRecord(value) {
36
36
  return typeof value === "object" && value !== null && !Array.isArray(value);
37
37
  }
38
- function assertPositiveNumber(value, path7, errors) {
38
+ function assertPositiveNumber(value, path9, errors) {
39
39
  if (value === void 0) {
40
40
  return;
41
41
  }
42
42
  if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
43
- errors.push(`${path7} must be a positive number`);
43
+ errors.push(`${path9} must be a positive number`);
44
44
  }
45
45
  }
46
- function assertBoolean(value, path7, errors) {
46
+ function assertBoolean(value, path9, errors) {
47
47
  if (value === void 0) {
48
48
  return;
49
49
  }
50
50
  if (typeof value !== "boolean") {
51
- errors.push(`${path7} must be a boolean`);
51
+ errors.push(`${path9} must be a boolean`);
52
52
  }
53
53
  }
54
- function assertStringArray(value, path7, errors) {
54
+ function assertStringArray(value, path9, errors) {
55
55
  if (value === void 0) {
56
56
  return;
57
57
  }
58
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`);
59
+ errors.push(`${path9} must be an array of non-empty strings`);
60
60
  }
61
61
  }
62
62
  function validateCommandSection(value, section, errors) {
@@ -169,6 +169,7 @@ import fs from "fs/promises";
169
169
  import path from "path";
170
170
  import { pathToFileURL } from "url";
171
171
  var CONFIG_FILENAMES = [
172
+ "tailwind-unwind.config.ts",
172
173
  "tailwind-unwind.config.js",
173
174
  "tailwind-unwind.config.mjs",
174
175
  "tailwind-unwind.config.cjs",
@@ -263,7 +264,7 @@ function normalizeLoadedConfig(raw) {
263
264
  }
264
265
  function mergeCommandConfig(command, fileConfig) {
265
266
  const { analyze, generate: generate2, apply, ...root } = fileConfig;
266
- const commandSection = command === "analyze" ? analyze : command === "generate" ? generate2 : apply;
267
+ const commandSection = command === "analyze" ? analyze : command === "generate" ? generate2 : command === "apply" ? apply : void 0;
267
268
  return {
268
269
  ...root,
269
270
  ...commandSection
@@ -323,6 +324,11 @@ async function resolveConfigFile(explicitPath, searchRoots) {
323
324
  return null;
324
325
  }
325
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
+ }
326
332
  const moduleUrl = pathToFileURL(configPath).href;
327
333
  const imported = await import(moduleUrl);
328
334
  return imported;
@@ -1048,8 +1054,8 @@ var traverse = resolveTraverse(babelTraverse);
1048
1054
  function collectVariantRegistry(ast) {
1049
1055
  const registry = /* @__PURE__ */ new Map();
1050
1056
  traverse(ast, {
1051
- VariableDeclarator(path7) {
1052
- registerVariantDeclarator(path7.node, registry);
1057
+ VariableDeclarator(path9) {
1058
+ registerVariantDeclarator(path9.node, registry);
1053
1059
  }
1054
1060
  });
1055
1061
  return registry;
@@ -1286,8 +1292,8 @@ function resolveTraverse2(module) {
1286
1292
  throw new Error("Failed to load @babel/traverse");
1287
1293
  }
1288
1294
  var traverse2 = resolveTraverse2(babelTraverse2);
1289
- function isJSXElementWithClassAttribute(path7) {
1290
- const opening = path7.node.openingElement;
1295
+ function isJSXElementWithClassAttribute(path9) {
1296
+ const opening = path9.node.openingElement;
1291
1297
  return opening.attributes.some(
1292
1298
  (attr) => attr.type === "JSXAttribute" && isClassAttribute(attr)
1293
1299
  );
@@ -1297,11 +1303,11 @@ function collectExtractionsFromAst(ast, filePath) {
1297
1303
  const warnings = [];
1298
1304
  const variantRegistry = collectVariantRegistry(ast);
1299
1305
  traverse2(ast, {
1300
- JSXElement(path7) {
1301
- if (!isJSXElementWithClassAttribute(path7)) {
1306
+ JSXElement(path9) {
1307
+ if (!isJSXElementWithClassAttribute(path9)) {
1302
1308
  return;
1303
1309
  }
1304
- const opening = path7.node.openingElement;
1310
+ const opening = path9.node.openingElement;
1305
1311
  for (const attr of opening.attributes) {
1306
1312
  if (attr.type !== "JSXAttribute") continue;
1307
1313
  const extraction = extractFromJSXAttribute(attr, variantRegistry);
@@ -1352,23 +1358,95 @@ var IGNORE_PATTERNS = IGNORED_DIRECTORIES.map(
1352
1358
  (dir) => `**/${dir}/**`
1353
1359
  );
1354
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
+
1355
1433
  // src/scanner/fileWalker.ts
1356
1434
  import fg from "fast-glob";
1357
- import path2 from "path";
1358
- var SOURCE_EXTENSIONS = ["tsx", "jsx", "ts", "js"];
1435
+ import path3 from "path";
1436
+ var SOURCE_EXTENSIONS2 = ["tsx", "jsx", "ts", "js"];
1359
1437
  function toAbsolutePattern(basePath, pattern) {
1360
1438
  const normalized = pattern.replace(/\\/g, "/");
1361
- if (path2.isAbsolute(normalized)) {
1439
+ if (path3.isAbsolute(normalized)) {
1362
1440
  return normalized;
1363
1441
  }
1364
- return path2.join(basePath, normalized).replace(/\\/g, "/");
1442
+ return path3.join(basePath, normalized).replace(/\\/g, "/");
1365
1443
  }
1366
1444
  function buildIncludePatterns(basePath, include) {
1367
1445
  if (include && include.length > 0) {
1368
1446
  return include.map((pattern) => toAbsolutePattern(basePath, pattern));
1369
1447
  }
1370
- return SOURCE_EXTENSIONS.map(
1371
- (ext) => path2.join(basePath, `**/*.${ext}`).replace(/\\/g, "/")
1448
+ return SOURCE_EXTENSIONS2.map(
1449
+ (ext) => path3.join(basePath, `**/*.${ext}`).replace(/\\/g, "/")
1372
1450
  );
1373
1451
  }
1374
1452
  function buildIgnorePatterns(exclude) {
@@ -1382,7 +1460,7 @@ function buildIgnorePatterns(exclude) {
1382
1460
  return [...IGNORE_PATTERNS, ...userExcludes];
1383
1461
  }
1384
1462
  async function walkSourceFiles(targetPath, options = {}) {
1385
- const absolutePath = path2.resolve(targetPath);
1463
+ const absolutePath = path3.resolve(targetPath);
1386
1464
  const patterns = buildIncludePatterns(absolutePath, options.include);
1387
1465
  const ignore = buildIgnorePatterns(options.exclude);
1388
1466
  const files = await fg(patterns, {
@@ -1397,7 +1475,7 @@ async function walkSourceFiles(targetPath, options = {}) {
1397
1475
 
1398
1476
  // src/core/scanProject.ts
1399
1477
  import fs3 from "fs/promises";
1400
- import path3 from "path";
1478
+ import path4 from "path";
1401
1479
  async function pathExists2(targetPath) {
1402
1480
  try {
1403
1481
  await fs3.access(targetPath);
@@ -1407,18 +1485,23 @@ async function pathExists2(targetPath) {
1407
1485
  }
1408
1486
  }
1409
1487
  async function scanProject(options) {
1410
- const resolvedPath = path3.resolve(options.targetPath);
1488
+ const resolvedPath = path4.resolve(options.targetPath);
1411
1489
  if (!await pathExists2(resolvedPath)) {
1412
1490
  throw new Error(`Path does not exist: ${resolvedPath}`);
1413
1491
  }
1414
- const files = await walkSourceFiles(resolvedPath, {
1415
- include: options.include,
1416
- exclude: options.exclude
1417
- });
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
+ }
1418
1502
  if (files.length === 0) {
1419
- throw new Error(
1420
- `No source files (.tsx, .jsx, .ts, .js) found in: ${resolvedPath}`
1421
- );
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);
1422
1505
  }
1423
1506
  const occurrences = [];
1424
1507
  const warnings = [];
@@ -1488,8 +1571,121 @@ async function scanProject(options) {
1488
1571
  };
1489
1572
  }
1490
1573
 
1491
- // src/reporters/consoleReporter.ts
1574
+ // src/commands/init.ts
1575
+ import fs4 from "fs/promises";
1576
+ import path5 from "path";
1492
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";
1493
1689
  function formatNumber(value) {
1494
1690
  return value.toLocaleString("en-US");
1495
1691
  }
@@ -1505,55 +1701,55 @@ function printConsoleReport(report, options = {}) {
1505
1701
  const { stats } = report;
1506
1702
  const topLimit = options.topLimit ?? 10;
1507
1703
  console.log("");
1508
- console.log(chalk.bold.cyan("\u{1F4CA} Tailwind Analysis Report"));
1509
- console.log(chalk.cyan("\u2501".repeat(41)));
1510
- 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))}`);
1511
1707
  console.log(
1512
- `Components with className: ${chalk.white(formatNumber(stats.componentsWithClassName))}`
1708
+ `Components with className: ${chalk2.white(formatNumber(stats.componentsWithClassName))}`
1513
1709
  );
1514
1710
  console.log(
1515
- `Unique class combinations: ${chalk.white(formatNumber(stats.uniqueCombinations))}`
1711
+ `Unique class combinations: ${chalk2.white(formatNumber(stats.uniqueCombinations))}`
1516
1712
  );
1517
1713
  console.log("");
1518
1714
  if (stats.topCombinations.length === 0) {
1519
1715
  console.log(
1520
- chalk.yellow(
1716
+ chalk2.yellow(
1521
1717
  "No frequent class combinations found matching the current filters."
1522
1718
  )
1523
1719
  );
1524
1720
  } else {
1525
1721
  console.log(
1526
- 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:`)
1527
1723
  );
1528
1724
  console.log("");
1529
1725
  stats.topCombinations.forEach((combo, index) => {
1530
1726
  const displayClasses = normalizeClasses(combo.classes);
1531
1727
  console.log(
1532
- chalk.white(`${index + 1}. `) + chalk.yellow(`"${displayClasses}"`)
1728
+ chalk2.white(`${index + 1}. `) + chalk2.yellow(`"${displayClasses}"`)
1533
1729
  );
1534
1730
  console.log(
1535
- chalk.gray(` Occurrences: `) + chalk.white(String(combo.occurrences))
1731
+ chalk2.gray(` Occurrences: `) + chalk2.white(String(combo.occurrences))
1536
1732
  );
1537
1733
  console.log(
1538
- chalk.gray(` Suggestion: `) + chalk.green(combo.suggestion)
1734
+ chalk2.gray(` Suggestion: `) + chalk2.green(combo.suggestion)
1539
1735
  );
1540
1736
  if (combo.extractable) {
1541
1737
  console.log(
1542
- chalk.gray(` Extractable: `) + chalk.green("yes \u2014 use generate/apply")
1738
+ chalk2.gray(` Extractable: `) + chalk2.green("yes \u2014 use generate/apply")
1543
1739
  );
1544
1740
  } else {
1545
1741
  console.log(
1546
- chalk.gray(` Extractable: `) + chalk.yellow("subset only \u2014 analyze hint")
1742
+ chalk2.gray(` Extractable: `) + chalk2.yellow("subset only \u2014 analyze hint")
1547
1743
  );
1548
1744
  }
1549
1745
  console.log(
1550
- chalk.gray(` Found in: `) + chalk.dim(formatLocations(combo.locations))
1746
+ chalk2.gray(` Found in: `) + chalk2.dim(formatLocations(combo.locations))
1551
1747
  );
1552
1748
  console.log("");
1553
1749
  });
1554
1750
  }
1555
1751
  console.log(
1556
- chalk.magenta(
1752
+ chalk2.magenta(
1557
1753
  `\u{1F4A1} Potential code reduction: ${stats.potentialReductionPercent}%`
1558
1754
  )
1559
1755
  );
@@ -1562,18 +1758,18 @@ function printConsoleReport(report, options = {}) {
1562
1758
  ).length;
1563
1759
  if (extractableCount > 0) {
1564
1760
  console.log(
1565
- chalk.magenta(
1761
+ chalk2.magenta(
1566
1762
  `\u{1F4A1} ${extractableCount} pattern(s) ready for generate/apply`
1567
1763
  )
1568
1764
  );
1569
1765
  }
1570
1766
  console.log(
1571
- chalk.magenta(
1767
+ chalk2.magenta(
1572
1768
  "\u{1F4A1} Generate CSS: npx tailwind-unwind generate <path> --output styles.css"
1573
1769
  )
1574
1770
  );
1575
1771
  console.log(
1576
- chalk.magenta(
1772
+ chalk2.magenta(
1577
1773
  "\u{1F4A1} Apply classes: npx tailwind-unwind apply <path> --output styles.css"
1578
1774
  )
1579
1775
  );
@@ -1586,7 +1782,7 @@ function printJsonReport(report) {
1586
1782
  }
1587
1783
 
1588
1784
  // src/commands/analyze.ts
1589
- import chalk2 from "chalk";
1785
+ import chalk3 from "chalk";
1590
1786
  async function analyzeCommand(targetPath, options = {}) {
1591
1787
  let scanResult;
1592
1788
  try {
@@ -1599,16 +1795,17 @@ async function analyzeCommand(targetPath, options = {}) {
1599
1795
  dedupeSubsets: options.dedupeSubsets,
1600
1796
  include: options.include,
1601
1797
  exclude: options.exclude,
1798
+ changed: options.changed,
1602
1799
  extractableMinOccurrences: 3
1603
1800
  });
1604
1801
  } catch (error) {
1605
1802
  const message = error instanceof Error ? error.message : String(error);
1606
- console.error(chalk2.red(`Error: ${message}`));
1803
+ console.error(chalk3.red(`Error: ${message}`));
1607
1804
  process.exit(1);
1608
1805
  }
1609
1806
  if (options.format !== "json") {
1610
1807
  for (const warning of scanResult.warnings) {
1611
- console.warn(chalk2.yellow(`\u26A0 ${warning}`));
1808
+ console.warn(chalk3.yellow(`\u26A0 ${warning}`));
1612
1809
  }
1613
1810
  }
1614
1811
  const report = scanResult.report;
@@ -1620,9 +1817,36 @@ async function analyzeCommand(targetPath, options = {}) {
1620
1817
  return report;
1621
1818
  }
1622
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
+
1623
1847
  // src/codemod/formatSource.ts
1624
1848
  import { createRequire } from "module";
1625
- import path4 from "path";
1849
+ import path6 from "path";
1626
1850
  var require2 = createRequire(import.meta.url);
1627
1851
  async function loadPrettier() {
1628
1852
  try {
@@ -1662,7 +1886,7 @@ async function formatModifiedFiles(files, sources, cwd = process.cwd()) {
1662
1886
  continue;
1663
1887
  }
1664
1888
  const result = await formatSource(source, {
1665
- filePath: path4.resolve(cwd, file),
1889
+ filePath: path6.resolve(cwd, file),
1666
1890
  cwd
1667
1891
  });
1668
1892
  if (result.formatted) {
@@ -1858,8 +2082,8 @@ function replaceClassNamesInSource(source, replacementMap, filePath) {
1858
2082
  }
1859
2083
  const variantRegistry = collectVariantRegistry(ast);
1860
2084
  traverse3(ast, {
1861
- JSXElement(path7) {
1862
- const opening = path7.node.openingElement;
2085
+ JSXElement(path9) {
2086
+ const opening = path9.node.openingElement;
1863
2087
  for (const attr of opening.attributes) {
1864
2088
  if (attr.type !== "JSXAttribute" || !isClassAttribute(attr)) {
1865
2089
  continue;
@@ -2042,7 +2266,7 @@ function buildComponentsFromCombinations(combinations, options) {
2042
2266
  }
2043
2267
 
2044
2268
  // src/core/loadAnalyzeReport.ts
2045
- import fs4 from "fs/promises";
2269
+ import fs5 from "fs/promises";
2046
2270
  function isAnalysisReport(value) {
2047
2271
  if (typeof value !== "object" || value === null) {
2048
2272
  return false;
@@ -2051,7 +2275,7 @@ function isAnalysisReport(value) {
2051
2275
  return typeof report.targetPath === "string" && typeof report.stats === "object" && Array.isArray(report.stats.topCombinations);
2052
2276
  }
2053
2277
  async function loadExtractableCombinations(reportPath, options = {}) {
2054
- const raw = await fs4.readFile(reportPath, "utf-8");
2278
+ const raw = await fs5.readFile(reportPath, "utf-8");
2055
2279
  const parsed = JSON.parse(raw);
2056
2280
  if (!isAnalysisReport(parsed)) {
2057
2281
  throw new Error(`Invalid analyze report: ${reportPath}`);
@@ -2079,9 +2303,9 @@ function printApplyJsonReport(report) {
2079
2303
  }
2080
2304
 
2081
2305
  // src/commands/apply.ts
2082
- import fs5 from "fs/promises";
2083
- import path5 from "path";
2084
- import chalk3 from "chalk";
2306
+ import fs6 from "fs/promises";
2307
+ import path7 from "path";
2308
+ import chalk4 from "chalk";
2085
2309
  async function applyCommand(targetPath, options) {
2086
2310
  let scanResult;
2087
2311
  try {
@@ -2089,16 +2313,17 @@ async function applyCommand(targetPath, options) {
2089
2313
  targetPath,
2090
2314
  include: options.include,
2091
2315
  exclude: options.exclude,
2316
+ changed: options.changed,
2092
2317
  extractableMinOccurrences: options.minOccurrences ?? 3
2093
2318
  });
2094
2319
  } catch (error) {
2095
2320
  const message = error instanceof Error ? error.message : String(error);
2096
- console.error(chalk3.red(`Error: ${message}`));
2321
+ console.error(chalk4.red(`Error: ${message}`));
2097
2322
  process.exit(1);
2098
2323
  }
2099
2324
  for (const warning of scanResult.warnings) {
2100
2325
  if (options.format !== "json") {
2101
- console.warn(chalk3.yellow(`\u26A0 ${warning}`));
2326
+ console.warn(chalk4.yellow(`\u26A0 ${warning}`));
2102
2327
  }
2103
2328
  }
2104
2329
  let components;
@@ -2145,18 +2370,18 @@ async function applyCommand(targetPath, options) {
2145
2370
  }
2146
2371
  } catch (error) {
2147
2372
  const message = error instanceof Error ? error.message : String(error);
2148
- console.error(chalk3.red(`Error: ${message}`));
2373
+ console.error(chalk4.red(`Error: ${message}`));
2149
2374
  process.exit(1);
2150
2375
  }
2151
2376
  if (components.length === 0) {
2152
2377
  console.error(
2153
- chalk3.yellow(
2378
+ chalk4.yellow(
2154
2379
  "No repeated className sets found. Try lowering --min-occurrences."
2155
2380
  )
2156
2381
  );
2157
2382
  process.exit(1);
2158
2383
  }
2159
- const outputPath = path5.resolve(options.output);
2384
+ const outputPath = path7.resolve(options.output);
2160
2385
  let filesModified = 0;
2161
2386
  let replacementsTotal = 0;
2162
2387
  const allReplacements = [];
@@ -2164,7 +2389,7 @@ async function applyCommand(targetPath, options) {
2164
2389
  const modifiedSources = /* @__PURE__ */ new Map();
2165
2390
  const modifiedFiles = [];
2166
2391
  for (const file of scanResult.files) {
2167
- const original = await fs5.readFile(file, "utf-8");
2392
+ const original = await fs6.readFile(file, "utf-8");
2168
2393
  const result2 = replaceClassNamesInSource(
2169
2394
  original,
2170
2395
  replacementMap,
@@ -2192,12 +2417,13 @@ async function applyCommand(targetPath, options) {
2192
2417
  for (const file of modifiedFiles) {
2193
2418
  const source = modifiedSources.get(file);
2194
2419
  if (source) {
2195
- await fs5.writeFile(file, source, "utf-8");
2420
+ await fs6.writeFile(file, source, "utf-8");
2196
2421
  }
2197
2422
  }
2198
- await fs5.mkdir(path5.dirname(outputPath), { recursive: true });
2199
- await fs5.writeFile(outputPath, css, "utf-8");
2423
+ await fs6.mkdir(path7.dirname(outputPath), { recursive: true });
2424
+ await fs6.writeFile(outputPath, css, "utf-8");
2200
2425
  }
2426
+ const savings = calculateSavings(allReplacements);
2201
2427
  const result = {
2202
2428
  filesModified,
2203
2429
  replacementsTotal,
@@ -2206,7 +2432,8 @@ async function applyCommand(targetPath, options) {
2206
2432
  components,
2207
2433
  replacements: allReplacements,
2208
2434
  skipped: allSkipped,
2209
- prettierFormatted
2435
+ prettierFormatted,
2436
+ savings
2210
2437
  };
2211
2438
  if (options.format === "json") {
2212
2439
  printApplyJsonReport({
@@ -2218,58 +2445,75 @@ async function applyCommand(targetPath, options) {
2218
2445
  componentsGenerated: components.length,
2219
2446
  components,
2220
2447
  replacements: allReplacements,
2221
- skipped: allSkipped
2448
+ skipped: allSkipped,
2449
+ savings
2222
2450
  });
2223
2451
  return result;
2224
2452
  }
2225
2453
  console.log("");
2226
2454
  if (options.dryRun) {
2227
- 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"));
2228
2456
  } else {
2229
- console.log(chalk3.bold.green("\u2705 Classes applied successfully"));
2457
+ console.log(chalk4.bold.green("\u2705 Classes applied successfully"));
2230
2458
  }
2231
- console.log(chalk3.gray(` CSS output: `) + chalk3.white(outputPath));
2459
+ console.log(chalk4.gray(` CSS output: `) + chalk4.white(outputPath));
2232
2460
  console.log(
2233
- chalk3.gray(` Component classes: `) + chalk3.white(String(components.length))
2461
+ chalk4.gray(` Component classes: `) + chalk4.white(String(components.length))
2234
2462
  );
2235
2463
  console.log(
2236
- chalk3.gray(` Files modified: `) + chalk3.white(String(filesModified))
2464
+ chalk4.gray(` Files modified: `) + chalk4.white(String(filesModified))
2237
2465
  );
2238
2466
  console.log(
2239
- chalk3.gray(` Replacements: `) + chalk3.white(String(replacementsTotal))
2467
+ chalk4.gray(` Replacements: `) + chalk4.white(String(replacementsTotal))
2240
2468
  );
2241
2469
  if (prettierFormatted.length > 0) {
2242
2470
  console.log(
2243
- chalk3.gray(` Prettier formatted: `) + chalk3.white(String(prettierFormatted.length))
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}%`)
2244
2488
  );
2245
2489
  }
2246
2490
  if (allReplacements.length > 0) {
2247
2491
  console.log("");
2248
- console.log(chalk3.bold("Replacements:"));
2492
+ console.log(chalk4.bold("Replacements:"));
2249
2493
  for (const item of allReplacements) {
2250
2494
  const line = item.line ? `:${item.line}` : "";
2251
- const partialTag = item.partial ? chalk3.dim(" (partial)") : "";
2495
+ const partialTag = item.partial ? chalk4.dim(" (partial)") : "";
2252
2496
  console.log(
2253
- 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
2254
2498
  );
2255
2499
  }
2256
2500
  }
2257
2501
  if (allSkipped.length > 0) {
2258
2502
  console.log("");
2259
- console.log(chalk3.bold.yellow(`Skipped (${allSkipped.length}):`));
2503
+ console.log(chalk4.bold.yellow(`Skipped (${allSkipped.length}):`));
2260
2504
  for (const item of allSkipped) {
2261
2505
  const line = item.line ? `:${item.line}` : "";
2262
2506
  const classes = item.classes.join(" ");
2263
2507
  console.log(
2264
- 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}"`)
2265
2509
  );
2266
2510
  }
2267
2511
  }
2268
2512
  console.log("");
2269
2513
  if (!options.dryRun) {
2270
2514
  console.log(
2271
- chalk3.cyan(
2272
- `Import ${path5.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.`
2273
2517
  )
2274
2518
  );
2275
2519
  console.log("");
@@ -2278,9 +2522,9 @@ async function applyCommand(targetPath, options) {
2278
2522
  }
2279
2523
 
2280
2524
  // src/commands/generate.ts
2281
- import fs6 from "fs/promises";
2282
- import path6 from "path";
2283
- import chalk4 from "chalk";
2525
+ import fs7 from "fs/promises";
2526
+ import path8 from "path";
2527
+ import chalk5 from "chalk";
2284
2528
  async function generateCommand(targetPath, options) {
2285
2529
  let scanResult = null;
2286
2530
  let components;
@@ -2302,6 +2546,7 @@ async function generateCommand(targetPath, options) {
2302
2546
  targetPath,
2303
2547
  include: options.include,
2304
2548
  exclude: options.exclude,
2549
+ changed: options.changed,
2305
2550
  extractableMinOccurrences: options.minOccurrences ?? 3
2306
2551
  });
2307
2552
  if (options.extractableOnly) {
@@ -2331,19 +2576,19 @@ async function generateCommand(targetPath, options) {
2331
2576
  }
2332
2577
  } catch (error) {
2333
2578
  const message = error instanceof Error ? error.message : String(error);
2334
- console.error(chalk4.red(`Error: ${message}`));
2579
+ console.error(chalk5.red(`Error: ${message}`));
2335
2580
  process.exit(1);
2336
2581
  }
2337
2582
  if (scanResult) {
2338
2583
  for (const warning of scanResult.warnings) {
2339
2584
  if (options.format !== "json") {
2340
- console.warn(chalk4.yellow(`\u26A0 ${warning}`));
2585
+ console.warn(chalk5.yellow(`\u26A0 ${warning}`));
2341
2586
  }
2342
2587
  }
2343
2588
  }
2344
- const outputPath = path6.resolve(options.output);
2345
- await fs6.mkdir(path6.dirname(outputPath), { recursive: true });
2346
- await fs6.writeFile(outputPath, css, "utf-8");
2589
+ const outputPath = path8.resolve(options.output);
2590
+ await fs7.mkdir(path8.dirname(outputPath), { recursive: true });
2591
+ await fs7.writeFile(outputPath, css, "utf-8");
2347
2592
  const result = {
2348
2593
  outputPath,
2349
2594
  componentsGenerated: components.length,
@@ -2361,28 +2606,28 @@ async function generateCommand(targetPath, options) {
2361
2606
  return result;
2362
2607
  }
2363
2608
  console.log("");
2364
- console.log(chalk4.bold.green("\u2705 CSS generated successfully"));
2365
- 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));
2366
2611
  console.log(
2367
- chalk4.gray(` Components: `) + chalk4.white(String(components.length))
2612
+ chalk5.gray(` Components: `) + chalk5.white(String(components.length))
2368
2613
  );
2369
2614
  if (components.length > 0) {
2370
2615
  console.log("");
2371
- console.log(chalk4.bold("Generated classes:"));
2616
+ console.log(chalk5.bold("Generated classes:"));
2372
2617
  for (const component of components) {
2373
2618
  console.log(
2374
- 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(" "))
2375
2620
  );
2376
2621
  }
2377
2622
  console.log("");
2378
2623
  console.log(
2379
- chalk4.cyan(
2624
+ chalk5.cyan(
2380
2625
  "Run apply to replace className strings: npx tailwind-unwind apply <path> --output styles.css"
2381
2626
  )
2382
2627
  );
2383
2628
  } else {
2384
2629
  console.log(
2385
- chalk4.yellow(
2630
+ chalk5.yellow(
2386
2631
  "\nNo repeated className sets matched the filters. Try lowering --min-occurrences."
2387
2632
  )
2388
2633
  );
@@ -2417,11 +2662,16 @@ export {
2417
2662
  parseFile,
2418
2663
  IGNORED_DIRECTORIES,
2419
2664
  IGNORE_PATTERNS,
2665
+ getChangedSourceFiles,
2666
+ isGitRepository,
2667
+ getChangedFilesInScope,
2420
2668
  walkSourceFiles,
2421
2669
  scanProject,
2670
+ initCommand,
2422
2671
  printConsoleReport,
2423
2672
  printJsonReport,
2424
2673
  analyzeCommand,
2674
+ calculateSavings,
2425
2675
  formatSource,
2426
2676
  formatModifiedFiles,
2427
2677
  replaceClassNamesInSource,
@@ -2438,4 +2688,4 @@ export {
2438
2688
  applyCommand,
2439
2689
  generateCommand
2440
2690
  };
2441
- //# sourceMappingURL=chunk-4GXMK3NB.js.map
2691
+ //# sourceMappingURL=chunk-UXXIEFP4.js.map