tailwind-unwind 0.1.1 → 0.2.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.
@@ -1,3 +1,332 @@
1
+ // src/config/validate.ts
2
+ var CLASS_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
3
+ var KNOWN_ROOT_KEYS = /* @__PURE__ */ new Set([
4
+ "include",
5
+ "exclude",
6
+ "names",
7
+ "prefix",
8
+ "output",
9
+ "minOccurrences",
10
+ "minSize",
11
+ "maxSize",
12
+ "top",
13
+ "dedupeSubsets",
14
+ "dryRun",
15
+ "analyze",
16
+ "generate",
17
+ "apply"
18
+ ]);
19
+ var KNOWN_COMMAND_KEYS = /* @__PURE__ */ new Set([
20
+ "minOccurrences",
21
+ "minSize",
22
+ "maxSize",
23
+ "top",
24
+ "prefix",
25
+ "output",
26
+ "dedupeSubsets",
27
+ "dryRun"
28
+ ]);
29
+ function isRecord(value) {
30
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31
+ }
32
+ function assertPositiveNumber(value, path6, errors) {
33
+ if (value === void 0) {
34
+ return;
35
+ }
36
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
37
+ errors.push(`${path6} must be a positive number`);
38
+ }
39
+ }
40
+ function assertBoolean(value, path6, errors) {
41
+ if (value === void 0) {
42
+ return;
43
+ }
44
+ if (typeof value !== "boolean") {
45
+ errors.push(`${path6} must be a boolean`);
46
+ }
47
+ }
48
+ function assertStringArray(value, path6, errors) {
49
+ if (value === void 0) {
50
+ return;
51
+ }
52
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && item.length > 0)) {
53
+ errors.push(`${path6} must be an array of non-empty strings`);
54
+ }
55
+ }
56
+ function validateCommandSection(value, section, errors) {
57
+ if (value === void 0) {
58
+ return;
59
+ }
60
+ if (!isRecord(value)) {
61
+ errors.push(`${section} must be an object`);
62
+ return;
63
+ }
64
+ for (const key of Object.keys(value)) {
65
+ if (!KNOWN_COMMAND_KEYS.has(key)) {
66
+ errors.push(`Unknown key "${key}" in ${section}`);
67
+ }
68
+ }
69
+ assertPositiveNumber(value.minOccurrences, `${section}.minOccurrences`, errors);
70
+ assertPositiveNumber(value.minSize, `${section}.minSize`, errors);
71
+ assertPositiveNumber(value.maxSize, `${section}.maxSize`, errors);
72
+ assertPositiveNumber(value.top, `${section}.top`, errors);
73
+ assertBoolean(value.dedupeSubsets, `${section}.dedupeSubsets`, errors);
74
+ assertBoolean(value.dryRun, `${section}.dryRun`, errors);
75
+ if (value.prefix !== void 0 && (typeof value.prefix !== "string" || value.prefix.length === 0)) {
76
+ errors.push(`${section}.prefix must be a non-empty string`);
77
+ }
78
+ if (value.output !== void 0 && (typeof value.output !== "string" || value.output.length === 0)) {
79
+ errors.push(`${section}.output must be a non-empty string`);
80
+ }
81
+ }
82
+ function validateNames(value, errors) {
83
+ if (value === void 0) {
84
+ return;
85
+ }
86
+ if (!isRecord(value)) {
87
+ errors.push('names must be an object of "utility string": "class-name" pairs');
88
+ return;
89
+ }
90
+ for (const [utilities, name] of Object.entries(value)) {
91
+ if (typeof utilities !== "string" || utilities.trim().length === 0) {
92
+ errors.push("names keys must be non-empty utility strings");
93
+ continue;
94
+ }
95
+ if (typeof name !== "string" || !CLASS_NAME_PATTERN.test(name)) {
96
+ errors.push(
97
+ `names["${utilities}"] must be a valid class name (letters, numbers, hyphens)`
98
+ );
99
+ }
100
+ }
101
+ }
102
+ function validateConfigFile(raw, configPath) {
103
+ if (!isRecord(raw)) {
104
+ throw new Error(`Invalid config in ${configPath}: root value must be an object`);
105
+ }
106
+ const source = isRecord(raw.default) ? raw.default : raw;
107
+ const errors = [];
108
+ for (const key of Object.keys(source)) {
109
+ if (!KNOWN_ROOT_KEYS.has(key)) {
110
+ errors.push(`Unknown config key "${key}"`);
111
+ }
112
+ }
113
+ assertStringArray(source.include, "include", errors);
114
+ assertStringArray(source.exclude, "exclude", errors);
115
+ assertPositiveNumber(source.minOccurrences, "minOccurrences", errors);
116
+ assertPositiveNumber(source.minSize, "minSize", errors);
117
+ assertPositiveNumber(source.maxSize, "maxSize", errors);
118
+ assertPositiveNumber(source.top, "top", errors);
119
+ assertBoolean(source.dedupeSubsets, "dedupeSubsets", errors);
120
+ assertBoolean(source.dryRun, "dryRun", errors);
121
+ validateNames(source.names, errors);
122
+ validateCommandSection(source.analyze, "analyze", errors);
123
+ validateCommandSection(source.generate, "generate", errors);
124
+ validateCommandSection(source.apply, "apply", errors);
125
+ if (source.prefix !== void 0 && (typeof source.prefix !== "string" || source.prefix.length === 0)) {
126
+ errors.push("prefix must be a non-empty string");
127
+ }
128
+ if (source.output !== void 0 && (typeof source.output !== "string" || source.output.length === 0)) {
129
+ errors.push("output must be a non-empty string");
130
+ }
131
+ if (errors.length > 0) {
132
+ throw new Error(
133
+ `Invalid config in ${configPath}:
134
+ ${errors.map((error) => ` - ${error}`).join("\n")}`
135
+ );
136
+ }
137
+ }
138
+ function normalizeNamesConfig(names) {
139
+ const map = /* @__PURE__ */ new Map();
140
+ if (!names) {
141
+ return map;
142
+ }
143
+ for (const [utilities, baseName] of Object.entries(names)) {
144
+ const tokens = utilities.trim().split(/\s+/).filter((token) => token.length > 0);
145
+ const key = [...tokens].sort().join(" ");
146
+ map.set(key, baseName);
147
+ }
148
+ return map;
149
+ }
150
+
151
+ // src/config/loadConfig.ts
152
+ import fs from "fs/promises";
153
+ import path from "path";
154
+ import { pathToFileURL } from "url";
155
+ var CONFIG_FILENAMES = [
156
+ "tailwind-unwind.config.js",
157
+ "tailwind-unwind.config.mjs",
158
+ "tailwind-unwind.config.cjs",
159
+ "tailwind-unwind.config.json",
160
+ ".tailwind-unwindrc",
161
+ ".tailwind-unwindrc.json"
162
+ ];
163
+ function isRecord2(value) {
164
+ return typeof value === "object" && value !== null && !Array.isArray(value);
165
+ }
166
+ function pickNumber(source, key) {
167
+ const value = source[key];
168
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
169
+ }
170
+ function pickCommandConfig(source) {
171
+ const config = {};
172
+ const minOccurrences = pickNumber(source, "minOccurrences");
173
+ if (minOccurrences !== void 0) config.minOccurrences = minOccurrences;
174
+ const minSize = pickNumber(source, "minSize");
175
+ if (minSize !== void 0) config.minSize = minSize;
176
+ const maxSize = pickNumber(source, "maxSize");
177
+ if (maxSize !== void 0) config.maxSize = maxSize;
178
+ const top = pickNumber(source, "top");
179
+ if (top !== void 0) config.top = top;
180
+ if (typeof source.prefix === "string" && source.prefix.length > 0) {
181
+ config.prefix = source.prefix;
182
+ }
183
+ if (typeof source.output === "string" && source.output.length > 0) {
184
+ config.output = source.output;
185
+ }
186
+ if (typeof source.dedupeSubsets === "boolean") {
187
+ config.dedupeSubsets = source.dedupeSubsets;
188
+ }
189
+ if (typeof source.dryRun === "boolean") {
190
+ config.dryRun = source.dryRun;
191
+ }
192
+ return config;
193
+ }
194
+ function pickNames(source) {
195
+ if (!isRecord2(source.names)) {
196
+ return void 0;
197
+ }
198
+ const names = {};
199
+ for (const [utilities, name] of Object.entries(source.names)) {
200
+ if (typeof utilities === "string" && typeof name === "string") {
201
+ names[utilities] = name;
202
+ }
203
+ }
204
+ return Object.keys(names).length > 0 ? names : void 0;
205
+ }
206
+ function normalizeLoadedConfig(raw) {
207
+ if (!isRecord2(raw)) {
208
+ return {};
209
+ }
210
+ const source = isRecord2(raw.default) ? raw.default : raw;
211
+ const config = {
212
+ ...pickCommandConfig(source)
213
+ };
214
+ const names = pickNames(source);
215
+ if (names) {
216
+ config.names = names;
217
+ }
218
+ if (Array.isArray(source.include)) {
219
+ config.include = source.include.filter(
220
+ (item) => typeof item === "string" && item.length > 0
221
+ );
222
+ }
223
+ if (Array.isArray(source.exclude)) {
224
+ config.exclude = source.exclude.filter(
225
+ (item) => typeof item === "string" && item.length > 0
226
+ );
227
+ }
228
+ if (isRecord2(source.analyze)) {
229
+ config.analyze = pickCommandConfig(source.analyze);
230
+ }
231
+ if (isRecord2(source.generate)) {
232
+ config.generate = pickCommandConfig(source.generate);
233
+ }
234
+ if (isRecord2(source.apply)) {
235
+ config.apply = pickCommandConfig(source.apply);
236
+ }
237
+ return config;
238
+ }
239
+ function mergeCommandConfig(command, fileConfig) {
240
+ const { analyze, generate: generate2, apply, ...root } = fileConfig;
241
+ const commandSection = command === "analyze" ? analyze : command === "generate" ? generate2 : apply;
242
+ return {
243
+ ...root,
244
+ ...commandSection
245
+ };
246
+ }
247
+ async function pathExists(targetPath) {
248
+ try {
249
+ await fs.access(targetPath);
250
+ return true;
251
+ } catch {
252
+ return false;
253
+ }
254
+ }
255
+ async function collectSearchRoots(cwd, targetPath) {
256
+ const roots = [path.resolve(cwd)];
257
+ if (!targetPath) {
258
+ return roots;
259
+ }
260
+ let current = path.resolve(targetPath);
261
+ try {
262
+ const stat = await fs.stat(current);
263
+ if (stat.isFile()) {
264
+ current = path.dirname(current);
265
+ }
266
+ } catch {
267
+ return roots;
268
+ }
269
+ while (true) {
270
+ const resolved = path.resolve(current);
271
+ if (!roots.includes(resolved)) {
272
+ roots.push(resolved);
273
+ }
274
+ const parent = path.dirname(resolved);
275
+ if (parent === resolved) {
276
+ break;
277
+ }
278
+ current = parent;
279
+ }
280
+ return roots;
281
+ }
282
+ async function resolveConfigFile(explicitPath, searchRoots) {
283
+ if (explicitPath) {
284
+ const resolved = path.resolve(explicitPath);
285
+ if (!await pathExists(resolved)) {
286
+ throw new Error(`Config file not found: ${resolved}`);
287
+ }
288
+ return resolved;
289
+ }
290
+ for (const root of searchRoots) {
291
+ for (const filename of CONFIG_FILENAMES) {
292
+ const candidate = path.join(root, filename);
293
+ if (await pathExists(candidate)) {
294
+ return candidate;
295
+ }
296
+ }
297
+ }
298
+ return null;
299
+ }
300
+ async function importConfigModule(configPath) {
301
+ const moduleUrl = pathToFileURL(configPath).href;
302
+ const imported = await import(moduleUrl);
303
+ return imported;
304
+ }
305
+ async function readJsonConfig(configPath) {
306
+ const raw = await fs.readFile(configPath, "utf-8");
307
+ return JSON.parse(raw);
308
+ }
309
+ async function loadCommandOptions(command, cliOptions, options = {}) {
310
+ const cwd = options.cwd ?? process.cwd();
311
+ const searchRoots = await collectSearchRoots(cwd, options.targetPath);
312
+ const configPath = await resolveConfigFile(cliOptions.configPath, searchRoots);
313
+ if (!configPath) {
314
+ return cliOptions;
315
+ }
316
+ const isJson = configPath.endsWith(".json") || configPath.endsWith(".tailwind-unwindrc");
317
+ const loaded = isJson ? await readJsonConfig(configPath) : await importConfigModule(configPath);
318
+ validateConfigFile(loaded, configPath);
319
+ const fileConfig = mergeCommandConfig(command, normalizeLoadedConfig(loaded));
320
+ const { configPath: _ignored, ...cliOverrides } = cliOptions;
321
+ return {
322
+ ...fileConfig,
323
+ ...Object.fromEntries(
324
+ Object.entries(cliOverrides).filter(([, value]) => value !== void 0)
325
+ ),
326
+ configPath
327
+ };
328
+ }
329
+
1
330
  // src/analyzer/suggestions.ts
