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