@rielj/nyx 0.0.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.
- package/dist/cli.js +535 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +346 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import fs2 from "fs";
|
|
5
|
+
import path2 from "path";
|
|
6
|
+
import { defineCommand, runMain } from "citty";
|
|
7
|
+
import pc2 from "picocolors";
|
|
8
|
+
|
|
9
|
+
// src/scanner.ts
|
|
10
|
+
import { Scanner } from "@tailwindcss/oxide";
|
|
11
|
+
function extractCandidatesWithPositions(content, extension = "html") {
|
|
12
|
+
const scanner = new Scanner({});
|
|
13
|
+
const result = scanner.getCandidatesWithPositions({ content, extension });
|
|
14
|
+
const candidates = [];
|
|
15
|
+
for (const { candidate: rawCandidate, position: start } of result) {
|
|
16
|
+
candidates.push({ rawCandidate, start, end: start + rawCandidate.length });
|
|
17
|
+
}
|
|
18
|
+
return candidates;
|
|
19
|
+
}
|
|
20
|
+
function createScanner(sources) {
|
|
21
|
+
const scanner = new Scanner({ sources });
|
|
22
|
+
return {
|
|
23
|
+
get files() {
|
|
24
|
+
return scanner.files;
|
|
25
|
+
},
|
|
26
|
+
extractCandidatesWithPositions(content, extension) {
|
|
27
|
+
return extractCandidatesWithPositions(content, extension);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/canonicalize.ts
|
|
33
|
+
function offsetToLineColumn(content, offset) {
|
|
34
|
+
let line = 1;
|
|
35
|
+
let column = 1;
|
|
36
|
+
for (let i = 0; i < offset && i < content.length; i++) {
|
|
37
|
+
if (content[i] === "\n") {
|
|
38
|
+
line++;
|
|
39
|
+
column = 1;
|
|
40
|
+
} else {
|
|
41
|
+
column++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { line, column };
|
|
45
|
+
}
|
|
46
|
+
function getLineContent(content, offset) {
|
|
47
|
+
let start = offset;
|
|
48
|
+
while (start > 0 && content[start - 1] !== "\n") {
|
|
49
|
+
start--;
|
|
50
|
+
}
|
|
51
|
+
let end = offset;
|
|
52
|
+
while (end < content.length && content[end] !== "\n") {
|
|
53
|
+
end++;
|
|
54
|
+
}
|
|
55
|
+
return content.slice(start, end);
|
|
56
|
+
}
|
|
57
|
+
function findNonCanonicalClasses(designSystem, content, extension, filePath, options = {}) {
|
|
58
|
+
const candidates = extractCandidatesWithPositions(content, extension);
|
|
59
|
+
const diagnostics = [];
|
|
60
|
+
for (const { rawCandidate, start } of candidates) {
|
|
61
|
+
const [canonical] = designSystem.canonicalizeCandidates([rawCandidate], {
|
|
62
|
+
rem: options.rem,
|
|
63
|
+
collapse: false
|
|
64
|
+
});
|
|
65
|
+
if (canonical && canonical !== rawCandidate) {
|
|
66
|
+
const { line, column } = offsetToLineColumn(content, start);
|
|
67
|
+
diagnostics.push({
|
|
68
|
+
file: filePath,
|
|
69
|
+
line,
|
|
70
|
+
column,
|
|
71
|
+
offset: start,
|
|
72
|
+
length: rawCandidate.length,
|
|
73
|
+
original: rawCandidate,
|
|
74
|
+
canonical,
|
|
75
|
+
lineContent: getLineContent(content, start)
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return diagnostics;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/design-system.ts
|
|
83
|
+
import fs from "fs";
|
|
84
|
+
import path from "path";
|
|
85
|
+
import { __unstable__loadDesignSystem } from "@tailwindcss/node";
|
|
86
|
+
var TAILWIND_IMPORT_RE = /@import\s+['"]tailwindcss['"]/;
|
|
87
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".nuxt", ".output"]);
|
|
88
|
+
function detectCssEntryPoint(base) {
|
|
89
|
+
const found = findCssWithTailwind(base, 3);
|
|
90
|
+
return found;
|
|
91
|
+
}
|
|
92
|
+
function findCssWithTailwind(dir, maxDepth) {
|
|
93
|
+
if (maxDepth < 0) return null;
|
|
94
|
+
let entries;
|
|
95
|
+
try {
|
|
96
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (!entry.isFile() || !entry.name.endsWith(".css")) continue;
|
|
102
|
+
const fullPath = path.join(dir, entry.name);
|
|
103
|
+
try {
|
|
104
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
105
|
+
if (TAILWIND_IMPORT_RE.test(content)) {
|
|
106
|
+
return fullPath;
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
|
|
114
|
+
const result = findCssWithTailwind(path.join(dir, entry.name), maxDepth - 1);
|
|
115
|
+
if (result) return result;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
async function loadDesignSystem(options) {
|
|
120
|
+
let cssPath;
|
|
121
|
+
if (options.css) {
|
|
122
|
+
cssPath = path.resolve(options.base, options.css);
|
|
123
|
+
} else {
|
|
124
|
+
const detected = detectCssEntryPoint(options.base);
|
|
125
|
+
if (!detected) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
'Could not find a CSS entry point with `@import "tailwindcss"`. Specify one with --css <path>.'
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
cssPath = detected;
|
|
131
|
+
}
|
|
132
|
+
if (!fs.existsSync(cssPath)) {
|
|
133
|
+
throw new Error(`CSS file not found: ${cssPath}`);
|
|
134
|
+
}
|
|
135
|
+
const cssContent = fs.readFileSync(cssPath, "utf-8");
|
|
136
|
+
return __unstable__loadDesignSystem(cssContent, {
|
|
137
|
+
base: path.dirname(cssPath)
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/fixer.ts
|
|
142
|
+
function applyFixes(content, diagnostics) {
|
|
143
|
+
if (diagnostics.length === 0) return content;
|
|
144
|
+
const changes = diagnostics.map((d) => ({
|
|
145
|
+
start: d.offset,
|
|
146
|
+
end: d.offset + d.length,
|
|
147
|
+
replacement: d.canonical
|
|
148
|
+
}));
|
|
149
|
+
return spliceChangesIntoString(content, changes);
|
|
150
|
+
}
|
|
151
|
+
function spliceChangesIntoString(str, changes) {
|
|
152
|
+
if (!changes[0]) return str;
|
|
153
|
+
changes.sort((a, b) => a.end - b.end || a.start - b.start);
|
|
154
|
+
let result = "";
|
|
155
|
+
let previous = changes[0];
|
|
156
|
+
result += str.slice(0, previous.start);
|
|
157
|
+
result += previous.replacement;
|
|
158
|
+
for (let i = 1; i < changes.length; ++i) {
|
|
159
|
+
const change = changes[i];
|
|
160
|
+
result += str.slice(previous.end, change.start);
|
|
161
|
+
result += change.replacement;
|
|
162
|
+
previous = change;
|
|
163
|
+
}
|
|
164
|
+
result += str.slice(previous.end);
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/reporter.ts
|
|
169
|
+
import pc from "picocolors";
|
|
170
|
+
function formatDiagnostics(diagnosticsByFile, options) {
|
|
171
|
+
if (options.json) {
|
|
172
|
+
return formatJson(diagnosticsByFile);
|
|
173
|
+
}
|
|
174
|
+
if (options.quiet) {
|
|
175
|
+
return formatQuiet(diagnosticsByFile, options.mode, options.kind);
|
|
176
|
+
}
|
|
177
|
+
return formatCodeframe(diagnosticsByFile, options.mode, options.kind);
|
|
178
|
+
}
|
|
179
|
+
function formatJson(diagnosticsByFile) {
|
|
180
|
+
const output = {};
|
|
181
|
+
for (const [file, diagnostics] of diagnosticsByFile) {
|
|
182
|
+
output[file] = diagnostics.map((d) => ({
|
|
183
|
+
line: d.line,
|
|
184
|
+
column: d.column,
|
|
185
|
+
original: d.original,
|
|
186
|
+
canonical: d.canonical
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
return JSON.stringify(output, null, 2);
|
|
190
|
+
}
|
|
191
|
+
function formatQuiet(diagnosticsByFile, mode, kind) {
|
|
192
|
+
const { totalDiagnostics, totalFiles } = countTotals(diagnosticsByFile);
|
|
193
|
+
if (totalDiagnostics === 0) return "";
|
|
194
|
+
return formatSummary(totalDiagnostics, totalFiles, mode, kind);
|
|
195
|
+
}
|
|
196
|
+
function formatCodeframe(diagnosticsByFile, mode, kind) {
|
|
197
|
+
const lines = [];
|
|
198
|
+
const { totalDiagnostics, totalFiles } = countTotals(diagnosticsByFile);
|
|
199
|
+
const isFormat = kind === "format";
|
|
200
|
+
if (totalDiagnostics === 0) return "";
|
|
201
|
+
for (const [file, diagnostics] of diagnosticsByFile) {
|
|
202
|
+
for (const d of diagnostics) {
|
|
203
|
+
lines.push("");
|
|
204
|
+
lines.push(
|
|
205
|
+
`${pc.cyan(file)}${pc.dim(":")}${pc.yellow(String(d.line))}${pc.dim(":")}${pc.yellow(String(d.column))} ${pc.dim("warn")} ${pc.dim(isFormat ? "unsortedClasses" : "suggestCanonicalClasses")}`
|
|
206
|
+
);
|
|
207
|
+
lines.push(
|
|
208
|
+
isFormat ? ` Classes should be sorted as ${pc.green(pc.bold(d.canonical))}` : ` The class ${pc.red(pc.bold(d.original))} can be written as ${pc.green(pc.bold(d.canonical))}`
|
|
209
|
+
);
|
|
210
|
+
const contextLines = getContextLines(d);
|
|
211
|
+
lines.push("");
|
|
212
|
+
for (const cl of contextLines) {
|
|
213
|
+
lines.push(cl);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
lines.push("");
|
|
218
|
+
lines.push(formatSummary(totalDiagnostics, totalFiles, mode, kind));
|
|
219
|
+
return lines.join("\n");
|
|
220
|
+
}
|
|
221
|
+
function getContextLines(d) {
|
|
222
|
+
const lines = [];
|
|
223
|
+
const lineNum = String(d.line);
|
|
224
|
+
const gutter = " ".repeat(lineNum.length + 2);
|
|
225
|
+
lines.push(` ${pc.dim(lineNum)} ${pc.dim("\u2502")} ${d.lineContent}`);
|
|
226
|
+
const colOffset = d.column - 1;
|
|
227
|
+
const underline = " ".repeat(colOffset) + pc.red("~".repeat(d.original.length));
|
|
228
|
+
lines.push(` ${gutter}${pc.dim("\u2502")} ${underline}`);
|
|
229
|
+
const fixedLine = d.lineContent.slice(0, colOffset) + d.canonical + d.lineContent.slice(colOffset + d.original.length);
|
|
230
|
+
lines.push(` ${gutter}${pc.dim("\u2502")} ${pc.dim("\u2139 Suggested fix:")}`);
|
|
231
|
+
lines.push(` ${pc.dim(lineNum)} ${pc.dim("\u2502")} ${fixedLine}`);
|
|
232
|
+
const fixUnderline = " ".repeat(colOffset) + pc.green("~".repeat(d.canonical.length));
|
|
233
|
+
lines.push(` ${gutter}${pc.dim("\u2502")} ${fixUnderline}`);
|
|
234
|
+
return lines;
|
|
235
|
+
}
|
|
236
|
+
function countTotals(diagnosticsByFile) {
|
|
237
|
+
let totalDiagnostics = 0;
|
|
238
|
+
let totalFiles = 0;
|
|
239
|
+
for (const [, diagnostics] of diagnosticsByFile) {
|
|
240
|
+
if (diagnostics.length > 0) {
|
|
241
|
+
totalDiagnostics += diagnostics.length;
|
|
242
|
+
totalFiles++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return { totalDiagnostics, totalFiles };
|
|
246
|
+
}
|
|
247
|
+
function formatSummary(count, files, mode, kind) {
|
|
248
|
+
const fileWord = files === 1 ? "file" : "files";
|
|
249
|
+
if (kind === "format") {
|
|
250
|
+
const groupWord = count === 1 ? "class group" : "class groups";
|
|
251
|
+
if (mode === "fix") {
|
|
252
|
+
return pc.green(`Sorted ${count} ${groupWord} in ${files} ${fileWord}.`);
|
|
253
|
+
}
|
|
254
|
+
return pc.yellow(
|
|
255
|
+
`Found ${count} unsorted ${groupWord} in ${files} ${fileWord}. Run ${pc.bold("nyx format --fix")} to apply.`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const classWord = count === 1 ? "class" : "classes";
|
|
259
|
+
if (mode === "fix") {
|
|
260
|
+
return pc.green(`Fixed ${count} non-canonical ${classWord} in ${files} ${fileWord}.`);
|
|
261
|
+
}
|
|
262
|
+
return pc.yellow(
|
|
263
|
+
`Found ${count} non-canonical ${classWord} in ${files} ${fileWord}. Run ${pc.bold("nyx lint --fix")} to apply.`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/sorter.ts
|
|
268
|
+
var CLASS_ATTR_RE = /class(?:Name)?\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
269
|
+
function offsetToLineColumn2(content, offset) {
|
|
270
|
+
let line = 1;
|
|
271
|
+
let column = 1;
|
|
272
|
+
for (let i = 0; i < offset && i < content.length; i++) {
|
|
273
|
+
if (content[i] === "\n") {
|
|
274
|
+
line++;
|
|
275
|
+
column = 1;
|
|
276
|
+
} else {
|
|
277
|
+
column++;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return { line, column };
|
|
281
|
+
}
|
|
282
|
+
function getLineContent2(content, offset) {
|
|
283
|
+
let start = offset;
|
|
284
|
+
while (start > 0 && content[start - 1] !== "\n") {
|
|
285
|
+
start--;
|
|
286
|
+
}
|
|
287
|
+
let end = offset;
|
|
288
|
+
while (end < content.length && content[end] !== "\n") {
|
|
289
|
+
end++;
|
|
290
|
+
}
|
|
291
|
+
return content.slice(start, end);
|
|
292
|
+
}
|
|
293
|
+
function findUnsortedClasses(designSystem, content, filePath, options = {}) {
|
|
294
|
+
const strategy = options.strategy ?? "tailwind";
|
|
295
|
+
const diagnostics = [];
|
|
296
|
+
CLASS_ATTR_RE.lastIndex = 0;
|
|
297
|
+
let match;
|
|
298
|
+
while ((match = CLASS_ATTR_RE.exec(content)) !== null) {
|
|
299
|
+
const classValue = match[1] ?? match[2];
|
|
300
|
+
if (!classValue) continue;
|
|
301
|
+
const quoteChar = match[1] !== void 0 ? '"' : "'";
|
|
302
|
+
const valueOffset = match.index + match[0].indexOf(quoteChar) + 1;
|
|
303
|
+
const classes = classValue.split(/\s+/).filter(Boolean);
|
|
304
|
+
if (classes.length <= 1) continue;
|
|
305
|
+
const sorted = strategy === "alphabetical" ? sortAlphabetical(classes) : sortTailwind(designSystem, classes);
|
|
306
|
+
const sortedValue = sorted.join(" ");
|
|
307
|
+
const originalValue = classes.join(" ");
|
|
308
|
+
if (sortedValue !== originalValue) {
|
|
309
|
+
const { line, column } = offsetToLineColumn2(content, valueOffset);
|
|
310
|
+
diagnostics.push({
|
|
311
|
+
file: filePath,
|
|
312
|
+
line,
|
|
313
|
+
column,
|
|
314
|
+
offset: valueOffset,
|
|
315
|
+
length: classValue.length,
|
|
316
|
+
original: classValue,
|
|
317
|
+
canonical: sortedValue,
|
|
318
|
+
lineContent: getLineContent2(content, valueOffset)
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return diagnostics;
|
|
323
|
+
}
|
|
324
|
+
function sortAlphabetical(classes) {
|
|
325
|
+
return [...classes].sort((a, b) => a.localeCompare(b));
|
|
326
|
+
}
|
|
327
|
+
function sortTailwind(designSystem, classes) {
|
|
328
|
+
const ordered = designSystem.getClassOrder(classes);
|
|
329
|
+
const unknown = [];
|
|
330
|
+
const known = [];
|
|
331
|
+
for (const [cls, order] of ordered) {
|
|
332
|
+
if (order === null) {
|
|
333
|
+
unknown.push(cls);
|
|
334
|
+
} else {
|
|
335
|
+
known.push([cls, order]);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
known.sort((a, b) => {
|
|
339
|
+
if (a[1] < b[1]) return -1;
|
|
340
|
+
if (a[1] > b[1]) return 1;
|
|
341
|
+
return 0;
|
|
342
|
+
});
|
|
343
|
+
return [...unknown, ...known.map(([cls]) => cls)];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/cli.ts
|
|
347
|
+
var EXTENSIONS = /* @__PURE__ */ new Set([
|
|
348
|
+
"html",
|
|
349
|
+
"htm",
|
|
350
|
+
"jsx",
|
|
351
|
+
"tsx",
|
|
352
|
+
"vue",
|
|
353
|
+
"svelte",
|
|
354
|
+
"astro",
|
|
355
|
+
"erb",
|
|
356
|
+
"ejs",
|
|
357
|
+
"hbs",
|
|
358
|
+
"handlebars",
|
|
359
|
+
"php",
|
|
360
|
+
"blade.php",
|
|
361
|
+
"twig",
|
|
362
|
+
"njk",
|
|
363
|
+
"liquid",
|
|
364
|
+
"pug",
|
|
365
|
+
"slim",
|
|
366
|
+
"haml",
|
|
367
|
+
"mdx",
|
|
368
|
+
"md",
|
|
369
|
+
"rs",
|
|
370
|
+
"ex",
|
|
371
|
+
"heex",
|
|
372
|
+
"clj",
|
|
373
|
+
"cljs"
|
|
374
|
+
]);
|
|
375
|
+
function getExtension(filePath) {
|
|
376
|
+
const base = path2.basename(filePath);
|
|
377
|
+
const parts = base.split(".");
|
|
378
|
+
if (parts.length > 2) {
|
|
379
|
+
return parts.slice(-2).join(".");
|
|
380
|
+
}
|
|
381
|
+
return parts.pop() || "html";
|
|
382
|
+
}
|
|
383
|
+
function isTemplateFile(filePath) {
|
|
384
|
+
if (filePath.endsWith(".css")) return false;
|
|
385
|
+
const ext = getExtension(filePath);
|
|
386
|
+
return EXTENSIONS.has(ext);
|
|
387
|
+
}
|
|
388
|
+
var format = defineCommand({
|
|
389
|
+
meta: { name: "format", description: "Sort Tailwind CSS classes" },
|
|
390
|
+
args: {
|
|
391
|
+
css: { type: "string", description: "Path to CSS entry point" },
|
|
392
|
+
strategy: { type: "string", description: "Sort strategy: tailwind or alphabetical (default: tailwind)" },
|
|
393
|
+
fix: { type: "boolean", description: "Auto-fix issues", default: false },
|
|
394
|
+
json: { type: "boolean", description: "Output diagnostics as JSON", default: false },
|
|
395
|
+
quiet: { type: "boolean", description: "Only show summary", default: false }
|
|
396
|
+
},
|
|
397
|
+
run: async ({ args, rawArgs }) => {
|
|
398
|
+
const paths = rawArgs.filter((a) => !a.startsWith("-"));
|
|
399
|
+
await run(paths, args, ["format"]);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
var lint = defineCommand({
|
|
403
|
+
meta: { name: "lint", description: "Check for non-canonical Tailwind CSS classes" },
|
|
404
|
+
args: {
|
|
405
|
+
css: { type: "string", description: "Path to CSS entry point" },
|
|
406
|
+
rem: { type: "string", description: "Root font size in px (default: 16)" },
|
|
407
|
+
collapse: { type: "boolean", description: "Collapse e.g. mt-2 mr-2 mb-2 ml-2 \u2192 m-2", default: false },
|
|
408
|
+
fix: { type: "boolean", description: "Auto-fix issues", default: false },
|
|
409
|
+
json: { type: "boolean", description: "Output diagnostics as JSON", default: false },
|
|
410
|
+
quiet: { type: "boolean", description: "Only show summary", default: false }
|
|
411
|
+
},
|
|
412
|
+
run: async ({ args, rawArgs }) => {
|
|
413
|
+
const paths = rawArgs.filter((a) => !a.startsWith("-"));
|
|
414
|
+
await run(paths, args, ["lint"]);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
var check = defineCommand({
|
|
418
|
+
meta: { name: "check", description: "Run format and lint checks" },
|
|
419
|
+
args: {
|
|
420
|
+
css: { type: "string", description: "Path to CSS entry point" },
|
|
421
|
+
strategy: { type: "string", description: "Sort strategy: tailwind or alphabetical (default: tailwind)" },
|
|
422
|
+
rem: { type: "string", description: "Root font size in px (default: 16)" },
|
|
423
|
+
collapse: { type: "boolean", description: "Collapse e.g. mt-2 mr-2 mb-2 ml-2 \u2192 m-2", default: false },
|
|
424
|
+
fix: { type: "boolean", description: "Auto-fix issues", default: false },
|
|
425
|
+
json: { type: "boolean", description: "Output diagnostics as JSON", default: false },
|
|
426
|
+
quiet: { type: "boolean", description: "Only show summary", default: false }
|
|
427
|
+
},
|
|
428
|
+
run: async ({ args, rawArgs }) => {
|
|
429
|
+
const paths = rawArgs.filter((a) => !a.startsWith("-"));
|
|
430
|
+
await run(paths, args, ["format", "lint"]);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
var main = defineCommand({
|
|
434
|
+
meta: {
|
|
435
|
+
name: "nyx",
|
|
436
|
+
version: "0.1.0",
|
|
437
|
+
description: "Tailwind CSS class formatter and linter"
|
|
438
|
+
},
|
|
439
|
+
subCommands: { format, lint, check }
|
|
440
|
+
});
|
|
441
|
+
async function run(paths, args, kinds) {
|
|
442
|
+
const base = process.cwd();
|
|
443
|
+
const remValue = args.rem ? Number(args.rem) : 16;
|
|
444
|
+
const mode = args.fix ? "fix" : "check";
|
|
445
|
+
const strategy = args.strategy ?? "tailwind";
|
|
446
|
+
let designSystem;
|
|
447
|
+
try {
|
|
448
|
+
designSystem = await loadDesignSystem({ base, css: args.css });
|
|
449
|
+
} catch (err) {
|
|
450
|
+
console.error(pc2.red(err.message));
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
const filesToProcess = discoverFiles(base, paths);
|
|
454
|
+
if (filesToProcess.length === 0) {
|
|
455
|
+
console.error(pc2.yellow("No template files found to process."));
|
|
456
|
+
process.exit(0);
|
|
457
|
+
}
|
|
458
|
+
let hasIssues = false;
|
|
459
|
+
for (const kind of kinds) {
|
|
460
|
+
const diagnosticsByFile = /* @__PURE__ */ new Map();
|
|
461
|
+
let totalDiagnostics = 0;
|
|
462
|
+
for (const filePath of filesToProcess) {
|
|
463
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
464
|
+
const ext = getExtension(filePath);
|
|
465
|
+
const relativePath = path2.relative(base, filePath);
|
|
466
|
+
const diagnostics = kind === "format" ? findUnsortedClasses(designSystem, content, relativePath, { strategy }) : findNonCanonicalClasses(designSystem, content, ext, relativePath, {
|
|
467
|
+
rem: remValue,
|
|
468
|
+
collapse: args.collapse
|
|
469
|
+
});
|
|
470
|
+
if (diagnostics.length > 0) {
|
|
471
|
+
if (args.fix) {
|
|
472
|
+
const fixed = applyFixes(content, diagnostics);
|
|
473
|
+
fs2.writeFileSync(filePath, fixed, "utf-8");
|
|
474
|
+
}
|
|
475
|
+
diagnosticsByFile.set(relativePath, diagnostics);
|
|
476
|
+
totalDiagnostics += diagnostics.length;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const output = formatDiagnostics(diagnosticsByFile, {
|
|
480
|
+
mode,
|
|
481
|
+
kind,
|
|
482
|
+
json: args.json,
|
|
483
|
+
quiet: args.quiet
|
|
484
|
+
});
|
|
485
|
+
if (output) {
|
|
486
|
+
console.log(output);
|
|
487
|
+
}
|
|
488
|
+
if (totalDiagnostics === 0) {
|
|
489
|
+
if (!args.quiet && !args.json) {
|
|
490
|
+
const msg = kind === "format" ? "All classes are properly sorted." : "No non-canonical classes found.";
|
|
491
|
+
console.log(pc2.green(msg));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (totalDiagnostics > 0) {
|
|
495
|
+
hasIssues = true;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (!args.fix && hasIssues) {
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function discoverFiles(base, paths) {
|
|
503
|
+
if (paths.length > 0) {
|
|
504
|
+
const files = [];
|
|
505
|
+
for (const p of paths) {
|
|
506
|
+
const resolved = path2.resolve(base, p);
|
|
507
|
+
if (fs2.statSync(resolved).isDirectory()) {
|
|
508
|
+
files.push(...walkDir(resolved));
|
|
509
|
+
} else if (isTemplateFile(resolved)) {
|
|
510
|
+
files.push(resolved);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return files;
|
|
514
|
+
}
|
|
515
|
+
const scanner = createScanner([
|
|
516
|
+
{ base, pattern: "**/*", negated: false }
|
|
517
|
+
]);
|
|
518
|
+
return scanner.files.filter((f) => !f.endsWith(".css") && isTemplateFile(f));
|
|
519
|
+
}
|
|
520
|
+
function walkDir(dir) {
|
|
521
|
+
const files = [];
|
|
522
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
523
|
+
for (const entry of entries) {
|
|
524
|
+
const fullPath = path2.join(dir, entry.name);
|
|
525
|
+
if (entry.isDirectory()) {
|
|
526
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
527
|
+
files.push(...walkDir(fullPath));
|
|
528
|
+
} else if (isTemplateFile(fullPath)) {
|
|
529
|
+
files.push(fullPath);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return files;
|
|
533
|
+
}
|
|
534
|
+
runMain(main);
|
|
535
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/scanner.ts","../src/canonicalize.ts","../src/design-system.ts","../src/fixer.ts","../src/reporter.ts","../src/sorter.ts"],"sourcesContent":["import fs from 'node:fs'\nimport path from 'node:path'\nimport { defineCommand, runMain } from 'citty'\nimport pc from 'picocolors'\nimport { findNonCanonicalClasses, type Diagnostic } from './canonicalize.js'\nimport { loadDesignSystem } from './design-system.js'\nimport { applyFixes } from './fixer.js'\nimport { formatDiagnostics } from './reporter.js'\nimport { createScanner } from './scanner.js'\nimport { findUnsortedClasses, type SortStrategy } from './sorter.js'\n\nconst EXTENSIONS = new Set([\n 'html', 'htm', 'jsx', 'tsx', 'vue', 'svelte', 'astro',\n 'erb', 'ejs', 'hbs', 'handlebars', 'php', 'blade.php',\n 'twig', 'njk', 'liquid', 'pug', 'slim', 'haml',\n 'mdx', 'md', 'rs', 'ex', 'heex', 'clj', 'cljs',\n])\n\nfunction getExtension(filePath: string): string {\n const base = path.basename(filePath)\n // Handle compound extensions like .blade.php\n const parts = base.split('.')\n if (parts.length > 2) {\n return parts.slice(-2).join('.')\n }\n return parts.pop() || 'html'\n}\n\nfunction isTemplateFile(filePath: string): boolean {\n if (filePath.endsWith('.css')) return false\n const ext = getExtension(filePath)\n return EXTENSIONS.has(ext)\n}\n\nconst format = defineCommand({\n meta: { name: 'format', description: 'Sort Tailwind CSS classes' },\n args: {\n css: { type: 'string', description: 'Path to CSS entry point' },\n strategy: { type: 'string', description: 'Sort strategy: tailwind or alphabetical (default: tailwind)' },\n fix: { type: 'boolean', description: 'Auto-fix issues', default: false },\n json: { type: 'boolean', description: 'Output diagnostics as JSON', default: false },\n quiet: { type: 'boolean', description: 'Only show summary', default: false },\n },\n run: async ({ args, rawArgs }) => {\n const paths = rawArgs.filter((a) => !a.startsWith('-'))\n await run(paths, args, ['format'])\n },\n})\n\nconst lint = defineCommand({\n meta: { name: 'lint', description: 'Check for non-canonical Tailwind CSS classes' },\n args: {\n css: { type: 'string', description: 'Path to CSS entry point' },\n rem: { type: 'string', description: 'Root font size in px (default: 16)' },\n collapse: { type: 'boolean', description: 'Collapse e.g. mt-2 mr-2 mb-2 ml-2 → m-2', default: false },\n fix: { type: 'boolean', description: 'Auto-fix issues', default: false },\n json: { type: 'boolean', description: 'Output diagnostics as JSON', default: false },\n quiet: { type: 'boolean', description: 'Only show summary', default: false },\n },\n run: async ({ args, rawArgs }) => {\n const paths = rawArgs.filter((a) => !a.startsWith('-'))\n await run(paths, args, ['lint'])\n },\n})\n\nconst check = defineCommand({\n meta: { name: 'check', description: 'Run format and lint checks' },\n args: {\n css: { type: 'string', description: 'Path to CSS entry point' },\n strategy: { type: 'string', description: 'Sort strategy: tailwind or alphabetical (default: tailwind)' },\n rem: { type: 'string', description: 'Root font size in px (default: 16)' },\n collapse: { type: 'boolean', description: 'Collapse e.g. mt-2 mr-2 mb-2 ml-2 → m-2', default: false },\n fix: { type: 'boolean', description: 'Auto-fix issues', default: false },\n json: { type: 'boolean', description: 'Output diagnostics as JSON', default: false },\n quiet: { type: 'boolean', description: 'Only show summary', default: false },\n },\n run: async ({ args, rawArgs }) => {\n const paths = rawArgs.filter((a) => !a.startsWith('-'))\n await run(paths, args, ['format', 'lint'])\n },\n})\n\nconst main = defineCommand({\n meta: {\n name: 'nyx',\n version: '0.1.0',\n description: 'Tailwind CSS class formatter and linter',\n },\n subCommands: { format, lint, check },\n})\n\nasync function run(\n paths: string[],\n args: {\n css?: string\n strategy?: string\n rem?: string\n collapse?: boolean\n fix?: boolean\n json?: boolean\n quiet?: boolean\n },\n kinds: ('format' | 'lint')[],\n) {\n const base = process.cwd()\n const remValue = args.rem ? Number(args.rem) : 16\n const mode = args.fix ? 'fix' : 'check'\n const strategy = (args.strategy as SortStrategy) ?? 'tailwind'\n\n // Load design system\n let designSystem\n try {\n designSystem = await loadDesignSystem({ base, css: args.css })\n } catch (err) {\n console.error(pc.red((err as Error).message))\n process.exit(1)\n }\n\n // Discover files\n const filesToProcess = discoverFiles(base, paths)\n\n if (filesToProcess.length === 0) {\n console.error(pc.yellow('No template files found to process.'))\n process.exit(0)\n }\n\n // Process files for each kind (format first, then lint on potentially fixed content)\n let hasIssues = false\n\n for (const kind of kinds) {\n const diagnosticsByFile = new Map<string, Diagnostic[]>()\n let totalDiagnostics = 0\n\n for (const filePath of filesToProcess) {\n const content = fs.readFileSync(filePath, 'utf-8')\n const ext = getExtension(filePath)\n const relativePath = path.relative(base, filePath)\n\n const diagnostics = kind === 'format'\n ? findUnsortedClasses(designSystem, content, relativePath, { strategy })\n : findNonCanonicalClasses(designSystem, content, ext, relativePath, {\n rem: remValue,\n collapse: args.collapse,\n })\n\n if (diagnostics.length > 0) {\n if (args.fix) {\n const fixed = applyFixes(content, diagnostics)\n fs.writeFileSync(filePath, fixed, 'utf-8')\n }\n diagnosticsByFile.set(relativePath, diagnostics)\n totalDiagnostics += diagnostics.length\n }\n }\n\n // Report\n const output = formatDiagnostics(diagnosticsByFile, {\n mode,\n kind,\n json: args.json,\n quiet: args.quiet,\n })\n\n if (output) {\n console.log(output)\n }\n\n if (totalDiagnostics === 0) {\n if (!args.quiet && !args.json) {\n const msg = kind === 'format'\n ? 'All classes are properly sorted.'\n : 'No non-canonical classes found.'\n console.log(pc.green(msg))\n }\n }\n\n if (totalDiagnostics > 0) {\n hasIssues = true\n }\n }\n\n // Exit with code 1 in check mode when issues found (for CI)\n if (!args.fix && hasIssues) {\n process.exit(1)\n }\n}\n\nfunction discoverFiles(base: string, paths: string[]): string[] {\n if (paths.length > 0) {\n // Explicit paths provided — expand directories\n const files: string[] = []\n for (const p of paths) {\n const resolved = path.resolve(base, p)\n if (fs.statSync(resolved).isDirectory()) {\n files.push(...walkDir(resolved))\n } else if (isTemplateFile(resolved)) {\n files.push(resolved)\n }\n }\n return files\n }\n\n // Use oxide Scanner to discover files\n const scanner = createScanner([\n { base, pattern: '**/*', negated: false },\n ])\n\n return scanner.files.filter((f) => !f.endsWith('.css') && isTemplateFile(f))\n}\n\nfunction walkDir(dir: string): string[] {\n const files: string[] = []\n const entries = fs.readdirSync(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n if (entry.name === 'node_modules' || entry.name === '.git') continue\n files.push(...walkDir(fullPath))\n } else if (isTemplateFile(fullPath)) {\n files.push(fullPath)\n }\n }\n return files\n}\n\nrunMain(main)\n","import { Scanner } from '@tailwindcss/oxide'\n\nexport interface CandidateWithPosition {\n rawCandidate: string\n start: number\n end: number\n}\n\nexport function extractCandidatesWithPositions(\n content: string,\n extension: string = 'html',\n): CandidateWithPosition[] {\n const scanner = new Scanner({})\n const result = scanner.getCandidatesWithPositions({ content, extension })\n\n const candidates: CandidateWithPosition[] = []\n for (const { candidate: rawCandidate, position: start } of result) {\n candidates.push({ rawCandidate, start, end: start + rawCandidate.length })\n }\n return candidates\n}\n\nexport interface SourceEntry {\n base: string\n pattern: string\n negated: boolean\n}\n\nexport function createScanner(sources: SourceEntry[]) {\n const scanner = new Scanner({ sources })\n return {\n get files(): string[] {\n return scanner.files\n },\n extractCandidatesWithPositions(content: string, extension: string) {\n return extractCandidatesWithPositions(content, extension)\n },\n }\n}\n","import type { DesignSystem } from './design-system.js'\nimport { extractCandidatesWithPositions } from './scanner.js'\n\nexport interface CanonicalizeOptions {\n rem?: number\n collapse?: boolean\n}\n\nexport interface Diagnostic {\n file: string\n line: number\n column: number\n offset: number\n length: number\n original: string\n canonical: string\n lineContent: string\n}\n\nfunction offsetToLineColumn(content: string, offset: number): { line: number; column: number } {\n let line = 1\n let column = 1\n for (let i = 0; i < offset && i < content.length; i++) {\n if (content[i] === '\\n') {\n line++\n column = 1\n } else {\n column++\n }\n }\n return { line, column }\n}\n\nfunction getLineContent(content: string, offset: number): string {\n let start = offset\n while (start > 0 && content[start - 1] !== '\\n') {\n start--\n }\n let end = offset\n while (end < content.length && content[end] !== '\\n') {\n end++\n }\n return content.slice(start, end)\n}\n\nexport function findNonCanonicalClasses(\n designSystem: DesignSystem,\n content: string,\n extension: string,\n filePath: string,\n options: CanonicalizeOptions = {},\n): Diagnostic[] {\n const candidates = extractCandidatesWithPositions(content, extension)\n const diagnostics: Diagnostic[] = []\n\n for (const { rawCandidate, start } of candidates) {\n const [canonical] = designSystem.canonicalizeCandidates([rawCandidate], {\n rem: options.rem,\n collapse: false,\n })\n\n if (canonical && canonical !== rawCandidate) {\n const { line, column } = offsetToLineColumn(content, start)\n diagnostics.push({\n file: filePath,\n line,\n column,\n offset: start,\n length: rawCandidate.length,\n original: rawCandidate,\n canonical,\n lineContent: getLineContent(content, start),\n })\n }\n }\n\n return diagnostics\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\nimport { __unstable__loadDesignSystem } from '@tailwindcss/node'\n\nconst TAILWIND_IMPORT_RE = /@import\\s+['\"]tailwindcss['\"]/\n\nconst SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt', '.output'])\n\nfunction detectCssEntryPoint(base: string): string | null {\n // Walk the project directory looking for any .css file with @import \"tailwindcss\"\n const found = findCssWithTailwind(base, 3)\n return found\n}\n\nfunction findCssWithTailwind(dir: string, maxDepth: number): string | null {\n if (maxDepth < 0) return null\n\n let entries: fs.Dirent[]\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true })\n } catch {\n return null\n }\n\n // Check CSS files in this directory first\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith('.css')) continue\n const fullPath = path.join(dir, entry.name)\n try {\n const content = fs.readFileSync(fullPath, 'utf-8')\n if (TAILWIND_IMPORT_RE.test(content)) {\n return fullPath\n }\n } catch {\n continue\n }\n }\n\n // Then recurse into subdirectories\n for (const entry of entries) {\n if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue\n const result = findCssWithTailwind(path.join(dir, entry.name), maxDepth - 1)\n if (result) return result\n }\n\n return null\n}\n\nexport type DesignSystem = Awaited<ReturnType<typeof __unstable__loadDesignSystem>>\n\nexport async function loadDesignSystem(options: {\n base: string\n css?: string\n}): Promise<DesignSystem> {\n let cssPath: string\n\n if (options.css) {\n cssPath = path.resolve(options.base, options.css)\n } else {\n const detected = detectCssEntryPoint(options.base)\n if (!detected) {\n throw new Error(\n 'Could not find a CSS entry point with `@import \"tailwindcss\"`. ' +\n 'Specify one with --css <path>.',\n )\n }\n cssPath = detected\n }\n\n if (!fs.existsSync(cssPath)) {\n throw new Error(`CSS file not found: ${cssPath}`)\n }\n\n const cssContent = fs.readFileSync(cssPath, 'utf-8')\n return __unstable__loadDesignSystem(cssContent, {\n base: path.dirname(cssPath),\n })\n}\n","import type { Diagnostic } from './canonicalize.js'\n\ninterface StringChange {\n start: number\n end: number\n replacement: string\n}\n\nexport function applyFixes(content: string, diagnostics: Diagnostic[]): string {\n if (diagnostics.length === 0) return content\n\n const changes: StringChange[] = diagnostics.map((d) => ({\n start: d.offset,\n end: d.offset + d.length,\n replacement: d.canonical,\n }))\n\n return spliceChangesIntoString(content, changes)\n}\n\nfunction spliceChangesIntoString(str: string, changes: StringChange[]): string {\n if (!changes[0]) return str\n\n changes.sort((a, b) => a.end - b.end || a.start - b.start)\n\n let result = ''\n let previous = changes[0]\n\n result += str.slice(0, previous.start)\n result += previous.replacement\n\n for (let i = 1; i < changes.length; ++i) {\n const change = changes[i]\n result += str.slice(previous.end, change.start)\n result += change.replacement\n previous = change\n }\n\n result += str.slice(previous.end)\n return result\n}\n","import pc from 'picocolors'\nimport type { Diagnostic } from './canonicalize.js'\n\nexport interface ReporterOptions {\n json?: boolean\n quiet?: boolean\n mode: 'check' | 'fix'\n kind?: 'format' | 'lint'\n}\n\nexport function formatDiagnostics(\n diagnosticsByFile: Map<string, Diagnostic[]>,\n options: ReporterOptions,\n): string {\n if (options.json) {\n return formatJson(diagnosticsByFile)\n }\n if (options.quiet) {\n return formatQuiet(diagnosticsByFile, options.mode, options.kind)\n }\n return formatCodeframe(diagnosticsByFile, options.mode, options.kind)\n}\n\nfunction formatJson(diagnosticsByFile: Map<string, Diagnostic[]>): string {\n const output: Record<string, { line: number; column: number; original: string; canonical: string }[]> = {}\n for (const [file, diagnostics] of diagnosticsByFile) {\n output[file] = diagnostics.map((d) => ({\n line: d.line,\n column: d.column,\n original: d.original,\n canonical: d.canonical,\n }))\n }\n return JSON.stringify(output, null, 2)\n}\n\nfunction formatQuiet(\n diagnosticsByFile: Map<string, Diagnostic[]>,\n mode: 'check' | 'fix',\n kind?: 'format' | 'lint',\n): string {\n const { totalDiagnostics, totalFiles } = countTotals(diagnosticsByFile)\n if (totalDiagnostics === 0) return ''\n return formatSummary(totalDiagnostics, totalFiles, mode, kind)\n}\n\nfunction formatCodeframe(\n diagnosticsByFile: Map<string, Diagnostic[]>,\n mode: 'check' | 'fix',\n kind?: 'format' | 'lint',\n): string {\n const lines: string[] = []\n const { totalDiagnostics, totalFiles } = countTotals(diagnosticsByFile)\n const isFormat = kind === 'format'\n\n if (totalDiagnostics === 0) return ''\n\n for (const [file, diagnostics] of diagnosticsByFile) {\n for (const d of diagnostics) {\n lines.push('')\n lines.push(\n `${pc.cyan(file)}${pc.dim(':')}${pc.yellow(String(d.line))}${pc.dim(':')}${pc.yellow(String(d.column))} ${pc.dim('warn')} ${pc.dim(isFormat ? 'unsortedClasses' : 'suggestCanonicalClasses')}`,\n )\n lines.push(\n isFormat\n ? ` Classes should be sorted as ${pc.green(pc.bold(d.canonical))}`\n : ` The class ${pc.red(pc.bold(d.original))} can be written as ${pc.green(pc.bold(d.canonical))}`,\n )\n\n const contextLines = getContextLines(d)\n lines.push('')\n for (const cl of contextLines) {\n lines.push(cl)\n }\n }\n }\n\n lines.push('')\n lines.push(formatSummary(totalDiagnostics, totalFiles, mode, kind))\n\n return lines.join('\\n')\n}\n\nfunction getContextLines(d: Diagnostic): string[] {\n const lines: string[] = []\n const lineNum = String(d.line)\n const gutter = ' '.repeat(lineNum.length + 2)\n\n // The line with the issue\n lines.push(` ${pc.dim(lineNum)} ${pc.dim('│')} ${d.lineContent}`)\n\n // The underline\n const colOffset = d.column - 1\n const underline = ' '.repeat(colOffset) + pc.red('~'.repeat(d.original.length))\n lines.push(` ${gutter}${pc.dim('│')} ${underline}`)\n\n // Suggested fix\n const fixedLine = d.lineContent.slice(0, colOffset) + d.canonical + d.lineContent.slice(colOffset + d.original.length)\n lines.push(` ${gutter}${pc.dim('│')} ${pc.dim('ℹ Suggested fix:')}`)\n lines.push(` ${pc.dim(lineNum)} ${pc.dim('│')} ${fixedLine}`)\n\n const fixUnderline = ' '.repeat(colOffset) + pc.green('~'.repeat(d.canonical.length))\n lines.push(` ${gutter}${pc.dim('│')} ${fixUnderline}`)\n\n return lines\n}\n\nfunction countTotals(diagnosticsByFile: Map<string, Diagnostic[]>) {\n let totalDiagnostics = 0\n let totalFiles = 0\n for (const [, diagnostics] of diagnosticsByFile) {\n if (diagnostics.length > 0) {\n totalDiagnostics += diagnostics.length\n totalFiles++\n }\n }\n return { totalDiagnostics, totalFiles }\n}\n\nfunction formatSummary(count: number, files: number, mode: 'check' | 'fix', kind?: 'format' | 'lint'): string {\n const fileWord = files === 1 ? 'file' : 'files'\n\n if (kind === 'format') {\n const groupWord = count === 1 ? 'class group' : 'class groups'\n if (mode === 'fix') {\n return pc.green(`Sorted ${count} ${groupWord} in ${files} ${fileWord}.`)\n }\n return pc.yellow(\n `Found ${count} unsorted ${groupWord} in ${files} ${fileWord}. Run ${pc.bold('nyx format --fix')} to apply.`,\n )\n }\n\n const classWord = count === 1 ? 'class' : 'classes'\n if (mode === 'fix') {\n return pc.green(`Fixed ${count} non-canonical ${classWord} in ${files} ${fileWord}.`)\n }\n return pc.yellow(\n `Found ${count} non-canonical ${classWord} in ${files} ${fileWord}. Run ${pc.bold('nyx lint --fix')} to apply.`,\n )\n}\n","import type { DesignSystem } from './design-system.js'\nimport type { Diagnostic } from './canonicalize.js'\n\nexport type SortStrategy = 'tailwind' | 'alphabetical'\n\nexport interface SortOptions {\n strategy?: SortStrategy\n}\n\nconst CLASS_ATTR_RE = /class(?:Name)?\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')/g\n\nfunction offsetToLineColumn(content: string, offset: number): { line: number; column: number } {\n let line = 1\n let column = 1\n for (let i = 0; i < offset && i < content.length; i++) {\n if (content[i] === '\\n') {\n line++\n column = 1\n } else {\n column++\n }\n }\n return { line, column }\n}\n\nfunction getLineContent(content: string, offset: number): string {\n let start = offset\n while (start > 0 && content[start - 1] !== '\\n') {\n start--\n }\n let end = offset\n while (end < content.length && content[end] !== '\\n') {\n end++\n }\n return content.slice(start, end)\n}\n\nexport function findUnsortedClasses(\n designSystem: DesignSystem,\n content: string,\n filePath: string,\n options: SortOptions = {},\n): Diagnostic[] {\n const strategy = options.strategy ?? 'tailwind'\n const diagnostics: Diagnostic[] = []\n\n CLASS_ATTR_RE.lastIndex = 0\n let match: RegExpExecArray | null\n\n while ((match = CLASS_ATTR_RE.exec(content)) !== null) {\n const classValue = match[1] ?? match[2]\n if (!classValue) continue\n\n const quoteChar = match[1] !== undefined ? '\"' : \"'\"\n const valueOffset = match.index + match[0].indexOf(quoteChar) + 1\n\n const classes = classValue.split(/\\s+/).filter(Boolean)\n if (classes.length <= 1) continue\n\n const sorted = strategy === 'alphabetical'\n ? sortAlphabetical(classes)\n : sortTailwind(designSystem, classes)\n const sortedValue = sorted.join(' ')\n const originalValue = classes.join(' ')\n\n if (sortedValue !== originalValue) {\n const { line, column } = offsetToLineColumn(content, valueOffset)\n diagnostics.push({\n file: filePath,\n line,\n column,\n offset: valueOffset,\n length: classValue.length,\n original: classValue,\n canonical: sortedValue,\n lineContent: getLineContent(content, valueOffset),\n })\n }\n }\n\n return diagnostics\n}\n\nfunction sortAlphabetical(classes: string[]): string[] {\n return [...classes].sort((a, b) => a.localeCompare(b))\n}\n\nfunction sortTailwind(designSystem: DesignSystem, classes: string[]): string[] {\n const ordered = designSystem.getClassOrder(classes)\n\n const unknown: string[] = []\n const known: [string, bigint][] = []\n\n for (const [cls, order] of ordered) {\n if (order === null) {\n unknown.push(cls)\n } else {\n known.push([cls, order])\n }\n }\n\n known.sort((a, b) => {\n if (a[1] < b[1]) return -1\n if (a[1] > b[1]) return 1\n return 0\n })\n\n return [...unknown, ...known.map(([cls]) => cls)]\n}\n"],"mappings":";;;AAAA,OAAOA,SAAQ;AACf,OAAOC,WAAU;AACjB,SAAS,eAAe,eAAe;AACvC,OAAOC,SAAQ;;;ACHf,SAAS,eAAe;AAQjB,SAAS,+BACd,SACA,YAAoB,QACK;AACzB,QAAM,UAAU,IAAI,QAAQ,CAAC,CAAC;AAC9B,QAAM,SAAS,QAAQ,2BAA2B,EAAE,SAAS,UAAU,CAAC;AAExE,QAAM,aAAsC,CAAC;AAC7C,aAAW,EAAE,WAAW,cAAc,UAAU,MAAM,KAAK,QAAQ;AACjE,eAAW,KAAK,EAAE,cAAc,OAAO,KAAK,QAAQ,aAAa,OAAO,CAAC;AAAA,EAC3E;AACA,SAAO;AACT;AAQO,SAAS,cAAc,SAAwB;AACpD,QAAM,UAAU,IAAI,QAAQ,EAAE,QAAQ,CAAC;AACvC,SAAO;AAAA,IACL,IAAI,QAAkB;AACpB,aAAO,QAAQ;AAAA,IACjB;AAAA,IACA,+BAA+B,SAAiB,WAAmB;AACjE,aAAO,+BAA+B,SAAS,SAAS;AAAA,IAC1D;AAAA,EACF;AACF;;;ACnBA,SAAS,mBAAmB,SAAiB,QAAkD;AAC7F,MAAI,OAAO;AACX,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,UAAU,IAAI,QAAQ,QAAQ,KAAK;AACrD,QAAI,QAAQ,CAAC,MAAM,MAAM;AACvB;AACA,eAAS;AAAA,IACX,OAAO;AACL;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,MAAM,OAAO;AACxB;AAEA,SAAS,eAAe,SAAiB,QAAwB;AAC/D,MAAI,QAAQ;AACZ,SAAO,QAAQ,KAAK,QAAQ,QAAQ,CAAC,MAAM,MAAM;AAC/C;AAAA,EACF;AACA,MAAI,MAAM;AACV,SAAO,MAAM,QAAQ,UAAU,QAAQ,GAAG,MAAM,MAAM;AACpD;AAAA,EACF;AACA,SAAO,QAAQ,MAAM,OAAO,GAAG;AACjC;AAEO,SAAS,wBACd,cACA,SACA,WACA,UACA,UAA+B,CAAC,GAClB;AACd,QAAM,aAAa,+BAA+B,SAAS,SAAS;AACpE,QAAM,cAA4B,CAAC;AAEnC,aAAW,EAAE,cAAc,MAAM,KAAK,YAAY;AAChD,UAAM,CAAC,SAAS,IAAI,aAAa,uBAAuB,CAAC,YAAY,GAAG;AAAA,MACtE,KAAK,QAAQ;AAAA,MACb,UAAU;AAAA,IACZ,CAAC;AAED,QAAI,aAAa,cAAc,cAAc;AAC3C,YAAM,EAAE,MAAM,OAAO,IAAI,mBAAmB,SAAS,KAAK;AAC1D,kBAAY,KAAK;AAAA,QACf,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR,QAAQ,aAAa;AAAA,QACrB,UAAU;AAAA,QACV;AAAA,QACA,aAAa,eAAe,SAAS,KAAK;AAAA,MAC5C,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;;;AC7EA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,oCAAoC;AAE7C,IAAM,qBAAqB;AAE3B,IAAM,YAAY,oBAAI,IAAI,CAAC,gBAAgB,QAAQ,QAAQ,SAAS,SAAS,SAAS,SAAS,CAAC;AAEhG,SAAS,oBAAoB,MAA6B;AAExD,QAAM,QAAQ,oBAAoB,MAAM,CAAC;AACzC,SAAO;AACT;AAEA,SAAS,oBAAoB,KAAa,UAAiC;AACzE,MAAI,WAAW,EAAG,QAAO;AAEzB,MAAI;AACJ,MAAI;AACF,cAAU,GAAG,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC;AAAA,EACvD,QAAQ;AACN,WAAO;AAAA,EACT;AAGA,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,OAAO,KAAK,CAAC,MAAM,KAAK,SAAS,MAAM,EAAG;AACrD,UAAM,WAAW,KAAK,KAAK,KAAK,MAAM,IAAI;AAC1C,QAAI;AACF,YAAM,UAAU,GAAG,aAAa,UAAU,OAAO;AACjD,UAAI,mBAAmB,KAAK,OAAO,GAAG;AACpC,eAAO;AAAA,MACT;AAAA,IACF,QAAQ;AACN;AAAA,IACF;AAAA,EACF;AAGA,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,YAAY,KAAK,UAAU,IAAI,MAAM,IAAI,EAAG;AACvD,UAAM,SAAS,oBAAoB,KAAK,KAAK,KAAK,MAAM,IAAI,GAAG,WAAW,CAAC;AAC3E,QAAI,OAAQ,QAAO;AAAA,EACrB;AAEA,SAAO;AACT;AAIA,eAAsB,iBAAiB,SAGb;AACxB,MAAI;AAEJ,MAAI,QAAQ,KAAK;AACf,cAAU,KAAK,QAAQ,QAAQ,MAAM,QAAQ,GAAG;AAAA,EAClD,OAAO;AACL,UAAM,WAAW,oBAAoB,QAAQ,IAAI;AACjD,QAAI,CAAC,UAAU;AACb,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AACA,cAAU;AAAA,EACZ;AAEA,MAAI,CAAC,GAAG,WAAW,OAAO,GAAG;AAC3B,UAAM,IAAI,MAAM,uBAAuB,OAAO,EAAE;AAAA,EAClD;AAEA,QAAM,aAAa,GAAG,aAAa,SAAS,OAAO;AACnD,SAAO,6BAA6B,YAAY;AAAA,IAC9C,MAAM,KAAK,QAAQ,OAAO;AAAA,EAC5B,CAAC;AACH;;;ACrEO,SAAS,WAAW,SAAiB,aAAmC;AAC7E,MAAI,YAAY,WAAW,EAAG,QAAO;AAErC,QAAM,UAA0B,YAAY,IAAI,CAAC,OAAO;AAAA,IACtD,OAAO,EAAE;AAAA,IACT,KAAK,EAAE,SAAS,EAAE;AAAA,IAClB,aAAa,EAAE;AAAA,EACjB,EAAE;AAEF,SAAO,wBAAwB,SAAS,OAAO;AACjD;AAEA,SAAS,wBAAwB,KAAa,SAAiC;AAC7E,MAAI,CAAC,QAAQ,CAAC,EAAG,QAAO;AAExB,UAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK;AAEzD,MAAI,SAAS;AACb,MAAI,WAAW,QAAQ,CAAC;AAExB,YAAU,IAAI,MAAM,GAAG,SAAS,KAAK;AACrC,YAAU,SAAS;AAEnB,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,EAAE,GAAG;AACvC,UAAM,SAAS,QAAQ,CAAC;AACxB,cAAU,IAAI,MAAM,SAAS,KAAK,OAAO,KAAK;AAC9C,cAAU,OAAO;AACjB,eAAW;AAAA,EACb;AAEA,YAAU,IAAI,MAAM,SAAS,GAAG;AAChC,SAAO;AACT;;;ACxCA,OAAO,QAAQ;AAUR,SAAS,kBACd,mBACA,SACQ;AACR,MAAI,QAAQ,MAAM;AAChB,WAAO,WAAW,iBAAiB;AAAA,EACrC;AACA,MAAI,QAAQ,OAAO;AACjB,WAAO,YAAY,mBAAmB,QAAQ,MAAM,QAAQ,IAAI;AAAA,EAClE;AACA,SAAO,gBAAgB,mBAAmB,QAAQ,MAAM,QAAQ,IAAI;AACtE;AAEA,SAAS,WAAW,mBAAsD;AACxE,QAAM,SAAkG,CAAC;AACzG,aAAW,CAAC,MAAM,WAAW,KAAK,mBAAmB;AACnD,WAAO,IAAI,IAAI,YAAY,IAAI,CAAC,OAAO;AAAA,MACrC,MAAM,EAAE;AAAA,MACR,QAAQ,EAAE;AAAA,MACV,UAAU,EAAE;AAAA,MACZ,WAAW,EAAE;AAAA,IACf,EAAE;AAAA,EACJ;AACA,SAAO,KAAK,UAAU,QAAQ,MAAM,CAAC;AACvC;AAEA,SAAS,YACP,mBACA,MACA,MACQ;AACR,QAAM,EAAE,kBAAkB,WAAW,IAAI,YAAY,iBAAiB;AACtE,MAAI,qBAAqB,EAAG,QAAO;AACnC,SAAO,cAAc,kBAAkB,YAAY,MAAM,IAAI;AAC/D;AAEA,SAAS,gBACP,mBACA,MACA,MACQ;AACR,QAAM,QAAkB,CAAC;AACzB,QAAM,EAAE,kBAAkB,WAAW,IAAI,YAAY,iBAAiB;AACtE,QAAM,WAAW,SAAS;AAE1B,MAAI,qBAAqB,EAAG,QAAO;AAEnC,aAAW,CAAC,MAAM,WAAW,KAAK,mBAAmB;AACnD,eAAW,KAAK,aAAa;AAC3B,YAAM,KAAK,EAAE;AACb,YAAM;AAAA,QACJ,GAAG,GAAG,KAAK,IAAI,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,GAAG,OAAO,OAAO,EAAE,IAAI,CAAC,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,GAAG,OAAO,OAAO,EAAE,MAAM,CAAC,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,GAAG,IAAI,WAAW,oBAAoB,yBAAyB,CAAC;AAAA,MAC9L;AACA,YAAM;AAAA,QACJ,WACI,iCAAiC,GAAG,MAAM,GAAG,KAAK,EAAE,SAAS,CAAC,CAAC,KAC/D,eAAe,GAAG,IAAI,GAAG,KAAK,EAAE,QAAQ,CAAC,CAAC,sBAAsB,GAAG,MAAM,GAAG,KAAK,EAAE,SAAS,CAAC,CAAC;AAAA,MACpG;AAEA,YAAM,eAAe,gBAAgB,CAAC;AACtC,YAAM,KAAK,EAAE;AACb,iBAAW,MAAM,cAAc;AAC7B,cAAM,KAAK,EAAE;AAAA,MACf;AAAA,IACF;AAAA,EACF;AAEA,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,cAAc,kBAAkB,YAAY,MAAM,IAAI,CAAC;AAElE,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,gBAAgB,GAAyB;AAChD,QAAM,QAAkB,CAAC;AACzB,QAAM,UAAU,OAAO,EAAE,IAAI;AAC7B,QAAM,SAAS,IAAI,OAAO,QAAQ,SAAS,CAAC;AAG5C,QAAM,KAAK,KAAK,GAAG,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,QAAG,CAAC,IAAI,EAAE,WAAW,EAAE;AAGjE,QAAM,YAAY,EAAE,SAAS;AAC7B,QAAM,YAAY,IAAI,OAAO,SAAS,IAAI,GAAG,IAAI,IAAI,OAAO,EAAE,SAAS,MAAM,CAAC;AAC9E,QAAM,KAAK,KAAK,MAAM,GAAG,GAAG,IAAI,QAAG,CAAC,IAAI,SAAS,EAAE;AAGnD,QAAM,YAAY,EAAE,YAAY,MAAM,GAAG,SAAS,IAAI,EAAE,YAAY,EAAE,YAAY,MAAM,YAAY,EAAE,SAAS,MAAM;AACrH,QAAM,KAAK,KAAK,MAAM,GAAG,GAAG,IAAI,QAAG,CAAC,IAAI,GAAG,IAAI,uBAAkB,CAAC,EAAE;AACpE,QAAM,KAAK,KAAK,GAAG,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,QAAG,CAAC,IAAI,SAAS,EAAE;AAE7D,QAAM,eAAe,IAAI,OAAO,SAAS,IAAI,GAAG,MAAM,IAAI,OAAO,EAAE,UAAU,MAAM,CAAC;AACpF,QAAM,KAAK,KAAK,MAAM,GAAG,GAAG,IAAI,QAAG,CAAC,IAAI,YAAY,EAAE;AAEtD,SAAO;AACT;AAEA,SAAS,YAAY,mBAA8C;AACjE,MAAI,mBAAmB;AACvB,MAAI,aAAa;AACjB,aAAW,CAAC,EAAE,WAAW,KAAK,mBAAmB;AAC/C,QAAI,YAAY,SAAS,GAAG;AAC1B,0BAAoB,YAAY;AAChC;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,kBAAkB,WAAW;AACxC;AAEA,SAAS,cAAc,OAAe,OAAe,MAAuB,MAAkC;AAC5G,QAAM,WAAW,UAAU,IAAI,SAAS;AAExC,MAAI,SAAS,UAAU;AACrB,UAAM,YAAY,UAAU,IAAI,gBAAgB;AAChD,QAAI,SAAS,OAAO;AAClB,aAAO,GAAG,MAAM,UAAU,KAAK,IAAI,SAAS,OAAO,KAAK,IAAI,QAAQ,GAAG;AAAA,IACzE;AACA,WAAO,GAAG;AAAA,MACR,SAAS,KAAK,aAAa,SAAS,OAAO,KAAK,IAAI,QAAQ,SAAS,GAAG,KAAK,kBAAkB,CAAC;AAAA,IAClG;AAAA,EACF;AAEA,QAAM,YAAY,UAAU,IAAI,UAAU;AAC1C,MAAI,SAAS,OAAO;AAClB,WAAO,GAAG,MAAM,SAAS,KAAK,kBAAkB,SAAS,OAAO,KAAK,IAAI,QAAQ,GAAG;AAAA,EACtF;AACA,SAAO,GAAG;AAAA,IACR,SAAS,KAAK,kBAAkB,SAAS,OAAO,KAAK,IAAI,QAAQ,SAAS,GAAG,KAAK,gBAAgB,CAAC;AAAA,EACrG;AACF;;;AClIA,IAAM,gBAAgB;AAEtB,SAASC,oBAAmB,SAAiB,QAAkD;AAC7F,MAAI,OAAO;AACX,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,UAAU,IAAI,QAAQ,QAAQ,KAAK;AACrD,QAAI,QAAQ,CAAC,MAAM,MAAM;AACvB;AACA,eAAS;AAAA,IACX,OAAO;AACL;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,MAAM,OAAO;AACxB;AAEA,SAASC,gBAAe,SAAiB,QAAwB;AAC/D,MAAI,QAAQ;AACZ,SAAO,QAAQ,KAAK,QAAQ,QAAQ,CAAC,MAAM,MAAM;AAC/C;AAAA,EACF;AACA,MAAI,MAAM;AACV,SAAO,MAAM,QAAQ,UAAU,QAAQ,GAAG,MAAM,MAAM;AACpD;AAAA,EACF;AACA,SAAO,QAAQ,MAAM,OAAO,GAAG;AACjC;AAEO,SAAS,oBACd,cACA,SACA,UACA,UAAuB,CAAC,GACV;AACd,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,cAA4B,CAAC;AAEnC,gBAAc,YAAY;AAC1B,MAAI;AAEJ,UAAQ,QAAQ,cAAc,KAAK,OAAO,OAAO,MAAM;AACrD,UAAM,aAAa,MAAM,CAAC,KAAK,MAAM,CAAC;AACtC,QAAI,CAAC,WAAY;AAEjB,UAAM,YAAY,MAAM,CAAC,MAAM,SAAY,MAAM;AACjD,UAAM,cAAc,MAAM,QAAQ,MAAM,CAAC,EAAE,QAAQ,SAAS,IAAI;AAEhE,UAAM,UAAU,WAAW,MAAM,KAAK,EAAE,OAAO,OAAO;AACtD,QAAI,QAAQ,UAAU,EAAG;AAEzB,UAAM,SAAS,aAAa,iBACxB,iBAAiB,OAAO,IACxB,aAAa,cAAc,OAAO;AACtC,UAAM,cAAc,OAAO,KAAK,GAAG;AACnC,UAAM,gBAAgB,QAAQ,KAAK,GAAG;AAEtC,QAAI,gBAAgB,eAAe;AACjC,YAAM,EAAE,MAAM,OAAO,IAAID,oBAAmB,SAAS,WAAW;AAChE,kBAAY,KAAK;AAAA,QACf,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR,QAAQ,WAAW;AAAA,QACnB,UAAU;AAAA,QACV,WAAW;AAAA,QACX,aAAaC,gBAAe,SAAS,WAAW;AAAA,MAClD,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,iBAAiB,SAA6B;AACrD,SAAO,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AACvD;AAEA,SAAS,aAAa,cAA4B,SAA6B;AAC7E,QAAM,UAAU,aAAa,cAAc,OAAO;AAElD,QAAM,UAAoB,CAAC;AAC3B,QAAM,QAA4B,CAAC;AAEnC,aAAW,CAAC,KAAK,KAAK,KAAK,SAAS;AAClC,QAAI,UAAU,MAAM;AAClB,cAAQ,KAAK,GAAG;AAAA,IAClB,OAAO;AACL,YAAM,KAAK,CAAC,KAAK,KAAK,CAAC;AAAA,IACzB;AAAA,EACF;AAEA,QAAM,KAAK,CAAC,GAAG,MAAM;AACnB,QAAI,EAAE,CAAC,IAAI,EAAE,CAAC,EAAG,QAAO;AACxB,QAAI,EAAE,CAAC,IAAI,EAAE,CAAC,EAAG,QAAO;AACxB,WAAO;AAAA,EACT,CAAC;AAED,SAAO,CAAC,GAAG,SAAS,GAAG,MAAM,IAAI,CAAC,CAAC,GAAG,MAAM,GAAG,CAAC;AAClD;;;ANjGA,IAAM,aAAa,oBAAI,IAAI;AAAA,EACzB;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EAAU;AAAA,EAC9C;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EAAc;AAAA,EAAO;AAAA,EAC1C;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAU;AAAA,EAAO;AAAA,EAAQ;AAAA,EACxC;AAAA,EAAO;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAQ;AAAA,EAAO;AAC1C,CAAC;AAED,SAAS,aAAa,UAA0B;AAC9C,QAAM,OAAOC,MAAK,SAAS,QAAQ;AAEnC,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,MAAI,MAAM,SAAS,GAAG;AACpB,WAAO,MAAM,MAAM,EAAE,EAAE,KAAK,GAAG;AAAA,EACjC;AACA,SAAO,MAAM,IAAI,KAAK;AACxB;AAEA,SAAS,eAAe,UAA2B;AACjD,MAAI,SAAS,SAAS,MAAM,EAAG,QAAO;AACtC,QAAM,MAAM,aAAa,QAAQ;AACjC,SAAO,WAAW,IAAI,GAAG;AAC3B;AAEA,IAAM,SAAS,cAAc;AAAA,EAC3B,MAAM,EAAE,MAAM,UAAU,aAAa,4BAA4B;AAAA,EACjE,MAAM;AAAA,IACJ,KAAK,EAAE,MAAM,UAAU,aAAa,0BAA0B;AAAA,IAC9D,UAAU,EAAE,MAAM,UAAU,aAAa,8DAA8D;AAAA,IACvG,KAAK,EAAE,MAAM,WAAW,aAAa,mBAAmB,SAAS,MAAM;AAAA,IACvE,MAAM,EAAE,MAAM,WAAW,aAAa,8BAA8B,SAAS,MAAM;AAAA,IACnF,OAAO,EAAE,MAAM,WAAW,aAAa,qBAAqB,SAAS,MAAM;AAAA,EAC7E;AAAA,EACA,KAAK,OAAO,EAAE,MAAM,QAAQ,MAAM;AAChC,UAAM,QAAQ,QAAQ,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,CAAC;AACtD,UAAM,IAAI,OAAO,MAAM,CAAC,QAAQ,CAAC;AAAA,EACnC;AACF,CAAC;AAED,IAAM,OAAO,cAAc;AAAA,EACzB,MAAM,EAAE,MAAM,QAAQ,aAAa,+CAA+C;AAAA,EAClF,MAAM;AAAA,IACJ,KAAK,EAAE,MAAM,UAAU,aAAa,0BAA0B;AAAA,IAC9D,KAAK,EAAE,MAAM,UAAU,aAAa,qCAAqC;AAAA,IACzE,UAAU,EAAE,MAAM,WAAW,aAAa,gDAA2C,SAAS,MAAM;AAAA,IACpG,KAAK,EAAE,MAAM,WAAW,aAAa,mBAAmB,SAAS,MAAM;AAAA,IACvE,MAAM,EAAE,MAAM,WAAW,aAAa,8BAA8B,SAAS,MAAM;AAAA,IACnF,OAAO,EAAE,MAAM,WAAW,aAAa,qBAAqB,SAAS,MAAM;AAAA,EAC7E;AAAA,EACA,KAAK,OAAO,EAAE,MAAM,QAAQ,MAAM;AAChC,UAAM,QAAQ,QAAQ,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,CAAC;AACtD,UAAM,IAAI,OAAO,MAAM,CAAC,MAAM,CAAC;AAAA,EACjC;AACF,CAAC;AAED,IAAM,QAAQ,cAAc;AAAA,EAC1B,MAAM,EAAE,MAAM,SAAS,aAAa,6BAA6B;AAAA,EACjE,MAAM;AAAA,IACJ,KAAK,EAAE,MAAM,UAAU,aAAa,0BAA0B;AAAA,IAC9D,UAAU,EAAE,MAAM,UAAU,aAAa,8DAA8D;AAAA,IACvG,KAAK,EAAE,MAAM,UAAU,aAAa,qCAAqC;AAAA,IACzE,UAAU,EAAE,MAAM,WAAW,aAAa,gDAA2C,SAAS,MAAM;AAAA,IACpG,KAAK,EAAE,MAAM,WAAW,aAAa,mBAAmB,SAAS,MAAM;AAAA,IACvE,MAAM,EAAE,MAAM,WAAW,aAAa,8BAA8B,SAAS,MAAM;AAAA,IACnF,OAAO,EAAE,MAAM,WAAW,aAAa,qBAAqB,SAAS,MAAM;AAAA,EAC7E;AAAA,EACA,KAAK,OAAO,EAAE,MAAM,QAAQ,MAAM;AAChC,UAAM,QAAQ,QAAQ,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,CAAC;AACtD,UAAM,IAAI,OAAO,MAAM,CAAC,UAAU,MAAM,CAAC;AAAA,EAC3C;AACF,CAAC;AAED,IAAM,OAAO,cAAc;AAAA,EACzB,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,SAAS;AAAA,IACT,aAAa;AAAA,EACf;AAAA,EACA,aAAa,EAAE,QAAQ,MAAM,MAAM;AACrC,CAAC;AAED,eAAe,IACb,OACA,MASA,OACA;AACA,QAAM,OAAO,QAAQ,IAAI;AACzB,QAAM,WAAW,KAAK,MAAM,OAAO,KAAK,GAAG,IAAI;AAC/C,QAAM,OAAO,KAAK,MAAM,QAAQ;AAChC,QAAM,WAAY,KAAK,YAA6B;AAGpD,MAAI;AACJ,MAAI;AACF,mBAAe,MAAM,iBAAiB,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;AAAA,EAC/D,SAAS,KAAK;AACZ,YAAQ,MAAMC,IAAG,IAAK,IAAc,OAAO,CAAC;AAC5C,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,QAAM,iBAAiB,cAAc,MAAM,KAAK;AAEhD,MAAI,eAAe,WAAW,GAAG;AAC/B,YAAQ,MAAMA,IAAG,OAAO,qCAAqC,CAAC;AAC9D,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,MAAI,YAAY;AAEhB,aAAW,QAAQ,OAAO;AACxB,UAAM,oBAAoB,oBAAI,IAA0B;AACxD,QAAI,mBAAmB;AAEvB,eAAW,YAAY,gBAAgB;AACrC,YAAM,UAAUC,IAAG,aAAa,UAAU,OAAO;AACjD,YAAM,MAAM,aAAa,QAAQ;AACjC,YAAM,eAAeF,MAAK,SAAS,MAAM,QAAQ;AAEjD,YAAM,cAAc,SAAS,WACzB,oBAAoB,cAAc,SAAS,cAAc,EAAE,SAAS,CAAC,IACrE,wBAAwB,cAAc,SAAS,KAAK,cAAc;AAAA,QAChE,KAAK;AAAA,QACL,UAAU,KAAK;AAAA,MACjB,CAAC;AAEL,UAAI,YAAY,SAAS,GAAG;AAC1B,YAAI,KAAK,KAAK;AACZ,gBAAM,QAAQ,WAAW,SAAS,WAAW;AAC7C,UAAAE,IAAG,cAAc,UAAU,OAAO,OAAO;AAAA,QAC3C;AACA,0BAAkB,IAAI,cAAc,WAAW;AAC/C,4BAAoB,YAAY;AAAA,MAClC;AAAA,IACF;AAGA,UAAM,SAAS,kBAAkB,mBAAmB;AAAA,MAClD;AAAA,MACA;AAAA,MACA,MAAM,KAAK;AAAA,MACX,OAAO,KAAK;AAAA,IACd,CAAC;AAED,QAAI,QAAQ;AACV,cAAQ,IAAI,MAAM;AAAA,IACpB;AAEA,QAAI,qBAAqB,GAAG;AAC1B,UAAI,CAAC,KAAK,SAAS,CAAC,KAAK,MAAM;AAC7B,cAAM,MAAM,SAAS,WACjB,qCACA;AACJ,gBAAQ,IAAID,IAAG,MAAM,GAAG,CAAC;AAAA,MAC3B;AAAA,IACF;AAEA,QAAI,mBAAmB,GAAG;AACxB,kBAAY;AAAA,IACd;AAAA,EACF;AAGA,MAAI,CAAC,KAAK,OAAO,WAAW;AAC1B,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,SAAS,cAAc,MAAc,OAA2B;AAC9D,MAAI,MAAM,SAAS,GAAG;AAEpB,UAAM,QAAkB,CAAC;AACzB,eAAW,KAAK,OAAO;AACrB,YAAM,WAAWD,MAAK,QAAQ,MAAM,CAAC;AACrC,UAAIE,IAAG,SAAS,QAAQ,EAAE,YAAY,GAAG;AACvC,cAAM,KAAK,GAAG,QAAQ,QAAQ,CAAC;AAAA,MACjC,WAAW,eAAe,QAAQ,GAAG;AACnC,cAAM,KAAK,QAAQ;AAAA,MACrB;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAGA,QAAM,UAAU,cAAc;AAAA,IAC5B,EAAE,MAAM,SAAS,QAAQ,SAAS,MAAM;AAAA,EAC1C,CAAC;AAED,SAAO,QAAQ,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,KAAK,eAAe,CAAC,CAAC;AAC7E;AAEA,SAAS,QAAQ,KAAuB;AACtC,QAAM,QAAkB,CAAC;AACzB,QAAM,UAAUA,IAAG,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC;AAC3D,aAAW,SAAS,SAAS;AAC3B,UAAM,WAAWF,MAAK,KAAK,KAAK,MAAM,IAAI;AAC1C,QAAI,MAAM,YAAY,GAAG;AACvB,UAAI,MAAM,SAAS,kBAAkB,MAAM,SAAS,OAAQ;AAC5D,YAAM,KAAK,GAAG,QAAQ,QAAQ,CAAC;AAAA,IACjC,WAAW,eAAe,QAAQ,GAAG;AACnC,YAAM,KAAK,QAAQ;AAAA,IACrB;AAAA,EACF;AACA,SAAO;AACT;AAEA,QAAQ,IAAI;","names":["fs","path","pc","offsetToLineColumn","getLineContent","path","pc","fs"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { __unstable__loadDesignSystem } from '@tailwindcss/node';
|
|
2
|
+
|
|
3
|
+
type DesignSystem = Awaited<ReturnType<typeof __unstable__loadDesignSystem>>;
|
|
4
|
+
declare function loadDesignSystem(options: {
|
|
5
|
+
base: string;
|
|
6
|
+
css?: string;
|
|
7
|
+
}): Promise<DesignSystem>;
|
|
8
|
+
|
|
9
|
+
interface CandidateWithPosition {
|
|
10
|
+
rawCandidate: string;
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
}
|
|
14
|
+
declare function extractCandidatesWithPositions(content: string, extension?: string): CandidateWithPosition[];
|
|
15
|
+
interface SourceEntry {
|
|
16
|
+
base: string;
|
|
17
|
+
pattern: string;
|
|
18
|
+
negated: boolean;
|
|
19
|
+
}
|
|
20
|
+
declare function createScanner(sources: SourceEntry[]): {
|
|
21
|
+
readonly files: string[];
|
|
22
|
+
extractCandidatesWithPositions(content: string, extension: string): CandidateWithPosition[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
interface CanonicalizeOptions {
|
|
26
|
+
rem?: number;
|
|
27
|
+
collapse?: boolean;
|
|
28
|
+
}
|
|
29
|
+
interface Diagnostic {
|
|
30
|
+
file: string;
|
|
31
|
+
line: number;
|
|
32
|
+
column: number;
|
|
33
|
+
offset: number;
|
|
34
|
+
length: number;
|
|
35
|
+
original: string;
|
|
36
|
+
canonical: string;
|
|
37
|
+
lineContent: string;
|
|
38
|
+
}
|
|
39
|
+
declare function findNonCanonicalClasses(designSystem: DesignSystem, content: string, extension: string, filePath: string, options?: CanonicalizeOptions): Diagnostic[];
|
|
40
|
+
|
|
41
|
+
declare function applyFixes(content: string, diagnostics: Diagnostic[]): string;
|
|
42
|
+
|
|
43
|
+
type SortStrategy = 'tailwind' | 'alphabetical';
|
|
44
|
+
interface SortOptions {
|
|
45
|
+
strategy?: SortStrategy;
|
|
46
|
+
}
|
|
47
|
+
declare function findUnsortedClasses(designSystem: DesignSystem, content: string, filePath: string, options?: SortOptions): Diagnostic[];
|
|
48
|
+
|
|
49
|
+
interface ReporterOptions {
|
|
50
|
+
json?: boolean;
|
|
51
|
+
quiet?: boolean;
|
|
52
|
+
mode: 'check' | 'fix';
|
|
53
|
+
kind?: 'format' | 'lint';
|
|
54
|
+
}
|
|
55
|
+
declare function formatDiagnostics(diagnosticsByFile: Map<string, Diagnostic[]>, options: ReporterOptions): string;
|
|
56
|
+
|
|
57
|
+
export { type CandidateWithPosition, type CanonicalizeOptions, type DesignSystem, type Diagnostic, type ReporterOptions, type SortOptions, type SortStrategy, type SourceEntry, applyFixes, createScanner, extractCandidatesWithPositions, findNonCanonicalClasses, findUnsortedClasses, formatDiagnostics, loadDesignSystem };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
// src/design-system.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { __unstable__loadDesignSystem } from "@tailwindcss/node";
|
|
5
|
+
var TAILWIND_IMPORT_RE = /@import\s+['"]tailwindcss['"]/;
|
|
6
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".nuxt", ".output"]);
|
|
7
|
+
function detectCssEntryPoint(base) {
|
|
8
|
+
const found = findCssWithTailwind(base, 3);
|
|
9
|
+
return found;
|
|
10
|
+
}
|
|
11
|
+
function findCssWithTailwind(dir, maxDepth) {
|
|
12
|
+
if (maxDepth < 0) return null;
|
|
13
|
+
let entries;
|
|
14
|
+
try {
|
|
15
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (!entry.isFile() || !entry.name.endsWith(".css")) continue;
|
|
21
|
+
const fullPath = path.join(dir, entry.name);
|
|
22
|
+
try {
|
|
23
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
24
|
+
if (TAILWIND_IMPORT_RE.test(content)) {
|
|
25
|
+
return fullPath;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
|
|
33
|
+
const result = findCssWithTailwind(path.join(dir, entry.name), maxDepth - 1);
|
|
34
|
+
if (result) return result;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
async function loadDesignSystem(options) {
|
|
39
|
+
let cssPath;
|
|
40
|
+
if (options.css) {
|
|
41
|
+
cssPath = path.resolve(options.base, options.css);
|
|
42
|
+
} else {
|
|
43
|
+
const detected = detectCssEntryPoint(options.base);
|
|
44
|
+
if (!detected) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'Could not find a CSS entry point with `@import "tailwindcss"`. Specify one with --css <path>.'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
cssPath = detected;
|
|
50
|
+
}
|
|
51
|
+
if (!fs.existsSync(cssPath)) {
|
|
52
|
+
throw new Error(`CSS file not found: ${cssPath}`);
|
|
53
|
+
}
|
|
54
|
+
const cssContent = fs.readFileSync(cssPath, "utf-8");
|
|
55
|
+
return __unstable__loadDesignSystem(cssContent, {
|
|
56
|
+
base: path.dirname(cssPath)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/scanner.ts
|
|
61
|
+
import { Scanner } from "@tailwindcss/oxide";
|
|
62
|
+
function extractCandidatesWithPositions(content, extension = "html") {
|
|
63
|
+
const scanner = new Scanner({});
|
|
64
|
+
const result = scanner.getCandidatesWithPositions({ content, extension });
|
|
65
|
+
const candidates = [];
|
|
66
|
+
for (const { candidate: rawCandidate, position: start } of result) {
|
|
67
|
+
candidates.push({ rawCandidate, start, end: start + rawCandidate.length });
|
|
68
|
+
}
|
|
69
|
+
return candidates;
|
|
70
|
+
}
|
|
71
|
+
function createScanner(sources) {
|
|
72
|
+
const scanner = new Scanner({ sources });
|
|
73
|
+
return {
|
|
74
|
+
get files() {
|
|
75
|
+
return scanner.files;
|
|
76
|
+
},
|
|
77
|
+
extractCandidatesWithPositions(content, extension) {
|
|
78
|
+
return extractCandidatesWithPositions(content, extension);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/canonicalize.ts
|
|
84
|
+
function offsetToLineColumn(content, offset) {
|
|
85
|
+
let line = 1;
|
|
86
|
+
let column = 1;
|
|
87
|
+
for (let i = 0; i < offset && i < content.length; i++) {
|
|
88
|
+
if (content[i] === "\n") {
|
|
89
|
+
line++;
|
|
90
|
+
column = 1;
|
|
91
|
+
} else {
|
|
92
|
+
column++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { line, column };
|
|
96
|
+
}
|
|
97
|
+
function getLineContent(content, offset) {
|
|
98
|
+
let start = offset;
|
|
99
|
+
while (start > 0 && content[start - 1] !== "\n") {
|
|
100
|
+
start--;
|
|
101
|
+
}
|
|
102
|
+
let end = offset;
|
|
103
|
+
while (end < content.length && content[end] !== "\n") {
|
|
104
|
+
end++;
|
|
105
|
+
}
|
|
106
|
+
return content.slice(start, end);
|
|
107
|
+
}
|
|
108
|
+
function findNonCanonicalClasses(designSystem, content, extension, filePath, options = {}) {
|
|
109
|
+
const candidates = extractCandidatesWithPositions(content, extension);
|
|
110
|
+
const diagnostics = [];
|
|
111
|
+
for (const { rawCandidate, start } of candidates) {
|
|
112
|
+
const [canonical] = designSystem.canonicalizeCandidates([rawCandidate], {
|
|
113
|
+
rem: options.rem,
|
|
114
|
+
collapse: false
|
|
115
|
+
});
|
|
116
|
+
if (canonical && canonical !== rawCandidate) {
|
|
117
|
+
const { line, column } = offsetToLineColumn(content, start);
|
|
118
|
+
diagnostics.push({
|
|
119
|
+
file: filePath,
|
|
120
|
+
line,
|
|
121
|
+
column,
|
|
122
|
+
offset: start,
|
|
123
|
+
length: rawCandidate.length,
|
|
124
|
+
original: rawCandidate,
|
|
125
|
+
canonical,
|
|
126
|
+
lineContent: getLineContent(content, start)
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return diagnostics;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/fixer.ts
|
|
134
|
+
function applyFixes(content, diagnostics) {
|
|
135
|
+
if (diagnostics.length === 0) return content;
|
|
136
|
+
const changes = diagnostics.map((d) => ({
|
|
137
|
+
start: d.offset,
|
|
138
|
+
end: d.offset + d.length,
|
|
139
|
+
replacement: d.canonical
|
|
140
|
+
}));
|
|
141
|
+
return spliceChangesIntoString(content, changes);
|
|
142
|
+
}
|
|
143
|
+
function spliceChangesIntoString(str, changes) {
|
|
144
|
+
if (!changes[0]) return str;
|
|
145
|
+
changes.sort((a, b) => a.end - b.end || a.start - b.start);
|
|
146
|
+
let result = "";
|
|
147
|
+
let previous = changes[0];
|
|
148
|
+
result += str.slice(0, previous.start);
|
|
149
|
+
result += previous.replacement;
|
|
150
|
+
for (let i = 1; i < changes.length; ++i) {
|
|
151
|
+
const change = changes[i];
|
|
152
|
+
result += str.slice(previous.end, change.start);
|
|
153
|
+
result += change.replacement;
|
|
154
|
+
previous = change;
|
|
155
|
+
}
|
|
156
|
+
result += str.slice(previous.end);
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/sorter.ts
|
|
161
|
+
var CLASS_ATTR_RE = /class(?:Name)?\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
162
|
+
function offsetToLineColumn2(content, offset) {
|
|
163
|
+
let line = 1;
|
|
164
|
+
let column = 1;
|
|
165
|
+
for (let i = 0; i < offset && i < content.length; i++) {
|
|
166
|
+
if (content[i] === "\n") {
|
|
167
|
+
line++;
|
|
168
|
+
column = 1;
|
|
169
|
+
} else {
|
|
170
|
+
column++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { line, column };
|
|
174
|
+
}
|
|
175
|
+
function getLineContent2(content, offset) {
|
|
176
|
+
let start = offset;
|
|
177
|
+
while (start > 0 && content[start - 1] !== "\n") {
|
|
178
|
+
start--;
|
|
179
|
+
}
|
|
180
|
+
let end = offset;
|
|
181
|
+
while (end < content.length && content[end] !== "\n") {
|
|
182
|
+
end++;
|
|
183
|
+
}
|
|
184
|
+
return content.slice(start, end);
|
|
185
|
+
}
|
|
186
|
+
function findUnsortedClasses(designSystem, content, filePath, options = {}) {
|
|
187
|
+
const strategy = options.strategy ?? "tailwind";
|
|
188
|
+
const diagnostics = [];
|
|
189
|
+
CLASS_ATTR_RE.lastIndex = 0;
|
|
190
|
+
let match;
|
|
191
|
+
while ((match = CLASS_ATTR_RE.exec(content)) !== null) {
|
|
192
|
+
const classValue = match[1] ?? match[2];
|
|
193
|
+
if (!classValue) continue;
|
|
194
|
+
const quoteChar = match[1] !== void 0 ? '"' : "'";
|
|
195
|
+
const valueOffset = match.index + match[0].indexOf(quoteChar) + 1;
|
|
196
|
+
const classes = classValue.split(/\s+/).filter(Boolean);
|
|
197
|
+
if (classes.length <= 1) continue;
|
|
198
|
+
const sorted = strategy === "alphabetical" ? sortAlphabetical(classes) : sortTailwind(designSystem, classes);
|
|
199
|
+
const sortedValue = sorted.join(" ");
|
|
200
|
+
const originalValue = classes.join(" ");
|
|
201
|
+
if (sortedValue !== originalValue) {
|
|
202
|
+
const { line, column } = offsetToLineColumn2(content, valueOffset);
|
|
203
|
+
diagnostics.push({
|
|
204
|
+
file: filePath,
|
|
205
|
+
line,
|
|
206
|
+
column,
|
|
207
|
+
offset: valueOffset,
|
|
208
|
+
length: classValue.length,
|
|
209
|
+
original: classValue,
|
|
210
|
+
canonical: sortedValue,
|
|
211
|
+
lineContent: getLineContent2(content, valueOffset)
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return diagnostics;
|
|
216
|
+
}
|
|
217
|
+
function sortAlphabetical(classes) {
|
|
218
|
+
return [...classes].sort((a, b) => a.localeCompare(b));
|
|
219
|
+
}
|
|
220
|
+
function sortTailwind(designSystem, classes) {
|
|
221
|
+
const ordered = designSystem.getClassOrder(classes);
|
|
222
|
+
const unknown = [];
|
|
223
|
+
const known = [];
|
|
224
|
+
for (const [cls, order] of ordered) {
|
|
225
|
+
if (order === null) {
|
|
226
|
+
unknown.push(cls);
|
|
227
|
+
} else {
|
|
228
|
+
known.push([cls, order]);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
known.sort((a, b) => {
|
|
232
|
+
if (a[1] < b[1]) return -1;
|
|
233
|
+
if (a[1] > b[1]) return 1;
|
|
234
|
+
return 0;
|
|
235
|
+
});
|
|
236
|
+
return [...unknown, ...known.map(([cls]) => cls)];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/reporter.ts
|
|
240
|
+
import pc from "picocolors";
|
|
241
|
+
function formatDiagnostics(diagnosticsByFile, options) {
|
|
242
|
+
if (options.json) {
|
|
243
|
+
return formatJson(diagnosticsByFile);
|
|
244
|
+
}
|
|
245
|
+
if (options.quiet) {
|
|
246
|
+
return formatQuiet(diagnosticsByFile, options.mode, options.kind);
|
|
247
|
+
}
|
|
248
|
+
return formatCodeframe(diagnosticsByFile, options.mode, options.kind);
|
|
249
|
+
}
|
|
250
|
+
function formatJson(diagnosticsByFile) {
|
|
251
|
+
const output = {};
|
|
252
|
+
for (const [file, diagnostics] of diagnosticsByFile) {
|
|
253
|
+
output[file] = diagnostics.map((d) => ({
|
|
254
|
+
line: d.line,
|
|
255
|
+
column: d.column,
|
|
256
|
+
original: d.original,
|
|
257
|
+
canonical: d.canonical
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
return JSON.stringify(output, null, 2);
|
|
261
|
+
}
|
|
262
|
+
function formatQuiet(diagnosticsByFile, mode, kind) {
|
|
263
|
+
const { totalDiagnostics, totalFiles } = countTotals(diagnosticsByFile);
|
|
264
|
+
if (totalDiagnostics === 0) return "";
|
|
265
|
+
return formatSummary(totalDiagnostics, totalFiles, mode, kind);
|
|
266
|
+
}
|
|
267
|
+
function formatCodeframe(diagnosticsByFile, mode, kind) {
|
|
268
|
+
const lines = [];
|
|
269
|
+
const { totalDiagnostics, totalFiles } = countTotals(diagnosticsByFile);
|
|
270
|
+
const isFormat = kind === "format";
|
|
271
|
+
if (totalDiagnostics === 0) return "";
|
|
272
|
+
for (const [file, diagnostics] of diagnosticsByFile) {
|
|
273
|
+
for (const d of diagnostics) {
|
|
274
|
+
lines.push("");
|
|
275
|
+
lines.push(
|
|
276
|
+
`${pc.cyan(file)}${pc.dim(":")}${pc.yellow(String(d.line))}${pc.dim(":")}${pc.yellow(String(d.column))} ${pc.dim("warn")} ${pc.dim(isFormat ? "unsortedClasses" : "suggestCanonicalClasses")}`
|
|
277
|
+
);
|
|
278
|
+
lines.push(
|
|
279
|
+
isFormat ? ` Classes should be sorted as ${pc.green(pc.bold(d.canonical))}` : ` The class ${pc.red(pc.bold(d.original))} can be written as ${pc.green(pc.bold(d.canonical))}`
|
|
280
|
+
);
|
|
281
|
+
const contextLines = getContextLines(d);
|
|
282
|
+
lines.push("");
|
|
283
|
+
for (const cl of contextLines) {
|
|
284
|
+
lines.push(cl);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
lines.push("");
|
|
289
|
+
lines.push(formatSummary(totalDiagnostics, totalFiles, mode, kind));
|
|
290
|
+
return lines.join("\n");
|
|
291
|
+
}
|
|
292
|
+
function getContextLines(d) {
|
|
293
|
+
const lines = [];
|
|
294
|
+
const lineNum = String(d.line);
|
|
295
|
+
const gutter = " ".repeat(lineNum.length + 2);
|
|
296
|
+
lines.push(` ${pc.dim(lineNum)} ${pc.dim("\u2502")} ${d.lineContent}`);
|
|
297
|
+
const colOffset = d.column - 1;
|
|
298
|
+
const underline = " ".repeat(colOffset) + pc.red("~".repeat(d.original.length));
|
|
299
|
+
lines.push(` ${gutter}${pc.dim("\u2502")} ${underline}`);
|
|
300
|
+
const fixedLine = d.lineContent.slice(0, colOffset) + d.canonical + d.lineContent.slice(colOffset + d.original.length);
|
|
301
|
+
lines.push(` ${gutter}${pc.dim("\u2502")} ${pc.dim("\u2139 Suggested fix:")}`);
|
|
302
|
+
lines.push(` ${pc.dim(lineNum)} ${pc.dim("\u2502")} ${fixedLine}`);
|
|
303
|
+
const fixUnderline = " ".repeat(colOffset) + pc.green("~".repeat(d.canonical.length));
|
|
304
|
+
lines.push(` ${gutter}${pc.dim("\u2502")} ${fixUnderline}`);
|
|
305
|
+
return lines;
|
|
306
|
+
}
|
|
307
|
+
function countTotals(diagnosticsByFile) {
|
|
308
|
+
let totalDiagnostics = 0;
|
|
309
|
+
let totalFiles = 0;
|
|
310
|
+
for (const [, diagnostics] of diagnosticsByFile) {
|
|
311
|
+
if (diagnostics.length > 0) {
|
|
312
|
+
totalDiagnostics += diagnostics.length;
|
|
313
|
+
totalFiles++;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return { totalDiagnostics, totalFiles };
|
|
317
|
+
}
|
|
318
|
+
function formatSummary(count, files, mode, kind) {
|
|
319
|
+
const fileWord = files === 1 ? "file" : "files";
|
|
320
|
+
if (kind === "format") {
|
|
321
|
+
const groupWord = count === 1 ? "class group" : "class groups";
|
|
322
|
+
if (mode === "fix") {
|
|
323
|
+
return pc.green(`Sorted ${count} ${groupWord} in ${files} ${fileWord}.`);
|
|
324
|
+
}
|
|
325
|
+
return pc.yellow(
|
|
326
|
+
`Found ${count} unsorted ${groupWord} in ${files} ${fileWord}. Run ${pc.bold("nyx format --fix")} to apply.`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
const classWord = count === 1 ? "class" : "classes";
|
|
330
|
+
if (mode === "fix") {
|
|
331
|
+
return pc.green(`Fixed ${count} non-canonical ${classWord} in ${files} ${fileWord}.`);
|
|
332
|
+
}
|
|
333
|
+
return pc.yellow(
|
|
334
|
+
`Found ${count} non-canonical ${classWord} in ${files} ${fileWord}. Run ${pc.bold("nyx lint --fix")} to apply.`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
export {
|
|
338
|
+
applyFixes,
|
|
339
|
+
createScanner,
|
|
340
|
+
extractCandidatesWithPositions,
|
|
341
|
+
findNonCanonicalClasses,
|
|
342
|
+
findUnsortedClasses,
|
|
343
|
+
formatDiagnostics,
|
|
344
|
+
loadDesignSystem
|
|
345
|
+
};
|
|
346
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/design-system.ts","../src/scanner.ts","../src/canonicalize.ts","../src/fixer.ts","../src/sorter.ts","../src/reporter.ts"],"sourcesContent":["import fs from 'node:fs'\nimport path from 'node:path'\nimport { __unstable__loadDesignSystem } from '@tailwindcss/node'\n\nconst TAILWIND_IMPORT_RE = /@import\\s+['\"]tailwindcss['\"]/\n\nconst SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt', '.output'])\n\nfunction detectCssEntryPoint(base: string): string | null {\n // Walk the project directory looking for any .css file with @import \"tailwindcss\"\n const found = findCssWithTailwind(base, 3)\n return found\n}\n\nfunction findCssWithTailwind(dir: string, maxDepth: number): string | null {\n if (maxDepth < 0) return null\n\n let entries: fs.Dirent[]\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true })\n } catch {\n return null\n }\n\n // Check CSS files in this directory first\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith('.css')) continue\n const fullPath = path.join(dir, entry.name)\n try {\n const content = fs.readFileSync(fullPath, 'utf-8')\n if (TAILWIND_IMPORT_RE.test(content)) {\n return fullPath\n }\n } catch {\n continue\n }\n }\n\n // Then recurse into subdirectories\n for (const entry of entries) {\n if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue\n const result = findCssWithTailwind(path.join(dir, entry.name), maxDepth - 1)\n if (result) return result\n }\n\n return null\n}\n\nexport type DesignSystem = Awaited<ReturnType<typeof __unstable__loadDesignSystem>>\n\nexport async function loadDesignSystem(options: {\n base: string\n css?: string\n}): Promise<DesignSystem> {\n let cssPath: string\n\n if (options.css) {\n cssPath = path.resolve(options.base, options.css)\n } else {\n const detected = detectCssEntryPoint(options.base)\n if (!detected) {\n throw new Error(\n 'Could not find a CSS entry point with `@import \"tailwindcss\"`. ' +\n 'Specify one with --css <path>.',\n )\n }\n cssPath = detected\n }\n\n if (!fs.existsSync(cssPath)) {\n throw new Error(`CSS file not found: ${cssPath}`)\n }\n\n const cssContent = fs.readFileSync(cssPath, 'utf-8')\n return __unstable__loadDesignSystem(cssContent, {\n base: path.dirname(cssPath),\n })\n}\n","import { Scanner } from '@tailwindcss/oxide'\n\nexport interface CandidateWithPosition {\n rawCandidate: string\n start: number\n end: number\n}\n\nexport function extractCandidatesWithPositions(\n content: string,\n extension: string = 'html',\n): CandidateWithPosition[] {\n const scanner = new Scanner({})\n const result = scanner.getCandidatesWithPositions({ content, extension })\n\n const candidates: CandidateWithPosition[] = []\n for (const { candidate: rawCandidate, position: start } of result) {\n candidates.push({ rawCandidate, start, end: start + rawCandidate.length })\n }\n return candidates\n}\n\nexport interface SourceEntry {\n base: string\n pattern: string\n negated: boolean\n}\n\nexport function createScanner(sources: SourceEntry[]) {\n const scanner = new Scanner({ sources })\n return {\n get files(): string[] {\n return scanner.files\n },\n extractCandidatesWithPositions(content: string, extension: string) {\n return extractCandidatesWithPositions(content, extension)\n },\n }\n}\n","import type { DesignSystem } from './design-system.js'\nimport { extractCandidatesWithPositions } from './scanner.js'\n\nexport interface CanonicalizeOptions {\n rem?: number\n collapse?: boolean\n}\n\nexport interface Diagnostic {\n file: string\n line: number\n column: number\n offset: number\n length: number\n original: string\n canonical: string\n lineContent: string\n}\n\nfunction offsetToLineColumn(content: string, offset: number): { line: number; column: number } {\n let line = 1\n let column = 1\n for (let i = 0; i < offset && i < content.length; i++) {\n if (content[i] === '\\n') {\n line++\n column = 1\n } else {\n column++\n }\n }\n return { line, column }\n}\n\nfunction getLineContent(content: string, offset: number): string {\n let start = offset\n while (start > 0 && content[start - 1] !== '\\n') {\n start--\n }\n let end = offset\n while (end < content.length && content[end] !== '\\n') {\n end++\n }\n return content.slice(start, end)\n}\n\nexport function findNonCanonicalClasses(\n designSystem: DesignSystem,\n content: string,\n extension: string,\n filePath: string,\n options: CanonicalizeOptions = {},\n): Diagnostic[] {\n const candidates = extractCandidatesWithPositions(content, extension)\n const diagnostics: Diagnostic[] = []\n\n for (const { rawCandidate, start } of candidates) {\n const [canonical] = designSystem.canonicalizeCandidates([rawCandidate], {\n rem: options.rem,\n collapse: false,\n })\n\n if (canonical && canonical !== rawCandidate) {\n const { line, column } = offsetToLineColumn(content, start)\n diagnostics.push({\n file: filePath,\n line,\n column,\n offset: start,\n length: rawCandidate.length,\n original: rawCandidate,\n canonical,\n lineContent: getLineContent(content, start),\n })\n }\n }\n\n return diagnostics\n}\n","import type { Diagnostic } from './canonicalize.js'\n\ninterface StringChange {\n start: number\n end: number\n replacement: string\n}\n\nexport function applyFixes(content: string, diagnostics: Diagnostic[]): string {\n if (diagnostics.length === 0) return content\n\n const changes: StringChange[] = diagnostics.map((d) => ({\n start: d.offset,\n end: d.offset + d.length,\n replacement: d.canonical,\n }))\n\n return spliceChangesIntoString(content, changes)\n}\n\nfunction spliceChangesIntoString(str: string, changes: StringChange[]): string {\n if (!changes[0]) return str\n\n changes.sort((a, b) => a.end - b.end || a.start - b.start)\n\n let result = ''\n let previous = changes[0]\n\n result += str.slice(0, previous.start)\n result += previous.replacement\n\n for (let i = 1; i < changes.length; ++i) {\n const change = changes[i]\n result += str.slice(previous.end, change.start)\n result += change.replacement\n previous = change\n }\n\n result += str.slice(previous.end)\n return result\n}\n","import type { DesignSystem } from './design-system.js'\nimport type { Diagnostic } from './canonicalize.js'\n\nexport type SortStrategy = 'tailwind' | 'alphabetical'\n\nexport interface SortOptions {\n strategy?: SortStrategy\n}\n\nconst CLASS_ATTR_RE = /class(?:Name)?\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')/g\n\nfunction offsetToLineColumn(content: string, offset: number): { line: number; column: number } {\n let line = 1\n let column = 1\n for (let i = 0; i < offset && i < content.length; i++) {\n if (content[i] === '\\n') {\n line++\n column = 1\n } else {\n column++\n }\n }\n return { line, column }\n}\n\nfunction getLineContent(content: string, offset: number): string {\n let start = offset\n while (start > 0 && content[start - 1] !== '\\n') {\n start--\n }\n let end = offset\n while (end < content.length && content[end] !== '\\n') {\n end++\n }\n return content.slice(start, end)\n}\n\nexport function findUnsortedClasses(\n designSystem: DesignSystem,\n content: string,\n filePath: string,\n options: SortOptions = {},\n): Diagnostic[] {\n const strategy = options.strategy ?? 'tailwind'\n const diagnostics: Diagnostic[] = []\n\n CLASS_ATTR_RE.lastIndex = 0\n let match: RegExpExecArray | null\n\n while ((match = CLASS_ATTR_RE.exec(content)) !== null) {\n const classValue = match[1] ?? match[2]\n if (!classValue) continue\n\n const quoteChar = match[1] !== undefined ? '\"' : \"'\"\n const valueOffset = match.index + match[0].indexOf(quoteChar) + 1\n\n const classes = classValue.split(/\\s+/).filter(Boolean)\n if (classes.length <= 1) continue\n\n const sorted = strategy === 'alphabetical'\n ? sortAlphabetical(classes)\n : sortTailwind(designSystem, classes)\n const sortedValue = sorted.join(' ')\n const originalValue = classes.join(' ')\n\n if (sortedValue !== originalValue) {\n const { line, column } = offsetToLineColumn(content, valueOffset)\n diagnostics.push({\n file: filePath,\n line,\n column,\n offset: valueOffset,\n length: classValue.length,\n original: classValue,\n canonical: sortedValue,\n lineContent: getLineContent(content, valueOffset),\n })\n }\n }\n\n return diagnostics\n}\n\nfunction sortAlphabetical(classes: string[]): string[] {\n return [...classes].sort((a, b) => a.localeCompare(b))\n}\n\nfunction sortTailwind(designSystem: DesignSystem, classes: string[]): string[] {\n const ordered = designSystem.getClassOrder(classes)\n\n const unknown: string[] = []\n const known: [string, bigint][] = []\n\n for (const [cls, order] of ordered) {\n if (order === null) {\n unknown.push(cls)\n } else {\n known.push([cls, order])\n }\n }\n\n known.sort((a, b) => {\n if (a[1] < b[1]) return -1\n if (a[1] > b[1]) return 1\n return 0\n })\n\n return [...unknown, ...known.map(([cls]) => cls)]\n}\n","import pc from 'picocolors'\nimport type { Diagnostic } from './canonicalize.js'\n\nexport interface ReporterOptions {\n json?: boolean\n quiet?: boolean\n mode: 'check' | 'fix'\n kind?: 'format' | 'lint'\n}\n\nexport function formatDiagnostics(\n diagnosticsByFile: Map<string, Diagnostic[]>,\n options: ReporterOptions,\n): string {\n if (options.json) {\n return formatJson(diagnosticsByFile)\n }\n if (options.quiet) {\n return formatQuiet(diagnosticsByFile, options.mode, options.kind)\n }\n return formatCodeframe(diagnosticsByFile, options.mode, options.kind)\n}\n\nfunction formatJson(diagnosticsByFile: Map<string, Diagnostic[]>): string {\n const output: Record<string, { line: number; column: number; original: string; canonical: string }[]> = {}\n for (const [file, diagnostics] of diagnosticsByFile) {\n output[file] = diagnostics.map((d) => ({\n line: d.line,\n column: d.column,\n original: d.original,\n canonical: d.canonical,\n }))\n }\n return JSON.stringify(output, null, 2)\n}\n\nfunction formatQuiet(\n diagnosticsByFile: Map<string, Diagnostic[]>,\n mode: 'check' | 'fix',\n kind?: 'format' | 'lint',\n): string {\n const { totalDiagnostics, totalFiles } = countTotals(diagnosticsByFile)\n if (totalDiagnostics === 0) return ''\n return formatSummary(totalDiagnostics, totalFiles, mode, kind)\n}\n\nfunction formatCodeframe(\n diagnosticsByFile: Map<string, Diagnostic[]>,\n mode: 'check' | 'fix',\n kind?: 'format' | 'lint',\n): string {\n const lines: string[] = []\n const { totalDiagnostics, totalFiles } = countTotals(diagnosticsByFile)\n const isFormat = kind === 'format'\n\n if (totalDiagnostics === 0) return ''\n\n for (const [file, diagnostics] of diagnosticsByFile) {\n for (const d of diagnostics) {\n lines.push('')\n lines.push(\n `${pc.cyan(file)}${pc.dim(':')}${pc.yellow(String(d.line))}${pc.dim(':')}${pc.yellow(String(d.column))} ${pc.dim('warn')} ${pc.dim(isFormat ? 'unsortedClasses' : 'suggestCanonicalClasses')}`,\n )\n lines.push(\n isFormat\n ? ` Classes should be sorted as ${pc.green(pc.bold(d.canonical))}`\n : ` The class ${pc.red(pc.bold(d.original))} can be written as ${pc.green(pc.bold(d.canonical))}`,\n )\n\n const contextLines = getContextLines(d)\n lines.push('')\n for (const cl of contextLines) {\n lines.push(cl)\n }\n }\n }\n\n lines.push('')\n lines.push(formatSummary(totalDiagnostics, totalFiles, mode, kind))\n\n return lines.join('\\n')\n}\n\nfunction getContextLines(d: Diagnostic): string[] {\n const lines: string[] = []\n const lineNum = String(d.line)\n const gutter = ' '.repeat(lineNum.length + 2)\n\n // The line with the issue\n lines.push(` ${pc.dim(lineNum)} ${pc.dim('│')} ${d.lineContent}`)\n\n // The underline\n const colOffset = d.column - 1\n const underline = ' '.repeat(colOffset) + pc.red('~'.repeat(d.original.length))\n lines.push(` ${gutter}${pc.dim('│')} ${underline}`)\n\n // Suggested fix\n const fixedLine = d.lineContent.slice(0, colOffset) + d.canonical + d.lineContent.slice(colOffset + d.original.length)\n lines.push(` ${gutter}${pc.dim('│')} ${pc.dim('ℹ Suggested fix:')}`)\n lines.push(` ${pc.dim(lineNum)} ${pc.dim('│')} ${fixedLine}`)\n\n const fixUnderline = ' '.repeat(colOffset) + pc.green('~'.repeat(d.canonical.length))\n lines.push(` ${gutter}${pc.dim('│')} ${fixUnderline}`)\n\n return lines\n}\n\nfunction countTotals(diagnosticsByFile: Map<string, Diagnostic[]>) {\n let totalDiagnostics = 0\n let totalFiles = 0\n for (const [, diagnostics] of diagnosticsByFile) {\n if (diagnostics.length > 0) {\n totalDiagnostics += diagnostics.length\n totalFiles++\n }\n }\n return { totalDiagnostics, totalFiles }\n}\n\nfunction formatSummary(count: number, files: number, mode: 'check' | 'fix', kind?: 'format' | 'lint'): string {\n const fileWord = files === 1 ? 'file' : 'files'\n\n if (kind === 'format') {\n const groupWord = count === 1 ? 'class group' : 'class groups'\n if (mode === 'fix') {\n return pc.green(`Sorted ${count} ${groupWord} in ${files} ${fileWord}.`)\n }\n return pc.yellow(\n `Found ${count} unsorted ${groupWord} in ${files} ${fileWord}. Run ${pc.bold('nyx format --fix')} to apply.`,\n )\n }\n\n const classWord = count === 1 ? 'class' : 'classes'\n if (mode === 'fix') {\n return pc.green(`Fixed ${count} non-canonical ${classWord} in ${files} ${fileWord}.`)\n }\n return pc.yellow(\n `Found ${count} non-canonical ${classWord} in ${files} ${fileWord}. Run ${pc.bold('nyx lint --fix')} to apply.`,\n )\n}\n"],"mappings":"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","names":["offsetToLineColumn","getLineContent"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rielj/nyx",
|
|
3
|
+
"repository": {
|
|
4
|
+
"type": "git",
|
|
5
|
+
"url": "https://github.com/rielj/nyx"
|
|
6
|
+
},
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "rielj",
|
|
9
|
+
"email": "bulaybulay.rielj@gmail.com",
|
|
10
|
+
"url": "https://rielj.xyz"
|
|
11
|
+
},
|
|
12
|
+
"version": "0.0.1",
|
|
13
|
+
"description": "Tailwind CSS canonical class fixer CLI",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"nyx": "./dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup",
|
|
31
|
+
"dev": "tsup --watch",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest",
|
|
34
|
+
"typecheck": "tsc --noEmit"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@tailwindcss/node": "^4.1.15",
|
|
38
|
+
"@tailwindcss/oxide": "^4.1.15",
|
|
39
|
+
"citty": "^0.1.6",
|
|
40
|
+
"picocolors": "^1.1.1",
|
|
41
|
+
"tailwindcss": "^4.1.15"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^25.2.3",
|
|
45
|
+
"tsup": "^8.4.0",
|
|
46
|
+
"typescript": "^5.7.0",
|
|
47
|
+
"vitest": "^3.0.0"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20"
|
|
51
|
+
},
|
|
52
|
+
"license": "MIT"
|
|
53
|
+
}
|