tailwind-unwind 0.3.0 → 0.5.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 = [];
@@ -1447,8 +1530,10 @@ async function scanProject(options) {
1447
1530
  topLimit: options.topLimit,
1448
1531
  dedupeSubsets: options.dedupeSubsets
1449
1532
  });
1533
+ const analyzeMinOccurrences = options.minOccurrences ?? 5;
1534
+ const extractableMinOccurrences = options.extractableMinOccurrences ?? 3;
1450
1535
  const extractableSets = findRepeatedClassSets(occurrences, {
1451
- minOccurrences: options.extractableMinOccurrences ?? 3,
1536
+ minOccurrences: extractableMinOccurrences,
1452
1537
  minSize: options.minSize,
1453
1538
  maxSize: options.maxSize,
1454
1539
  topLimit: Number.POSITIVE_INFINITY
@@ -1475,7 +1560,10 @@ async function scanProject(options) {
1475
1560
  0
1476
1561
  ),
1477
1562
  topCombinations,
1478
- potentialReductionPercent
1563
+ potentialReductionPercent,
1564
+ extractablePatternCount: extractableSets.length,
1565
+ analyzeMinOccurrences,
1566
+ extractableMinOccurrences
1479
1567
  },
1480
1568
  parseWarnings: warnings
1481
1569
  };
@@ -1484,12 +1572,118 @@ async function scanProject(options) {
1484
1572
  files,
1485
1573
  occurrences,
1486
1574
  warnings,
1487
- report
1575
+ report,
1576
+ extractableCombinations: extractableSets
1488
1577
  };
1489
1578
  }
1490
1579
 
1491
- // src/reporters/consoleReporter.ts
1580
+ // src/commands/init.ts
1581
+ import fs4 from "fs/promises";
1582
+ import path5 from "path";
1492
1583
  import chalk from "chalk";
1584
+ function suggestionToName(suggestion) {
1585
+ return suggestion.replace(/^\./, "");
1586
+ }
1587
+ function detectIncludePattern(targetPath) {
1588
+ const normalized = targetPath.replace(/\\/g, "/");
1589
+ if (normalized.endsWith("/src") || normalized === "src") {
1590
+ return ["src/**/*.tsx", "src/**/*.jsx"];
1591
+ }
1592
+ return ["**/*.tsx", "**/*.jsx"];
1593
+ }
1594
+ function buildConfigFromScan(scanResult, targetPath, options) {
1595
+ const extractable = scanResult.report.stats.topCombinations.filter(
1596
+ (combo) => combo.extractable
1597
+ );
1598
+ const names = {};
1599
+ for (const combo of extractable.slice(0, options.top ?? 10)) {
1600
+ const utilities = [...combo.classes].sort().join(" ");
1601
+ names[utilities] = suggestionToName(combo.suggestion);
1602
+ }
1603
+ return {
1604
+ include: detectIncludePattern(targetPath),
1605
+ exclude: ["**/*.test.tsx", "**/*.stories.tsx"],
1606
+ names: Object.keys(names).length > 0 ? names : void 0,
1607
+ analyze: {
1608
+ minOccurrences: options.minOccurrences ?? 5,
1609
+ top: options.top ?? 10,
1610
+ dedupeSubsets: options.dedupeSubsets ?? true
1611
+ },
1612
+ generate: {
1613
+ minOccurrences: 3,
1614
+ prefix: options.prefix ?? "twu-",
1615
+ output: "src/styles/components.css",
1616
+ top: 20,
1617
+ extractableOnly: true
1618
+ },
1619
+ apply: {
1620
+ minOccurrences: 3,
1621
+ prefix: options.prefix ?? "twu-",
1622
+ output: "src/styles/components.css",
1623
+ prettier: true,
1624
+ extractableOnly: true
1625
+ }
1626
+ };
1627
+ }
1628
+ async function initCommand(targetPath, options = {}) {
1629
+ const resolvedPath = path5.resolve(targetPath);
1630
+ const outputPath = path5.resolve(
1631
+ options.output ?? path5.join(resolvedPath, "tailwind-unwind.config.json")
1632
+ );
1633
+ if (!options.force && await fileExists(outputPath)) {
1634
+ throw new Error(
1635
+ `Config already exists: ${outputPath}. Use --force to overwrite.`
1636
+ );
1637
+ }
1638
+ const scanResult = await scanProject({
1639
+ targetPath: resolvedPath,
1640
+ minOccurrences: options.minOccurrences ?? 5,
1641
+ minSize: options.minSize,
1642
+ maxSize: options.maxSize,
1643
+ topLimit: options.top ?? 10,
1644
+ dedupeSubsets: options.dedupeSubsets ?? true,
1645
+ include: options.include,
1646
+ exclude: options.exclude,
1647
+ extractableMinOccurrences: 3
1648
+ });
1649
+ const config = buildConfigFromScan(scanResult, resolvedPath, options);
1650
+ const json = `${JSON.stringify(config, null, 2)}
1651
+ `;
1652
+ await fs4.mkdir(path5.dirname(outputPath), { recursive: true });
1653
+ await fs4.writeFile(outputPath, json, "utf-8");
1654
+ const extractableCount = scanResult.report.stats.topCombinations.filter(
1655
+ (combo) => combo.extractable
1656
+ ).length;
1657
+ console.log("");
1658
+ console.log(chalk.bold.green("\u2705 Config created"));
1659
+ console.log(chalk.gray(" Output: ") + chalk.white(outputPath));
1660
+ console.log(
1661
+ chalk.gray(" Extractable patterns: ") + chalk.white(String(extractableCount))
1662
+ );
1663
+ console.log(
1664
+ chalk.gray(" Custom names: ") + chalk.white(String(Object.keys(config.names ?? {}).length))
1665
+ );
1666
+ console.log("");
1667
+ console.log(chalk.cyan("Next steps:"));
1668
+ console.log(chalk.white(" npx tailwind-unwind check"));
1669
+ console.log(chalk.white(" npx tailwind-unwind generate"));
1670
+ console.log("");
1671
+ return {
1672
+ configPath: outputPath,
1673
+ extractablePatterns: extractableCount
1674
+ };
1675
+ }
1676
+ async function fileExists(targetPath) {
1677
+ try {
1678
+ await fs4.access(targetPath);
1679
+ return true;
1680
+ } catch {
1681
+ return false;
1682
+ }
1683
+ }
1684
+
1685
+ // src/reporters/consoleReporter.ts
1686
+ import chalk2 from "chalk";
1493
1687
  function formatNumber(value) {
1494
1688
  return value.toLocaleString("en-US");
1495
1689
  }
