cleanwind 0.1.3 → 0.3.1

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 +665 -108
  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
@@ -1,20 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import path4 from "path";
4
5
  import { Command } from "commander";
5
6
 
6
7
  // ../core/dist/index.js
7
8
  import { existsSync } from "fs";
8
9
  import path from "path";
9
10
  import { createJiti } from "jiti";
10
- import * as t2 from "@babel/types";
11
+ import * as t from "@babel/types";
11
12
  import MagicString from "magic-string";
12
13
  import { parse } from "@babel/parser";
13
- import * as t from "@babel/types";
14
14
  import * as t3 from "@babel/types";
15
15
  import MagicString2 from "magic-string";
16
+ import * as t2 from "@babel/types";
16
17
  import { promises as fs } from "fs";
17
18
  import path3 from "path";
19
+ import * as prettier from "prettier";
18
20
  import { execFileSync } from "child_process";
19
21
  import path2 from "path";
20
22
  import fg from "fast-glob";
@@ -22,10 +24,14 @@ var defaultConfig = {
22
24
  imports: true,
23
25
  tailwind: true,
24
26
  removeDuplicateImports: true,
27
+ removeUnusedImports: false,
25
28
  removeDuplicateClasses: true,
26
29
  sortImports: true,
30
+ importAliases: ["@/"],
27
31
  sortTailwindClasses: true,
32
+ tailwindFunctions: ["clsx", "classnames", "cva", "cn", "twMerge"],
28
33
  detectConflicts: true,
34
+ format: false,
29
35
  include: ["src/**/*.{js,jsx,ts,tsx}"],
30
36
  exclude: ["node_modules", ".next", "dist"]
31
37
  };
@@ -77,26 +83,6 @@ function parseSource(source) {
77
83
  });
78
84
  }
79
85
  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