2
331
  var BREAKPOINT_PREFIX = /^(sm|md|lg|xl|2xl):/;
3
332
  function baseClass(cls) {
@@ -793,7 +1122,7 @@ function parseSourceToAst(source) {
793
1122
 
794
1123
  // src/parser/jsxParser.ts
795
1124
  import babelTraverse from "@babel/traverse";
796
- import fs from "fs/promises";
1125
+ import fs2 from "fs/promises";
797
1126
  function resolveTraverse(module) {
798
1127
  if (typeof module === "function") {
799
1128
  return module;
@@ -805,8 +1134,8 @@ function resolveTraverse(module) {
805
1134
  throw new Error("Failed to load @babel/traverse");
806
1135
  }
807
1136
  var traverse = resolveTraverse(babelTraverse);
808
- function isJSXElementWithClassAttribute(path5) {
809
- const opening = path5.node.openingElement;
1137
+ function isJSXElementWithClassAttribute(path6) {
1138
+ const opening = path6.node.openingElement;
810
1139
  return opening.attributes.some(
811
1140
  (attr) => attr.type === "JSXAttribute" && isClassAttribute(attr)
812
1141
  );
@@ -815,11 +1144,11 @@ function collectExtractionsFromAst(ast, filePath) {
815
1144
  const extractions = [];
816
1145
  const warnings = [];
817
1146
  traverse(ast, {
818
- JSXElement(path5) {
819
- if (!isJSXElementWithClassAttribute(path5)) {
1147
+ JSXElement(path6) {
1148
+ if (!isJSXElementWithClassAttribute(path6)) {
820
1149
  return;
821
1150
  }
822
- const opening = path5.node.openingElement;
1151
+ const opening = path6.node.openingElement;
823
1152
  for (const attr of opening.attributes) {
824
1153
  if (attr.type !== "JSXAttribute") continue;
825
1154
  const extraction = extractFromJSXAttribute(attr);
@@ -854,7 +1183,7 @@ function parseSource(source, filePath = "unknown") {
854
1183
  return { filePath, extractions, warnings };
855
1184
  }
856
1185
  async function parseFile(filePath) {
857
- const source = await fs.readFile(filePath, "utf-8");
1186
+ const source = await fs2.readFile(filePath, "utf-8");
858
1187
  return parseSource(source, filePath);
859
1188
  }
860
1189
 
@@ -872,40 +1201,67 @@ var IGNORE_PATTERNS = IGNORED_DIRECTORIES.map(
872
1201
 
873
1202
  // src/scanner/fileWalker.ts
874
1203
  import fg from "fast-glob";
875
- import path from "path";
1204
+ import path2 from "path";
876
1205
  var SOURCE_EXTENSIONS = ["tsx", "jsx", "ts", "js"];
877
- async function walkSourceFiles(targetPath) {
878
- const absolutePath = path.resolve(targetPath);
879
- const patterns = SOURCE_EXTENSIONS.map(
880
- (ext) => path.join(absolutePath, `**/*.${ext}`).replace(/\\/g, "/")
1206
+ function toAbsolutePattern(basePath, pattern) {
1207
+ const normalized = pattern.replace(/\\/g, "/");
1208
+ if (path2.isAbsolute(normalized)) {
1209
+ return normalized;
1210
+ }
1211
+ return path2.join(basePath, normalized).replace(/\\/g, "/");
1212
+ }
1213
+ function buildIncludePatterns(basePath, include) {
1214
+ if (include && include.length > 0) {
1215
+ return include.map((pattern) => toAbsolutePattern(basePath, pattern));
1216
+ }
1217
+ return SOURCE_EXTENSIONS.map(
1218
+ (ext) => path2.join(basePath, `**/*.${ext}`).replace(/\\/g, "/")
881
1219
  );
1220
+ }
1221
+ function buildIgnorePatterns(exclude) {
1222
+ const userExcludes = (exclude ?? []).map((pattern) => {
1223
+ const normalized = pattern.replace(/\\/g, "/");
1224
+ if (normalized.startsWith("**")) {
1225
+ return normalized;
1226
+ }
1227
+ return `**/${normalized}`;
1228
+ });
1229
+ return [...IGNORE_PATTERNS, ...userExcludes];
1230
+ }
1231
+ async function walkSourceFiles(targetPath, options = {}) {
1232
+ const absolutePath = path2.resolve(targetPath);
1233
+ const patterns = buildIncludePatterns(absolutePath, options.include);
1234
+ const ignore = buildIgnorePatterns(options.exclude);
882
1235
  const files = await fg(patterns, {
883
1236
  absolute: true,
884
1237
  onlyFiles: true,
885
1238
  unique: true,
886
- ignore: IGNORE_PATTERNS,
1239
+ ignore,
887
1240
  dot: false
888
1241
  });
889
1242
  return files.sort();
890
1243
  }
891
1244
 
892
1245
  // src/core/scanProject.ts
893
- import fs2 from "fs/promises";
894
- import path2 from "path";
895
- async function pathExists(targetPath) {
1246
+ import fs3 from "fs/promises";
1247
+ import path3 from "path";
1248
+ async function pathExists2(targetPath) {
896
1249
  try {
897
- await fs2.access(targetPath);
1250
+ await fs3.access(targetPath);
898
1251
  return true;
899
1252
  } catch {
900
1253
  return false;
901
1254
  }
902
1255
  }
903
1256
  async function scanProject(options) {
904
- const resolvedPath = path2.resolve(options.targetPath);
905
- if (!await pathExists(resolvedPath)) {
1257
+ const resolvedPath = path3.resolve(options.targetPath);
1258
+ if (!await pathExists2(resolvedPath)) {
906
1259
  throw new Error(`Path does not exist: ${resolvedPath}`);
907
1260
  }
908
- const files = await walkSourceFiles(resolvedPath);
1261
+ const files = await walkSourceFiles(resolvedPath, {
1262
+ include: options.include,
1263
+ exclude: options.exclude
1264
+ });
909
1265
  if (files.length === 0) {
910
1266
  throw new Error(
911
1267
  `No source files (.tsx, .jsx, .ts, .js) found in: ${resolvedPath}`
@@ -931,13 +1287,26 @@ async function scanProject(options) {
931
1287
  (occurrence) => normalizeClasses([...new Set(occurrence.classes)])
932
1288
  )
933
1289
  );
934
- const topCombinations = findFrequentPatterns(occurrences, {
1290
+ const frequentCombinations = findFrequentPatterns(occurrences, {
935
1291
  minOccurrences: options.minOccurrences,
936
1292
  minSize: options.minSize,
937
1293
  maxSize: options.maxSize,
938
1294
  topLimit: options.topLimit,
939
1295
  dedupeSubsets: options.dedupeSubsets
940
1296
  });
1297
+ const extractableSets = findRepeatedClassSets(occurrences, {
1298
+ minOccurrences: options.extractableMinOccurrences ?? 3,
1299
+ minSize: options.minSize,
1300
+ maxSize: options.maxSize,
1301
+ topLimit: Number.POSITIVE_INFINITY
1302
+ });
1303
+ const extractableKeys = new Set(
1304
+ extractableSets.map((combo) => combo.normalized)
1305
+ );
1306
+ const topCombinations = frequentCombinations.map((combo) => ({
1307
+ ...combo,
1308
+ extractable: extractableKeys.has(combo.normalized)
1309
+ }));
941
1310
  const potentialReductionPercent = calculatePotentialReduction(
942
1311
  occurrences,
943
1312
  topCombinations
@@ -1015,6 +1384,15 @@ function printConsoleReport(report, options = {}) {
1015
1384
  console.log(
1016
1385
  chalk.gray(` Suggestion: `) + chalk.green(combo.suggestion)
1017
1386
  );
1387
+ if (combo.extractable) {
1388
+ console.log(
1389
+ chalk.gray(` Extractable: `) + chalk.green("yes \u2014 use generate/apply")
1390
+ );
1391
+ } else {
1392
+ console.log(
1393
+ chalk.gray(` Extractable: `) + chalk.yellow("subset only \u2014 analyze hint")
1394
+ );
1395
+ }
1018
1396
  console.log(
1019
1397
  chalk.gray(` Found in: `) + chalk.dim(formatLocations(combo.locations))
1020
1398
  );
@@ -1026,6 +1404,16 @@ function printConsoleReport(report, options = {}) {
1026
1404
  `\u{1F4A1} Potential code reduction: ${stats.potentialReductionPercent}%`
1027
1405
  )
1028
1406
  );
1407
+ const extractableCount = stats.topCombinations.filter(
1408
+ (combo) => combo.extractable
1409
+ ).length;
1410
+ if (extractableCount > 0) {
1411
+ console.log(
1412
+ chalk.magenta(
1413
+ `\u{1F4A1} ${extractableCount} pattern(s) ready for generate/apply`
1414
+ )
1415
+ );
1416
+ }
1029
1417
  console.log(
1030
1418
  chalk.magenta(
1031
1419
  "\u{1F4A1} Generate CSS: npx tailwind-unwind generate <path> --output styles.css"
@@ -1055,7 +1443,10 @@ async function analyzeCommand(targetPath, options = {}) {
1055
1443
  minSize: options.minSize,
1056
1444
  maxSize: options.maxSize,
1057
1445
  topLimit: options.top,
1058
- dedupeSubsets: options.dedupeSubsets
1446
+ dedupeSubsets: options.dedupeSubsets,
1447
+ include: options.include,
1448
+ exclude: options.exclude,
1449
+ extractableMinOccurrences: 3
1059
1450
  });
1060
1451
  } catch (error) {
1061
1452
  const message = error instanceof Error ? error.message : String(error);
@@ -1102,6 +1493,18 @@ function resolveGenerator(module) {
1102
1493
  }
1103
1494
  var traverse2 = resolveTraverse2(babelTraverse2);
1104
1495
  var generate = resolveGenerator(babelGenerate);
1496
+ function isClassMergeCallee2(expression) {
1497
+ if (expression.type === "Identifier") {
1498
+ return CLASS_MERGE_CALLEES.has(expression.name);
1499
+ }
1500
+ if (expression.type === "MemberExpression" && expression.property.type === "Identifier") {
1501
+ return CLASS_MERGE_CALLEES.has(expression.property.name);
1502
+ }
1503
+ return false;
1504
+ }
1505
+ function isClassMergeCall(expression) {
1506
+ return expression.type === "CallExpression" && expression.callee.type !== "V8IntrinsicIdentifier" && isClassMergeCallee2(expression.callee);
1507
+ }
1105
1508
  function lookupReplacement(extraction, replacementMap) {
1106
1509
  if (extraction.isDynamic || extraction.classes.length === 0) {
1107
1510
  return null;
@@ -1112,6 +1515,88 @@ function lookupReplacement(extraction, replacementMap) {
1112
1515
  function setStringClassAttribute(attr, className) {
1113
1516
  attr.value = t.stringLiteral(className);
1114
1517
  }
1518
+ function tryReplaceMergeCall(call, replacementMap) {
1519
+ const staticClasses = [];
1520
+ const dynamicArgs = [];
1521
+ for (const arg of call.arguments) {
1522
+ if (arg.type === "SpreadElement" || arg.type === "ArgumentPlaceholder") {
1523
+ dynamicArgs.push(arg);
1524
+ continue;
1525
+ }
1526
+ const extracted = extractClassesFromExpression(arg);
1527
+ if (extracted.isDynamic) {
1528
+ dynamicArgs.push(arg);
1529
+ continue;
1530
+ }
1531
+ staticClasses.push(...extracted.classes);
1532
+ }
1533
+ const combinedKey = normalizeClasses([...new Set(staticClasses)]);
1534
+ const combinedReplacement = replacementMap.get(combinedKey);
1535
+ if (combinedReplacement && staticClasses.length > 0) {
1536
+ if (dynamicArgs.length === 0) {
1537
+ return {
1538
+ expression: t.stringLiteral(combinedReplacement),
1539
+ from: combinedKey,
1540
+ to: combinedReplacement,
1541
+ partial: false
1542
+ };
1543
+ }
1544
+ return {
1545
+ expression: t.callExpression(call.callee, [
1546
+ t.stringLiteral(combinedReplacement),
1547
+ ...dynamicArgs
1548
+ ]),
1549
+ from: combinedKey,
1550
+ to: combinedReplacement,
1551
+ partial: true
1552
+ };
1553
+ }
1554
+ let replaced = false;
1555
+ let replacedTo = "";
1556
+ let replacedFrom = "";
1557
+ const newArgs = [...call.arguments];
1558
+ for (let index = 0; index < call.arguments.length; index += 1) {
1559
+ const arg = call.arguments[index];
1560
+ if (arg === void 0 || arg.type === "SpreadElement" || arg.type === "ArgumentPlaceholder") {
1561
+ continue;
1562
+ }
1563
+ const extracted = extractClassesFromExpression(arg);
1564
+ if (extracted.isDynamic || extracted.classes.length === 0) {
1565
+ continue;
1566
+ }
1567
+ const argKey = normalizeClasses(extracted.classes);
1568
+ const replacement = replacementMap.get(argKey);
1569
+ if (!replacement) {
1570
+ continue;
1571
+ }
1572
+ newArgs[index] = t.stringLiteral(replacement);
1573
+ if (!replaced) {
1574
+ replacedFrom = argKey;
1575
+ replacedTo = replacement;
1576
+ }
1577
+ replaced = true;
1578
+ }
1579
+ if (!replaced) {
1580
+ return null;
1581
+ }
1582
+ return {
1583
+ expression: t.callExpression(call.callee, newArgs),
1584
+ from: replacedFrom,
1585
+ to: replacedTo,
1586
+ partial: true
1587
+ };
1588
+ }
1589
+ function tryReplaceClassAttribute(attr, replacementMap) {
1590
+ const value = attr.value;
1591
+ if (value?.type !== "JSXExpressionContainer") {
1592
+ return null;
1593
+ }
1594
+ const expression = value.expression;
1595
+ if (expression.type === "JSXEmptyExpression" || !isClassMergeCall(expression)) {
1596
+ return null;
1597
+ }
1598
+ return tryReplaceMergeCall(expression, replacementMap);
1599
+ }
1115
1600
  function replaceClassNamesInSource(source, replacementMap, filePath) {
1116
1601
  const replacements = [];
1117
1602
  const skipped = [];
@@ -1126,14 +1611,30 @@ function replaceClassNamesInSource(source, replacementMap, filePath) {
1126
1611
  throw new Error(`Failed to parse ${filePath}: ${message}`);
1127
1612
  }
1128
1613
  traverse2(ast, {
1129
- JSXElement(path5) {
1130
- const opening = path5.node.openingElement;
1614
+ JSXElement(path6) {
1615
+ const opening = path6.node.openingElement;
1131
1616
  for (const attr of opening.attributes) {
1132
1617
  if (attr.type !== "JSXAttribute" || !isClassAttribute(attr)) {
1133
1618
  continue;
1134
1619
  }
1135
1620
  const extraction = extractFromJSXAttribute(attr);
1136
1621
  if (!extraction) continue;
1622
+ const mergeReplacement = tryReplaceClassAttribute(attr, replacementMap);
1623
+ if (mergeReplacement) {
1624
+ if (mergeReplacement.expression.type === "StringLiteral") {
1625
+ setStringClassAttribute(attr, mergeReplacement.expression.value);
1626
+ } else {
1627
+ attr.value = t.jsxExpressionContainer(mergeReplacement.expression);
1628
+ }
1629
+ replacements.push({
1630
+ filePath,
1631
+ line: extraction.line,
1632
+ from: mergeReplacement.from,
1633
+ to: mergeReplacement.to,
1634
+ partial: mergeReplacement.partial
1635
+ });
1636
+ continue;
1637
+ }
1137
1638
  const replacement = lookupReplacement(extraction, replacementMap);
1138
1639
  if (!replacement) {
1139
1640
  if (extraction.classes.length > 0) {
@@ -1192,10 +1693,15 @@ function withClassPrefix(baseName, prefix) {
1192
1693
  }
1193
1694
 
1194
1695
  // src/generator/cssGenerator.ts
1696
+ function resolveBaseClassName(classes, customNames) {
1697
+ const key = normalizeClasses(classes);
1698
+ return customNames.get(key) ?? suggestClassName(classes);
1699
+ }
1195
1700
  function assignComponentClassNames(combinations, options = {}) {
1196
1701
  const used = /* @__PURE__ */ new Set();
1702
+ const customNames = normalizeNamesConfig(options.names);
1197
1703
  return combinations.map((combo) => {
1198
- const base = suggestClassName(combo.classes);
1704
+ const base = resolveBaseClassName(combo.classes, customNames);
1199
1705
  let className = withClassPrefix(base, options.prefix);
1200
1706
  let suffix = 2;
1201
1707
  while (used.has(className)) {
@@ -1212,7 +1718,8 @@ function assignComponentClassNames(combinations, options = {}) {
1212
1718
  }
1213
1719
  function generateComponentCss(options) {
1214
1720
  const components = assignComponentClassNames(options.combinations, {
1215
- prefix: options.prefix
1721
+ prefix: options.prefix,
1722
+ names: options.names
1216
1723
  });
1217
1724
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1218
1725
  const classPrefix = normalizeClassPrefix(options.prefix);
@@ -1258,7 +1765,8 @@ function buildComponents(occurrences, options) {
1258
1765
  const { css, components } = generateComponentCss({
1259
1766
  sourcePath: options.sourcePath,
1260
1767
  combinations,
1261
- prefix: options.prefix
1768
+ prefix: options.prefix,
1769
+ names: options.names
1262
1770
  });
1263
1771
  const replacementMap = /* @__PURE__ */ new Map();
1264
1772
  for (const component of components) {
@@ -1269,13 +1777,17 @@ function buildComponents(occurrences, options) {
1269
1777
  }
1270
1778
 
1271
1779
  // src/commands/apply.ts
1272
- import fs3 from "fs/promises";
1273
- import path3 from "path";
1780
+ import fs4 from "fs/promises";
1781
+ import path4 from "path";
1274
1782
  import chalk3 from "chalk";
1275
1783
  async function applyCommand(targetPath, options) {
1276
1784
  let scanResult;
1277
1785
  try {
1278
- scanResult = await scanProject({ targetPath });
1786
+ scanResult = await scanProject({
1787
+ targetPath,
1788
+ include: options.include,
1789
+ exclude: options.exclude
1790
+ });
1279
1791
  } catch (error) {
1280
1792
  const message = error instanceof Error ? error.message : String(error);
1281
1793
  console.error(chalk3.red(`Error: ${message}`));
@@ -1292,7 +1804,8 @@ async function applyCommand(targetPath, options) {
1292
1804
  minSize: options.minSize,
1293
1805
  maxSize: options.maxSize,
1294
1806
  topLimit: options.top,
1295
- prefix: options.prefix
1807
+ prefix: options.prefix,
1808
+ names: options.names
1296
1809
  }
1297
1810
  );
1298
1811
  if (components.length === 0) {
@@ -1303,12 +1816,13 @@ async function applyCommand(targetPath, options) {
1303
1816
  );
1304
1817
  process.exit(1);
1305
1818
  }
1306
- const outputPath = path3.resolve(options.output);
1819
+ const outputPath = path4.resolve(options.output);
1307
1820
  let filesModified = 0;
1308
1821
  let replacementsTotal = 0;
1309
1822
  const allReplacements = [];
1823
+ const allSkipped = [];
1310
1824
  for (const file of scanResult.files) {
1311
- const original = await fs3.readFile(file, "utf-8");
1825
+ const original = await fs4.readFile(file, "utf-8");
1312
1826
  const result = replaceClassNamesInSource(
1313
1827
  original,
1314
1828
  replacementMap,
@@ -1316,16 +1830,17 @@ async function applyCommand(targetPath, options) {
1316
1830
  );
1317
1831
  replacementsTotal += result.replacements.length;
1318
1832
  allReplacements.push(...result.replacements);
1833
+ allSkipped.push(...result.skipped);
1319
1834
  if (result.changed) {
1320
1835
  filesModified += 1;
1321
1836
  if (!options.dryRun) {
1322
- await fs3.writeFile(file, result.source, "utf-8");
1837
+ await fs4.writeFile(file, result.source, "utf-8");
1323
1838
  }
1324
1839
  }
1325
1840
  }
1326
1841
  if (!options.dryRun) {
1327
- await fs3.mkdir(path3.dirname(outputPath), { recursive: true });
1328
- await fs3.writeFile(outputPath, css, "utf-8");
1842
+ await fs4.mkdir(path4.dirname(outputPath), { recursive: true });
1843
+ await fs4.writeFile(outputPath, css, "utf-8");
1329
1844
  }
1330
1845
  console.log("");
1331
1846
  if (options.dryRun) {
@@ -1348,8 +1863,22 @@ async function applyCommand(targetPath, options) {
1348
1863
  console.log(chalk3.bold("Replacements:"));
1349
1864
  for (const item of allReplacements) {
1350
1865
  const line = item.line ? `:${item.line}` : "";
1866
+ const partialTag = item.partial ? chalk3.dim(" (partial)") : "";
1351
1867
  console.log(
1352
- chalk3.gray(` ${item.filePath}${line}`) + chalk3.white(` "${item.from}" `) + chalk3.cyan("\u2192") + chalk3.green(` "${item.to}"`)
1868
+ chalk3.gray(` ${item.filePath}${line}`) + chalk3.white(` "${item.from}" `) + chalk3.cyan("\u2192") + chalk3.green(` "${item.to}"`) + partialTag
1869
+ );
1870
+ }
1871
+ }
1872
+ if (allSkipped.length > 0) {
1873
+ console.log("");
1874
+ console.log(
1875
+ chalk3.bold.yellow(`Skipped (${allSkipped.length}):`)
1876
+ );
1877
+ for (const item of allSkipped) {
1878
+ const line = item.line ? `:${item.line}` : "";
1879
+ const classes = item.classes.join(" ");
1880
+ console.log(
1881
+ chalk3.gray(` ${item.filePath}${line}`) + chalk3.yellow(` [${item.reason}]`) + chalk3.dim(` "${classes}"`)
1353
1882
  );
1354
1883
  }
1355
1884
  }
@@ -1357,7 +1886,7 @@ async function applyCommand(targetPath, options) {
1357
1886
  if (!options.dryRun) {
1358
1887
  console.log(
1359
1888
  chalk3.cyan(
1360
- `Import ${path3.basename(outputPath)} in your global CSS if you haven't already.`
1889
+ `Import ${path4.basename(outputPath)} in your global CSS if you haven't already.`
1361
1890
  )
1362
1891
  );
1363
1892
  console.log("");
@@ -1371,13 +1900,17 @@ async function applyCommand(targetPath, options) {
1371
1900
  }
1372
1901
 
1373
1902
  // src/commands/generate.ts
1374
- import fs4 from "fs/promises";
1375
- import path4 from "path";
1903
+ import fs5 from "fs/promises";
1904
+ import path5 from "path";
1376
1905
  import chalk4 from "chalk";
1377
1906
  async function generateCommand(targetPath, options) {
1378
1907
  let scanResult;
1379
1908
  try {
1380
- scanResult = await scanProject({ targetPath });
1909
+ scanResult = await scanProject({
1910
+ targetPath,
1911
+ include: options.include,
1912
+ exclude: options.exclude
1913
+ });
1381
1914
  } catch (error) {
1382
1915
  const message = error instanceof Error ? error.message : String(error);
1383
1916
  console.error(chalk4.red(`Error: ${message}`));
@@ -1392,11 +1925,12 @@ async function generateCommand(targetPath, options) {
1392
1925
  minSize: options.minSize,
1393
1926
  maxSize: options.maxSize,
1394
1927
  topLimit: options.top,
1395
- prefix: options.prefix
1928
+ prefix: options.prefix,
1929
+ names: options.names
1396
1930
  });
1397
- const outputPath = path4.resolve(options.output);
1398
- await fs4.mkdir(path4.dirname(outputPath), { recursive: true });
1399
- await fs4.writeFile(outputPath, css, "utf-8");
1931
+ const outputPath = path5.resolve(options.output);
1932
+ await fs5.mkdir(path5.dirname(outputPath), { recursive: true });
1933
+ await fs5.writeFile(outputPath, css, "utf-8");
1400
1934
  console.log("");
1401
1935
  console.log(chalk4.bold.green("\u2705 CSS generated successfully"));
1402
1936
  console.log(chalk4.gray(` Output: `) + chalk4.white(outputPath));
@@ -1433,6 +1967,9 @@ async function generateCommand(targetPath, options) {
1433
1967
  }
1434
1968
 
1435
1969
  export {
1970
+ validateConfigFile,
1971
+ normalizeNamesConfig,
1972
+ loadCommandOptions,
1436
1973
  suggestClassName,
1437
1974
  isStrictSubset,
1438
1975
  dedupeSubsetCombinations,
@@ -1466,4 +2003,4 @@ export {
1466
2003
  applyCommand,
1467
2004
  generateCommand
1468
2005
  };
1469
- //# sourceMappingURL=chunk-N7HD4T2I.js.map
2006
+ //# sourceMappingURL=chunk-FASYIEVZ.js.map