@@ -1505,78 +1699,87 @@ function printConsoleReport(report, options = {}) {
1505
1699
  const { stats } = report;
1506
1700
  const topLimit = options.topLimit ?? 10;
1507
1701
  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))}`);
1702
+ console.log(chalk2.bold.cyan("\u{1F4CA} Tailwind Analysis Report"));
1703
+ console.log(chalk2.cyan("\u2501".repeat(41)));
1704
+ console.log(`Files scanned: ${chalk2.white(formatNumber(stats.filesScanned))}`);
1511
1705
  console.log(
1512
- `Components with className: ${chalk.white(formatNumber(stats.componentsWithClassName))}`
1706
+ `Components with className: ${chalk2.white(formatNumber(stats.componentsWithClassName))}`
1513
1707
  );
1514
1708
  console.log(
1515
- `Unique class combinations: ${chalk.white(formatNumber(stats.uniqueCombinations))}`
1709
+ `Unique class combinations: ${chalk2.white(formatNumber(stats.uniqueCombinations))}`
1516
1710
  );
1517
1711
  console.log("");
1518
1712
  if (stats.topCombinations.length === 0) {
1519
1713
  console.log(
1520
- chalk.yellow(
1714
+ chalk2.yellow(
1521
1715
  "No frequent class combinations found matching the current filters."
1522
1716
  )
1523
1717
  );
1524
1718
  } else {
1525
1719
  console.log(
1526
- chalk.bold.green(`\u{1F3C6} Top ${Math.min(topLimit, stats.topCombinations.length)} most frequent combinations:`)
1720
+ chalk2.bold.green(`\u{1F3C6} Top ${Math.min(topLimit, stats.topCombinations.length)} most frequent combinations:`)
1527
1721
  );
1528
1722
  console.log("");
1529
1723
  stats.topCombinations.forEach((combo, index) => {
1530
1724
  const displayClasses = normalizeClasses(combo.classes);
1531
1725
  console.log(
1532
- chalk.white(`${index + 1}. `) + chalk.yellow(`"${displayClasses}"`)
1726
+ chalk2.white(`${index + 1}. `) + chalk2.yellow(`"${displayClasses}"`)
1533
1727
  );
1534
1728
  console.log(
1535
- chalk.gray(` Occurrences: `) + chalk.white(String(combo.occurrences))
1729
+ chalk2.gray(` Occurrences: `) + chalk2.white(String(combo.occurrences))
1536
1730
  );
1537
1731
  console.log(
1538
- chalk.gray(` Suggestion: `) + chalk.green(combo.suggestion)
1732
+ chalk2.gray(` Suggestion: `) + chalk2.green(combo.suggestion)
1539
1733
  );
1540
1734
  if (combo.extractable) {
1541
1735
  console.log(
1542
- chalk.gray(` Extractable: `) + chalk.green("yes \u2014 use generate/apply")
1736
+ chalk2.gray(` Extractable: `) + chalk2.green("yes \u2014 use generate/apply")
1543
1737
  );
1544
1738
  } else {
1545
1739
  console.log(
1546
- chalk.gray(` Extractable: `) + chalk.yellow("subset only \u2014 analyze hint")
1740
+ chalk2.gray(` Extractable: `) + chalk2.yellow("subset only \u2014 analyze hint")
1547
1741
  );
1548
1742
  }
1549
1743
  console.log(
1550
- chalk.gray(` Found in: `) + chalk.dim(formatLocations(combo.locations))
1744
+ chalk2.gray(` Found in: `) + chalk2.dim(formatLocations(combo.locations))
1551
1745
  );
1552
1746
  console.log("");
1553
1747
  });
1554
1748
  }
1555
1749
  console.log(
1556
- chalk.magenta(
1750
+ chalk2.magenta(
1557
1751
  `\u{1F4A1} Potential code reduction: ${stats.potentialReductionPercent}%`
1558
1752
  )
1559
1753
  );
1560
- const extractableCount = stats.topCombinations.filter(
1754
+ const extractableInTop = stats.topCombinations.filter(
1561
1755
  (combo) => combo.extractable
1562
1756
  ).length;
1563
- if (extractableCount > 0) {
1757
+ if (stats.analyzeMinOccurrences !== stats.extractableMinOccurrences) {
1758
+ console.log("");
1564
1759
  console.log(
1565
- chalk.magenta(
1566
- `\u{1F4A1} ${extractableCount} pattern(s) ready for generate/apply`
1760
+ chalk2.yellow(
1761
+ `Note: this list uses min-occurrences ${stats.analyzeMinOccurrences}; generate/apply extract exact duplicates with \u2265${stats.extractableMinOccurrences}.`
1762
+ )
1763
+ );
1764
+ if (stats.extractablePatternCount > extractableInTop) {
1765
+ console.log(
1766
+ chalk2.yellow(
1767
+ ` ${stats.extractablePatternCount} extractable pattern(s) total in project.`
1768
+ )
1769
+ );
1770
+ }
1771
+ } else if (stats.extractablePatternCount > 0) {
1772
+ console.log(
1773
+ chalk2.magenta(
1774
+ `\u{1F4A1} ${stats.extractablePatternCount} extractable pattern(s) ready for generate/apply`
1567
1775
  )
1568
1776
  );
1569
1777
  }
1570
1778
  console.log(
1571
- chalk.magenta(
1572
- "\u{1F4A1} Generate CSS: npx tailwind-unwind generate <path> --output styles.css"
1573
- )
1574
- );
1575
- console.log(
1576
- chalk.magenta(
1577
- "\u{1F4A1} Apply classes: npx tailwind-unwind apply <path> --output styles.css"
1578
- )
1779
+ chalk2.magenta("\u{1F4A1} Quick check: npx tailwind-unwind check")
1579
1780
  );
1781
+ console.log(chalk2.magenta("\u{1F4A1} Generate CSS: npx tailwind-unwind generate"));
1782
+ console.log(chalk2.magenta("\u{1F4A1} Apply classes: npx tailwind-unwind apply"));
1580
1783
  console.log("");
1581
1784
  }
1582
1785
 
@@ -1586,7 +1789,7 @@ function printJsonReport(report) {
1586
1789
  }
1587
1790
 
1588
1791
  // src/commands/analyze.ts
1589
- import chalk2 from "chalk";
1792
+ import chalk3 from "chalk";
1590
1793
  async function analyzeCommand(targetPath, options = {}) {
1591
1794
  let scanResult;
1592
1795
  try {
@@ -1599,16 +1802,17 @@ async function analyzeCommand(targetPath, options = {}) {
1599
1802
  dedupeSubsets: options.dedupeSubsets,
1600
1803
  include: options.include,
1601
1804
  exclude: options.exclude,
1602
- extractableMinOccurrences: 3
1805
+ changed: options.changed,
1806
+ extractableMinOccurrences: options.extractableMinOccurrences
1603
1807
  });
1604
1808
  } catch (error) {
1605
1809
  const message = error instanceof Error ? error.message : String(error);
1606
- console.error(chalk2.red(`Error: ${message}`));
1810
+ console.error(chalk3.red(`Error: ${message}`));
1607
1811
  process.exit(1);
1608
1812
  }
1609
1813
  if (options.format !== "json") {
1610
1814
  for (const warning of scanResult.warnings) {
1611
- console.warn(chalk2.yellow(`\u26A0 ${warning}`));
1815
+ console.warn(chalk3.yellow(`\u26A0 ${warning}`));
1612
1816
  }
1613
1817
  }
1614
1818
  const report = scanResult.report;
@@ -1620,9 +1824,36 @@ async function analyzeCommand(targetPath, options = {}) {
1620
1824
  return report;
1621
1825
  }
1622
1826
 
1827
+ // src/cli/defaults.ts
1828
+ var DEFAULT_TARGET_PATH = ".";
1829
+ var DEFAULT_OUTPUT_PATH = "styles.css";
1830
+ var ANALYZE_DEFAULTS = {
1831
+ minOccurrences: 5,
1832
+ minSize: 2,
1833
+ maxSize: 5,
1834
+ top: 10
1835
+ };
1836
+ var GENERATE_DEFAULTS = {
1837
+ minOccurrences: 3,
1838
+ minSize: 2,
1839
+ maxSize: 5,
1840
+ top: 10,
1841
+ prefix: "twu-",
1842
+ output: DEFAULT_OUTPUT_PATH
1843
+ };
1844
+ function resolveTargetPath(targetPath) {
1845
+ if (typeof targetPath === "string" && targetPath.trim().length > 0) {
1846
+ return targetPath;
1847
+ }
1848
+ return DEFAULT_TARGET_PATH;
1849
+ }
1850
+ function resolveOutputPath(cliOutput, configOutput) {
1851
+ return cliOutput ?? configOutput ?? DEFAULT_OUTPUT_PATH;
1852
+ }
1853
+
1623
1854
  // src/codemod/formatSource.ts
1624
1855
  import { createRequire } from "module";
1625
- import path4 from "path";
1856
+ import path6 from "path";
1626
1857
  var require2 = createRequire(import.meta.url);
1627
1858
  async function loadPrettier() {
1628
1859
  try {
@@ -1662,7 +1893,7 @@ async function formatModifiedFiles(files, sources, cwd = process.cwd()) {
1662
1893
  continue;
1663
1894
  }
1664
1895
  const result = await formatSource(source, {
1665
- filePath: path4.resolve(cwd, file),
1896
+ filePath: path6.resolve(cwd, file),
1666
1897
  cwd
1667
1898
  });
1668
1899
  if (result.formatted) {
@@ -1858,8 +2089,8 @@ function replaceClassNamesInSource(source, replacementMap, filePath) {
1858
2089
  }
1859
2090
  const variantRegistry = collectVariantRegistry(ast);
1860
2091
  traverse3(ast, {
1861
- JSXElement(path7) {
1862
- const opening = path7.node.openingElement;
2092
+ JSXElement(path9) {
2093
+ const opening = path9.node.openingElement;
1863
2094
  for (const attr of opening.attributes) {
1864
2095
  if (attr.type !== "JSXAttribute" || !isClassAttribute(attr)) {
1865
2096
  continue;
@@ -2042,7 +2273,7 @@ function buildComponentsFromCombinations(combinations, options) {
2042
2273
  }
2043
2274
 
2044
2275
  // src/core/loadAnalyzeReport.ts
2045
- import fs4 from "fs/promises";
2276
+ import fs5 from "fs/promises";
2046
2277
  function isAnalysisReport(value) {
2047
2278
  if (typeof value !== "object" || value === null) {
2048
2279
  return false;
@@ -2051,7 +2282,7 @@ function isAnalysisReport(value) {
2051
2282
  return typeof report.targetPath === "string" && typeof report.stats === "object" && Array.isArray(report.stats.topCombinations);
2052
2283
  }
2053
2284
  async function loadExtractableCombinations(reportPath, options = {}) {
2054
- const raw = await fs4.readFile(reportPath, "utf-8");
2285
+ const raw = await fs5.readFile(reportPath, "utf-8");
2055
2286
  const parsed = JSON.parse(raw);
2056
2287
  if (!isAnalysisReport(parsed)) {
2057
2288
  throw new Error(`Invalid analyze report: ${reportPath}`);
@@ -2070,6 +2301,33 @@ async function loadExtractableCombinations(reportPath, options = {}) {
2070
2301
  };
2071
2302
  }
2072
2303
 
2304
+ // src/analyzer/savings.ts
2305
+ function calculateSavings(replacements) {
2306
+ if (replacements.length === 0) {
2307
+ return {
2308
+ replacementCount: 0,
2309
+ utilityTokensBefore: 0,
2310
+ utilityTokensAfter: 0,
2311
+ tokensSaved: 0,
2312
+ percentReduction: 0
2313
+ };
2314
+ }
2315
+ let utilityTokensBefore = 0;
2316
+ for (const replacement of replacements) {
2317
+ utilityTokensBefore += replacement.from.split(/\s+/).filter(Boolean).length;
2318
+ }
2319
+ const utilityTokensAfter = replacements.length;
2320
+ const tokensSaved = Math.max(0, utilityTokensBefore - utilityTokensAfter);
2321
+ const percentReduction = utilityTokensBefore === 0 ? 0 : Math.round(tokensSaved / utilityTokensBefore * 100);
2322
+ return {
2323
+ replacementCount: replacements.length,
2324
+ utilityTokensBefore,
2325
+ utilityTokensAfter,
2326
+ tokensSaved,
2327
+ percentReduction
2328
+ };
2329
+ }
2330
+
2073
2331
  // src/reporters/operationJsonReporter.ts
2074
2332
  function printGenerateJsonReport(report) {
2075
2333
  console.log(JSON.stringify(report, null, 2));
@@ -2078,10 +2336,61 @@ function printApplyJsonReport(report) {
2078
2336
  console.log(JSON.stringify(report, null, 2));
2079
2337
  }
2080
2338
 
2339
+ // src/reporters/skippedReporter.ts
2340
+ import chalk4 from "chalk";
2341
+ function groupSkippedByReason(skipped) {
2342
+ const groups = /* @__PURE__ */ new Map();
2343
+ for (const item of skipped) {
2344
+ const bucket = groups.get(item.reason) ?? [];
2345
+ bucket.push(item);
2346
+ groups.set(item.reason, bucket);
2347
+ }
2348
+ return [...groups.entries()].map(([reason, items]) => ({
2349
+ reason,
2350
+ count: items.length,
2351
+ items
2352
+ })).sort((left, right) => right.count - left.count);
2353
+ }
2354
+ function printSkippedReport(skipped, options = {}) {
2355
+ if (skipped.length === 0) {
2356
+ return;
2357
+ }
2358
+ const groups = groupSkippedByReason(skipped);
2359
+ console.log("");
2360
+ console.log(chalk4.bold.yellow(`Skipped (${skipped.length}):`));
2361
+ if (options.verbose) {
2362
+ for (const item of skipped) {
2363
+ const line = item.line ? `:${item.line}` : "";
2364
+ console.log(
2365
+ chalk4.gray(` ${item.filePath}${line}`) + chalk4.yellow(` [${item.reason}]`) + chalk4.dim(` "${item.classes.join(" ")}"`)
2366
+ );
2367
+ }
2368
+ return;
2369
+ }
2370
+ for (const group of groups) {
2371
+ console.log(
2372
+ chalk4.yellow(` ${group.reason}: `) + chalk4.white(String(group.count))
2373
+ );
2374
+ const preview = group.items.slice(0, 2);
2375
+ for (const item of preview) {
2376
+ const line = item.line ? `:${item.line}` : "";
2377
+ console.log(
2378
+ chalk4.gray(` ${item.filePath}${line}`) + chalk4.dim(` "${item.classes.join(" ")}"`)
2379
+ );
2380
+ }
2381
+ if (group.items.length > 2) {
2382
+ console.log(
2383
+ chalk4.dim(` (+${group.items.length - 2} more with same reason)`)
2384
+ );
2385
+ }
2386
+ }
2387
+ console.log(chalk4.dim(" Use --verbose-skipped to list every skipped location."));
2388
+ }
2389
+
2081
2390
  // src/commands/apply.ts
2082
- import fs5 from "fs/promises";
2083
- import path5 from "path";
2084
- import chalk3 from "chalk";
2391
+ import fs6 from "fs/promises";
2392
+ import path7 from "path";
2393
+ import chalk5 from "chalk";
2085
2394
  async function applyCommand(targetPath, options) {
2086
2395
  let scanResult;
2087
2396
  try {
@@ -2089,16 +2398,17 @@ async function applyCommand(targetPath, options) {
2089
2398
  targetPath,
2090
2399
  include: options.include,
2091
2400
  exclude: options.exclude,
2401
+ changed: options.changed,
2092
2402
  extractableMinOccurrences: options.minOccurrences ?? 3
2093
2403
  });
2094
2404
  } catch (error) {
2095
2405
  const message = error instanceof Error ? error.message : String(error);
2096
- console.error(chalk3.red(`Error: ${message}`));
2406
+ console.error(chalk5.red(`Error: ${message}`));
2097
2407
  process.exit(1);
2098
2408
  }
2099
2409
  for (const warning of scanResult.warnings) {
2100
- if (options.format !== "json") {
2101
- console.warn(chalk3.yellow(`\u26A0 ${warning}`));
2410
+ if (!options.quiet && options.format !== "json") {
2411
+ console.warn(chalk5.yellow(`\u26A0 ${warning}`));
2102
2412
  }
2103
2413
  }
2104
2414
  let components;
@@ -2145,18 +2455,31 @@ async function applyCommand(targetPath, options) {
2145
2455
  }
2146
2456
  } catch (error) {
2147
2457
  const message = error instanceof Error ? error.message : String(error);
2148
- console.error(chalk3.red(`Error: ${message}`));
2458
+ console.error(chalk5.red(`Error: ${message}`));
2149
2459
  process.exit(1);
2150
2460
  }
2151
2461
  if (components.length === 0) {
2152
- console.error(
2153
- chalk3.yellow(
2154
- "No repeated className sets found. Try lowering --min-occurrences."
2155
- )
2156
- );
2157
- process.exit(1);
2462
+ if (!options.quiet) {
2463
+ console.error(
2464
+ chalk5.yellow(
2465
+ "No repeated className sets found. Try lowering --min-occurrences."
2466
+ )
2467
+ );
2468
+ process.exit(1);
2469
+ }
2470
+ return {
2471
+ filesModified: 0,
2472
+ replacementsTotal: 0,
2473
+ outputPath: path7.resolve(options.output),
2474
+ componentsGenerated: 0,
2475
+ components: [],
2476
+ replacements: [],
2477
+ skipped: [],
2478
+ prettierFormatted: [],
2479
+ savings: calculateSavings([])
2480
+ };
2158
2481
  }
2159
- const outputPath = path5.resolve(options.output);
2482
+ const outputPath = path7.resolve(options.output);
2160
2483
  let filesModified = 0;
2161
2484
  let replacementsTotal = 0;
2162
2485
  const allReplacements = [];
@@ -2164,7 +2487,7 @@ async function applyCommand(targetPath, options) {
2164
2487
  const modifiedSources = /* @__PURE__ */ new Map();
2165
2488
  const modifiedFiles = [];
2166
2489
  for (const file of scanResult.files) {
2167
- const original = await fs5.readFile(file, "utf-8");
2490
+ const original = await fs6.readFile(file, "utf-8");
2168
2491
  const result2 = replaceClassNamesInSource(
2169
2492
  original,
2170
2493
  replacementMap,
@@ -2192,12 +2515,13 @@ async function applyCommand(targetPath, options) {
2192
2515
  for (const file of modifiedFiles) {
2193
2516
  const source = modifiedSources.get(file);
2194
2517
  if (source) {
2195
- await fs5.writeFile(file, source, "utf-8");
2518
+ await fs6.writeFile(file, source, "utf-8");
2196
2519
  }
2197
2520
  }
2198
- await fs5.mkdir(path5.dirname(outputPath), { recursive: true });
2199
- await fs5.writeFile(outputPath, css, "utf-8");
2521
+ await fs6.mkdir(path7.dirname(outputPath), { recursive: true });
2522
+ await fs6.writeFile(outputPath, css, "utf-8");
2200
2523
  }
2524
+ const savings = calculateSavings(allReplacements);
2201
2525
  const result = {
2202
2526
  filesModified,
2203
2527
  replacementsTotal,
@@ -2206,9 +2530,10 @@ async function applyCommand(targetPath, options) {
2206
2530
  components,
2207
2531
  replacements: allReplacements,
2208
2532
  skipped: allSkipped,
2209
- prettierFormatted
2533
+ prettierFormatted,
2534
+ savings
2210
2535
  };
2211
- if (options.format === "json") {
2536
+ if (options.format === "json" && !options.quiet) {
2212
2537
  printApplyJsonReport({
2213
2538
  command: "apply",
2214
2539
  dryRun: Boolean(options.dryRun),
@@ -2218,69 +2543,263 @@ async function applyCommand(targetPath, options) {
2218
2543
  componentsGenerated: components.length,
2219
2544
  components,
2220
2545
  replacements: allReplacements,
2221
- skipped: allSkipped
2546
+ skipped: allSkipped,
2547
+ savings
2222
2548
  });
2223
2549
  return result;
2224
2550
  }
2551
+ if (options.quiet) {
2552
+ return result;
2553
+ }
2225
2554
  console.log("");
2226
2555
  if (options.dryRun) {
2227
- console.log(chalk3.bold.yellow("\u{1F50D} Dry run \u2014 no files were modified"));
2556
+ console.log(chalk5.bold.yellow("\u{1F50D} Dry run \u2014 no files were modified"));
2228
2557
  } else {
2229
- console.log(chalk3.bold.green("\u2705 Classes applied successfully"));
2558
+ console.log(chalk5.bold.green("\u2705 Classes applied successfully"));
2230
2559
  }
2231
- console.log(chalk3.gray(` CSS output: `) + chalk3.white(outputPath));
2560
+ console.log(chalk5.gray(` CSS output: `) + chalk5.white(outputPath));
2232
2561
  console.log(
2233
- chalk3.gray(` Component classes: `) + chalk3.white(String(components.length))
2562
+ chalk5.gray(` Component classes: `) + chalk5.white(String(components.length))
2234
2563
  );
2235
2564
  console.log(
2236
- chalk3.gray(` Files modified: `) + chalk3.white(String(filesModified))
2565
+ chalk5.gray(` Files modified: `) + chalk5.white(String(filesModified))
2237
2566
  );
2238
2567
  console.log(
2239
- chalk3.gray(` Replacements: `) + chalk3.white(String(replacementsTotal))
2568
+ chalk5.gray(` Replacements: `) + chalk5.white(String(replacementsTotal))
2240
2569
  );
2241
2570
  if (prettierFormatted.length > 0) {
2242
2571
  console.log(
2243
- chalk3.gray(` Prettier formatted: `) + chalk3.white(String(prettierFormatted.length))
2572
+ chalk5.gray(` Prettier formatted: `) + chalk5.white(String(prettierFormatted.length))
2573
+ );
2574
+ }
2575
+ if (savings.replacementCount > 0) {
2576
+ console.log("");
2577
+ console.log(chalk5.bold("Savings:"));
2578
+ console.log(
2579
+ chalk5.gray(" Utility tokens before: ") + chalk5.white(String(savings.utilityTokensBefore))
2580
+ );
2581
+ console.log(
2582
+ chalk5.gray(" Utility tokens after: ") + chalk5.white(String(savings.utilityTokensAfter))
2583
+ );
2584
+ console.log(
2585
+ chalk5.gray(" Tokens saved: ") + chalk5.green(String(savings.tokensSaved))
2586
+ );
2587
+ console.log(
2588
+ chalk5.gray(" Reduction: ") + chalk5.green(`${savings.percentReduction}%`)
2244
2589
  );
2245
2590
  }
2246
2591
  if (allReplacements.length > 0) {
2247
2592
  console.log("");
2248
- console.log(chalk3.bold("Replacements:"));
2593
+ console.log(chalk5.bold("Replacements:"));
2249
2594
  for (const item of allReplacements) {
2250
2595
  const line = item.line ? `:${item.line}` : "";
2251
- const partialTag = item.partial ? chalk3.dim(" (partial)") : "";
2596
+ const partialTag = item.partial ? chalk5.dim(" (partial)") : "";
2252
2597
  console.log(
2253
- chalk3.gray(` ${item.filePath}${line}`) + chalk3.white(` "${item.from}" `) + chalk3.cyan("\u2192") + chalk3.green(` "${item.to}"`) + partialTag
2598
+ chalk5.gray(` ${item.filePath}${line}`) + chalk5.white(` "${item.from}" `) + chalk5.cyan("\u2192") + chalk5.green(` "${item.to}"`) + partialTag
2254
2599
  );
2255
2600
  }
2256
2601
  }
2257
- if (allSkipped.length > 0) {
2602
+ printSkippedReport(allSkipped, { verbose: options.verboseSkipped });
2603
+ console.log("");
2604
+ if (!options.dryRun) {
2605
+ console.log(
2606
+ chalk5.cyan(
2607
+ `Import ${path7.basename(outputPath)} in your global CSS if you haven't already.`
2608
+ )
2609
+ );
2258
2610
  console.log("");
