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.
- package/README.md +130 -239
- package/dist/{chunk-4GXMK3NB.js → chunk-RMTZCCPS.js} +664 -129
- package/dist/chunk-RMTZCCPS.js.map +1 -0
- package/dist/cli/index.js +133 -43
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +148 -39
- package/dist/index.js +21 -1
- package/package.json +3 -2
- package/dist/chunk-4GXMK3NB.js.map +0 -1
|
@@ -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,
|
|
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(`${
|
|
43
|
+
errors.push(`${path9} must be a positive number`);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
function assertBoolean(value,
|
|
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(`${
|
|
51
|
+
errors.push(`${path9} must be a boolean`);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
-
function assertStringArray(value,
|
|
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(`${
|
|
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(
|
|
1052
|
-
registerVariantDeclarator(
|
|
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(
|
|
1290
|
-
const opening =
|
|
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(
|
|
1301
|
-
if (!isJSXElementWithClassAttribute(
|
|
1306
|
+
JSXElement(path9) {
|
|
1307
|
+
if (!isJSXElementWithClassAttribute(path9)) {
|
|
1302
1308
|
return;
|
|
1303
1309
|
}
|
|
1304
|
-
const opening =
|
|
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
|
|
1358
|
-
var
|
|
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 (
|
|
1439
|
+
if (path3.isAbsolute(normalized)) {
|
|
1362
1440
|
return normalized;
|
|
1363
1441
|
}
|
|
1364
|
-
return
|
|
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
|
|
1371
|
-
(ext) =>
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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
|
-
|
|
1420
|
-
|
|
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:
|
|
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/
|
|
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(
|
|
1509
|
-
console.log(
|
|
1510
|
-
console.log(`Files scanned: ${
|
|
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: ${
|
|
1706
|
+
`Components with className: ${chalk2.white(formatNumber(stats.componentsWithClassName))}`
|
|
1513
1707
|
);
|
|
1514
1708
|
console.log(
|
|
1515
|
-
`Unique class combinations: ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1726
|
+
chalk2.white(`${index + 1}. `) + chalk2.yellow(`"${displayClasses}"`)
|
|
1533
1727
|
);
|
|
1534
1728
|
console.log(
|
|
1535
|
-
|
|
1729
|
+
chalk2.gray(` Occurrences: `) + chalk2.white(String(combo.occurrences))
|
|
1536
1730
|
);
|
|
1537
1731
|
console.log(
|
|
1538
|
-
|
|
1732
|
+
chalk2.gray(` Suggestion: `) + chalk2.green(combo.suggestion)
|
|
1539
1733
|
);
|
|
1540
1734
|
if (combo.extractable) {
|
|
1541
1735
|
console.log(
|
|
1542
|
-
|
|
1736
|
+
chalk2.gray(` Extractable: `) + chalk2.green("yes \u2014 use generate/apply")
|
|
1543
1737
|
);
|
|
1544
1738
|
} else {
|
|
1545
1739
|
console.log(
|
|
1546
|
-
|
|
1740
|
+
chalk2.gray(` Extractable: `) + chalk2.yellow("subset only \u2014 analyze hint")
|
|
1547
1741
|
);
|
|
1548
1742
|
}
|
|
1549
1743
|
console.log(
|
|
1550
|
-
|
|
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
|
-
|
|
1750
|
+
chalk2.magenta(
|
|
1557
1751
|
`\u{1F4A1} Potential code reduction: ${stats.potentialReductionPercent}%`
|
|
1558
1752
|
)
|
|
1559
1753
|
);
|
|
1560
|
-
const
|
|
1754
|
+
const extractableInTop = stats.topCombinations.filter(
|
|
1561
1755
|
(combo) => combo.extractable
|
|
1562
1756
|
).length;
|
|
1563
|
-
if (
|
|
1757
|
+
if (stats.analyzeMinOccurrences !== stats.extractableMinOccurrences) {
|
|
1758
|
+
console.log("");
|
|
1564
1759
|
console.log(
|
|
1565
|
-
|
|
1566
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
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(
|
|
1862
|
-
const opening =
|
|
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
|
|
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
|
|
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
|
|
2083
|
-
import
|
|
2084
|
-
import
|
|
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(
|
|
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(
|
|
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(
|
|
2458
|
+
console.error(chalk5.red(`Error: ${message}`));
|
|
2149
2459
|
process.exit(1);
|
|
2150
2460
|
}
|
|
2151
2461
|
if (components.length === 0) {
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
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 =
|
|
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
|
|
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
|
|
2518
|
+
await fs6.writeFile(file, source, "utf-8");
|
|
2196
2519
|
}
|
|
2197
2520
|
}
|
|
2198
|
-
await
|
|
2199
|
-
await
|
|
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(
|
|
2556
|
+
console.log(chalk5.bold.yellow("\u{1F50D} Dry run \u2014 no files were modified"));
|
|
2228
2557
|
} else {
|
|
2229
|
-
console.log(
|
|
2558
|
+
console.log(chalk5.bold.green("\u2705 Classes applied successfully"));
|
|
2230
2559
|
}
|
|
2231
|
-
console.log(
|
|
2560
|
+
console.log(chalk5.gray(` CSS output: `) + chalk5.white(outputPath));
|
|
2232
2561
|
console.log(
|
|
2233
|
-
|
|
2562
|
+
chalk5.gray(` Component classes: `) + chalk5.white(String(components.length))
|
|
2234
2563
|
);
|
|
2235
2564
|
console.log(
|
|
2236
|
-
|
|
2565
|
+
chalk5.gray(` Files modified: `) + chalk5.white(String(filesModified))
|
|
2237
2566
|
);
|
|
2238
2567
|
console.log(
|
|
2239
|
-
|
|
2568
|
+
chalk5.gray(` Replacements: `) + chalk5.white(String(replacementsTotal))
|
|
2240
2569
|
);
|
|
2241
2570
|
if (prettierFormatted.length > 0) {
|
|
2242
2571
|
console.log(
|
|
2243
|
-
|
|
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(
|
|
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 ?
|
|
2596
|
+
const partialTag = item.partial ? chalk5.dim(" (partial)") : "";
|
|
2252
2597
|
console.log(
|
|
2253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
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
|
-
|
|
2642
|
+
chalk6.yellow(
|
|
2643
|
+
` ${stats.extractablePatternCount - extractableInTop} more extractable pattern(s) exist below the analyze threshold.`
|
|
2644
|
+
)
|
|
2265
2645
|
);
|
|
2266
2646
|
}
|
|
2267
2647
|
}
|
|
2268
|
-
|
|
2269
|
-
|
|
2648
|
+
if (stats.extractablePatternCount === 0) {
|
|
2649
|
+
console.log("");
|
|
2270
2650
|
console.log(
|
|
2271
|
-
|
|
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
|
|
2282
|
-
import
|
|
2283
|
-
import
|
|
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(
|
|
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(
|
|
2860
|
+
console.warn(chalk8.yellow(`\u26A0 ${warning}`));
|
|
2341
2861
|
}
|
|
2342
2862
|
}
|
|
2343
2863
|
}
|
|
2344
|
-
const outputPath =
|
|
2345
|
-
await
|
|
2346
|
-
await
|
|
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(
|
|
2365
|
-
console.log(
|
|
2884
|
+
console.log(chalk8.bold.green("\u2705 CSS generated successfully"));
|
|
2885
|
+
console.log(chalk8.gray(` Output: `) + chalk8.white(outputPath));
|
|
2366
2886
|
console.log(
|
|
2367
|
-
|
|
2887
|
+
chalk8.gray(` Components: `) + chalk8.white(String(components.length))
|
|
2368
2888
|
);
|
|
2369
2889
|
if (components.length > 0) {
|
|
2370
2890
|
console.log("");
|
|
2371
|
-
console.log(
|
|
2891
|
+
console.log(chalk8.bold("Generated classes:"));
|
|
2372
2892
|
for (const component of components) {
|
|
2373
2893
|
console.log(
|
|
2374
|
-
|
|
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
|
-
|
|
2380
|
-
"Run apply to replace className strings: npx tailwind-unwind apply
|
|
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
|
-
|
|
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-
|
|
2976
|
+
//# sourceMappingURL=chunk-RMTZCCPS.js.map
|