cleanwind 0.1.3 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +31 -1
  2. package/dist/index.js +534 -92
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -4,7 +4,8 @@ Clean imports and Tailwind CSS class names from the command line.
4
4
 
5
5
  `cleanwind` removes duplicate import specifiers, merges imports from the same
6
6
  module, sorts import groups, deduplicates Tailwind CSS classes, sorts utilities,
7
- and reports conflicting classes.
7
+ reports conflicting classes, cleans static class strings in common helper calls,
8
+ and can format changed files with Prettier.
8
9
 
9
10
  ## Installation
10
11
 
@@ -20,6 +21,7 @@ For a local project install, run cleanwind through your package manager:
20
21
  npx cleanwind check
21
22
  npx cleanwind fix
22
23
  npx cleanwind fix --staged
24
+ npx cleanwind fix --format
23
25
  ```
24
26
 
25
27
  Or with npm/pnpm/yarn/bun:
@@ -62,10 +64,12 @@ npx cleanwind check --cwd ./apps/web
62
64
  npx cleanwind check --config ./cleanwind.config.ts
63
65
  npx cleanwind check --write
64
66
  npx cleanwind check --verbose
67
+ npx cleanwind check --format
65
68
 
66
69
  npx cleanwind fix
67
70
  npx cleanwind fix --staged
68
71
  npx cleanwind fix --check
72
+ npx cleanwind fix --format
69
73
  npx cleanwind fix --cwd ./apps/web --verbose
70
74
  ```
71
75
 
@@ -73,3 +77,29 @@ npx cleanwind fix --cwd ./apps/web --verbose
73
77
 
74
78
  The published CLI bundles the cleanwind core engine, so installing `cleanwind`
75
79
  does not require a separate core package.
80
+
81
+ ## Configuration
82
+
83
+ ```ts
84
+ export default {
85
+ imports: true,
86
+ tailwind: true,
87
+ removeDuplicateImports: true,
88
+ removeUnusedImports: false,
89
+ removeDuplicateClasses: true,
90
+ sortImports: true,
91
+ importAliases: ["@/"],
92
+ sortTailwindClasses: true,
93
+ tailwindFunctions: ["clsx", "classnames", "cva", "cn", "twMerge"],
94
+ detectConflicts: true,
95
+ format: false
96
+ };
97
+ ```
98
+
99
+ Use `removeUnusedImports: true` only when you want cleanwind to remove import
100
+ specifiers that appear unused. Use `format: "prettier"` or `--format` to format
101
+ output with Prettier after cleanup.
102
+
103
+ Use `importAliases` for project aliases such as `@/`, `~/`, or `~app/`. Use
104
+ `tailwindFunctions` to control which helper calls cleanwind scans for static
105
+ class strings.
package/dist/index.js CHANGED
@@ -7,14 +7,15 @@ import { Command } from "commander";
7
7
  import { existsSync } from "fs";
8
8
  import path from "path";
9
9
  import { createJiti } from "jiti";
10
- import * as t2 from "@babel/types";
10
+ import * as t from "@babel/types";
11
11
  import MagicString from "magic-string";
12
12
  import { parse } from "@babel/parser";
13
- import * as t from "@babel/types";
14
13
  import * as t3 from "@babel/types";
15
14
  import MagicString2 from "magic-string";
15
+ import * as t2 from "@babel/types";
16
16
  import { promises as fs } from "fs";
17
17
  import path3 from "path";
18
+ import * as prettier from "prettier";
18
19
  import { execFileSync } from "child_process";
19
20
  import path2 from "path";
20
21
  import fg from "fast-glob";
@@ -22,10 +23,14 @@ var defaultConfig = {
22
23
  imports: true,
23
24
  tailwind: true,
24
25
  removeDuplicateImports: true,
26
+ removeUnusedImports: false,
25
27
  removeDuplicateClasses: true,
26
28
  sortImports: true,
29
+ importAliases: ["@/"],
27
30
  sortTailwindClasses: true,
31
+ tailwindFunctions: ["clsx", "classnames", "cva", "cn", "twMerge"],
28
32
  detectConflicts: true,
33
+ format: false,
29
34
  include: ["src/**/*.{js,jsx,ts,tsx}"],
30
35
  exclude: ["node_modules", ".next", "dist"]
31
36
  };
@@ -77,26 +82,6 @@ function parseSource(source) {
77
82
  });
78
83
  }
79
84
  var visitorKeys = t.VISITOR_KEYS;