86
  var ImportCleaner = class {
101
87
  /** Checks whether imports would change after cleanwind normalization. */
102
88
  static check(context) {
@@ -115,105 +101,277 @@ var ImportCleaner = class {
115
101
  /** Returns source text with unused, duplicated, and unordered imports normalized. */
116
102
  static fix(context) {
117
103
  const ast = parseSource(context.source);
118
- const imports = ast.program.body.filter(t2.isImportDeclaration);
104
+ const imports = ast.program.body.filter(t.isImportDeclaration);
119
105
  if (imports.length === 0) {
120
106
  return { output: context.source, issues: [] };
121
107
  }
122
- const usedIdentifiers = collectUsedIdentifiers(ast);
123
- const records = buildImportRecords(imports, usedIdentifiers, context.config.sortImports);
108
+ const usedIdentifiers = context.config.removeUnusedImports ? collectUsedImportIdentifiers(ast) : void 0;
109
+ const records = buildImportRecords(imports, usedIdentifiers, {
110
+ mergeDuplicates: context.config.removeDuplicateImports,
111
+ sortImports: context.config.sortImports,
112
+ importAliases: context.config.importAliases
113
+ });
124
114
  const spans = imports.map((declaration) => spanForImport(context.source, declaration));
125
115
  const output = replaceImports(context.source, spans, renderImportBlock(records));
126
116
  return { output, issues: [] };
127
117
  }
128
118
  };
129
- function collectUsedIdentifiers(ast) {
130
- const identifiers = /* @__PURE__ */ new Set();
119
+ function collectUsedImportIdentifiers(ast) {
120
+ const used = /* @__PURE__ */ new Set();
121
+ const programScope = createScope();
131
122
  for (const statement of ast.program.body) {
132
- if (t2.isImportDeclaration(statement)) {
123
+ if (!t.isImportDeclaration(statement)) {
133
124
  continue;
134
125
  }
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
- });
126
+ for (const specifier of statement.specifiers) {
127
+ programScope.bindings.set(specifier.local.name, "import");
128
+ }
143
129
  }
144
- return identifiers;
130
+ for (const statement of ast.program.body) {
131
+ if (t.isImportDeclaration(statement)) {
132
+ continue;
133
+ }
134
+ collectHoistedBindings(statement, programScope);
135
+ walkUsage(statement, void 0, [programScope], used);
136
+ }
137
+ return used;
145
138
  }
146
139
  function isIdentifierReference(parent, node) {
147
140
  if (!parent) {
148
141
  return true;
149
142
  }
150
- if (t2.isVariableDeclarator(parent) && parent.id === node) return false;
151
- if ((t2.isFunctionDeclaration(parent) || t2.isFunctionExpression(parent)) && parent.id === node)
143
+ if (t.isVariableDeclarator(parent) && parent.id === node) return false;
144
+ if ((t.isFunctionDeclaration(parent) || t.isFunctionExpression(parent)) && parent.id === node)
152
145
  return false;
153
- if ((t2.isClassDeclaration(parent) || t2.isClassExpression(parent)) && parent.id === node)
146
+ if ((t.isClassDeclaration(parent) || t.isClassExpression(parent)) && parent.id === node)
154
147
  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;
148
+ if (t.isObjectProperty(parent) && parent.key === node && !parent.computed) return false;
149
+ if (t.isObjectMethod(parent) && parent.key === node && !parent.computed) return false;
150
+ if (t.isMemberExpression(parent) && parent.property === node && !parent.computed) return false;
151
+ if (t.isLabeledStatement(parent) && parent.label === node) return false;
152
+ if (t.isTSTypeAliasDeclaration(parent) && parent.id === node) return false;
153
+ if (t.isTSInterfaceDeclaration(parent) && parent.id === node) return false;
161
154
  return true;
162
155
  }
163
156
  function isJSXIdentifierReference(parent, node) {
164
157
  if (!parent) {
165
158
  return false;
166
159
  }
167
- if (t2.isJSXAttribute(parent)) {
160
+ if (t.isJSXAttribute(parent)) {
168
161
  return false;
169
162
  }
170
- return t2.isJSXOpeningElement(parent) && parent.name === node || t2.isJSXClosingElement(parent) && parent.name === node || t2.isJSXMemberExpression(parent) && parent.object === node;
163
+ return t.isJSXOpeningElement(parent) && parent.name === node || t.isJSXClosingElement(parent) && parent.name === node || t.isJSXMemberExpression(parent) && parent.object === node;
164
+ }
165
+ function walkUsage(node, parent, scopes, used) {
166
+ if (t.isImportDeclaration(node)) {
167
+ return;
168
+ }
169
+ if (t.isFunctionDeclaration(node)) {
170
+ declareIdentifier(node.id, currentScope(scopes));
171
+ const functionScope = createChildScope();
172
+ declarePatterns(node.params, functionScope);
173
+ if (node.body) {
174
+ walkUsage(node.body, node, [...scopes, functionScope], used);
175
+ }
176
+ return;
177
+ }
178
+ if (t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) {
179
+ const functionScope = createChildScope();
180
+ if (t.isFunctionExpression(node)) {
181
+ declareIdentifier(node.id, functionScope);
182
+ }
183
+ declarePatterns(node.params, functionScope);
184
+ walkChildren(node, parent, [...scopes, functionScope], used, /* @__PURE__ */ new Set(["id", "params"]));
185
+ return;
186
+ }
187
+ if (t.isBlockStatement(node) || t.isProgram(node)) {
188
+ const blockScope = t.isProgram(node) ? currentScope(scopes) : createChildScope();
189
+ collectHoistedBindings(node, blockScope);
190
+ const nextScopes = t.isProgram(node) ? scopes : [...scopes, blockScope];
191
+ walkChildren(node, parent, nextScopes, used);
192
+ return;
193
+ }
194
+ if (t.isVariableDeclarator(node)) {
195
+ declarePattern(node.id, currentScope(scopes));
196
+ if (node.init) {
197
+ walkUsage(node.init, node, scopes, used);
198
+ }
199
+ return;
200
+ }
201
+ if (t.isClassDeclaration(node)) {
202
+ declareIdentifier(node.id, currentScope(scopes));
203
+ walkChildren(node, parent, scopes, used, /* @__PURE__ */ new Set(["id"]));
204
+ return;
205
+ }
206
+ if (t.isCatchClause(node)) {
207
+ const catchScope = createChildScope();
208
+ if (node.param) {
209
+ declarePattern(node.param, catchScope);
210
+ }
211
+ walkUsage(node.body, node, [...scopes, catchScope], used);
212
+ return;
213
+ }
214
+ if (t.isTSTypeAliasDeclaration(node) || t.isTSInterfaceDeclaration(node)) {
215
+ declareIdentifier(node.id, currentScope(scopes));
216
+ walkChildren(node, parent, scopes, used, /* @__PURE__ */ new Set(["id"]));
217
+ return;
218
+ }
219
+ if (t.isIdentifier(node) && isIdentifierReference(parent, node) && resolvesToImport(node.name, scopes)) {
220
+ used.add(node.name);
221
+ }
222
+ if (t.isJSXIdentifier(node) && isJSXIdentifierReference(parent, node) && resolvesToImport(node.name, scopes)) {
223
+ used.add(node.name);
224
+ }
225
+ walkChildren(node, parent, scopes, used);
226
+ }
227
+ function walkChildren(node, parent, scopes, used, skipKeys = /* @__PURE__ */ new Set()) {
228
+ const keys = visitorKeys[node.type] ?? [];
229
+ const record = node;
230
+ for (const key of keys) {
231
+ if (skipKeys.has(key)) {
232
+ continue;
233
+ }
234
+ const value = record[key];
235
+ if (Array.isArray(value)) {
236
+ for (const child of value) {
237
+ if (isNode(child)) {
238
+ walkUsage(child, node, scopes, used);
239
+ }
240
+ }
241
+ } else if (isNode(value)) {
242
+ walkUsage(value, node, scopes, used);
243
+ }
244
+ }
245
+ }
246
+ function collectHoistedBindings(node, scope) {
247
+ const body = t.isProgram(node) || t.isBlockStatement(node) ? node.body : [node];
248
+ for (const statement of body) {
249
+ if (t.isFunctionDeclaration(statement)) {
250
+ declareIdentifier(statement.id, scope);
251
+ } else if (t.isClassDeclaration(statement)) {
252
+ declareIdentifier(statement.id, scope);
253
+ } else if (t.isVariableDeclaration(statement)) {
254
+ for (const declaration of statement.declarations) {
255
+ declarePattern(declaration.id, scope);
256
+ }
257
+ } else if (t.isTSTypeAliasDeclaration(statement) || t.isTSInterfaceDeclaration(statement)) {
258
+ declareIdentifier(statement.id, scope);
259
+ }
260
+ }
261
+ }
262
+ function createScope() {
263
+ return { bindings: /* @__PURE__ */ new Map() };
264
+ }
265
+ function createChildScope() {
266
+ return createScope();
267
+ }
268
+ function currentScope(scopes) {
269
+ return scopes[scopes.length - 1] ?? createScope();
270
+ }
271
+ function declareIdentifier(node, scope) {
272
+ if (node) {
273
+ scope.bindings.set(node.name, "local");
274
+ }
275
+ }
276
+ function declarePatterns(patterns, scope) {
277
+ for (const pattern of patterns) {
278
+ declarePattern(pattern, scope);
279
+ }
280
+ }
281
+ function declarePattern(pattern, scope) {
282
+ if (t.isIdentifier(pattern)) {
283
+ scope.bindings.set(pattern.name, "local");
284
+ } else if (t.isTSParameterProperty(pattern)) {
285
+ declarePattern(pattern.parameter, scope);
286
+ } else if (t.isRestElement(pattern)) {
287
+ declarePattern(pattern.argument, scope);
288
+ } else if (t.isAssignmentPattern(pattern)) {
289
+ declarePattern(pattern.left, scope);
290
+ } else if (t.isObjectPattern(pattern)) {
291
+ for (const property of pattern.properties) {
292
+ if (t.isObjectProperty(property)) {
293
+ declarePattern(property.value, scope);
294
+ } else if (t.isRestElement(property)) {
295
+ declarePattern(property.argument, scope);
296
+ }
297
+ }
298
+ } else if (t.isArrayPattern(pattern)) {
299
+ for (const element of pattern.elements) {
300
+ if (element) {
301
+ declarePattern(element, scope);
302
+ }
303
+ }
304
+ }
305
+ }
306
+ function resolvesToImport(name, scopes) {
307
+ for (let index = scopes.length - 1; index >= 0; index -= 1) {
308
+ const binding = scopes[index]?.bindings.get(name);
309
+ if (binding) {
310
+ return binding === "import";
311
+ }
312
+ }
313
+ return false;
171
314
  }
172
- function buildImportRecords(declarations, usedIdentifiers, sort) {
315
+ function isNode(value) {
316
+ return typeof value === "object" && value !== null && "type" in value;
317
+ }
318
+ function buildImportRecords(declarations, usedIdentifiers, options) {
173
319
  const records = /* @__PURE__ */ new Map();
174
- const sideEffects = [];
175
- for (const declaration of declarations) {
320
+ const orderedRecords = [];
321
+ let sideEffectChunk = 0;
322
+ declarations.forEach((declaration, order) => {
176
323
  const source = declaration.source.value;
177
324
  const sideEffect = declaration.specifiers.length === 0;
178
325
  if (sideEffect) {
179
- sideEffects.push({
326
+ orderedRecords.push({
327
+ key: `side-effect:${order}`,
180
328
  source,
181
329
  named: [],
182
330
  sideEffect: true,
183
- group: classifyImport(source)
331
+ group: classifyImport(source, options.importAliases),
332
+ order
184
333
  });
185
- continue;
334
+ sideEffectChunk += 1;
335
+ return;
186
336
  }
187
- const key = source;
337
+ const key = options.mergeDuplicates ? `${sideEffectChunk}:${source}` : `${order}:${source}`;
188
338
  const record = records.get(key) ?? {
189
339
  source,
340
+ key,
190
341
  named: [],
191
342
  sideEffect: false,
192
- group: classifyImport(source)
343
+ group: classifyImport(source, options.importAliases),
344
+ order
193
345
  };
194
346
  for (const specifier of declaration.specifiers) {
195
347
  const local = specifier.local.name;
196
- if (!usedIdentifiers.has(local)) {
348
+ if (usedIdentifiers && !usedIdentifiers.has(local)) {
197
349
  continue;
198
350
  }
199
- if (t2.isImportDefaultSpecifier(specifier)) {
351
+ if (t.isImportDefaultSpecifier(specifier)) {
200
352
  record.defaultName ??= local;
201
- } else if (t2.isImportNamespaceSpecifier(specifier)) {
353
+ } else if (t.isImportNamespaceSpecifier(specifier)) {
202
354
  record.namespaceName ??= local;
203
- } else if (t2.isImportSpecifier(specifier)) {
355
+ } else if (t.isImportSpecifier(specifier)) {
204
356
  const declarationKind = declaration.importKind === "type" ? "type" : "value";
205
357
  record.named.push(importPartFromSpecifier(specifier, declarationKind));
206
358
  }
207
359
  }
208
360
  if (record.defaultName || record.namespaceName || record.named.length > 0) {
209
- records.set(key, dedupeRecord(record));
361
+ const nextRecord = options.mergeDuplicates ? dedupeRecord(record) : record;
362
+ records.set(key, nextRecord);
363
+ const existingIndex = orderedRecords.findIndex((item) => item.key === key);
364
+ if (existingIndex >= 0) {
365
+ orderedRecords[existingIndex] = nextRecord;
366
+ } else {
367
+ orderedRecords.push(nextRecord);
368
+ }
210
369
  }
211
- }
212
- const merged = [...records.values(), ...sideEffects];
213
- return sort ? sortRecords(merged) : merged;
370
+ });
371
+ return options.sortImports ? sortRecords(orderedRecords) : orderedRecords;
214
372
  }
215
373
  function importPartFromSpecifier(specifier, declarationKind) {
216
- const imported = t2.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value;
374
+ const imported = t.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value;
217
375
  return {
218
376
  imported,
219
377
  local: specifier.local.name,
@@ -236,20 +394,37 @@ function sortRecords(records) {
236
394
  alias: 1,
237
395
  relative: 2
238
396
  };
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;
397
+ const sorted = [];
398
+ let chunk = [];
399
+ const flushChunk = () => {
400
+ sorted.push(
401
+ ...chunk.sort((left, right) => {
402
+ const groupDelta = groupRank[left.group] - groupRank[right.group];
403
+ if (groupDelta !== 0) {
404
+ return groupDelta;
405
+ }
406
+ const sourceDelta = left.source.localeCompare(right.source);
407
+ if (sourceDelta !== 0) {
408
+ return sourceDelta;
409
+ }
410
+ return left.order - right.order;
411
+ })
412
+ );
413
+ chunk = [];
414
+ };
415
+ for (const record of records.sort((left, right) => left.order - right.order)) {
416
+ if (record.sideEffect) {
417
+ flushChunk();
418
+ sorted.push(record);
419
+ continue;
247
420
  }
248
- return left.source.localeCompare(right.source);
249
- });
421
+ chunk.push(record);
422
+ }
423
+ flushChunk();
424
+ return sorted;
250
425
  }
251
- function classifyImport(source) {
252
- if (source.startsWith("@/")) {
426
+ function classifyImport(source, importAliases) {
427
+ if (importAliases.some((alias) => source.startsWith(alias))) {
253
428
  return "alias";
254
429
  }
255
430
  if (source.startsWith(".")) {
@@ -258,11 +433,23 @@ function classifyImport(source) {
258
433
  return "package";
259
434
  }
260
435
  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")}
436
+ if (records.length === 0) {
437
+ return "";
438
+ }
439
+ const rendered = [];
440
+ records.forEach((record, index) => {
441
+ const previous = records[index - 1];
442
+ if (previous && shouldSeparateImports(previous, record)) {
443
+ rendered.push("");
444
+ }
445
+ rendered.push(renderImportRecord(record));
446
+ });
447
+ return `${rendered.join("\n")}
264
448
 
265
- ` : "";
449
+ `;
450
+ }
451
+ function shouldSeparateImports(left, right) {
452
+ return left.sideEffect || right.sideEffect || left.group !== right.group;
266
453
  }
267
454
  function renderImportRecord(record) {
268
455
  if (record.sideEffect) {
@@ -303,6 +490,27 @@ function replaceImports(source, spans, importBlock) {
303
490
  magic.appendLeft(firstStart, importBlock);
304
491
  return magic.toString().replace(/^\n+/u, "");
305
492
  }
493
+ var visitorKeys2 = t2.VISITOR_KEYS;
494
+ function walkNode(node, enter, parent) {
495
+ enter(node, parent);
496
+ const keys = visitorKeys2[node.type] ?? [];
497
+ const record = node;
498
+ for (const key of keys) {
499
+ const value = record[key];
500
+ if (Array.isArray(value)) {
501
+ for (const child of value) {
502
+ if (isNode2(child)) {
503
+ walkNode(child, enter, node);
504
+ }
505
+ }
506
+ } else if (isNode2(value)) {
507
+ walkNode(value, enter, node);
508
+ }
509
+ }
510
+ }
511
+ function isNode2(value) {
512
+ return typeof value === "object" && value !== null && "type" in value;
513
+ }
306
514
  var displayClasses = /* @__PURE__ */ new Set([
307
515
  "block",
308
516
  "inline-block",
@@ -315,9 +523,43 @@ var displayClasses = /* @__PURE__ */ new Set([
315
523
  "hidden"
316
524
  ]);
317
525
  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;
526
+ var borderStyleClasses = /* @__PURE__ */ new Set(["solid", "dashed", "dotted", "double", "hidden", "none"]);
527
+ var variantOrder = [
528
+ "sm",
529
+ "md",
530
+ "lg",
531
+ "xl",
532
+ "2xl",
533
+ "dark",
534
+ "motion-safe",
535
+ "motion-reduce",
536
+ "portrait",
537
+ "landscape",
538
+ "first",
539
+ "last",
540
+ "odd",
541
+ "even",
542
+ "visited",
543
+ "checked",
544
+ "empty",
545
+ "enabled",
546
+ "disabled",
547
+ "group-hover",
548
+ "group-focus",
549
+ "peer-hover",
550
+ "peer-focus",
551
+ "hover",
552
+ "focus",
553
+ "focus-within",
554
+ "focus-visible",
555
+ "active",
556
+ "invalid",
557
+ "placeholder-shown"
558
+ ];
559
+ function normalizeClassList(value, options) {
560
+ const parsed = splitClasses(value);
561
+ const deduped = options.removeDuplicates ? [...new Set(parsed)] : parsed;
562
+ const classes = options.sortClasses ? [...deduped].sort(compareTailwindClasses) : deduped;
321
563
  return classes.join(" ");
322
564
  }
323
565
  function splitClasses(value) {
@@ -326,7 +568,7 @@ function splitClasses(value) {
326
568
  function compareTailwindClasses(left, right) {
327
569
  const leftClass = classifyClass(left);
328
570
  const rightClass = classifyClass(right);
329
- const variantDelta = leftClass.variant.localeCompare(rightClass.variant);
571
+ const variantDelta = compareVariants(leftClass.variant, rightClass.variant);
330
572
  if (variantDelta !== 0) {
331
573
  return variantDelta;
332
574
  }
@@ -346,16 +588,53 @@ function conflictKey(className) {
346
588
  if (positionClasses.has(utility)) {
347
589
  return `${variantPrefix}position`;
348
590
  }
591
+ if (/^-?z-/u.test(utility)) {
592
+ return `${variantPrefix}z-index`;
593
+ }
594
+ if (utility.startsWith("overflow-")) {
595
+ return `${variantPrefix}${utility.startsWith("overflow-x-") ? "overflow-x" : utility.startsWith("overflow-y-") ? "overflow-y" : "overflow"}`;
596
+ }
349
597
  const spacing = spacingConflictKey(utility);
350
598
  if (spacing) {
351
599
  return `${variantPrefix}${spacing}`;
352
600
  }
601
+ if (/^(w|h|min-w|min-h|max-w|max-h)-/u.test(utility)) {
602
+ return `${variantPrefix}${utility.split("-").slice(0, utility.startsWith("min-") || utility.startsWith("max-") ? 2 : 1).join("-")}`;
603
+ }
353
604
  if (/^text-(xs|sm|base|lg|xl|[2-9]xl)$/u.test(utility)) {
354
605
  return `${variantPrefix}font-size`;
355
606
  }
607
+ if (/^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/u.test(utility)) {
608
+ return `${variantPrefix}font-weight`;
609
+ }
610
+ if (/^leading-/u.test(utility)) {
611
+ return `${variantPrefix}line-height`;
612
+ }
356
613
  if (utility.startsWith("bg-")) {
357
614
  return `${variantPrefix}background-color`;
358
615
  }
616
+ if (utility.startsWith("rounded")) {
617
+ return `${variantPrefix}${roundedConflictKey(utility)}`;
618
+ }
619
+ const border = borderConflictKey(utility);
620
+ if (border) {
621
+ return `${variantPrefix}${border}`;
622
+ }
623
+ if (/^flex-(row|row-reverse|col|col-reverse)$/u.test(utility)) {
624
+ return `${variantPrefix}flex-direction`;
625
+ }
626
+ if (/^flex-(wrap|wrap-reverse|nowrap)$/u.test(utility)) {
627
+ return `${variantPrefix}flex-wrap`;
628
+ }
629
+ if (/^grid-cols-/u.test(utility)) {
630
+ return `${variantPrefix}grid-template-columns`;
631
+ }
632
+ if (/^grid-rows-/u.test(utility)) {
633
+ return `${variantPrefix}grid-template-rows`;
634
+ }
635
+ if (/^gap[xy]?-/u.test(utility)) {
636
+ return `${variantPrefix}${utility.startsWith("gap-x-") ? "gap-x" : utility.startsWith("gap-y-") ? "gap-y" : "gap"}`;
637
+ }
359
638
  return void 0;
360
639
  }
361
640
  function spacingConflictKey(utility) {
@@ -396,6 +675,57 @@ function splitVariant(className) {
396
675
  function stripImportant(utility) {
397
676
  return utility.startsWith("!") ? utility.slice(1) : utility;
398
677
  }
678
+ function compareVariants(left, right) {
679
+ const leftParts = left ? left.split(":") : [];
680
+ const rightParts = right ? right.split(":") : [];
681
+ const length = Math.max(leftParts.length, rightParts.length);
682
+ for (let index = 0; index < length; index += 1) {
683
+ const leftPart = leftParts[index] ?? "";
684
+ const rightPart = rightParts[index] ?? "";
685
+ const delta = variantRank(leftPart) - variantRank(rightPart);
686
+ if (delta !== 0) {
687
+ return delta;
688
+ }
689
+ const lexical = leftPart.localeCompare(rightPart);
690
+ if (lexical !== 0) {
691
+ return lexical;
692
+ }
693
+ }
694
+ return 0;
695
+ }
696
+ function variantRank(variant) {
697
+ if (!variant) {
698
+ return -1;
699
+ }
700
+ const index = variantOrder.indexOf(variant);
701
+ return index >= 0 ? index : 1e3;
702
+ }
703
+ function roundedConflictKey(utility) {
704
+ const match = /^rounded(?:-(?<side>[trbl]|tl|tr|br|bl))?-/u.exec(utility);
705
+ return match?.groups?.side ? `border-radius-${match.groups.side}` : "border-radius";
706
+ }
707
+ function borderConflictKey(utility) {
708
+ if (utility === "border-collapse" || utility === "border-separate") {
709
+ return "border-collapse";
710
+ }
711
+ const match = /^border(?:-(?<side>[trblxy]))?(?:-(?<value>.+))?$/u.exec(utility);
712
+ if (!match?.groups) {
713
+ return void 0;
714
+ }
715
+ const side = match.groups.side;
716
+ const value = match.groups.value;
717
+ const sideSuffix = side ? `-${side}` : "";
718
+ if (!value || isBorderWidthValue(value)) {
719
+ return `border-width${sideSuffix}`;
720
+ }
721
+ if (!side && borderStyleClasses.has(value)) {
722
+ return "border-style";
723
+ }
724
+ return `border-color${sideSuffix}`;
725
+ }
726
+ function isBorderWidthValue(value) {
727
+ return /^(0|2|4|8|\d+|\[.+\])$/u.test(value);
728
+ }
399
729
  function rankUtility(utility) {
400
730
  if (utility.startsWith("container")) return 0;
401
731
  if (positionClasses.has(utility) || utility.startsWith("inset-")) return 10;
@@ -436,11 +766,14 @@ var TailwindCleaner = class {
436
766
  /** Returns source text with duplicated and unordered Tailwind classes normalized. */
437
767
  static fix(context) {
438
768
  const ast = parseSource(context.source);
439
- const segments = collectClassSegments(ast);
769
+ const segments = collectClassSegments(ast, context.config.tailwindFunctions);
440
770
  const magic = new MagicString2(context.source);
441
771
  const conflicts = [];
442
772
  for (const segment of segments) {
443
- const nextValue = normalizeClassList(segment.value, context.config.sortTailwindClasses);
773
+ const nextValue = normalizeClassList(segment.value, {
774
+ removeDuplicates: context.config.removeDuplicateClasses,
775
+ sortClasses: context.config.sortTailwindClasses
776
+ });
444
777
  if (nextValue !== segment.value) {
445
778
  magic.overwrite(segment.start, segment.end, nextValue);
446
779
  }
@@ -462,8 +795,9 @@ var TailwindCleaner = class {
462
795
  };
463
796
  }
464
797
  };
465
- function collectClassSegments(ast) {
798
+ function collectClassSegments(ast, tailwindFunctions) {
466
799
  const segments = [];
800
+ const functionNames = new Set(tailwindFunctions);
467
801
  walkNode(ast, (node) => {
468
802
  if (t3.isJSXAttribute(node)) {
469
803
  if (!isClassAttribute(node)) {
@@ -480,6 +814,8 @@ function collectClassSegments(ast) {
480
814
  segments.push(segmentFromTemplateElement(quasi));
481
815
  }
482
816
  }
817
+ } else if (t3.isCallExpression(node) && isTailwindFunctionCall(node, functionNames)) {
818
+ collectCallExpressionSegments(node, segments);
483
819
  }
484
820
  });
485
821
  return segments;
@@ -507,6 +843,74 @@ function segmentFromTemplateElement(node) {
507
843
  line: node.loc?.start.line ?? 1
508
844
  };
509
845
  }
846
+ function collectCallExpressionSegments(node, segments) {
847
+ for (const argument of node.arguments) {
848
+ collectExpressionSegments(argument, segments);
849
+ }
850
+ }
851
+ function collectExpressionSegments(node, segments) {
852
+ if (t3.isStringLiteral(node)) {
853
+ segments.push(segmentFromStringLiteral(node));
854
+ return;
855
+ }
856
+ if (t3.isTemplateLiteral(node) && node.expressions.length === 0) {
857
+ const quasi = node.quasis[0];
858
+ if (quasi) {
859
+ segments.push(segmentFromTemplateElement(quasi));
860
+ }
861
+ return;
862
+ }
863
+ if (t3.isArrayExpression(node)) {
864
+ for (const element of node.elements) {
865
+ if (element) {
866
+ collectExpressionSegments(element, segments);
867
+ }
868
+ }
869
+ return;
870
+ }
871
+ if (t3.isObjectExpression(node)) {
872
+ for (const property of node.properties) {
873
+ if (t3.isObjectProperty(property)) {
874
+ collectObjectPropertySegments(property, segments);
875
+ }
876
+ }
877
+ return;
878
+ }
879
+ if (t3.isConditionalExpression(node)) {
880
+ collectExpressionSegments(node.consequent, segments);
881
+ collectExpressionSegments(node.alternate, segments);
882
+ return;
883
+ }
884
+ if (t3.isLogicalExpression(node)) {
885
+ collectExpressionSegments(node.right, segments);
886
+ }
887
+ }
888
+ function collectObjectPropertySegments(node, segments) {
889
+ if (t3.isStringLiteral(node.key)) {
890
+ segments.push(segmentFromStringLiteral(node.key));
891
+ } else if (t3.isTemplateLiteral(node.key) && node.key.expressions.length === 0) {
892
+ const quasi = node.key.quasis[0];
893
+ if (quasi) {
894
+ segments.push(segmentFromTemplateElement(quasi));
895
+ }
896
+ }
897
+ if (t3.isExpression(node.value)) {
898
+ collectExpressionSegments(node.value, segments);
899
+ }
900
+ }
901
+ function isTailwindFunctionCall(node, functionNames) {
902
+ const name = calleeName(node.callee);
903
+ return name ? functionNames.has(name) : false;
904
+ }
905
+ function calleeName(node) {
906
+ if (t3.isIdentifier(node)) {
907
+ return node.name;
908
+ }
909
+ if (t3.isMemberExpression(node) && t3.isIdentifier(node.property) && !node.computed) {
910
+ return node.property.name;
911
+ }
912
+ return void 0;
913
+ }
510
914
  function detectConflicts(file, line, classes) {
511
915
  const groups = /* @__PURE__ */ new Map();
512
916
  for (const className of classes) {
@@ -612,46 +1016,104 @@ async function processProjectFiles(options, mode) {
612
1016
  const result = runCleaner(ImportCleaner, mode, { filePath, source: output, config });
613
1017
  issues.push(...result.issues);
614
1018
  output = result.output ?? output;
1019
+ if (hasParseIssue(result.issues)) {
1020
+ processed.push({ filePath, source, output, issues, conflicts });
1021
+ continue;
1022
+ }
615
1023
  }
616
1024
  if (config.tailwind) {
617
1025
  const result = runCleaner(TailwindCleaner, mode, { filePath, source: output, config });
618
1026
  issues.push(...result.issues);
619
1027
  conflicts.push(...result.conflicts ?? []);
620
1028
  output = result.output ?? output;
1029
+ if (hasParseIssue(result.issues)) {
1030
+ processed.push({ filePath, source, output, issues, conflicts });
1031
+ continue;
1032
+ }
1033
+ }
1034
+ if (shouldFormat(config, options)) {
1035
+ const result = await formatWithPrettier(filePath, output);
1036
+ issues.push(...result.issues);
1037
+ output = result.output;
621
1038
  }
622
1039
  processed.push({ filePath, source, output, issues, conflicts });
623
1040
  }
624
1041
  return processed;
625
1042
  }
626
1043
  function runCleaner(cleaner, mode, context) {
627
- return mode === "check" ? cleaner.check(context) : cleaner.fix(context);
1044
+ try {
1045
+ return mode === "check" ? cleaner.check(context) : cleaner.fix(context);
1046
+ } catch (error) {
1047
+ return {
1048
+ output: context.source,
1049
+ issues: [
1050
+ {
1051
+ file: context.filePath,
1052
+ line: 1,
1053
+ kind: "parse",
1054
+ message: `Unable to parse file: ${errorMessage(error)}`
1055
+ }
1056
+ ]
1057
+ };
1058
+ }
1059
+ }
1060
+ function shouldFormat(config, options) {
1061
+ return options.format === true || config.format === "prettier";
1062
+ }
1063
+ async function formatWithPrettier(filePath, source) {
1064
+ try {
1065
+ const resolvedConfig = await prettier.resolveConfig(filePath);
1066
+ const output = await prettier.format(source, {
1067
+ ...resolvedConfig,
1068
+ filepath: filePath
1069
+ });
1070
+ return { output, issues: [] };
1071
+ } catch (error) {
1072
+ return {
1073
+ output: source,
1074
+ issues: [
1075
+ {
1076
+ file: filePath,
1077
+ line: 1,
1078
+ kind: "format",
1079
+ message: `Unable to format with Prettier: ${errorMessage(error)}`
1080
+ }
1081
+ ]
1082
+ };
1083
+ }
1084
+ }
1085
+ function errorMessage(error) {
1086
+ return error instanceof Error ? error.message : String(error);
1087
+ }
1088
+ function hasParseIssue(issues) {
1089
+ return issues.some((issue) => issue.kind === "parse");
628
1090
  }
629
1091
 
630
1092
  // src/index.ts
1093
+ var defaultListLimit = 12;
631
1094
  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) => {
1095
+ program.name("cleanwind").description("Clean imports and Tailwind CSS class names.").version("0.3.1");
1096
+ 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
1097
  const runOptions = toRunOptions(options);
635
1098
  if (options.write) {
636
1099
  const result2 = await fix({ ...runOptions, write: true });
637
- printFixResult(result2, options.verbose ?? false);
1100
+ printFixResult(result2, options.verbose ?? false, runOptions.cwd ?? process.cwd());
638
1101
  process.exitCode = result2.conflicts.length > 0 ? 1 : 0;
639
1102
  return;
640
1103
  }
641
1104
  const result = await check(runOptions);
642
- printCheckResult(result, options.verbose ?? false);
1105
+ printCheckResult(result, options.verbose ?? false, runOptions.cwd ?? process.cwd());
643
1106
  process.exitCode = result.ok ? 0 : 1;
644
1107
  });
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) => {
1108
+ 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
1109
  const result = await fix({
647
1110
  ...toRunOptions(options),
648
1111
  write: options.check ? false : options.write ?? true,
649
1112
  staged: options.staged ?? false
650
1113
  });
651
- printFixResult(result, options.verbose ?? false);
1114
+ printFixResult(result, options.verbose ?? false, options.cwd ?? process.cwd());
652
1115
  process.exitCode = result.conflicts.length > 0 ? 1 : 0;
653
1116
  });
654
- await program.parseAsync();
655
1117
  function toRunOptions(options) {
656
1118
  const runOptions = {};
657
1119
  if (options.cwd !== void 0) runOptions.cwd = options.cwd;
@@ -660,33 +1122,128 @@ function toRunOptions(options) {
660
1122
  if (options.check !== void 0) runOptions.check = options.check;
661
1123
  if (options.verbose !== void 0) runOptions.verbose = options.verbose;
662
1124
  if (options.staged !== void 0) runOptions.staged = options.staged;
1125
+ if (options.format !== void 0) runOptions.format = options.format;
663
1126
  return runOptions;
664
1127
  }
665
- function printCheckResult(result, verbose) {
1128
+ function printCheckResult(result, verbose, cwd) {
666
1129
  if (result.ok) {
667
- console.log("cleanwind: all files are clean.");
1130
+ console.log(`${color.green("cleanwind")} all files are clean.`);
668
1131
  return;
669
1132
  }
670
- console.log(`cleanwind: ${result.changedFiles.length} file(s) need cleanup.`);
671
- printDiagnostics(result, verbose);
1133
+ console.log(`${color.bold("cleanwind check")}`);
1134
+ console.log(
1135
+ summaryLine(
1136
+ result.changedFiles.length,
1137
+ result.conflicts.length,
1138
+ result.changedFiles.length === 1 ? "needs cleanup" : "need cleanup"
1139
+ )
1140
+ );
1141
+ printFileList("Files", result.changedFiles, cwd, verbose);
1142
+ printDiagnostics(result, verbose, cwd);
1143
+ printNextStep(result);
672
1144
  }
673
- function printFixResult(result, verbose) {
1145
+ function printFixResult(result, verbose, cwd) {
674
1146
  const written = result.writtenFiles.length;
675
1147
  const changed = result.changedFiles.length;
676
1148
  const mode = written > 0 ? "fixed" : "would change";
677
- console.log(`cleanwind: ${mode} ${written > 0 ? written : changed} file(s).`);
678
- printDiagnostics(result, verbose);
1149
+ console.log(`${color.bold("cleanwind fix")}`);
1150
+ console.log(summaryLine(written > 0 ? written : changed, result.conflicts.length, mode));
1151
+ printFileList(written > 0 ? "Written" : "Would change", written > 0 ? result.writtenFiles : result.changedFiles, cwd, verbose);
1152
+ printDiagnostics(result, verbose, cwd);
1153
+ printNextStep(result);
1154
+ }
1155
+ function summaryLine(fileCount, conflictCount, mode) {
1156
+ const files = `${fileCount} file${fileCount === 1 ? "" : "s"}`;
1157
+ const filePhrase = mode === "would change" ? `${mode} ${files}` : `${files} ${mode}`;
1158
+ const conflicts = conflictCount > 0 ? `, ${color.yellow(`${conflictCount} conflict${conflictCount === 1 ? "" : "s"}`)}` : "";
1159
+ return `${color.cyan(filePhrase)}${conflicts}.`;
1160
+ }
1161
+ function printFileList(title, files, cwd, verbose) {
1162
+ if (files.length === 0) {
1163
+ return;
1164
+ }
1165
+ const shown = verbose ? files : files.slice(0, defaultListLimit);
1166
+ console.log("");
1167
+ console.log(color.bold(title));
1168
+ for (const file of shown) {
1169
+ console.log(` ${formatPath(file, cwd)}`);
1170
+ }
1171
+ if (!verbose && files.length > shown.length) {
1172
+ console.log(color.dim(` ... ${files.length - shown.length} more. Run with --verbose to show all.`));
1173
+ }
679
1174
  }
680
- function printDiagnostics(result, verbose) {
681
- for (const conflict of result.conflicts) {
1175
+ function printDiagnostics(result, verbose, cwd) {
1176
+ printConflicts(result, verbose, cwd);
1177
+ printIssues(result, verbose, cwd);
1178
+ }
1179
+ function printConflicts(result, verbose, cwd) {
1180
+ if (result.conflicts.length === 0) {
1181
+ return;
1182
+ }
1183
+ const conflicts = verbose ? result.conflicts : result.conflicts.slice(0, defaultListLimit);
1184
+ let currentFile = "";
1185
+ console.error("");
1186
+ console.error(color.bold("Conflicts"));
1187
+ for (const conflict of conflicts) {
1188
+ const file = formatPath(conflict.file, cwd);
1189
+ if (file !== currentFile) {
1190
+ currentFile = file;
1191
+ console.error(` ${color.cyan(file)}`);
1192
+ }
1193
+ console.error(
1194
+ ` ${color.dim(String(conflict.line).padStart(4, " "))} ${conflict.conflictingClasses.join(" + ")}`
1195
+ );
1196
+ }
1197
+ if (!verbose && result.conflicts.length > conflicts.length) {
682
1198
  console.error(
683
- `${conflict.file}:${conflict.line} ${conflict.suggestion} (${conflict.conflictingClasses.join(" ")})`
1199
+ color.dim(
1200
+ ` ... ${result.conflicts.length - conflicts.length} more conflict(s). Run with --verbose to show all.`
1201
+ )
684
1202
  );
685
1203
  }
1204
+ }
1205
+ function printIssues(result, verbose, cwd) {
686
1206
  if (!verbose) {
687
1207
  return;
688
1208
  }
689
- for (const issue of result.issues) {
690
- console.error(`${issue.file}:${issue.line} [${issue.kind}] ${issue.message}`);
1209
+ const issues = result.issues.filter((issue) => issue.kind !== "conflict");
1210
+ if (issues.length === 0) {
1211
+ return;
1212
+ }
1213
+ console.error("");
1214
+ console.error(color.bold("Diagnostics"));
1215
+ for (const issue of issues) {
1216
+ console.error(
1217
+ ` ${color.cyan(formatPath(issue.file, cwd))}:${issue.line} ${color.dim(`[${issue.kind}]`)} ${issue.message}`
1218
+ );
1219
+ }
1220
+ }
1221
+ function printNextStep(result) {
1222
+ if (result.conflicts.length > 0) {
1223
+ console.log("");
1224
+ console.log(color.dim("Resolve conflicts manually, then run: npx cleanwind fix --format"));
1225
+ return;
1226
+ }
1227
+ if (result.changedFiles.length > 0) {
1228
+ console.log("");
1229
+ console.log(color.dim("Apply cleanup with: npx cleanwind fix --format"));
691
1230
  }
692
1231
  }
1232
+ function formatPath(file, cwd) {
1233
+ const relative = path4.relative(path4.resolve(cwd), file);
1234
+ return (relative && !relative.startsWith("..") ? relative : file).replaceAll("\\", "/");
1235
+ }
1236
+ function supportsColor() {
1237
+ return process.env.NO_COLOR === void 0 && process.stdout.isTTY === true;
1238
+ }
1239
+ function paint(code, value) {
1240
+ return supportsColor() ? `\x1B[${code}m${value}\x1B[0m` : value;
1241
+ }
1242
+ var color = {
1243
+ bold: (value) => paint("1", value),
1244
+ cyan: (value) => paint("36", value),
1245
+ dim: (value) => paint("2", value),
1246
+ green: (value) => paint("32", value),
1247
+ yellow: (value) => paint("33", value)
1248
+ };
1249
+ await program.parseAsync();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cleanwind",
3
- "version": "0.1.3",
3
+ "version": "0.3.1",
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:*"