cleanwind 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.js +627 -2
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ cleanwind fix --check
|
|
|
35
35
|
cleanwind fix --cwd ./apps/web --verbose
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
##
|
|
38
|
+
## Package
|
|
39
39
|
|
|
40
|
-
The CLI
|
|
41
|
-
|
|
40
|
+
The published CLI bundles the cleanwind core engine, so installing `cleanwind`
|
|
41
|
+
does not require a separate core package.
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,634 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
// ../core/dist/index.js
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { createJiti } from "jiti";
|
|
10
|
+
import * as t2 from "@babel/types";
|
|
11
|
+
import MagicString from "magic-string";
|
|
12
|
+
import { parse } from "@babel/parser";
|
|
13
|
+
import * as t from "@babel/types";
|
|
14
|
+
import * as t3 from "@babel/types";
|
|
15
|
+
import MagicString2 from "magic-string";
|
|
16
|
+
import { promises as fs } from "fs";
|
|
17
|
+
import path3 from "path";
|
|
18
|
+
import { execFileSync } from "child_process";
|
|
19
|
+
import path2 from "path";
|
|
20
|
+
import fg from "fast-glob";
|
|
21
|
+
var defaultConfig = {
|
|
22
|
+
imports: true,
|
|
23
|
+
tailwind: true,
|
|
24
|
+
removeDuplicateImports: true,
|
|
25
|
+
removeDuplicateClasses: true,
|
|
26
|
+
sortImports: true,
|
|
27
|
+
sortTailwindClasses: true,
|
|
28
|
+
detectConflicts: true,
|
|
29
|
+
include: ["src/**/*.{js,jsx,ts,tsx}"],
|
|
30
|
+
exclude: ["node_modules", ".next", "dist"]
|
|
31
|
+
};
|
|
32
|
+
var configFiles = [
|
|
33
|
+
"cleanwind.config.ts",
|
|
34
|
+
"cleanwind.config.mts",
|
|
35
|
+
"cleanwind.config.js",
|
|
36
|
+
"cleanwind.config.mjs"
|
|
37
|
+
];
|
|
38
|
+
async function loadConfig(cwd, configPath) {
|
|
39
|
+
const resolvedPath = configPath ? path.resolve(cwd, configPath) : findConfig(cwd);
|
|
40
|
+
if (!resolvedPath) {
|
|
41
|
+
return defaultConfig;
|
|
42
|
+
}
|
|
43
|
+
const jiti = createJiti(import.meta.url, { moduleCache: false });
|
|
44
|
+
const loaded = await jiti.import(resolvedPath, { default: true });
|
|
45
|
+
return {
|
|
46
|
+
...defaultConfig,
|
|
47
|
+
...loaded,
|
|
48
|
+
include: loaded.include ?? defaultConfig.include,
|
|
49
|
+
exclude: loaded.exclude ?? defaultConfig.exclude
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function findConfig(cwd) {
|
|
53
|
+
for (const file of configFiles) {
|
|
54
|
+
const candidate = path.join(cwd, file);
|
|
55
|
+
if (existsSync(candidate)) {
|
|
56
|
+
return candidate;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
var plugins = [
|
|
62
|
+
"jsx",
|
|
63
|
+
"typescript",
|
|
64
|
+
"decorators-legacy",
|
|
65
|
+
"classProperties",
|
|
66
|
+
"classPrivateProperties",
|
|
67
|
+
"classPrivateMethods",
|
|
68
|
+
"dynamicImport",
|
|
69
|
+
"importAttributes"
|
|
70
|
+
];
|
|
71
|
+
function parseSource(source) {
|
|
72
|
+
return parse(source, {
|
|
73
|
+
sourceType: "module",
|
|
74
|
+
plugins,
|
|
75
|
+
errorRecovery: true,
|
|
76
|
+
tokens: true
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
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
|
+
var ImportCleaner = class {
|
|
101
|
+
/** Checks whether imports would change after cleanwind normalization. */
|
|
102
|
+
static check(context) {
|
|
103
|
+
const fixed = this.fix(context);
|
|
104
|
+
const issues = [];
|
|
105
|
+
if (fixed.output !== context.source) {
|
|
106
|
+
issues.push({
|
|
107
|
+
file: context.filePath,
|
|
108
|
+
line: 1,
|
|
109
|
+
kind: "imports",
|
|
110
|
+
message: "Imports are not tidy."
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return { output: fixed.output, issues };
|
|
114
|
+
}
|
|
115
|
+
/** Returns source text with unused, duplicated, and unordered imports normalized. */
|
|
116
|
+
static fix(context) {
|
|
117
|
+
const ast = parseSource(context.source);
|
|
118
|
+
const imports = ast.program.body.filter(t2.isImportDeclaration);
|
|
119
|
+
if (imports.length === 0) {
|
|
120
|
+
return { output: context.source, issues: [] };
|
|
121
|
+
}
|
|
122
|
+
const usedIdentifiers = collectUsedIdentifiers(ast);
|
|
123
|
+
const records = buildImportRecords(imports, usedIdentifiers, context.config.sortImports);
|
|
124
|
+
const spans = imports.map((declaration) => spanForImport(context.source, declaration));
|
|
125
|
+
const output = replaceImports(context.source, spans, renderImportBlock(records));
|
|
126
|
+
return { output, issues: [] };
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
function collectUsedIdentifiers(ast) {
|
|
130
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
131
|
+
for (const statement of ast.program.body) {
|
|
132
|
+
if (t2.isImportDeclaration(statement)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
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
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return identifiers;
|
|
145
|
+
}
|
|
146
|
+
function isIdentifierReference(parent, node) {
|
|
147
|
+
if (!parent) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
if (t2.isVariableDeclarator(parent) && parent.id === node) return false;
|
|
151
|
+
if ((t2.isFunctionDeclaration(parent) || t2.isFunctionExpression(parent)) && parent.id === node)
|
|
152
|
+
return false;
|
|
153
|
+
if ((t2.isClassDeclaration(parent) || t2.isClassExpression(parent)) && parent.id === node)
|
|
154
|
+
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;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
function isJSXIdentifierReference(parent, node) {
|
|
164
|
+
if (!parent) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (t2.isJSXAttribute(parent)) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return t2.isJSXOpeningElement(parent) && parent.name === node || t2.isJSXClosingElement(parent) && parent.name === node || t2.isJSXMemberExpression(parent) && parent.object === node;
|
|
171
|
+
}
|
|
172
|
+
function buildImportRecords(declarations, usedIdentifiers, sort) {
|
|
173
|
+
const records = /* @__PURE__ */ new Map();
|
|
174
|
+
const sideEffects = [];
|
|
175
|
+
for (const declaration of declarations) {
|
|
176
|
+
const source = declaration.source.value;
|
|
177
|
+
const sideEffect = declaration.specifiers.length === 0;
|
|
178
|
+
if (sideEffect) {
|
|
179
|
+
sideEffects.push({
|
|
180
|
+
source,
|
|
181
|
+
named: [],
|
|
182
|
+
sideEffect: true,
|
|
183
|
+
group: classifyImport(source)
|
|
184
|
+
});
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const key = source;
|
|
188
|
+
const record = records.get(key) ?? {
|
|
189
|
+
source,
|
|
190
|
+
named: [],
|
|
191
|
+
sideEffect: false,
|
|
192
|
+
group: classifyImport(source)
|
|
193
|
+
};
|
|
194
|
+
for (const specifier of declaration.specifiers) {
|
|
195
|
+
const local = specifier.local.name;
|
|
196
|
+
if (!usedIdentifiers.has(local)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (t2.isImportDefaultSpecifier(specifier)) {
|
|
200
|
+
record.defaultName ??= local;
|
|
201
|
+
} else if (t2.isImportNamespaceSpecifier(specifier)) {
|
|
202
|
+
record.namespaceName ??= local;
|
|
203
|
+
} else if (t2.isImportSpecifier(specifier)) {
|
|
204
|
+
const declarationKind = declaration.importKind === "type" ? "type" : "value";
|
|
205
|
+
record.named.push(importPartFromSpecifier(specifier, declarationKind));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (record.defaultName || record.namespaceName || record.named.length > 0) {
|
|
209
|
+
records.set(key, dedupeRecord(record));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const merged = [...records.values(), ...sideEffects];
|
|
213
|
+
return sort ? sortRecords(merged) : merged;
|
|
214
|
+
}
|
|
215
|
+
function importPartFromSpecifier(specifier, declarationKind) {
|
|
216
|
+
const imported = t2.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value;
|
|
217
|
+
return {
|
|
218
|
+
imported,
|
|
219
|
+
local: specifier.local.name,
|
|
220
|
+
kind: specifier.importKind === "type" ? "type" : declarationKind
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function dedupeRecord(record) {
|
|
224
|
+
const named = /* @__PURE__ */ new Map();
|
|
225
|
+
for (const part of record.named) {
|
|
226
|
+
named.set(`${part.kind}:${part.imported}:${part.local}`, part);
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
...record,
|
|
230
|
+
named: [...named.values()].sort((left, right) => left.imported.localeCompare(right.imported))
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function sortRecords(records) {
|
|
234
|
+
const groupRank = {
|
|
235
|
+
package: 0,
|
|
236
|
+
alias: 1,
|
|
237
|
+
relative: 2
|
|
238
|
+
};
|
|
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;
|
|
247
|
+
}
|
|
248
|
+
return left.source.localeCompare(right.source);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function classifyImport(source) {
|
|
252
|
+
if (source.startsWith("@/")) {
|
|
253
|
+
return "alias";
|
|
254
|
+
}
|
|
255
|
+
if (source.startsWith(".")) {
|
|
256
|
+
return "relative";
|
|
257
|
+
}
|
|
258
|
+
return "package";
|
|
259
|
+
}
|
|
260
|
+
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")}
|
|
264
|
+
|
|
265
|
+
` : "";
|
|
266
|
+
}
|
|
267
|
+
function renderImportRecord(record) {
|
|
268
|
+
if (record.sideEffect) {
|
|
269
|
+
return `import ${JSON.stringify(record.source)};`;
|
|
270
|
+
}
|
|
271
|
+
const parts = [];
|
|
272
|
+
if (record.defaultName) {
|
|
273
|
+
parts.push(record.defaultName);
|
|
274
|
+
}
|
|
275
|
+
if (record.namespaceName) {
|
|
276
|
+
parts.push(`* as ${record.namespaceName}`);
|
|
277
|
+
}
|
|
278
|
+
if (record.named.length > 0) {
|
|
279
|
+
parts.push(`{ ${record.named.map(renderNamedPart).join(", ")} }`);
|
|
280
|
+
}
|
|
281
|
+
return `import ${parts.join(", ")} from ${JSON.stringify(record.source)};`;
|
|
282
|
+
}
|
|
283
|
+
function renderNamedPart(part) {
|
|
284
|
+
const prefix = part.kind === "type" ? "type " : "";
|
|
285
|
+
const names = part.imported === part.local ? part.imported : `${part.imported} as ${part.local}`;
|
|
286
|
+
return `${prefix}${names}`;
|
|
287
|
+
}
|
|
288
|
+
function spanForImport(source, declaration) {
|
|
289
|
+
const start = declaration.start ?? 0;
|
|
290
|
+
let end = declaration.end ?? start;
|
|
291
|
+
while (end < source.length && (source[end] === "\r" || source[end] === "\n")) {
|
|
292
|
+
end += 1;
|
|
293
|
+
}
|
|
294
|
+
return { start, end };
|
|
295
|
+
}
|
|
296
|
+
function replaceImports(source, spans, importBlock) {
|
|
297
|
+
const magic = new MagicString(source);
|
|
298
|
+
const sortedSpans = [...spans].sort((left, right) => left.start - right.start);
|
|
299
|
+
const firstStart = sortedSpans[0]?.start ?? 0;
|
|
300
|
+
for (const span of sortedSpans) {
|
|
301
|
+
magic.remove(span.start, span.end);
|
|
302
|
+
}
|
|
303
|
+
magic.appendLeft(firstStart, importBlock);
|
|
304
|
+
return magic.toString().replace(/^\n+/u, "");
|
|
305
|
+
}
|
|
306
|
+
var displayClasses = /* @__PURE__ */ new Set([
|
|
307
|
+
"block",
|
|
308
|
+
"inline-block",
|
|
309
|
+
"inline",
|
|
310
|
+
"flex",
|
|
311
|
+
"inline-flex",
|
|
312
|
+
"grid",
|
|
313
|
+
"inline-grid",
|
|
314
|
+
"contents",
|
|
315
|
+
"hidden"
|
|
316
|
+
]);
|
|
317
|
+
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;
|
|
321
|
+
return classes.join(" ");
|
|
322
|
+
}
|
|
323
|
+
function splitClasses(value) {
|
|
324
|
+
return value.split(/\s+/u).map((item) => item.trim()).filter(Boolean);
|
|
325
|
+
}
|
|
326
|
+
function compareTailwindClasses(left, right) {
|
|
327
|
+
const leftClass = classifyClass(left);
|
|
328
|
+
const rightClass = classifyClass(right);
|
|
329
|
+
const variantDelta = leftClass.variant.localeCompare(rightClass.variant);
|
|
330
|
+
if (variantDelta !== 0) {
|
|
331
|
+
return variantDelta;
|
|
332
|
+
}
|
|
333
|
+
const rankDelta = leftClass.rank - rightClass.rank;
|
|
334
|
+
if (rankDelta !== 0) {
|
|
335
|
+
return rankDelta;
|
|
336
|
+
}
|
|
337
|
+
return leftClass.utility.localeCompare(rightClass.utility);
|
|
338
|
+
}
|
|
339
|
+
function conflictKey(className) {
|
|
340
|
+
const classified = classifyClass(className);
|
|
341
|
+
const utility = stripImportant(classified.utility);
|
|
342
|
+
const variantPrefix = classified.variant ? `${classified.variant}:` : "";
|
|
343
|
+
if (displayClasses.has(utility)) {
|
|
344
|
+
return `${variantPrefix}display`;
|
|
345
|
+
}
|
|
346
|
+
if (positionClasses.has(utility)) {
|
|
347
|
+
return `${variantPrefix}position`;
|
|
348
|
+
}
|
|
349
|
+
const spacing = spacingConflictKey(utility);
|
|
350
|
+
if (spacing) {
|
|
351
|
+
return `${variantPrefix}${spacing}`;
|
|
352
|
+
}
|
|
353
|
+
if (/^text-(xs|sm|base|lg|xl|[2-9]xl)$/u.test(utility)) {
|
|
354
|
+
return `${variantPrefix}font-size`;
|
|
355
|
+
}
|
|
356
|
+
if (utility.startsWith("bg-")) {
|
|
357
|
+
return `${variantPrefix}background-color`;
|
|
358
|
+
}
|
|
359
|
+
return void 0;
|
|
360
|
+
}
|
|
361
|
+
function spacingConflictKey(utility) {
|
|
362
|
+
const match = /^(?<property>[mp])(?<axis>x|y|t|r|b|l)?-/u.exec(utility);
|
|
363
|
+
if (!match?.groups) {
|
|
364
|
+
return void 0;
|
|
365
|
+
}
|
|
366
|
+
const property = match.groups.property;
|
|
367
|
+
const axis = match.groups.axis ?? "all";
|
|
368
|
+
return `${property}-${axis}`;
|
|
369
|
+
}
|
|
370
|
+
function classifyClass(className) {
|
|
371
|
+
const parts = splitVariant(className);
|
|
372
|
+
const utility = parts.utility;
|
|
373
|
+
return {
|
|
374
|
+
variant: parts.variant,
|
|
375
|
+
utility,
|
|
376
|
+
rank: rankUtility(stripImportant(utility))
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function splitVariant(className) {
|
|
380
|
+
let bracketDepth = 0;
|
|
381
|
+
for (let index = className.length - 1; index >= 0; index -= 1) {
|
|
382
|
+
const char = className[index];
|
|
383
|
+
if (char === "]") {
|
|
384
|
+
bracketDepth += 1;
|
|
385
|
+
} else if (char === "[") {
|
|
386
|
+
bracketDepth -= 1;
|
|
387
|
+
} else if (char === ":" && bracketDepth === 0) {
|
|
388
|
+
return {
|
|
389
|
+
variant: className.slice(0, index),
|
|
390
|
+
utility: className.slice(index + 1)
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { variant: "", utility: className };
|
|
395
|
+
}
|
|
396
|
+
function stripImportant(utility) {
|
|
397
|
+
return utility.startsWith("!") ? utility.slice(1) : utility;
|
|
398
|
+
}
|
|
399
|
+
function rankUtility(utility) {
|
|
400
|
+
if (utility.startsWith("container")) return 0;
|
|
401
|
+
if (positionClasses.has(utility) || utility.startsWith("inset-")) return 10;
|
|
402
|
+
if (displayClasses.has(utility)) return 20;
|
|
403
|
+
if (utility.startsWith("float-") || utility.startsWith("clear-")) return 30;
|
|
404
|
+
if (utility.startsWith("flex-") || utility.startsWith("basis-") || utility.startsWith("grow") || utility.startsWith("shrink"))
|
|
405
|
+
return 40;
|
|
406
|
+
if (utility.startsWith("grid-") || utility.startsWith("col-") || utility.startsWith("row-") || utility.startsWith("gap-"))
|
|
407
|
+
return 50;
|
|
408
|
+
if (/^-?[mp][trblxy]?-/u.test(utility)) return 60;
|
|
409
|
+
if (/^(w|h|min-w|min-h|max-w|max-h)-/u.test(utility)) return 70;
|
|
410
|
+
if (utility.startsWith("font-") || utility.startsWith("text-") || utility.startsWith("leading-") || utility.startsWith("tracking-"))
|
|
411
|
+
return 80;
|
|
412
|
+
if (utility.startsWith("bg-")) return 90;
|
|
413
|
+
if (utility.startsWith("border") || utility.startsWith("rounded")) return 100;
|
|
414
|
+
if (utility.startsWith("shadow") || utility.startsWith("opacity-")) return 110;
|
|
415
|
+
if (utility.startsWith("transition") || utility.startsWith("duration-") || utility.startsWith("ease-"))
|
|
416
|
+
return 120;
|
|
417
|
+
if (utility.startsWith("animate-")) return 130;
|
|
418
|
+
return 1e3;
|
|
419
|
+
}
|
|
420
|
+
var TailwindCleaner = class {
|
|
421
|
+
/** Checks whether Tailwind class strings would change and reports conflicts. */
|
|
422
|
+
static check(context) {
|
|
423
|
+
const fixed = this.fix(context);
|
|
424
|
+
const issues = [];
|
|
425
|
+
if (fixed.output !== context.source) {
|
|
426
|
+
issues.push({
|
|
427
|
+
file: context.filePath,
|
|
428
|
+
line: 1,
|
|
429
|
+
kind: "tailwind",
|
|
430
|
+
message: "Tailwind classes are not tidy."
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
issues.push(...fixed.issues);
|
|
434
|
+
return fixed.conflicts ? { output: fixed.output, issues, conflicts: fixed.conflicts } : { output: fixed.output, issues };
|
|
435
|
+
}
|
|
436
|
+
/** Returns source text with duplicated and unordered Tailwind classes normalized. */
|
|
437
|
+
static fix(context) {
|
|
438
|
+
const ast = parseSource(context.source);
|
|
439
|
+
const segments = collectClassSegments(ast);
|
|
440
|
+
const magic = new MagicString2(context.source);
|
|
441
|
+
const conflicts = [];
|
|
442
|
+
for (const segment of segments) {
|
|
443
|
+
const nextValue = normalizeClassList(segment.value, context.config.sortTailwindClasses);
|
|
444
|
+
if (nextValue !== segment.value) {
|
|
445
|
+
magic.overwrite(segment.start, segment.end, nextValue);
|
|
446
|
+
}
|
|
447
|
+
if (context.config.detectConflicts) {
|
|
448
|
+
conflicts.push(
|
|
449
|
+
...detectConflicts(context.filePath, segment.line, splitClasses(segment.value))
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
output: magic.toString(),
|
|
455
|
+
issues: conflicts.map((conflict) => ({
|
|
456
|
+
file: conflict.file,
|
|
457
|
+
line: conflict.line,
|
|
458
|
+
kind: "conflict",
|
|
459
|
+
message: conflict.suggestion
|
|
460
|
+
})),
|
|
461
|
+
conflicts
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
function collectClassSegments(ast) {
|
|
466
|
+
const segments = [];
|
|
467
|
+
walkNode(ast, (node) => {
|
|
468
|
+
if (t3.isJSXAttribute(node)) {
|
|
469
|
+
if (!isClassAttribute(node)) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const value = node.value;
|
|
473
|
+
if (t3.isStringLiteral(value)) {
|
|
474
|
+
segments.push(segmentFromStringLiteral(value));
|
|
475
|
+
} else if (t3.isJSXExpressionContainer(value) && t3.isStringLiteral(value.expression)) {
|
|
476
|
+
segments.push(segmentFromStringLiteral(value.expression));
|
|
477
|
+
} else if (t3.isJSXExpressionContainer(value) && t3.isTemplateLiteral(value.expression) && value.expression.expressions.length === 0) {
|
|
478
|
+
const quasi = value.expression.quasis[0];
|
|
479
|
+
if (quasi) {
|
|
480
|
+
segments.push(segmentFromTemplateElement(quasi));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
return segments;
|
|
486
|
+
}
|
|
487
|
+
function isClassAttribute(node) {
|
|
488
|
+
return t3.isJSXIdentifier(node.name) && (node.name.name === "className" || node.name.name === "class");
|
|
489
|
+
}
|
|
490
|
+
function segmentFromStringLiteral(node) {
|
|
491
|
+
const start = (node.start ?? 0) + 1;
|
|
492
|
+
const end = (node.end ?? start) - 1;
|
|
493
|
+
return {
|
|
494
|
+
start,
|
|
495
|
+
end,
|
|
496
|
+
value: node.value,
|
|
497
|
+
line: node.loc?.start.line ?? 1
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function segmentFromTemplateElement(node) {
|
|
501
|
+
const start = (node.start ?? 0) + 1;
|
|
502
|
+
const end = (node.end ?? start) - 1;
|
|
503
|
+
return {
|
|
504
|
+
start,
|
|
505
|
+
end,
|
|
506
|
+
value: node.value.raw,
|
|
507
|
+
line: node.loc?.start.line ?? 1
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function detectConflicts(file, line, classes) {
|
|
511
|
+
const groups = /* @__PURE__ */ new Map();
|
|
512
|
+
for (const className of classes) {
|
|
513
|
+
const key = conflictKey(className);
|
|
514
|
+
if (!key) {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const group = groups.get(key) ?? [];
|
|
518
|
+
group.push(className);
|
|
519
|
+
groups.set(key, group);
|
|
520
|
+
}
|
|
521
|
+
return [...groups.values()].flatMap((group) => {
|
|
522
|
+
const unique = [...new Set(group)];
|
|
523
|
+
if (unique.length <= 1) {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
file,
|
|
528
|
+
line,
|
|
529
|
+
conflictingClasses: unique,
|
|
530
|
+
suggestion: `Conflicting Tailwind utilities found: ${unique.join(", ")}. Choose the intended utility.`
|
|
531
|
+
};
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
var sourceExtensions = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
535
|
+
async function findProjectFiles(cwd, config, staged) {
|
|
536
|
+
if (staged) {
|
|
537
|
+
return findStagedFiles(cwd, config);
|
|
538
|
+
}
|
|
539
|
+
const files = await fg(config.include, {
|
|
540
|
+
cwd,
|
|
541
|
+
absolute: true,
|
|
542
|
+
dot: true,
|
|
543
|
+
ignore: config.exclude
|
|
544
|
+
});
|
|
545
|
+
return files.map((file) => path2.normalize(file));
|
|
546
|
+
}
|
|
547
|
+
function findStagedFiles(cwd, config) {
|
|
548
|
+
const output = execFileSync("git", ["diff", "--name-only", "--cached", "--diff-filter=ACMR"], {
|
|
549
|
+
cwd,
|
|
550
|
+
encoding: "utf8"
|
|
551
|
+
});
|
|
552
|
+
return output.split(/\r?\n/u).map((file) => file.trim()).filter(Boolean).filter((file) => sourceExtensions.has(path2.extname(file))).filter(
|
|
553
|
+
(file) => !config.exclude.some((excluded) => file === excluded || file.startsWith(`${excluded}/`))
|
|
554
|
+
).map((file) => path2.resolve(cwd, file));
|
|
555
|
+
}
|
|
556
|
+
async function check(options = {}) {
|
|
557
|
+
const files = await processProjectFiles(options, "check");
|
|
558
|
+
const issues = [];
|
|
559
|
+
const conflicts = [];
|
|
560
|
+
const changedFiles = [];
|
|
561
|
+
for (const file of files) {
|
|
562
|
+
issues.push(...file.issues);
|
|
563
|
+
conflicts.push(...file.conflicts);
|
|
564
|
+
if (file.output !== file.source) {
|
|
565
|
+
changedFiles.push(file.filePath);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
ok: issues.length === 0 && conflicts.length === 0 && changedFiles.length === 0,
|
|
570
|
+
issues,
|
|
571
|
+
conflicts,
|
|
572
|
+
changedFiles
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
async function fix(options = {}) {
|
|
576
|
+
const files = await processProjectFiles(options, "fix");
|
|
577
|
+
const write = options.write ?? true;
|
|
578
|
+
const changes = [];
|
|
579
|
+
const writtenFiles = [];
|
|
580
|
+
const issues = files.flatMap((file) => file.issues);
|
|
581
|
+
const conflicts = files.flatMap((file) => file.conflicts);
|
|
582
|
+
for (const file of files) {
|
|
583
|
+
if (file.output === file.source) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
changes.push({ file: file.filePath, before: file.source, after: file.output });
|
|
587
|
+
if (write) {
|
|
588
|
+
await fs.writeFile(file.filePath, file.output, "utf8");
|
|
589
|
+
writtenFiles.push(file.filePath);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
ok: issues.length === 0 && conflicts.length === 0 && changes.length === 0,
|
|
594
|
+
issues,
|
|
595
|
+
conflicts,
|
|
596
|
+
changedFiles: changes.map((change) => change.file),
|
|
597
|
+
writtenFiles,
|
|
598
|
+
changes
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
async function processProjectFiles(options, mode) {
|
|
602
|
+
const cwd = path3.resolve(options.cwd ?? process.cwd());
|
|
603
|
+
const config = await loadConfig(cwd, options.configPath);
|
|
604
|
+
const files = await findProjectFiles(cwd, config, options.staged ?? false);
|
|
605
|
+
const processed = [];
|
|
606
|
+
for (const filePath of files) {
|
|
607
|
+
const source = await fs.readFile(filePath, "utf8");
|
|
608
|
+
let output = source;
|
|
609
|
+
const issues = [];
|
|
610
|
+
const conflicts = [];
|
|
611
|
+
if (config.imports) {
|
|
612
|
+
const result = runCleaner(ImportCleaner, mode, { filePath, source: output, config });
|
|
613
|
+
issues.push(...result.issues);
|
|
614
|
+
output = result.output ?? output;
|
|
615
|
+
}
|
|
616
|
+
if (config.tailwind) {
|
|
617
|
+
const result = runCleaner(TailwindCleaner, mode, { filePath, source: output, config });
|
|
618
|
+
issues.push(...result.issues);
|
|
619
|
+
conflicts.push(...result.conflicts ?? []);
|
|
620
|
+
output = result.output ?? output;
|
|
621
|
+
}
|
|
622
|
+
processed.push({ filePath, source, output, issues, conflicts });
|
|
623
|
+
}
|
|
624
|
+
return processed;
|
|
625
|
+
}
|
|
626
|
+
function runCleaner(cleaner, mode, context) {
|
|
627
|
+
return mode === "check" ? cleaner.check(context) : cleaner.fix(context);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/index.ts
|
|
6
631
|
var program = new Command();
|
|
7
|
-
program.name("cleanwind").description("Clean imports and Tailwind CSS class names.").version("0.1.
|
|
632
|
+
program.name("cleanwind").description("Clean imports and Tailwind CSS class names.").version("0.1.2");
|
|
8
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) => {
|
|
9
634
|
const runOptions = toRunOptions(options);
|
|
10
635
|
if (options.write) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cleanwind",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Clean imports and Tailwind CSS class names from the command line.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,11 +31,18 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
-
"build": "tsup
|
|
34
|
+
"build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
35
35
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@
|
|
39
|
-
"
|
|
38
|
+
"@babel/parser": "^7.26.3",
|
|
39
|
+
"@babel/types": "^7.26.3",
|
|
40
|
+
"commander": "^12.1.0",
|
|
41
|
+
"fast-glob": "^3.3.2",
|
|
42
|
+
"jiti": "^2.4.2",
|
|
43
|
+
"magic-string": "^0.30.17"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@cleanwind/core": "workspace:*"
|
|
40
47
|
}
|
|
41
48
|
}
|