2259
- console.log(chalk3.bold.yellow(`Skipped (${allSkipped.length}):`));
2260
- for (const item of allSkipped) {
2261
- const line = item.line ? `:${item.line}` : "";
2262
- const classes = item.classes.join(" ");
2611
+ }
2612
+ return result;
2613
+ }
2614
+
2615
+ // src/reporters/checkReporter.ts
2616
+ import chalk6 from "chalk";
2617
+ function printCheckJsonReport(report) {
2618
+ console.log(JSON.stringify(report, null, 2));
2619
+ }
2620
+ function printCheckConsoleReport(analysisReport, options) {
2621
+ const { stats } = analysisReport;
2622
+ const topLimit = options.topLimit ?? 5;
2623
+ const extractableInTop = stats.topCombinations.filter(
2624
+ (combo) => combo.extractable
2625
+ ).length;
2626
+ console.log("");
2627
+ console.log(chalk6.bold.cyan("\u2713 Tailwind check"));
2628
+ console.log(chalk6.cyan("\u2501".repeat(41)));
2629
+ console.log(`Files scanned: ${chalk6.white(String(stats.filesScanned))}`);
2630
+ console.log(
2631
+ `Extractable patterns: ${chalk6.white(String(stats.extractablePatternCount))}` + chalk6.gray(" (exact duplicates ready for generate/apply)")
2632
+ );
2633
+ if (stats.analyzeMinOccurrences !== stats.extractableMinOccurrences) {
2634
+ console.log("");
2635
+ console.log(
2636
+ chalk6.yellow(
2637
+ `Note: analyze lists patterns with \u2265${stats.analyzeMinOccurrences} occurrences; generate/apply extract exact duplicates with \u2265${stats.extractableMinOccurrences}.`
2638
+ )
2639
+ );
2640
+ if (stats.extractablePatternCount > extractableInTop) {
2263
2641
  console.log(
2264
- chalk3.gray(` ${item.filePath}${line}`) + chalk3.yellow(` [${item.reason}]`) + chalk3.dim(` "${classes}"`)
2642
+ chalk6.yellow(
2643
+ ` ${stats.extractablePatternCount - extractableInTop} more extractable pattern(s) exist below the analyze threshold.`
2644
+ )
2265
2645
  );
2266
2646
  }
2267
2647
  }
2268
- console.log("");
2269
- if (!options.dryRun) {
2648
+ if (stats.extractablePatternCount === 0) {
2649
+ console.log("");
2270
2650
  console.log(
2271
- chalk3.cyan(
2272
- `Import ${path5.basename(outputPath)} in your global CSS if you haven't already.`
2273
- )
2651
+ chalk6.green("No extractable duplicates found. Nothing to refactor right now.")
2274
2652
  );
2275
2653
  console.log("");
2654
+ console.log(options.passed ? chalk6.green("Check passed.") : chalk6.red("Check failed."));
2655
+ console.log("");
2656
+ return;
2657
+ }
2658
+ const previewCount = Math.min(topLimit, stats.topCombinations.length);
2659
+ if (previewCount > 0) {
2660
+ console.log("");
2661
+ console.log(chalk6.bold("Top extractable patterns:"));
2662
+ for (const combo of stats.topCombinations.filter((item) => item.extractable).slice(0, topLimit)) {
2663
+ console.log(
2664
+ chalk6.white(` \u2022 "${normalizeClasses(combo.classes)}"`) + chalk6.gray(` \u2014 ${combo.occurrences}\xD7 \u2192 `) + chalk6.green(combo.suggestion)
2665
+ );
2666
+ }
2667
+ }
2668
+ if (options.preview) {
2669
+ const preview = options.preview;
2670
+ console.log("");
2671
+ console.log(chalk6.bold("Apply preview (dry-run):"));
2672
+ console.log(
2673
+ chalk6.gray(" CSS output: ") + chalk6.white(options.outputPath)
2674
+ );
2675
+ console.log(
2676
+ chalk6.gray(" Component classes: ") + chalk6.white(String(preview.componentsGenerated))
2677
+ );
2678
+ console.log(
2679
+ chalk6.gray(" Files to modify: ") + chalk6.white(String(preview.filesModified))
2680
+ );
2681
+ console.log(
2682
+ chalk6.gray(" Replacements: ") + chalk6.white(String(preview.replacementsTotal))
2683
+ );
2684
+ if (preview.savings.replacementCount > 0) {
2685
+ console.log(
2686
+ chalk6.gray(" Token reduction: ") + chalk6.green(`${preview.savings.percentReduction}%`)
2687
+ );
2688
+ }
2689
+ printSkippedReport(preview.skipped, {
2690
+ verbose: options.verboseSkipped
2691
+ });
2692
+ }
2693
+ console.log("");
2694
+ if (options.passed) {
2695
+ console.log(chalk6.green("Check passed."));
2696
+ } else {
2697
+ console.log(chalk6.red("Check failed."));
2698
+ }
2699
+ console.log(chalk6.cyan("Next steps:"));
2700
+ console.log(chalk6.white(" npx tailwind-unwind generate"));
2701
+ console.log(chalk6.white(" npx tailwind-unwind apply --dry-run"));
2702
+ console.log("");
2703
+ }
2704
+
2705
+ // src/commands/check.ts
2706
+ import chalk7 from "chalk";
2707
+ async function checkCommand(targetPath, options) {
2708
+ let scanResult;
2709
+ try {
2710
+ scanResult = await scanProject({
2711
+ targetPath,
2712
+ minOccurrences: options.minOccurrences,
2713
+ minSize: options.minSize,
2714
+ maxSize: options.maxSize,
2715
+ topLimit: options.top,
2716
+ dedupeSubsets: options.dedupeSubsets,
2717
+ include: options.include,
2718
+ exclude: options.exclude,
2719
+ changed: options.changed,
2720
+ extractableMinOccurrences: options.extractableMinOccurrences
2721
+ });
2722
+ } catch (error) {
2723
+ const message = error instanceof Error ? error.message : String(error);
2724
+ console.error(chalk7.red(`Error: ${message}`));
2725
+ process.exit(1);
2726
+ }
2727
+ if (options.format !== "json") {
2728
+ for (const warning of scanResult.warnings) {
2729
+ console.warn(chalk7.yellow(`\u26A0 ${warning}`));
2730
+ }
2731
+ }
2732
+ const extractablePatternCount = scanResult.report.stats.extractablePatternCount;
2733
+ const failThreshold = options.failOnExtractable;
2734
+ const passed = failThreshold === void 0 ? true : extractablePatternCount <= failThreshold;
2735
+ let preview = null;
2736
+ if (extractablePatternCount > 0) {
2737
+ preview = await applyCommand(targetPath, {
2738
+ output: options.output,
2739
+ minOccurrences: options.extractableMinOccurrences ?? GENERATE_DEFAULTS.minOccurrences,
2740
+ minSize: options.minSize,
2741
+ maxSize: options.maxSize,
2742
+ top: options.top,
2743
+ prefix: options.prefix,
2744
+ include: options.include,
2745
+ exclude: options.exclude,
2746
+ changed: options.changed,
2747
+ configPath: options.configPath,
2748
+ names: options.names,
2749
+ extractableOnly: true,
2750
+ dryRun: true,
2751
+ quiet: true,
2752
+ verboseSkipped: options.verboseSkipped
2753
+ });
2754
+ }
2755
+ const result = {
2756
+ passed,
2757
+ extractablePatternCount,
2758
+ report: scanResult.report,
2759
+ preview
2760
+ };
2761
+ if (options.format === "json") {
2762
+ const jsonReport = {
2763
+ command: "check",
2764
+ passed,
2765
+ outputPath: options.output,
2766
+ extractablePatternCount,
2767
+ analyzeMinOccurrences: scanResult.report.stats.analyzeMinOccurrences,
2768
+ extractableMinOccurrences: scanResult.report.stats.extractableMinOccurrences,
2769
+ report: scanResult.report,
2770
+ preview: preview ? {
2771
+ componentsGenerated: preview.componentsGenerated,
2772
+ filesModified: preview.filesModified,
2773
+ replacementsTotal: preview.replacementsTotal,
2774
+ skippedTotal: preview.skipped.length,
2775
+ savings: preview.savings
2776
+ } : null
2777
+ };
2778
+ printCheckJsonReport(jsonReport);
2779
+ } else {
2780
+ printCheckConsoleReport(scanResult.report, {
2781
+ outputPath: options.output,
2782
+ topLimit: options.top,
2783
+ preview,
2784
+ verboseSkipped: options.verboseSkipped,
2785
+ passed
2786
+ });
2787
+ }
2788
+ if (!passed) {
2789
+ console.error(
2790
+ chalk7.red(
2791
+ `Found ${extractablePatternCount} extractable pattern(s); limit is ${failThreshold}.`
2792
+ )
2793
+ );
2794
+ process.exit(1);
2276
2795
  }
2277
2796
  return result;
2278
2797
  }
2279
2798
 
2280
2799
  // src/commands/generate.ts
2281
- import fs6 from "fs/promises";
2282
- import path6 from "path";
2283
- import chalk4 from "chalk";
2800
+ import fs7 from "fs/promises";
2801
+ import path8 from "path";
2802
+ import chalk8 from "chalk";
2284
2803
  async function generateCommand(targetPath, options) {
2285
2804
  let scanResult = null;
2286
2805
  let components;
@@ -2302,6 +2821,7 @@ async function generateCommand(targetPath, options) {
2302
2821
  targetPath,
2303
2822
  include: options.include,
2304
2823
  exclude: options.exclude,
2824
+ changed: options.changed,
2305
2825
  extractableMinOccurrences: options.minOccurrences ?? 3
2306
2826
  });
2307
2827
  if (options.extractableOnly) {
@@ -2331,19 +2851,19 @@ async function generateCommand(targetPath, options) {
2331
2851
  }
2332
2852
  } catch (error) {
2333
2853
  const message = error instanceof Error ? error.message : String(error);
2334
- console.error(chalk4.red(`Error: ${message}`));
2854
+ console.error(chalk8.red(`Error: ${message}`));
2335
2855
  process.exit(1);
2336
2856
  }
2337
2857
  if (scanResult) {
2338
2858
  for (const warning of scanResult.warnings) {
2339
2859
  if (options.format !== "json") {
2340
- console.warn(chalk4.yellow(`\u26A0 ${warning}`));
2860
+ console.warn(chalk8.yellow(`\u26A0 ${warning}`));
2341
2861
  }
2342
2862
  }
2343
2863
  }
2344
- const outputPath = path6.resolve(options.output);
2345
- await fs6.mkdir(path6.dirname(outputPath), { recursive: true });
2346
- await fs6.writeFile(outputPath, css, "utf-8");
2864
+ const outputPath = path8.resolve(options.output);
2865
+ await fs7.mkdir(path8.dirname(outputPath), { recursive: true });
2866
+ await fs7.writeFile(outputPath, css, "utf-8");
2347
2867
  const result = {
2348
2868
  outputPath,
2349
2869
  componentsGenerated: components.length,
@@ -2361,28 +2881,28 @@ async function generateCommand(targetPath, options) {
2361
2881
  return result;
2362
2882
  }
2363
2883
  console.log("");
2364
- console.log(chalk4.bold.green("\u2705 CSS generated successfully"));
2365
- console.log(chalk4.gray(` Output: `) + chalk4.white(outputPath));
2884
+ console.log(chalk8.bold.green("\u2705 CSS generated successfully"));
2885
+ console.log(chalk8.gray(` Output: `) + chalk8.white(outputPath));
2366
2886
  console.log(
2367
- chalk4.gray(` Components: `) + chalk4.white(String(components.length))
2887
+ chalk8.gray(` Components: `) + chalk8.white(String(components.length))
2368
2888
  );
2369
2889
  if (components.length > 0) {
2370
2890
  console.log("");
2371
- console.log(chalk4.bold("Generated classes:"));
2891
+ console.log(chalk8.bold("Generated classes:"));
2372
2892
  for (const component of components) {
2373
2893
  console.log(
2374
- chalk4.green(` .${component.className}`) + chalk4.gray(` \u2014 ${component.occurrences} occurrences, `) + chalk4.dim(component.classes.join(" "))
2894
+ chalk8.green(` .${component.className}`) + chalk8.gray(` \u2014 ${component.occurrences} occurrences, `) + chalk8.dim(component.classes.join(" "))
2375
2895
  );
2376
2896
  }
2377
2897
  console.log("");
2378
2898
  console.log(
2379
- chalk4.cyan(
2380
- "Run apply to replace className strings: npx tailwind-unwind apply <path> --output styles.css"
2899
+ chalk8.cyan(
2900
+ "Run apply to replace className strings: npx tailwind-unwind apply"
2381
2901
  )
2382
2902
  );
2383
2903
  } else {
2384
2904
  console.log(
2385
- chalk4.yellow(
2905
+ chalk8.yellow(
2386
2906
  "\nNo repeated className sets matched the filters. Try lowering --min-occurrences."
2387
2907
  )
2388
2908
  );
@@ -2417,11 +2937,20 @@ export {
2417
2937
  parseFile,
2418
2938
  IGNORED_DIRECTORIES,
2419
2939
  IGNORE_PATTERNS,
2940
+ getChangedSourceFiles,
2941
+ isGitRepository,
2942
+ getChangedFilesInScope,
2420
2943
  walkSourceFiles,
2421
2944
  scanProject,
2945
+ initCommand,
2422
2946
  printConsoleReport,
2423
2947
  printJsonReport,
2424
2948
  analyzeCommand,
2949
+ DEFAULT_TARGET_PATH,
2950
+ ANALYZE_DEFAULTS,
2951
+ GENERATE_DEFAULTS,
2952
+ resolveTargetPath,
2953
+ resolveOutputPath,
2425
2954
  formatSource,
2426
2955
  formatModifiedFiles,
2427
2956
  replaceClassNamesInSource,
@@ -2433,9 +2962,15 @@ export {
2433
2962
  buildComponents,
2434
2963
  buildComponentsFromCombinations,
2435
2964
  loadExtractableCombinations,
2965
+ calculateSavings,
2436
2966
  printGenerateJsonReport,
2437
2967
  printApplyJsonReport,
2968
+ groupSkippedByReason,
2969
+ printSkippedReport,
2438
2970
  applyCommand,
2971
+ printCheckJsonReport,
2972
+ printCheckConsoleReport,
2973
+ checkCommand,
2439
2974
  generateCommand
2440
2975
  };
2441
- //# sourceMappingURL=chunk-4GXMK3NB.js.map
2976
+ //# sourceMappingURL=chunk-RMTZCCPS.js.map