80
- function walkNode(node, enter, parent) {
81
- enter(node, parent);
82
- const keys = visitorKeys[node.type] ?? [];
83
- const record = node;
84
- for (const key of keys) {
85
- const value = record[key];
86
- if (Array.isArray(value)) {
87
- for (const child of value) {
88
- if (isNode(child)) {
89
- walkNode(child, enter, node);
90
- }
91
- }
92
- } else if (isNode(value)) {
93
- walkNode(value, enter, node);
94
- }
95
- }
96
- }
97
- function isNode(value) {
98
- return typeof value === "object" && value !== null && "type" in value;
99
- }
100
85
  var ImportCleaner = class {
101
86
  /** Checks whether imports would change after cleanwind normalization. */
102
87
  static check(context) {
@@ -115,105 +100,277 @@ var ImportCleaner = class {
115
100
  /** Returns source text with unused, duplicated, and unordered imports normalized. */
116
101
  static fix(context) {
117
102
  const ast = parseSource(context.source);
118
- const imports = ast.program.body.filter(t2.isImportDeclaration);
103
+ const imports = ast.program.body.filter(t.isImportDeclaration);
119
104
  if (imports.length === 0) {
120
105
  return { output: context.source, issues: [] };
121
106
  }
122
- const usedIdentifiers = collectUsedIdentifiers(ast);
123
- const records = buildImportRecords(imports, usedIdentifiers, context.config.sortImports);
107
+ const usedIdentifiers = context.config.removeUnusedImports ? collectUsedImportIdentifiers(ast) : void 0;
108
+ const records = buildImportRecords(imports, usedIdentifiers, {
109
+ mergeDuplicates: context.config.removeDuplicateImports,
110
+ sortImports: context.config.sortImports,
111
+ importAliases: context.config.importAliases
112
+ });
124
113
  const spans = imports.map((declaration) => spanForImport(context.source, declaration));
125
114
  const output = replaceImports(context.source, spans, renderImportBlock(records));
126
115
  return { output, issues: [] };
127
116
  }
128
117
  };
129
- function collectUsedIdentifiers(ast) {
130
- const identifiers = /* @__PURE__ */ new Set();
118
+ function collectUsedImportIdentifiers(ast) {
119
+ const used = /* @__PURE__ */ new Set();
120
+ const programScope = createScope();
131
121
  for (const statement of ast.program.body) {
132
- if (t2.isImportDeclaration(statement)) {
122
+ if (!t.isImportDeclaration(statement)) {
133
123
  continue;
134
124
  }
135
- walkNode(statement, (node, parent) => {
136
- if (t2.isIdentifier(node) && isIdentifierReference(parent, node)) {
137
- identifiers.add(node.name);
138
- }
139
- if (t2.isJSXIdentifier(node) && isJSXIdentifierReference(parent, node)) {
140
- identifiers.add(node.name);
141
- }
142
- });
125
+ for (const specifier of statement.specifiers) {
126
+ programScope.bindings.set(specifier.local.name, "import");
127
+ }
128
+ }
129
+ for (const statement of ast.program.body) {
130
+ if (t.isImportDeclaration(statement)) {
131
+ continue;
132
+ }
133
+ collectHoistedBindings(statement, programScope);
134
+ walkUsage(statement, void 0, [programScope], used);
143
135
  }
144
- return identifiers;
136
+ return used;
145
137
  }
146
138
  function isIdentifierReference(parent, node) {
147
139
  if (!parent) {
148
140
  return true;
149
141
  }
150
- if (t2.isVariableDeclarator(parent) && parent.id === node) return false;
151
- if ((t2.isFunctionDeclaration(parent) || t2.isFunctionExpression(parent)) && parent.id === node)
142
+ if (t.isVariableDeclarator(parent) && parent.id === node) return false;
143
+ if ((t.isFunctionDeclaration(parent) || t.isFunctionExpression(parent)) && parent.id === node)
152
144
  return false;
153
- if ((t2.isClassDeclaration(parent) || t2.isClassExpression(parent)) && parent.id === node)
145
+ if ((t.isClassDeclaration(parent) || t.isClassExpression(parent)) && parent.id === node)
154
146
  return false;
155
- if (t2.isObjectProperty(parent) && parent.key === node && !parent.computed) return false;
156
- if (t2.isObjectMethod(parent) && parent.key === node && !parent.computed) return false;
157
- if (t2.isMemberExpression(parent) && parent.property === node && !parent.computed) return false;
158
- if (t2.isLabeledStatement(parent) && parent.label === node) return false;
159
- if (t2.isTSTypeAliasDeclaration(parent) && parent.id === node) return false;
160
- if (t2.isTSInterfaceDeclaration(parent) && parent.id === node) return false;
147
+ if (t.isObjectProperty(parent) && parent.key === node && !parent.computed) return false;
148
+ if (t.isObjectMethod(parent) && parent.key === node && !parent.computed) return false;
149
+ if (t.isMemberExpression(parent) && parent.property === node && !parent.computed) return false;
150
+ if (t.isLabeledStatement(parent) && parent.label === node) return false;
151
+ if (t.isTSTypeAliasDeclaration(parent) && parent.id === node) return false;
152
+ if (t.isTSInterfaceDeclaration(parent) && parent.id === node) return false;
161
153
  return true;
162
154
  }
163
155
  function isJSXIdentifierReference(parent, node) {
164
156
  if (!parent) {
165
157
  return false;
166
158
  }
167
- if (t2.isJSXAttribute(parent)) {
159
+ if (t.isJSXAttribute(parent)) {
168
160
  return false;
169
161
  }
170
- return t2.isJSXOpeningElement(parent) && parent.name === node || t2.isJSXClosingElement(parent) && parent.name === node || t2.isJSXMemberExpression(parent) && parent.object === node;
162
+ return t.isJSXOpeningElement(parent) && parent.name === node || t.isJSXClosingElement(parent) && parent.name === node || t.isJSXMemberExpression(parent) && parent.object === node;
163
+ }
164
+ function walkUsage(node, parent, scopes, used) {
165
+ if (t.isImportDeclaration(node)) {
166
+ return;
167
+ }
168
+ if (t.isFunctionDeclaration(node)) {
169
+ declareIdentifier(node.id, currentScope(scopes));
170
+ const functionScope = createChildScope();
171
+ declarePatterns(node.params, functionScope);
172
+ if (node.body) {
173
+ walkUsage(node.body, node, [...scopes, functionScope], used);
174
+ }
175
+ return;
176
+ }
177
+ if (t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) {
178
+ const functionScope = createChildScope();
179
+ if (t.isFunctionExpression(node)) {
180
+ declareIdentifier(node.id, functionScope);
181
+ }
182
+ declarePatterns(node.params, functionScope);
183
+ walkChildren(node, parent, [...scopes, functionScope], used, /* @__PURE__ */ new Set(["id", "params"]));
184
+ return;
185
+ }
186
+ if (t.isBlockStatement(node) || t.isProgram(node)) {
187
+ const blockScope = t.isProgram(node) ? currentScope(scopes) : createChildScope();
188
+ collectHoistedBindings(node, blockScope);
189
+ const nextScopes = t.isProgram(node) ? scopes : [...scopes, blockScope];
190
+ walkChildren(node, parent, nextScopes, used);
191
+ return;
192
+ }
193
+ if (t.isVariableDeclarator(node)) {
194
+ declarePattern(node.id, currentScope(scopes));
195
+ if (node.init) {
196
+ walkUsage(node.init, node, scopes, used);
197
+ }
198
+ return;
199
+ }
200
+ if (t.isClassDeclaration(node)) {
201
+ declareIdentifier(node.id, currentScope(scopes));
202
+ walkChildren(node, parent, scopes, used, /* @__PURE__ */ new Set(["id"]));
203
+ return;
204
+ }
205
+ if (t.isCatchClause(node)) {
206
+ const catchScope = createChildScope();
207
+ if (node.param) {
208
+ declarePattern(node.param, catchScope);
209
+ }
210
+ walkUsage(node.body, node, [...scopes, catchScope], used);
211
+ return;
212
+ }
213
+ if (t.isTSTypeAliasDeclaration(node) || t.isTSInterfaceDeclaration(node)) {
214
+ declareIdentifier(node.id, currentScope(scopes));
215
+ walkChildren(node, parent, scopes, used, /* @__PURE__ */ new Set(["id"]));
216
+ return;
217
+ }
218
+ if (t.isIdentifier(node) && isIdentifierReference(parent, node) && resolvesToImport(node.name, scopes)) {
219
+ used.add(node.name);
220
+ }
221
+ if (t.isJSXIdentifier(node) && isJSXIdentifierReference(parent, node) && resolvesToImport(node.name, scopes)) {
222
+ used.add(node.name);
223
+ }
224
+ walkChildren(node, parent, scopes, used);
171
225
  }
172
- function buildImportRecords(declarations, usedIdentifiers, sort) {
226
+ function walkChildren(node, parent, scopes, used, skipKeys = /* @__PURE__ */ new Set()) {
227
+ const keys = visitorKeys[node.type] ?? [];
228
+ const record = node;
229
+ for (const key of keys) {
230
+ if (skipKeys.has(key)) {
231
+ continue;
232
+ }
233
+ const value = record[key];
234
+ if (Array.isArray(value)) {
235
+ for (const child of value) {
236
+ if (isNode(child)) {
237
+ walkUsage(child, node, scopes, used);
238
+ }
239
+ }
240
+ } else if (isNode(value)) {
241
+ walkUsage(value, node, scopes, used);
242
+ }
243
+ }
244
+ }
245
+ function collectHoistedBindings(node, scope) {
246
+ const body = t.isProgram(node) || t.isBlockStatement(node) ? node.body : [node];
247
+ for (const statement of body) {
248
+ if (t.isFunctionDeclaration(statement)) {
249
+ declareIdentifier(statement.id, scope);
250
+ } else if (t.isClassDeclaration(statement)) {
251
+ declareIdentifier(statement.id, scope);
252
+ } else if (t.isVariableDeclaration(statement)) {
253
+ for (const declaration of statement.declarations) {
254
+ declarePattern(declaration.id, scope);
255
+ }
256
+ } else if (t.isTSTypeAliasDeclaration(statement) || t.isTSInterfaceDeclaration(statement)) {
257
+ declareIdentifier(statement.id, scope);
258
+ }
259
+ }
260
+ }
261
+ function createScope() {
262
+ return { bindings: /* @__PURE__ */ new Map() };
263
+ }
264
+ function createChildScope() {
265
+ return createScope();
266
+ }
267
+ function currentScope(scopes) {
268
+ return scopes[scopes.length - 1] ?? createScope();
269
+ }
270
+ function declareIdentifier(node, scope) {
271
+ if (node) {
272
+ scope.bindings.set(node.name, "local");
273
+ }
274
+ }
275
+ function declarePatterns(patterns, scope) {
276
+ for (const pattern of patterns) {
277
+ declarePattern(pattern, scope);
278
+ }
279
+ }
280
+ function declarePattern(pattern, scope) {
281
+ if (t.isIdentifier(pattern)) {
282
+ scope.bindings.set(pattern.name, "local");
283
+ } else if (t.isTSParameterProperty(pattern)) {
284
+ declarePattern(pattern.parameter, scope);
285
+ } else if (t.isRestElement(pattern)) {
286
+ declarePattern(pattern.argument, scope);
287
+ } else if (t.isAssignmentPattern(pattern)) {
288
+ declarePattern(pattern.left, scope);
289
+ } else if (t.isObjectPattern(pattern)) {
290
+ for (const property of pattern.properties) {
291
+ if (t.isObjectProperty(property)) {
292
+ declarePattern(property.value, scope);
293
+ } else if (t.isRestElement(property)) {
294
+ declarePattern(property.argument, scope);
295
+ }
296
+ }
297
+ } else if (t.isArrayPattern(pattern)) {
298
+ for (const element of pattern.elements) {
299
+ if (element) {
300
+ declarePattern(element, scope);
301
+ }
302
+ }
303
+ }
304
+ }
305
+ function resolvesToImport(name, scopes) {
306
+ for (let index = scopes.length - 1; index >= 0; index -= 1) {
307
+ const binding = scopes[index]?.bindings.get(name);
308
+ if (binding) {
309
+ return binding === "import";
310
+ }
311
+ }
312
+ return false;
313
+ }
314
+ function isNode(value) {
315
+ return typeof value === "object" && value !== null && "type" in value;
316
+ }
317
+ function buildImportRecords(declarations, usedIdentifiers, options) {
173
318
  const records = /* @__PURE__ */ new Map();
174
- const sideEffects = [];
175
- for (const declaration of declarations) {
319
+ const orderedRecords = [];
320
+ let sideEffectChunk = 0;
321
+ declarations.forEach((declaration, order) => {
176
322
  const source = declaration.source.value;
177
323
  const sideEffect = declaration.specifiers.length === 0;
178
324
  if (sideEffect) {
179
- sideEffects.push({
325
+ orderedRecords.push({
326
+ key: `side-effect:${order}`,
180
327
  source,
181
328
  named: [],
182
329
  sideEffect: true,
183
- group: classifyImport(source)
330
+ group: classifyImport(source, options.importAliases),
331
+ order
184
332
  });
185
- continue;
333
+ sideEffectChunk += 1;
334
+ return;
186
335
  }
187
- const key = source;
336
+ const key = options.mergeDuplicates ? `${sideEffectChunk}:${source}` : `${order}:${source}`;
188
337
  const record = records.get(key) ?? {
189
338
  source,
339
+ key,
190
340
  named: [],
191
341
  sideEffect: false,
192
- group: classifyImport(source)
342
+ group: classifyImport(source, options.importAliases),
343
+ order
193
344
  };
194
345
  for (const specifier of declaration.specifiers) {
195
346
  const local = specifier.local.name;
196
- if (!usedIdentifiers.has(local)) {
347
+ if (usedIdentifiers && !usedIdentifiers.has(local)) {
197
348
  continue;
198
349
  }
199
- if (t2.isImportDefaultSpecifier(specifier)) {
350
+ if (t.isImportDefaultSpecifier(specifier)) {
200
351
  record.defaultName ??= local;
201
- } else if (t2.isImportNamespaceSpecifier(specifier)) {
352
+ } else if (t.isImportNamespaceSpecifier(specifier)) {
202
353
  record.namespaceName ??= local;
203
- } else if (t2.isImportSpecifier(specifier)) {
354
+ } else if (t.isImportSpecifier(specifier)) {
204
355
  const declarationKind = declaration.importKind === "type" ? "type" : "value";
205
356
  record.named.push(importPartFromSpecifier(specifier, declarationKind));
206
357
  }
207
358
  }
208
359
  if (record.defaultName || record.namespaceName || record.named.length > 0) {
209
- records.set(key, dedupeRecord(record));
360
+ const nextRecord = options.mergeDuplicates ? dedupeRecord(record) : record;
361
+ records.set(key, nextRecord);
362
+ const existingIndex = orderedRecords.findIndex((item) => item.key === key);
363
+ if (existingIndex >= 0) {
364
+ orderedRecords[existingIndex] = nextRecord;
365
+ } else {
366
+ orderedRecords.push(nextRecord);
367
+ }
210
368
  }
211
- }
212
- const merged = [...records.values(), ...sideEffects];
213
- return sort ? sortRecords(merged) : merged;
369
+ });
370
+ return options.sortImports ? sortRecords(orderedRecords) : orderedRecords;
214
371
  }
215
372
  function importPartFromSpecifier(specifier, declarationKind) {
216
- const imported = t2.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value;
373
+ const imported = t.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value;
217
374
  return {
218
375
  imported,
219
376
  local: specifier.local.name,
@@ -236,20 +393,37 @@ function sortRecords(records) {
236
393
  alias: 1,
237
394
  relative: 2
238
395
  };
239
- return [...records].sort((left, right) => {
240
- const groupDelta = groupRank[left.group] - groupRank[right.group];
241
- if (groupDelta !== 0) {
242
- return groupDelta;
243
- }
244
- const sideEffectDelta = Number(left.sideEffect) - Number(right.sideEffect);
245
- if (sideEffectDelta !== 0) {
246
- return sideEffectDelta;
396
+ const sorted = [];
397
+ let chunk = [];
398
+ const flushChunk = () => {
399
+ sorted.push(
400
+ ...chunk.sort((left, right) => {
401
+ const groupDelta = groupRank[left.group] - groupRank[right.group];
402
+ if (groupDelta !== 0) {
403
+ return groupDelta;
404
+ }
405
+ const sourceDelta = left.source.localeCompare(right.source);
406
+ if (sourceDelta !== 0) {
407
+ return sourceDelta;
408
+ }
409
+ return left.order - right.order;
410
+ })
411
+ );
412
+ chunk = [];
413
+ };
414
+ for (const record of records.sort((left, right) => left.order - right.order)) {
415
+ if (record.sideEffect) {
416
+ flushChunk();
417
+ sorted.push(record);
418
+ continue;
247
419
  }
248
- return left.source.localeCompare(right.source);
249
- });
420
+ chunk.push(record);
421
+ }
422
+ flushChunk();
423
+ return sorted;
250
424
  }
251
- function classifyImport(source) {
252
- if (source.startsWith("@/")) {
425
+ function classifyImport(source, importAliases) {
426
+ if (importAliases.some((alias) => source.startsWith(alias))) {
253
427
  return "alias";
254
428
  }
255
429
  if (source.startsWith(".")) {
@@ -258,11 +432,23 @@ function classifyImport(source) {
258
432
  return "package";
259
433
  }
260
434
  function renderImportBlock(records) {
261
- const groups = ["package", "alias", "relative"];
262
- const renderedGroups = groups.map((group) => records.filter((record) => record.group === group).map(renderImportRecord)).filter((group) => group.length > 0).map((group) => group.join("\n"));
263
- return renderedGroups.length > 0 ? `${renderedGroups.join("\n\n")}
435
+ if (records.length === 0) {
436
+ return "";
437
+ }
438
+ const rendered = [];
439
+ records.forEach((record, index) => {
440
+ const previous = records[index - 1];
441
+ if (previous && shouldSeparateImports(previous, record)) {
442
+ rendered.push("");
443
+ }
444
+ rendered.push(renderImportRecord(record));
445
+ });
446
+ return `${rendered.join("\n")}
264
447
 
265
- ` : "";
448
+ `;
449
+ }
450
+ function shouldSeparateImports(left, right) {
451
+ return left.sideEffect || right.sideEffect || left.group !== right.group;
266
452
  }
267
453
  function renderImportRecord(record) {
268
454
  if (record.sideEffect) {
@@ -303,6 +489,27 @@ function replaceImports(source, spans, importBlock) {
303
489
  magic.appendLeft(firstStart, importBlock);
304
490
  return magic.toString().replace(/^\n+/u, "");
305
491
  }
492
+ var visitorKeys2 = t2.VISITOR_KEYS;
493
+ function walkNode(node, enter, parent) {
494
+ enter(node, parent);
495
+ const keys = visitorKeys2[node.type] ?? [];
496
+ const record = node;
497
+ for (const key of keys) {
498
+ const value = record[key];
499
+ if (Array.isArray(value)) {
500
+ for (const child of value) {
501
+ if (isNode2(child)) {
502
+ walkNode(child, enter, node);
503
+ }
504
+ }
505
+ } else if (isNode2(value)) {
506
+ walkNode(value, enter, node);
507
+ }
508
+ }
509
+ }
510
+ function isNode2(value) {
511
+ return typeof value === "object" && value !== null && "type" in value;
512
+ }
306
513
  var displayClasses = /* @__PURE__ */ new Set([
307
514
  "block",
308
515
  "inline-block",
@@ -315,9 +522,42 @@ var displayClasses = /* @__PURE__ */ new Set([
315
522
  "hidden"
316
523
  ]);
317
524
  var positionClasses = /* @__PURE__ */ new Set(["static", "fixed", "absolute", "relative", "sticky"]);
318
- function normalizeClassList(value, sortClasses) {
319
- const unique = [...new Set(splitClasses(value))];
320
- const classes = sortClasses ? unique.sort(compareTailwindClasses) : unique;
525
+ var variantOrder = [
526
+ "sm",
527
+ "md",
528
+ "lg",
529
+ "xl",
530
+ "2xl",
531
+ "dark",
532
+ "motion-safe",
533
+ "motion-reduce",
534
+ "portrait",
535
+ "landscape",
536
+ "first",
537
+ "last",
538
+ "odd",
539
+ "even",
540
+ "visited",
541
+ "checked",
542
+ "empty",
543
+ "enabled",
544
+ "disabled",
545
+ "group-hover",
546
+ "group-focus",
547
+ "peer-hover",
548
+ "peer-focus",
549
+ "hover",
550
+ "focus",
551
+ "focus-within",
552
+ "focus-visible",
553
+ "active",
554
+ "invalid",
555
+ "placeholder-shown"
556
+ ];
557
+ function normalizeClassList(value, options) {
558
+ const parsed = splitClasses(value);
559
+ const deduped = options.removeDuplicates ? [...new Set(parsed)] : parsed;
560
+ const classes = options.sortClasses ? [...deduped].sort(compareTailwindClasses) : deduped;
321
561
  return classes.join(" ");
322
562
  }
323
563
  function splitClasses(value) {
@@ -326,7 +566,7 @@ function splitClasses(value) {
326
566
  function compareTailwindClasses(left, right) {
327
567
  const leftClass = classifyClass(left);
328
568
  const rightClass = classifyClass(right);
329
- const variantDelta = leftClass.variant.localeCompare(rightClass.variant);
569
+ const variantDelta = compareVariants(leftClass.variant, rightClass.variant);
330
570
  if (variantDelta !== 0) {
331
571
  return variantDelta;
332
572
  }
@@ -346,16 +586,52 @@ function conflictKey(className) {
346
586
  if (positionClasses.has(utility)) {
347
587
  return `${variantPrefix}position`;
348
588
  }
589
+ if (/^-?z-/u.test(utility)) {
590
+ return `${variantPrefix}z-index`;
591
+ }
592
+ if (utility.startsWith("overflow-")) {
593
+ return `${variantPrefix}${utility.startsWith("overflow-x-") ? "overflow-x" : utility.startsWith("overflow-y-") ? "overflow-y" : "overflow"}`;
594
+ }
349
595
  const spacing = spacingConflictKey(utility);
350
596
  if (spacing) {
351
597
  return `${variantPrefix}${spacing}`;
352
598
  }
599
+ if (/^(w|h|min-w|min-h|max-w|max-h)-/u.test(utility)) {
600
+ return `${variantPrefix}${utility.split("-").slice(0, utility.startsWith("min-") || utility.startsWith("max-") ? 2 : 1).join("-")}`;
601
+ }
353
602
  if (/^text-(xs|sm|base|lg|xl|[2-9]xl)$/u.test(utility)) {
354
603
  return `${variantPrefix}font-size`;
355
604
  }
605
+ if (/^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/u.test(utility)) {
606
+ return `${variantPrefix}font-weight`;
607
+ }
608
+ if (/^leading-/u.test(utility)) {
609
+ return `${variantPrefix}line-height`;
610
+ }
356
611
  if (utility.startsWith("bg-")) {
357
612
  return `${variantPrefix}background-color`;
358
613
  }
614
+ if (utility.startsWith("rounded")) {
615
+ return `${variantPrefix}${roundedConflictKey(utility)}`;
616
+ }
617
+ if (/^border(?:-[trblxy])?-/u.test(utility) || utility === "border") {
618
+ return `${variantPrefix}${borderConflictKey(utility)}`;
619
+ }
620
+ if (/^flex-(row|row-reverse|col|col-reverse)$/u.test(utility)) {
621
+ return `${variantPrefix}flex-direction`;
622
+ }
623
+ if (/^flex-(wrap|wrap-reverse|nowrap)$/u.test(utility)) {
624
+ return `${variantPrefix}flex-wrap`;
625
+ }
626
+ if (/^grid-cols-/u.test(utility)) {
627
+ return `${variantPrefix}grid-template-columns`;
628
+ }
629
+ if (/^grid-rows-/u.test(utility)) {
630
+ return `${variantPrefix}grid-template-rows`;
631
+ }
632
+ if (/^gap[xy]?-/u.test(utility)) {
633
+ return `${variantPrefix}${utility.startsWith("gap-x-") ? "gap-x" : utility.startsWith("gap-y-") ? "gap-y" : "gap"}`;
634
+ }
359
635
  return void 0;
360
636
  }
361
637
  function spacingConflictKey(utility) {
@@ -396,6 +672,39 @@ function splitVariant(className) {
396
672
  function stripImportant(utility) {
397
673
  return utility.startsWith("!") ? utility.slice(1) : utility;
398
674
  }
675
+ function compareVariants(left, right) {
676
+ const leftParts = left ? left.split(":") : [];
677
+ const rightParts = right ? right.split(":") : [];
678
+ const length = Math.max(leftParts.length, rightParts.length);
679
+ for (let index = 0; index < length; index += 1) {
680
+ const leftPart = leftParts[index] ?? "";
681
+ const rightPart = rightParts[index] ?? "";
682
+ const delta = variantRank(leftPart) - variantRank(rightPart);
683
+ if (delta !== 0) {
684
+ return delta;
685
+ }
686
+ const lexical = leftPart.localeCompare(rightPart);
687
+ if (lexical !== 0) {
688
+ return lexical;
689
+ }
690
+ }
691
+ return 0;
692
+ }
693
+ function variantRank(variant) {
694
+ if (!variant) {
695
+ return -1;
696
+ }
697
+ const index = variantOrder.indexOf(variant);
698
+ return index >= 0 ? index : 1e3;
699
+ }
700
+ function roundedConflictKey(utility) {
701
+ const match = /^rounded(?:-(?<side>[trbl]|tl|tr|br|bl))?-/u.exec(utility);
702
+ return match?.groups?.side ? `border-radius-${match.groups.side}` : "border-radius";
703
+ }
704
+ function borderConflictKey(utility) {
705
+ const match = /^border(?:-(?<side>[trblxy]))?(?:-|$)/u.exec(utility);
706
+ return match?.groups?.side ? `border-${match.groups.side}` : "border";
707
+ }
399
708
  function rankUtility(utility) {
400
709
  if (utility.startsWith("container")) return 0;
401
710
  if (positionClasses.has(utility) || utility.startsWith("inset-")) return 10;
@@ -436,11 +745,14 @@ var TailwindCleaner = class {
436
745
  /** Returns source text with duplicated and unordered Tailwind classes normalized. */
437
746
  static fix(context) {
438
747
  const ast = parseSource(context.source);
439
- const segments = collectClassSegments(ast);
748
+ const segments = collectClassSegments(ast, context.config.tailwindFunctions);
440
749
  const magic = new MagicString2(context.source);
441
750
  const conflicts = [];
442
751
  for (const segment of segments) {
443
- const nextValue = normalizeClassList(segment.value, context.config.sortTailwindClasses);
752
+ const nextValue = normalizeClassList(segment.value, {
753
+ removeDuplicates: context.config.removeDuplicateClasses,
754
+ sortClasses: context.config.sortTailwindClasses
755
+ });
444
756
  if (nextValue !== segment.value) {
445
757
  magic.overwrite(segment.start, segment.end, nextValue);
446
758
  }
@@ -462,8 +774,9 @@ var TailwindCleaner = class {
462
774
  };
463
775
  }
464
776
  };
465
- function collectClassSegments(ast) {
777
+ function collectClassSegments(ast, tailwindFunctions) {
466
778
  const segments = [];
779
+ const functionNames = new Set(tailwindFunctions);
467
780
  walkNode(ast, (node) => {
468
781
  if (t3.isJSXAttribute(node)) {
469
782
  if (!isClassAttribute(node)) {
@@ -480,6 +793,8 @@ function collectClassSegments(ast) {
480
793
  segments.push(segmentFromTemplateElement(quasi));
481
794
  }
482
795
  }
796
+ } else if (t3.isCallExpression(node) && isTailwindFunctionCall(node, functionNames)) {
797
+ collectCallExpressionSegments(node, segments);
483
798
  }
484
799
  });
485
800
  return segments;
@@ -507,6 +822,74 @@ function segmentFromTemplateElement(node) {
507
822
  line: node.loc?.start.line ?? 1
508
823
  };
509
824
  }
825
+ function collectCallExpressionSegments(node, segments) {
826
+ for (const argument of node.arguments) {
827
+ collectExpressionSegments(argument, segments);
828
+ }
829
+ }
830
+ function collectExpressionSegments(node, segments) {
831
+ if (t3.isStringLiteral(node)) {
832
+ segments.push(segmentFromStringLiteral(node));
833
+ return;
834
+ }
835
+ if (t3.isTemplateLiteral(node) && node.expressions.length === 0) {
836
+ const quasi = node.quasis[0];
837
+ if (quasi) {
838
+ segments.push(segmentFromTemplateElement(quasi));
839
+ }
840
+ return;
841
+ }
842
+ if (t3.isArrayExpression(node)) {
843
+ for (const element of node.elements) {
844
+ if (element) {
845
+ collectExpressionSegments(element, segments);
846
+ }
847
+ }
848
+ return;
849
+ }
850
+ if (t3.isObjectExpression(node)) {
851
+ for (const property of node.properties) {
852
+ if (t3.isObjectProperty(property)) {
853
+ collectObjectPropertySegments(property, segments);
854
+ }
855
+ }
856
+ return;
857
+ }
858
+ if (t3.isConditionalExpression(node)) {
859
+ collectExpressionSegments(node.consequent, segments);
860
+ collectExpressionSegments(node.alternate, segments);
861
+ return;
862
+ }
863
+ if (t3.isLogicalExpression(node)) {
864
+ collectExpressionSegments(node.right, segments);
865
+ }
866
+ }
867
+ function collectObjectPropertySegments(node, segments) {
868
+ if (t3.isStringLiteral(node.key)) {
869
+ segments.push(segmentFromStringLiteral(node.key));
870
+ } else if (t3.isTemplateLiteral(node.key) && node.key.expressions.length === 0) {
871
+ const quasi = node.key.quasis[0];
872
+ if (quasi) {
873
+ segments.push(segmentFromTemplateElement(quasi));
874
+ }
875
+ }
876
+ if (t3.isExpression(node.value)) {
877
+ collectExpressionSegments(node.value, segments);
878
+ }
879
+ }
880
+ function isTailwindFunctionCall(node, functionNames) {
881
+ const name = calleeName(node.callee);
882
+ return name ? functionNames.has(name) : false;
883
+ }
884
+ function calleeName(node) {
885
+ if (t3.isIdentifier(node)) {
886
+ return node.name;
887
+ }
888
+ if (t3.isMemberExpression(node) && t3.isIdentifier(node.property) && !node.computed) {
889
+ return node.property.name;
890
+ }
891
+ return void 0;
892
+ }
510
893
  function detectConflicts(file, line, classes) {
511
894
  const groups = /* @__PURE__ */ new Map();
512
895
  for (const className of classes) {
@@ -612,25 +995,83 @@ async function processProjectFiles(options, mode) {
612
995
  const result = runCleaner(ImportCleaner, mode, { filePath, source: output, config });
613
996
  issues.push(...result.issues);
614
997
  output = result.output ?? output;
998
+ if (hasParseIssue(result.issues)) {
999
+ processed.push({ filePath, source, output, issues, conflicts });
1000
+ continue;
1001
+ }
615
1002
  }
616
1003
  if (config.tailwind) {
617
1004
  const result = runCleaner(TailwindCleaner, mode, { filePath, source: output, config });
618
1005
  issues.push(...result.issues);
619
1006
  conflicts.push(...result.conflicts ?? []);
620
1007
  output = result.output ?? output;
1008
+ if (hasParseIssue(result.issues)) {
1009
+ processed.push({ filePath, source, output, issues, conflicts });
1010
+ continue;
1011
+ }
1012
+ }
1013
+ if (shouldFormat(config, options)) {
1014
+ const result = await formatWithPrettier(filePath, output);
1015
+ issues.push(...result.issues);
1016
+ output = result.output;
621
1017
  }
622
1018
  processed.push({ filePath, source, output, issues, conflicts });
623
1019
  }
624
1020
  return processed;
625
1021
  }
626
1022
  function runCleaner(cleaner, mode, context) {
627
- return mode === "check" ? cleaner.check(context) : cleaner.fix(context);
1023
+ try {
1024
+ return mode === "check" ? cleaner.check(context) : cleaner.fix(context);
1025
+ } catch (error) {
1026
+ return {
1027
+ output: context.source,
1028
+ issues: [
1029
+ {
1030
+ file: context.filePath,
1031
+ line: 1,
1032
+ kind: "parse",
1033
+ message: `Unable to parse file: ${errorMessage(error)}`
1034
+ }
1035
+ ]
1036
+ };
1037
+ }
1038
+ }
1039
+ function shouldFormat(config, options) {
1040
+ return options.format === true || config.format === "prettier";
1041
+ }
1042
+ async function formatWithPrettier(filePath, source) {
1043
+ try {
1044
+ const resolvedConfig = await prettier.resolveConfig(filePath);
1045
+ const output = await prettier.format(source, {
1046
+ ...resolvedConfig,
1047
+ filepath: filePath
1048
+ });
1049
+ return { output, issues: [] };
1050
+ } catch (error) {
1051
+ return {
1052
+ output: source,
1053
+ issues: [
1054
+ {
1055
+ file: filePath,
1056
+ line: 1,
1057
+ kind: "format",
1058
+ message: `Unable to format with Prettier: ${errorMessage(error)}`
1059
+ }
1060
+ ]
1061
+ };
1062
+ }
1063
+ }
1064
+ function errorMessage(error) {
1065
+ return error instanceof Error ? error.message : String(error);
1066
+ }
1067
+ function hasParseIssue(issues) {
1068
+ return issues.some((issue) => issue.kind === "parse");
628
1069
  }
629
1070
 
630
1071
  // src/index.ts
631
1072
  var program = new Command();
632
- program.name("cleanwind").description("Clean imports and Tailwind CSS class names.").version("0.1.3");
633
- program.command("check").description("Check files without writing changes.").option("--cwd <path>", "Working directory").option("--config <path>", "Path to cleanwind config").option("--write", "Write fixes while running check").option("--check", "Force check mode", true).option("--verbose", "Print detailed diagnostics").action(async (options) => {
1073
+ program.name("cleanwind").description("Clean imports and Tailwind CSS class names.").version("0.3.0");
1074
+ program.command("check").description("Check files without writing changes.").option("--cwd <path>", "Working directory").option("--config <path>", "Path to cleanwind config").option("--write", "Write fixes while running check").option("--check", "Force check mode", true).option("--verbose", "Print detailed diagnostics").option("--format", "Format output with Prettier after cleanup").action(async (options) => {
634
1075
  const runOptions = toRunOptions(options);
635
1076
  if (options.write) {
636
1077
  const result2 = await fix({ ...runOptions, write: true });
@@ -642,7 +1083,7 @@ program.command("check").description("Check files without writing changes.").opt
642
1083
  printCheckResult(result, options.verbose ?? false);
643
1084
  process.exitCode = result.ok ? 0 : 1;
644
1085
  });
645
- program.command("fix").description("Fix files in place.").option("--cwd <path>", "Working directory").option("--config <path>", "Path to cleanwind config").option("--write", "Write fixes", true).option("--check", "Preview fixes without writing").option("--verbose", "Print detailed diagnostics").option("--staged", "Only fix staged files").action(async (options) => {
1086
+ program.command("fix").description("Fix files in place.").option("--cwd <path>", "Working directory").option("--config <path>", "Path to cleanwind config").option("--write", "Write fixes", true).option("--check", "Preview fixes without writing").option("--verbose", "Print detailed diagnostics").option("--staged", "Only fix staged files").option("--format", "Format output with Prettier after cleanup").action(async (options) => {
646
1087
  const result = await fix({
647
1088
  ...toRunOptions(options),
648
1089
  write: options.check ? false : options.write ?? true,
@@ -660,6 +1101,7 @@ function toRunOptions(options) {
660
1101
  if (options.check !== void 0) runOptions.check = options.check;
661
1102
  if (options.verbose !== void 0) runOptions.verbose = options.verbose;
662
1103
  if (options.staged !== void 0) runOptions.staged = options.staged;
1104
+ if (options.format !== void 0) runOptions.format = options.format;
663
1105
  return runOptions;
664
1106
  }
665
1107
  function printCheckResult(result, verbose) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cleanwind",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "Clean imports and Tailwind CSS class names from the command line.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,7 +40,8 @@
40
40
  "commander": "^12.1.0",
41
41
  "fast-glob": "^3.3.2",
42
42
  "jiti": "^2.4.2",
43
- "magic-string": "^0.30.17"
43
+ "magic-string": "^0.30.17",
44
+ "prettier": "^3.4.2"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@cleanwind/core": "workspace:*"