tailwind-lint 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/cli.cjs +159 -92
- package/dist/{linter-B-Bi_uRJ.cjs → linter-CUpiAi7p.cjs} +145 -83
- package/dist/linter.cjs +1 -1
- package/dist/linter.d.cts +7 -7
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -75,6 +75,7 @@ tailwind-lint --verbose
|
|
|
75
75
|
- `-c, --config <path>` - Path to Tailwind config file (default: auto-discover)
|
|
76
76
|
- `-a, --auto` - Auto-discover files from the resolved Tailwind config or CSS `@source` directives
|
|
77
77
|
- `--fix` - Automatically fix problems that can be fixed
|
|
78
|
+
- `--dry-run` - Show the fixes `--fix` would apply (with a line diff) without writing any files
|
|
78
79
|
- `--format <text|json>` - Output format (`text` default, `json` for machine-readable output)
|
|
79
80
|
- `-v, --verbose` - Enable verbose logging for debugging
|
|
80
81
|
- `-h, --help` - Show help message
|
|
@@ -95,6 +96,9 @@ tailwind-lint --config ./src/app.css
|
|
|
95
96
|
# Lint and fix specific file types
|
|
96
97
|
tailwind-lint "**/*.vue" --fix
|
|
97
98
|
|
|
99
|
+
# Preview fixes without changing files
|
|
100
|
+
tailwind-lint --dry-run
|
|
101
|
+
|
|
98
102
|
# Lint with a specific CSS config (v4)
|
|
99
103
|
tailwind-lint --config ./styles/app.css
|
|
100
104
|
|
|
@@ -116,8 +120,8 @@ tailwind-lint --auto --format json
|
|
|
116
120
|
The JSON payload includes:
|
|
117
121
|
|
|
118
122
|
- `ok` - `true` when no errors are found
|
|
119
|
-
- `summary` - counts for `errors`, `warnings`, `fixed`, `filesWithIssues`, `totalFilesProcessed`
|
|
120
|
-
- `config` - resolved runtime values (`cwd`, `configPath`, `autoDiscover`, `fix`, `patterns`)
|
|
123
|
+
- `summary` - counts for `errors`, `warnings`, `fixable`, `fixed`, `filesWithIssues`, `totalFilesProcessed`
|
|
124
|
+
- `config` - resolved runtime values (`cwd`, `configPath`, `autoDiscover`, `fix`, `dryRun`, `patterns`)
|
|
121
125
|
- `files[]` - per-file diagnostics with 1-based `line`/`column` ranges
|
|
122
126
|
|
|
123
127
|
On JSON errors, the CLI keeps the same top-level shape and adds `error` so agent integrations do not need a separate parser for failure cases.
|
|
@@ -228,7 +232,9 @@ Use `--fix` to automatically resolve issues:
|
|
|
228
232
|
- **Canonical class suggestions** (v4) - Replaces arbitrary values with standard classes
|
|
229
233
|
- **Recommended variant order** (v3 & v4) - Reorders variants correctly
|
|
230
234
|
|
|
231
|
-
|
|
235
|
+
Use `--dry-run` to preview the changes without writing them.
|
|
236
|
+
|
|
237
|
+
Fixes that do not overlap are applied together, and files are revalidated until no fixable issues remain. The autofix process has a safety limit of 100 iterations per file to prevent infinite loops.
|
|
232
238
|
|
|
233
239
|
## Features
|
|
234
240
|
|
package/dist/cli.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_linter = require("./linter-
|
|
2
|
+
const require_linter = require("./linter-CUpiAi7p.cjs");
|
|
3
3
|
let node_path = require("node:path");
|
|
4
4
|
node_path = require_linter.__toESM(node_path, 1);
|
|
5
5
|
let ansis = require("ansis");
|
|
@@ -7,7 +7,40 @@ ansis = require_linter.__toESM(ansis, 1);
|
|
|
7
7
|
let node_fs = require("node:fs");
|
|
8
8
|
node_fs = require_linter.__toESM(node_fs, 1);
|
|
9
9
|
let commander = require("commander");
|
|
10
|
-
|
|
10
|
+
// src/diff.ts
|
|
11
|
+
/**
|
|
12
|
+
* Minimal line diff for previewing fixes. Quickfixes rewrite class lists in
|
|
13
|
+
* place, so lines usually map one-to-one; otherwise the region between the
|
|
14
|
+
* common prefix and suffix is reported as removed/added.
|
|
15
|
+
*/
|
|
16
|
+
function diffLines(before, after) {
|
|
17
|
+
const a = before.split("\n");
|
|
18
|
+
const b = after.split("\n");
|
|
19
|
+
const changes = [];
|
|
20
|
+
if (a.length === b.length) {
|
|
21
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) changes.push({
|
|
22
|
+
line: i + 1,
|
|
23
|
+
removed: a[i],
|
|
24
|
+
added: b[i]
|
|
25
|
+
});
|
|
26
|
+
return changes;
|
|
27
|
+
}
|
|
28
|
+
let start = 0;
|
|
29
|
+
while (start < a.length && start < b.length && a[start] === b[start]) start++;
|
|
30
|
+
let endA = a.length;
|
|
31
|
+
let endB = b.length;
|
|
32
|
+
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
|
|
33
|
+
endA--;
|
|
34
|
+
endB--;
|
|
35
|
+
}
|
|
36
|
+
for (let i = start; i < Math.max(endA, endB); i++) changes.push({
|
|
37
|
+
line: i + 1,
|
|
38
|
+
removed: i < endA ? a[i] : void 0,
|
|
39
|
+
added: i < endB ? b[i] : void 0
|
|
40
|
+
});
|
|
41
|
+
return changes;
|
|
42
|
+
}
|
|
43
|
+
// src/output.ts
|
|
11
44
|
function countBySeverity(diagnostics) {
|
|
12
45
|
let errors = 0;
|
|
13
46
|
let warnings = 0;
|
|
@@ -20,6 +53,15 @@ function countBySeverity(diagnostics) {
|
|
|
20
53
|
warnings
|
|
21
54
|
};
|
|
22
55
|
}
|
|
56
|
+
const ALWAYS_FIXABLE_CODES = /* @__PURE__ */ new Set(["cssConflict", "invalidApply"]);
|
|
57
|
+
function isFixable(diagnostic) {
|
|
58
|
+
if (ALWAYS_FIXABLE_CODES.has(String(diagnostic.code))) return true;
|
|
59
|
+
const { suggestions } = diagnostic;
|
|
60
|
+
return Array.isArray(suggestions) && suggestions.length > 0;
|
|
61
|
+
}
|
|
62
|
+
function countFixable(diagnostics) {
|
|
63
|
+
return diagnostics.filter(isFixable).length;
|
|
64
|
+
}
|
|
23
65
|
function toJsonSeverity(severity) {
|
|
24
66
|
if (severity === require_linter.SEVERITY.ERROR) return "error";
|
|
25
67
|
if (severity === require_linter.SEVERITY.WARNING) return "warning";
|
|
@@ -33,20 +75,22 @@ function toJsonDiagnostic(diagnostic) {
|
|
|
33
75
|
endColumn: diagnostic.range.end.character + 1,
|
|
34
76
|
severity: toJsonSeverity(diagnostic.severity),
|
|
35
77
|
code: diagnostic.code ?? null,
|
|
36
|
-
message: diagnostic.message,
|
|
78
|
+
message: typeof diagnostic.message === "string" ? diagnostic.message : diagnostic.message.value,
|
|
37
79
|
source: diagnostic.source
|
|
38
80
|
};
|
|
39
81
|
}
|
|
40
|
-
function createJsonOutput({ files, totalFilesProcessed, skippedFiles = [], cwd, configPath, autoDiscover, fix, patterns }) {
|
|
82
|
+
function createJsonOutput({ files, totalFilesProcessed, skippedFiles = [], cwd, configPath, autoDiscover, fix, dryRun = false, patterns }) {
|
|
41
83
|
let errors = 0;
|
|
42
84
|
let warnings = 0;
|
|
43
85
|
let fixed = 0;
|
|
86
|
+
let fixable = 0;
|
|
44
87
|
let filesWithIssues = 0;
|
|
45
88
|
const mappedFiles = files.map((file) => {
|
|
46
89
|
const severityCount = countBySeverity(file.diagnostics);
|
|
47
90
|
errors += severityCount.errors;
|
|
48
91
|
warnings += severityCount.warnings;
|
|
49
92
|
fixed += file.fixedCount || 0;
|
|
93
|
+
fixable += countFixable(file.diagnostics);
|
|
50
94
|
if (file.diagnostics.length > 0) filesWithIssues++;
|
|
51
95
|
return {
|
|
52
96
|
path: file.path,
|
|
@@ -61,6 +105,7 @@ function createJsonOutput({ files, totalFilesProcessed, skippedFiles = [], cwd,
|
|
|
61
105
|
errors,
|
|
62
106
|
warnings,
|
|
63
107
|
fixed,
|
|
108
|
+
fixable,
|
|
64
109
|
filesWithIssues,
|
|
65
110
|
totalFilesProcessed
|
|
66
111
|
},
|
|
@@ -69,13 +114,14 @@ function createJsonOutput({ files, totalFilesProcessed, skippedFiles = [], cwd,
|
|
|
69
114
|
configPath: configPath || null,
|
|
70
115
|
autoDiscover,
|
|
71
116
|
fix,
|
|
117
|
+
dryRun,
|
|
72
118
|
patterns
|
|
73
119
|
},
|
|
74
120
|
files: mappedFiles,
|
|
75
121
|
skippedFiles
|
|
76
122
|
};
|
|
77
123
|
}
|
|
78
|
-
function createJsonErrorOutput({ error, cwd, configPath, autoDiscover, fix, patterns }) {
|
|
124
|
+
function createJsonErrorOutput({ error, cwd, configPath, autoDiscover, fix, dryRun = false, patterns }) {
|
|
79
125
|
return {
|
|
80
126
|
ok: false,
|
|
81
127
|
error,
|
|
@@ -83,6 +129,7 @@ function createJsonErrorOutput({ error, cwd, configPath, autoDiscover, fix, patt
|
|
|
83
129
|
errors: 0,
|
|
84
130
|
warnings: 0,
|
|
85
131
|
fixed: 0,
|
|
132
|
+
fixable: 0,
|
|
86
133
|
filesWithIssues: 0,
|
|
87
134
|
totalFilesProcessed: 0
|
|
88
135
|
},
|
|
@@ -91,15 +138,17 @@ function createJsonErrorOutput({ error, cwd, configPath, autoDiscover, fix, patt
|
|
|
91
138
|
configPath: configPath || null,
|
|
92
139
|
autoDiscover,
|
|
93
140
|
fix,
|
|
141
|
+
dryRun,
|
|
94
142
|
patterns
|
|
95
143
|
},
|
|
96
144
|
files: [],
|
|
97
145
|
skippedFiles: []
|
|
98
146
|
};
|
|
99
147
|
}
|
|
100
|
-
|
|
101
|
-
//#region src/cli.ts
|
|
148
|
+
// src/cli.ts
|
|
102
149
|
const MAX_FILENAME_DISPLAY_LENGTH = 50;
|
|
150
|
+
const MAX_DIFF_LINES_PER_FILE = 10;
|
|
151
|
+
const CLEAR_LINE = "\r\x1B[2K";
|
|
103
152
|
function resolveOptions(files, options) {
|
|
104
153
|
const hasAutoFlag = !!options.auto;
|
|
105
154
|
const hasFiles = files.length > 0;
|
|
@@ -113,66 +162,97 @@ function resolveOptions(files, options) {
|
|
|
113
162
|
patterns = [];
|
|
114
163
|
autoDiscover = true;
|
|
115
164
|
}
|
|
165
|
+
const dryRun = options.dryRun || false;
|
|
116
166
|
return {
|
|
117
167
|
cwd,
|
|
118
168
|
configPath,
|
|
119
169
|
patterns: autoDiscover ? [] : patterns,
|
|
120
170
|
autoDiscover,
|
|
121
|
-
fix: options.fix ||
|
|
171
|
+
fix: options.fix || dryRun,
|
|
172
|
+
dryRun,
|
|
122
173
|
verbose: options.verbose || false
|
|
123
174
|
};
|
|
124
175
|
}
|
|
125
176
|
function truncateFilename(filename) {
|
|
126
177
|
return filename.length > MAX_FILENAME_DISPLAY_LENGTH ? `...${filename.slice(-50)}` : filename;
|
|
127
178
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
179
|
+
function plural(count, word) {
|
|
180
|
+
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
181
|
+
}
|
|
182
|
+
function formatDuration(ms) {
|
|
183
|
+
return ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(2)}s`;
|
|
184
|
+
}
|
|
185
|
+
function formatDiff(file) {
|
|
186
|
+
if (file.originalContent === void 0 || file.fixedContent === void 0) return [];
|
|
187
|
+
const changes = diffLines(file.originalContent, file.fixedContent);
|
|
188
|
+
const shown = changes.slice(0, MAX_DIFF_LINES_PER_FILE);
|
|
189
|
+
const width = String(shown.at(-1)?.line ?? 0).length;
|
|
190
|
+
const lines = [];
|
|
191
|
+
for (const change of shown) {
|
|
192
|
+
const lineNo = ansis.default.dim(String(change.line).padStart(width));
|
|
193
|
+
const blank = " ".repeat(width);
|
|
194
|
+
if (change.removed !== void 0) lines.push(` ${lineNo} ${ansis.default.red(`- ${change.removed.trim()}`)}`);
|
|
195
|
+
if (change.added !== void 0) {
|
|
196
|
+
const prefix = change.removed !== void 0 ? blank : lineNo;
|
|
197
|
+
lines.push(` ${prefix} ${ansis.default.green(`+ ${change.added.trim()}`)}`);
|
|
144
198
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
199
|
+
}
|
|
200
|
+
const hidden = changes.length - shown.length;
|
|
201
|
+
if (hidden > 0) lines.push(ansis.default.dim(` … ${plural(hidden, "more changed line")}`));
|
|
202
|
+
return lines;
|
|
203
|
+
}
|
|
204
|
+
function formatFile(file, dryRun) {
|
|
205
|
+
const lines = [ansis.default.underline.bold(file.path)];
|
|
206
|
+
if (file.fixed) {
|
|
207
|
+
const issues = plural(file.fixedCount || 0, "issue");
|
|
208
|
+
lines.push(dryRun ? ansis.default.cyan(` ~ Would fix ${issues}`) : ansis.default.green(` ✔ Fixed ${issues}`));
|
|
209
|
+
if (dryRun) lines.push(...formatDiff(file));
|
|
210
|
+
}
|
|
211
|
+
const positions = file.diagnostics.map((d) => `${d.range.start.line + 1}:${d.range.start.character + 1}`);
|
|
212
|
+
const positionWidth = Math.max(0, ...positions.map((p) => p.length));
|
|
213
|
+
for (const [index, diagnostic] of file.diagnostics.entries()) {
|
|
214
|
+
const severity = diagnostic.severity === require_linter.SEVERITY.ERROR ? ansis.default.red("error ") : ansis.default.yellow("warning");
|
|
215
|
+
const code = diagnostic.code ? ` ${ansis.default.dim(String(diagnostic.code))}` : "";
|
|
216
|
+
lines.push(` ${ansis.default.dim(positions[index].padEnd(positionWidth))} ${severity} ${diagnostic.message}${code}`);
|
|
217
|
+
}
|
|
218
|
+
return lines;
|
|
219
|
+
}
|
|
220
|
+
function formatSummary(files, totalFilesProcessed, durationMs, fixMode, dryRun) {
|
|
221
|
+
let errors = 0;
|
|
222
|
+
let warnings = 0;
|
|
223
|
+
let fixed = 0;
|
|
224
|
+
let fixable = 0;
|
|
225
|
+
let filesWithIssues = 0;
|
|
226
|
+
let filesFixed = 0;
|
|
227
|
+
for (const file of files) {
|
|
228
|
+
const counts = countBySeverity(file.diagnostics);
|
|
229
|
+
errors += counts.errors;
|
|
230
|
+
warnings += counts.warnings;
|
|
231
|
+
fixable += countFixable(file.diagnostics);
|
|
148
232
|
if (file.diagnostics.length > 0) filesWithIssues++;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const severity = diagnostic.severity === require_linter.SEVERITY.ERROR ? "error" : "warning";
|
|
153
|
-
const severityColor = diagnostic.severity === require_linter.SEVERITY.ERROR ? ansis.default.red(severity) : ansis.default.yellow(severity);
|
|
154
|
-
const code = diagnostic.code ? ansis.default.dim(` (${diagnostic.code})`) : "";
|
|
155
|
-
console.log(` ${ansis.default.dim(`${line}:${char}`)} ${severityColor} ${diagnostic.message}${code}`);
|
|
233
|
+
if (file.fixed) {
|
|
234
|
+
fixed += file.fixedCount || 0;
|
|
235
|
+
filesFixed++;
|
|
156
236
|
}
|
|
157
237
|
}
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
238
|
+
const lines = [];
|
|
239
|
+
if (fixed > 0) {
|
|
240
|
+
const text = `${plural(fixed, "issue")} in ${plural(filesFixed, "file")}`;
|
|
241
|
+
lines.push(dryRun ? ansis.default.cyan.bold(`~ Would fix ${text} (dry run, no files written)`) : ansis.default.green.bold(`✔ Fixed ${text}`));
|
|
242
|
+
}
|
|
243
|
+
const problems = errors + warnings;
|
|
244
|
+
if (problems === 0) {
|
|
245
|
+
if (fixed === 0) lines.push(ansis.default.green.bold("✔ No issues found"));
|
|
164
246
|
} else {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
const fileText = `${filesWithIssues} file${filesWithIssues !== 1 ? "s" : ""}`;
|
|
169
|
-
const summary = `Found ${parts.join(" and ")} in ${fileText}`;
|
|
170
|
-
if (totalFixed > 0) {
|
|
171
|
-
const issueText = `${totalFixed} issue${totalFixed !== 1 ? "s" : ""}`;
|
|
172
|
-
console.log(ansis.default.green.bold(`✔ Fixed ${issueText}`));
|
|
173
|
-
console.log(summary);
|
|
174
|
-
} else console.log(summary);
|
|
247
|
+
const color = errors > 0 ? ansis.default.red.bold : ansis.default.yellow.bold;
|
|
248
|
+
lines.push(color(`✖ ${plural(problems, "problem")} (${plural(errors, "error")}, ${plural(warnings, "warning")}) in ${plural(filesWithIssues, "file")}`));
|
|
249
|
+
if (fixable > 0 && !fixMode) lines.push(ansis.default.dim(` ${fixable} potentially fixable with ${ansis.default.yellow("--fix")} (preview with ${ansis.default.yellow("--dry-run")})`));
|
|
175
250
|
}
|
|
251
|
+
lines.push(ansis.default.dim(`Linted ${plural(totalFilesProcessed, "file")} in ${formatDuration(durationMs)}`));
|
|
252
|
+
return lines;
|
|
253
|
+
}
|
|
254
|
+
function writeAndExit(output, code) {
|
|
255
|
+
process.stdout.write(output, () => process.exit(code));
|
|
176
256
|
}
|
|
177
257
|
const program = new commander.Command();
|
|
178
258
|
const getVersion = () => {
|
|
@@ -209,13 +289,14 @@ program.configureHelp({ formatHelp: (cmd, helper) => {
|
|
|
209
289
|
}
|
|
210
290
|
return output;
|
|
211
291
|
} });
|
|
212
|
-
program.name("tailwind-lint").description("A CLI tool for linting Tailwind CSS class usage").version(getVersion()).argument("[files...]", "File patterns to lint (e.g., \"src/**/*.{js,jsx,ts,tsx}\")").option("-c, --config <path>", "Path to Tailwind config file (default: auto-discover)").option("-a, --auto", "Auto-discover files from Tailwind config or CSS @source patterns").option("--fix", "Automatically fix problems that can be fixed").option("-v, --verbose", "Enable verbose logging for debugging").option("--format <format>", "Output format: text or json", "text").addHelpText("after", `
|
|
292
|
+
program.name("tailwind-lint").description("A CLI tool for linting Tailwind CSS class usage").version(getVersion()).argument("[files...]", "File patterns to lint (e.g., \"src/**/*.{js,jsx,ts,tsx}\")").option("-c, --config <path>", "Path to Tailwind config file (default: auto-discover)").option("-a, --auto", "Auto-discover files from Tailwind config or CSS @source patterns").option("--fix", "Automatically fix problems that can be fixed").option("--dry-run", "Show which fixes --fix would apply without writing any files").option("-v, --verbose", "Enable verbose logging for debugging").option("--format <format>", "Output format: text or json", "text").addHelpText("after", `
|
|
213
293
|
${ansis.default.bold.cyan("Examples:")}
|
|
214
294
|
${ansis.default.dim("$")} tailwind-lint
|
|
215
295
|
${ansis.default.dim("$")} tailwind-lint ${ansis.default.green("\"src/**/*.{js,jsx,ts,tsx}\"")}
|
|
216
296
|
${ansis.default.dim("$")} tailwind-lint ${ansis.default.yellow("--config")} ${ansis.default.green("./tailwind.config.js")}
|
|
217
297
|
${ansis.default.dim("$")} tailwind-lint ${ansis.default.yellow("--config")} ${ansis.default.green("./src/app.css")}
|
|
218
298
|
${ansis.default.dim("$")} tailwind-lint ${ansis.default.green("\"src/**/*.tsx\"")} ${ansis.default.yellow("--fix")}
|
|
299
|
+
${ansis.default.dim("$")} tailwind-lint ${ansis.default.yellow("--dry-run")}
|
|
219
300
|
${ansis.default.dim("$")} tailwind-lint ${ansis.default.green("\"**/*.vue\"")}
|
|
220
301
|
|
|
221
302
|
${ansis.default.bold.cyan("Notes:")}
|
|
@@ -226,73 +307,60 @@ ${ansis.default.bold.cyan("Notes:")}
|
|
|
226
307
|
${ansis.default.dim("•")} Default pattern: ${ansis.default.dim(require_linter.DEFAULT_FILE_PATTERN)}
|
|
227
308
|
${ansis.default.dim("•")} Reads Tailwind workspace settings from ${ansis.default.dim(".zed/settings.json")} and ${ansis.default.dim(".vscode/settings.json")}
|
|
228
309
|
${ansis.default.dim("•")} Use ${ansis.default.yellow("--fix")} to automatically resolve fixable issues
|
|
310
|
+
${ansis.default.dim("•")} Use ${ansis.default.yellow("--dry-run")} to preview fixes without changing files
|
|
229
311
|
`).action(async (files, options) => {
|
|
230
312
|
const hasConfigFlag = !!options.config;
|
|
231
313
|
const hasAutoFlag = !!options.auto;
|
|
232
314
|
if (!(files.length > 0) && !hasAutoFlag && !hasConfigFlag) options.auto = true;
|
|
233
315
|
const resolved = resolveOptions(files, options);
|
|
234
|
-
const isJsonOutput =
|
|
316
|
+
const isJsonOutput = options.format === "json";
|
|
317
|
+
const showProgress = !isJsonOutput && !resolved.verbose && !!process.stdout.isTTY;
|
|
318
|
+
const startTime = performance.now();
|
|
235
319
|
try {
|
|
236
320
|
if (resolved.verbose && !isJsonOutput) {
|
|
237
321
|
console.log(ansis.default.cyan.bold("→ Configuration"));
|
|
238
322
|
console.log(ansis.default.dim(` Working directory: ${resolved.cwd}`));
|
|
239
323
|
console.log(ansis.default.dim(` Config path: ${resolved.configPath || "auto-discover"}`));
|
|
240
324
|
console.log(ansis.default.dim(` Fix mode: ${resolved.fix}`));
|
|
325
|
+
console.log(ansis.default.dim(` Dry run: ${resolved.dryRun}`));
|
|
241
326
|
console.log(ansis.default.dim(` Patterns: ${resolved.patterns.length > 0 ? resolved.patterns.join(", ") : "auto-discover"}`));
|
|
242
327
|
console.log();
|
|
243
328
|
}
|
|
329
|
+
if (showProgress) process.stdout.write(`${ansis.default.cyan("→")} Loading Tailwind configuration...`);
|
|
244
330
|
const results = await require_linter.lint({
|
|
245
331
|
...resolved,
|
|
246
332
|
onProgress: (current, total, file) => {
|
|
247
|
-
if (
|
|
248
|
-
if (
|
|
249
|
-
const displayFile = truncateFilename(file);
|
|
250
|
-
process.stdout.write(`\r${ansis.default.cyan("→")} Linting files... ${ansis.default.dim(`(${current}/${total})`)} ${ansis.default.dim(displayFile)}${" ".repeat(10)}`);
|
|
251
|
-
} else if (resolved.verbose) console.log(ansis.default.dim(` [${current}/${total}] Linting ${file}`));
|
|
333
|
+
if (showProgress) process.stdout.write(`${CLEAR_LINE}${ansis.default.cyan("→")} Linting files... ${ansis.default.dim(`(${current}/${total})`)} ${ansis.default.dim(truncateFilename(file))}`);
|
|
334
|
+
else if (resolved.verbose && !isJsonOutput) console.log(ansis.default.dim(` [${current}/${total}] Linting ${file}`));
|
|
252
335
|
}
|
|
253
336
|
});
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
if (isJsonOutput) console.log(JSON.stringify(createJsonOutput({
|
|
258
|
-
...resolved,
|
|
259
|
-
files: [],
|
|
260
|
-
totalFilesProcessed: 0,
|
|
261
|
-
skippedFiles: results.skippedFiles
|
|
262
|
-
})));
|
|
263
|
-
else {
|
|
264
|
-
console.log();
|
|
265
|
-
console.log(ansis.default.yellow("⚠ No files found to lint"));
|
|
266
|
-
}
|
|
267
|
-
process.exit(0);
|
|
268
|
-
}
|
|
269
|
-
if (results.files.length === 0) {
|
|
270
|
-
if (isJsonOutput) console.log(JSON.stringify(createJsonOutput({
|
|
271
|
-
...resolved,
|
|
272
|
-
files: [],
|
|
273
|
-
totalFilesProcessed: results.totalFilesProcessed,
|
|
274
|
-
skippedFiles: results.skippedFiles
|
|
275
|
-
})));
|
|
276
|
-
else console.log(ansis.default.green.bold("✔ No issues found"));
|
|
277
|
-
process.exit(0);
|
|
278
|
-
}
|
|
279
|
-
if (isJsonOutput) console.log(JSON.stringify(createJsonOutput({
|
|
337
|
+
if (showProgress) process.stdout.write(CLEAR_LINE);
|
|
338
|
+
const exitCode = results.files.some((file) => file.diagnostics.some((d) => d.severity === require_linter.SEVERITY.ERROR)) ? 1 : 0;
|
|
339
|
+
if (isJsonOutput) return writeAndExit(`${JSON.stringify(createJsonOutput({
|
|
280
340
|
...resolved,
|
|
281
341
|
files: results.files,
|
|
282
342
|
totalFilesProcessed: results.totalFilesProcessed,
|
|
283
343
|
skippedFiles: results.skippedFiles
|
|
284
|
-
})));
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
344
|
+
}))}\n`, exitCode);
|
|
345
|
+
const lines = [];
|
|
346
|
+
for (const skipped of results.skippedFiles) lines.push(ansis.default.yellow(`⚠ Skipped ${truncateFilename(skipped)} (exceeds size limit)`));
|
|
347
|
+
if (results.totalFilesProcessed === 0) {
|
|
348
|
+
lines.push(ansis.default.yellow("⚠ No files found to lint"));
|
|
349
|
+
return writeAndExit(`${lines.join("\n")}\n`, 0);
|
|
350
|
+
}
|
|
351
|
+
for (const file of results.files) lines.push("", ...formatFile(file, resolved.dryRun));
|
|
352
|
+
if (results.files.length > 0) lines.push("");
|
|
353
|
+
lines.push(...formatSummary(results.files, results.totalFilesProcessed, performance.now() - startTime, resolved.fix, resolved.dryRun));
|
|
354
|
+
return writeAndExit(`${lines.join("\n")}\n`, exitCode);
|
|
288
355
|
} catch (error) {
|
|
356
|
+
if (showProgress) process.stdout.write(CLEAR_LINE);
|
|
289
357
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
290
|
-
if (isJsonOutput)
|
|
358
|
+
if (isJsonOutput) return writeAndExit(`${JSON.stringify(createJsonErrorOutput({
|
|
291
359
|
error: errorMessage,
|
|
292
360
|
...resolved
|
|
293
|
-
})));
|
|
294
|
-
|
|
295
|
-
if (resolved.verbose && error instanceof Error
|
|
361
|
+
}))}\n`, 1);
|
|
362
|
+
console.error(ansis.default.red("✖ Error:"), errorMessage);
|
|
363
|
+
if (resolved.verbose && error instanceof Error) {
|
|
296
364
|
console.error(ansis.default.dim("\nStack trace:"));
|
|
297
365
|
console.error(ansis.default.dim(error.stack || error.toString()));
|
|
298
366
|
}
|
|
@@ -300,4 +368,3 @@ ${ansis.default.bold.cyan("Notes:")}
|
|
|
300
368
|
}
|
|
301
369
|
});
|
|
302
370
|
program.parse();
|
|
303
|
-
//#endregion
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// \0rolldown/runtime.js
|
|
2
2
|
var __create = Object.create;
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -19,7 +19,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
19
19
|
value: mod,
|
|
20
20
|
enumerable: true
|
|
21
21
|
}) : target, mod));
|
|
22
|
-
|
|
22
|
+
let node_fs_promises = require("node:fs/promises");
|
|
23
|
+
node_fs_promises = __toESM(node_fs_promises, 1);
|
|
23
24
|
let node_path = require("node:path");
|
|
24
25
|
node_path = __toESM(node_path, 1);
|
|
25
26
|
let _tailwindcss_language_service = require("@tailwindcss/language-service");
|
|
@@ -33,8 +34,7 @@ node_fs = __toESM(node_fs, 1);
|
|
|
33
34
|
let node_url = require("node:url");
|
|
34
35
|
let postcss = require("postcss");
|
|
35
36
|
postcss = __toESM(postcss, 1);
|
|
36
|
-
|
|
37
|
-
//#region src/constants.ts
|
|
37
|
+
// src/constants.ts
|
|
38
38
|
const DEFAULT_IGNORE_PATTERNS = [
|
|
39
39
|
"**/node_modules/**",
|
|
40
40
|
"**/dist/**",
|
|
@@ -125,8 +125,7 @@ function getLanguageId(filePath) {
|
|
|
125
125
|
const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase();
|
|
126
126
|
return LANGUAGE_MAP[ext] || "html";
|
|
127
127
|
}
|
|
128
|
-
|
|
129
|
-
//#region src/code-actions.ts
|
|
128
|
+
// src/code-actions.ts
|
|
130
129
|
async function getQuickfixes(state, document, uri, diagnostics) {
|
|
131
130
|
const lspDiagnostics = diagnostics.map((diag) => ({
|
|
132
131
|
range: diag.range,
|
|
@@ -151,49 +150,85 @@ async function getQuickfixes(state, document, uri, diagnostics) {
|
|
|
151
150
|
};
|
|
152
151
|
return (await (0, _tailwindcss_language_service.doCodeActions)(state, params, document)).filter((action) => action.kind === "quickfix" || action.kind?.startsWith(`quickfix.`));
|
|
153
152
|
}
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
153
|
+
function diagnosticKey(diagnostic) {
|
|
154
|
+
const { start, end } = diagnostic.range;
|
|
155
|
+
return `${diagnostic.code}:${start.line}:${start.character}:${end.line}:${end.character}:${diagnostic.message}`;
|
|
156
|
+
}
|
|
157
|
+
function overlaps(a, b) {
|
|
158
|
+
return a.start === b.start || a.start < b.end && b.start < a.end;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Picks the first quickfix for every diagnostic and keeps only those whose
|
|
162
|
+
* edits do not overlap, so a single validation pass can fix many issues.
|
|
163
|
+
* Overlapping fixes (e.g. two cssConflict fixes rewriting the same class list)
|
|
164
|
+
* are left for the next pass, which sees the updated document.
|
|
165
|
+
*/
|
|
166
|
+
function selectNonOverlappingEdits(actions, uri, document) {
|
|
167
|
+
const handled = /* @__PURE__ */ new Set();
|
|
168
|
+
const accepted = [];
|
|
169
|
+
let fixCount = 0;
|
|
170
|
+
for (const action of actions) {
|
|
171
|
+
const edits = action.edit?.changes?.[uri];
|
|
172
|
+
if (!edits || edits.length === 0) continue;
|
|
173
|
+
const keys = (action.diagnostics ?? []).map(diagnosticKey);
|
|
174
|
+
if (keys.some((key) => handled.has(key))) continue;
|
|
175
|
+
const offsetEdits = edits.map((edit) => ({
|
|
176
|
+
start: document.offsetAt(edit.range.start),
|
|
177
|
+
end: document.offsetAt(edit.range.end),
|
|
178
|
+
newText: edit.newText
|
|
179
|
+
}));
|
|
180
|
+
if (offsetEdits.some((edit, index) => [...accepted, ...offsetEdits.slice(0, index)].some((other) => overlaps(edit, other)))) continue;
|
|
181
|
+
for (const key of keys) handled.add(key);
|
|
182
|
+
accepted.push(...offsetEdits);
|
|
183
|
+
fixCount++;
|
|
166
184
|
}
|
|
167
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
edits: accepted,
|
|
187
|
+
fixCount
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function applyEdits(content, edits) {
|
|
191
|
+
const sorted = [...edits].sort((a, b) => a.start - b.start);
|
|
192
|
+
let result = "";
|
|
193
|
+
let cursor = 0;
|
|
194
|
+
for (const edit of sorted) {
|
|
195
|
+
result += content.slice(cursor, edit.start) + edit.newText;
|
|
196
|
+
cursor = edit.end;
|
|
197
|
+
}
|
|
198
|
+
return result + content.slice(cursor);
|
|
168
199
|
}
|
|
169
200
|
async function applyCodeActions(state, filePath, content, diagnostics) {
|
|
170
201
|
if (diagnostics.length === 0) return {
|
|
171
202
|
content,
|
|
172
203
|
changed: false,
|
|
173
|
-
fixedCount: 0
|
|
204
|
+
fixedCount: 0,
|
|
205
|
+
diagnostics
|
|
174
206
|
};
|
|
175
207
|
try {
|
|
176
208
|
const languageId = getLanguageId(filePath);
|
|
177
209
|
const uri = `file://${filePath}`;
|
|
178
210
|
let currentDocument = vscode_languageserver_textdocument.TextDocument.create(uri, languageId, 1, content);
|
|
179
211
|
let currentContent = content;
|
|
212
|
+
let currentDiagnostics = diagnostics;
|
|
180
213
|
let totalFixed = 0;
|
|
181
|
-
for (let iteration = 0
|
|
182
|
-
|
|
214
|
+
for (let iteration = 0;; iteration++) {
|
|
215
|
+
if (iteration > 0) currentDiagnostics = await (0, _tailwindcss_language_service.doValidate)(state, currentDocument);
|
|
183
216
|
if (currentDiagnostics.length === 0) break;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
217
|
+
if (iteration === 100) {
|
|
218
|
+
console.warn(`Warning: Reached maximum fix iterations (100) for ${filePath}. Some issues may remain unfixed.`);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
const { edits, fixCount } = selectNonOverlappingEdits(await getQuickfixes(state, currentDocument, uri, currentDiagnostics), uri, currentDocument);
|
|
222
|
+
if (fixCount === 0) break;
|
|
223
|
+
currentContent = applyEdits(currentContent, edits);
|
|
189
224
|
currentDocument = vscode_languageserver_textdocument.TextDocument.create(uri, languageId, currentDocument.version + 1, currentContent);
|
|
190
|
-
totalFixed
|
|
191
|
-
if (iteration === 99) console.warn(`Warning: Reached maximum fix iterations (100) for ${filePath}. Some issues may remain unfixed.`);
|
|
225
|
+
totalFixed += fixCount;
|
|
192
226
|
}
|
|
193
227
|
return {
|
|
194
228
|
content: currentContent,
|
|
195
229
|
changed: currentContent !== content,
|
|
196
|
-
fixedCount: totalFixed
|
|
230
|
+
fixedCount: totalFixed,
|
|
231
|
+
diagnostics: currentDiagnostics
|
|
197
232
|
};
|
|
198
233
|
} catch (error) {
|
|
199
234
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -202,14 +237,14 @@ async function applyCodeActions(state, filePath, content, diagnostics) {
|
|
|
202
237
|
return {
|
|
203
238
|
content,
|
|
204
239
|
changed: false,
|
|
205
|
-
fixedCount: 0
|
|
240
|
+
fixedCount: 0,
|
|
241
|
+
diagnostics
|
|
206
242
|
};
|
|
207
243
|
}
|
|
208
244
|
throw error;
|
|
209
245
|
}
|
|
210
246
|
}
|
|
211
|
-
|
|
212
|
-
//#region src/utils/fs.ts
|
|
247
|
+
// src/utils/fs.ts
|
|
213
248
|
function fileExists(filePath) {
|
|
214
249
|
try {
|
|
215
250
|
return node_fs.existsSync(filePath);
|
|
@@ -228,11 +263,6 @@ function readFileSync(filePath) {
|
|
|
228
263
|
if (!filePath || typeof filePath !== "string") throw new TypeError("File path must be a non-empty string");
|
|
229
264
|
return node_fs.readFileSync(filePath, "utf-8");
|
|
230
265
|
}
|
|
231
|
-
function writeFileSync(filePath, content) {
|
|
232
|
-
if (!filePath || typeof filePath !== "string") throw new TypeError("File path must be a non-empty string");
|
|
233
|
-
if (typeof content !== "string") throw new TypeError("Content must be a string");
|
|
234
|
-
node_fs.writeFileSync(filePath, content, "utf-8");
|
|
235
|
-
}
|
|
236
266
|
function readGitignorePatterns(cwd) {
|
|
237
267
|
const gitignorePath = node_path.join(cwd, ".gitignore");
|
|
238
268
|
if (!fileExists(gitignorePath)) return [];
|
|
@@ -246,8 +276,7 @@ function readGitignorePatterns(cwd) {
|
|
|
246
276
|
return [];
|
|
247
277
|
}
|
|
248
278
|
}
|
|
249
|
-
|
|
250
|
-
//#region src/utils/settings.ts
|
|
279
|
+
// src/utils/settings.ts
|
|
251
280
|
function loadWorkspaceTailwindSettings(cwd) {
|
|
252
281
|
return mergeTailwindSettings(readVsCodeTailwindSettings(cwd), readZedTailwindSettings(cwd));
|
|
253
282
|
}
|
|
@@ -396,8 +425,7 @@ function mergeEditorSettings(defaultSettings, overrides) {
|
|
|
396
425
|
}
|
|
397
426
|
};
|
|
398
427
|
}
|
|
399
|
-
|
|
400
|
-
//#region src/adapters/editor-state-adapter.ts
|
|
428
|
+
// src/adapters/editor-state-adapter.ts
|
|
401
429
|
function isDirectory(filePath) {
|
|
402
430
|
try {
|
|
403
431
|
return node_fs.statSync(filePath).isDirectory();
|
|
@@ -480,8 +508,7 @@ function createEditorState(cwd) {
|
|
|
480
508
|
}
|
|
481
509
|
};
|
|
482
510
|
}
|
|
483
|
-
|
|
484
|
-
//#region src/types.ts
|
|
511
|
+
// src/types.ts
|
|
485
512
|
/**
|
|
486
513
|
* Error thrown when adapter fails to load required modules
|
|
487
514
|
*/
|
|
@@ -493,8 +520,7 @@ var AdapterLoadError = class extends Error {
|
|
|
493
520
|
this.name = "AdapterLoadError";
|
|
494
521
|
}
|
|
495
522
|
};
|
|
496
|
-
|
|
497
|
-
//#region src/adapters/v3-adapter.ts
|
|
523
|
+
// src/adapters/v3-adapter.ts
|
|
498
524
|
const require$4 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href || __filename);
|
|
499
525
|
function interopDefault(module) {
|
|
500
526
|
return module && typeof module === "object" && "default" in module ? module.default : module;
|
|
@@ -543,8 +569,7 @@ function extractConfigMetadata(state) {
|
|
|
543
569
|
state.blocklist = config.blocklist ?? [];
|
|
544
570
|
if (config.corePlugins) state.corePlugins = Array.isArray(config.corePlugins) ? config.corePlugins : Object.keys(config.corePlugins);
|
|
545
571
|
}
|
|
546
|
-
|
|
547
|
-
//#region src/adapters/v4-adapter.ts
|
|
572
|
+
// src/adapters/v4-adapter.ts
|
|
548
573
|
const require$3 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href || __filename);
|
|
549
574
|
function resolveModule(id, paths) {
|
|
550
575
|
let firstError;
|
|
@@ -592,6 +617,21 @@ function fromPostcss(nodes) {
|
|
|
592
617
|
}
|
|
593
618
|
});
|
|
594
619
|
}
|
|
620
|
+
function memoizeCanonicalize(designSystem) {
|
|
621
|
+
const canonicalize = designSystem.canonicalizeCandidates;
|
|
622
|
+
if (typeof canonicalize === "function") {
|
|
623
|
+
const cache = /* @__PURE__ */ new Map();
|
|
624
|
+
designSystem.canonicalizeCandidates = ((candidates, options) => {
|
|
625
|
+
const key = `${JSON.stringify(options ?? null)}\0${candidates.join(" ")}`;
|
|
626
|
+
let result = cache.get(key);
|
|
627
|
+
if (!result) {
|
|
628
|
+
result = canonicalize.call(designSystem, candidates, options);
|
|
629
|
+
cache.set(key, result);
|
|
630
|
+
}
|
|
631
|
+
return [...result];
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
595
635
|
async function loadV4DesignSystem(state, resolvePaths, target, verbose = false) {
|
|
596
636
|
try {
|
|
597
637
|
const tailwindPath = require$3.resolve("tailwindcss", { paths: resolvePaths });
|
|
@@ -637,19 +677,29 @@ async function loadV4DesignSystem(state, resolvePaths, target, verbose = false)
|
|
|
637
677
|
};
|
|
638
678
|
}
|
|
639
679
|
});
|
|
680
|
+
const compileUncached = (classes) => {
|
|
681
|
+
if (designSystem.candidatesToAst) return designSystem.candidatesToAst(classes);
|
|
682
|
+
if (designSystem.candidatesToCss) return designSystem.candidatesToCss(classes).map((css) => {
|
|
683
|
+
if (!css) return [];
|
|
684
|
+
try {
|
|
685
|
+
return fromPostcss(postcss.default.parse(css).nodes);
|
|
686
|
+
} catch {
|
|
687
|
+
return [];
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
return classes.map(() => []);
|
|
691
|
+
};
|
|
692
|
+
memoizeCanonicalize(designSystem);
|
|
693
|
+
const compileCache = /* @__PURE__ */ new Map();
|
|
640
694
|
Object.assign(designSystem, {
|
|
641
695
|
dependencies: () => /* @__PURE__ */ new Set(),
|
|
642
696
|
compile(classes) {
|
|
643
|
-
|
|
644
|
-
if (
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
return [];
|
|
650
|
-
}
|
|
651
|
-
});
|
|
652
|
-
return classes.map(() => []);
|
|
697
|
+
const missing = [...new Set(classes.filter((c) => !compileCache.has(c)))];
|
|
698
|
+
if (missing.length > 0) {
|
|
699
|
+
const compiled = compileUncached(missing);
|
|
700
|
+
for (const [index, className] of missing.entries()) compileCache.set(className, compiled[index] ?? []);
|
|
701
|
+
}
|
|
702
|
+
return classes.map((c) => compileCache.get(c) ?? []);
|
|
653
703
|
}
|
|
654
704
|
});
|
|
655
705
|
state.designSystem = designSystem;
|
|
@@ -662,8 +712,7 @@ async function loadV4DesignSystem(state, resolvePaths, target, verbose = false)
|
|
|
662
712
|
throw new Error(`Failed to load v4 design system: ${String(error)}`);
|
|
663
713
|
}
|
|
664
714
|
}
|
|
665
|
-
|
|
666
|
-
//#region src/utils/config.ts
|
|
715
|
+
// src/utils/config.ts
|
|
667
716
|
const require$2 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href || __filename);
|
|
668
717
|
const CONFIG_DISCOVERY_MAX_DEPTH = 8;
|
|
669
718
|
const PROJECT_ROOT_MARKERS = ["package.json", ".git"];
|
|
@@ -838,8 +887,7 @@ function isPreferredCssFolder(relativePath) {
|
|
|
838
887
|
function splitDepth(value) {
|
|
839
888
|
return value.split(/[\\/]/).filter(Boolean).length;
|
|
840
889
|
}
|
|
841
|
-
|
|
842
|
-
//#region src/state.ts
|
|
890
|
+
// src/state.ts
|
|
843
891
|
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href || __filename);
|
|
844
892
|
function getTailwindVersion(paths) {
|
|
845
893
|
try {
|
|
@@ -916,8 +964,7 @@ async function createState(cwd, target, verbose = false) {
|
|
|
916
964
|
else await loadV3ClassMetadata(state, resolvePaths, verbose);
|
|
917
965
|
return state;
|
|
918
966
|
}
|
|
919
|
-
|
|
920
|
-
//#region src/linter.ts
|
|
967
|
+
// src/linter.ts
|
|
921
968
|
const SOURCE_FILE_PATTERN = "**/*.{js,jsx,ts,tsx,html,vue,svelte,astro,mdx}";
|
|
922
969
|
const GLOB_PATTERN_REGEX = /[*?[\]{}]/;
|
|
923
970
|
const MAX_LISTED_PACKAGES = 10;
|
|
@@ -1019,37 +1066,53 @@ function extractImportSourceDirectives(cssContent) {
|
|
|
1019
1066
|
disableAutoSource
|
|
1020
1067
|
};
|
|
1021
1068
|
}
|
|
1022
|
-
async function processFiles(state, cwd, files, fix, onProgress) {
|
|
1023
|
-
const results =
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1069
|
+
async function processFiles(state, cwd, files, fix, dryRun, onProgress) {
|
|
1070
|
+
const results = Array.from({ length: files.length });
|
|
1071
|
+
let next = 0;
|
|
1072
|
+
let started = 0;
|
|
1073
|
+
const worker = async () => {
|
|
1074
|
+
while (next < files.length) {
|
|
1075
|
+
const index = next++;
|
|
1076
|
+
const file = files[index];
|
|
1077
|
+
onProgress?.(++started, files.length, file);
|
|
1078
|
+
results[index] = await processFile(state, cwd, file, fix, dryRun);
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
await Promise.all(Array.from({ length: Math.min(10, files.length) }, worker));
|
|
1082
|
+
return results.filter((result) => result !== null);
|
|
1083
|
+
}
|
|
1084
|
+
async function readFileIfExists(filePath) {
|
|
1085
|
+
try {
|
|
1086
|
+
return await node_fs_promises.readFile(filePath, "utf-8");
|
|
1087
|
+
} catch {
|
|
1088
|
+
return null;
|
|
1031
1089
|
}
|
|
1032
|
-
return results;
|
|
1033
1090
|
}
|
|
1034
|
-
async function processFile(state, cwd, filePath, fix) {
|
|
1091
|
+
async function processFile(state, cwd, filePath, fix, dryRun) {
|
|
1035
1092
|
const absolutePath = node_path.isAbsolute(filePath) ? filePath : node_path.resolve(cwd, filePath);
|
|
1036
|
-
|
|
1037
|
-
|
|
1093
|
+
const content = await readFileIfExists(absolutePath);
|
|
1094
|
+
if (content === null) return null;
|
|
1038
1095
|
let diagnostics = await validateDocument(state, absolutePath, content);
|
|
1039
1096
|
let fixedCount = 0;
|
|
1097
|
+
let fixedContent;
|
|
1040
1098
|
if (fix && diagnostics.length > 0) {
|
|
1041
1099
|
const fixResult = await applyCodeActions(state, absolutePath, content, diagnostics);
|
|
1042
1100
|
if (fixResult.changed) {
|
|
1043
|
-
|
|
1101
|
+
if (dryRun) fixedContent = fixResult.content;
|
|
1102
|
+
else await node_fs_promises.writeFile(absolutePath, fixResult.content, "utf-8");
|
|
1044
1103
|
fixedCount = fixResult.fixedCount;
|
|
1045
|
-
diagnostics =
|
|
1104
|
+
diagnostics = fixResult.diagnostics;
|
|
1046
1105
|
}
|
|
1047
1106
|
}
|
|
1048
1107
|
return {
|
|
1049
1108
|
path: node_path.relative(cwd, absolutePath),
|
|
1050
1109
|
diagnostics,
|
|
1051
1110
|
fixed: fixedCount > 0,
|
|
1052
|
-
fixedCount
|
|
1111
|
+
fixedCount,
|
|
1112
|
+
...fixedContent !== void 0 && {
|
|
1113
|
+
originalContent: content,
|
|
1114
|
+
fixedContent
|
|
1115
|
+
}
|
|
1053
1116
|
};
|
|
1054
1117
|
}
|
|
1055
1118
|
async function initializeState(cwd, target, verbose = false) {
|
|
@@ -1062,7 +1125,7 @@ async function initializeState(cwd, target, verbose = false) {
|
|
|
1062
1125
|
throw new Error(`Failed to initialize Tailwind state: ${message}`);
|
|
1063
1126
|
}
|
|
1064
1127
|
}
|
|
1065
|
-
async function lint({ cwd, patterns, configPath, autoDiscover, fix = false, verbose = false, onProgress }) {
|
|
1128
|
+
async function lint({ cwd, patterns, configPath, autoDiscover, fix = false, dryRun = false, verbose = false, onProgress }) {
|
|
1066
1129
|
const targets = await resolveConfigTargets(cwd, configPath);
|
|
1067
1130
|
if (verbose) logTargets(cwd, targets);
|
|
1068
1131
|
const configRoots = targets.map((target) => target.root);
|
|
@@ -1100,7 +1163,7 @@ async function lint({ cwd, patterns, configPath, autoDiscover, fix = false, verb
|
|
|
1100
1163
|
if (!files) continue;
|
|
1101
1164
|
const offset = processedCount;
|
|
1102
1165
|
processedCount += files.length;
|
|
1103
|
-
const processed = await processFiles(await initializeState(cwd, target, verbose), cwd, files.sort((a, b) => a.localeCompare(b)), fix, onProgress && ((current, _total, file) => onProgress(offset + current, totalFiles, file)));
|
|
1166
|
+
const processed = await processFiles(await initializeState(cwd, target, verbose), cwd, files.sort((a, b) => a.localeCompare(b)), fix || dryRun, dryRun, onProgress && ((current, _total, file) => onProgress(offset + current, totalFiles, file)));
|
|
1104
1167
|
results.push(...processed);
|
|
1105
1168
|
}
|
|
1106
1169
|
return {
|
|
@@ -1138,7 +1201,6 @@ function logTargets(cwd, targets) {
|
|
|
1138
1201
|
function ownershipScore(target, absolutePath) {
|
|
1139
1202
|
return isInsideDir(target.root, absolutePath) ? target.root.length : -1;
|
|
1140
1203
|
}
|
|
1141
|
-
//#endregion
|
|
1142
1204
|
Object.defineProperty(exports, "DEFAULT_FILE_PATTERN", {
|
|
1143
1205
|
enumerable: true,
|
|
1144
1206
|
get: function() {
|
package/dist/linter.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_linter = require("./linter-
|
|
2
|
+
const require_linter = require("./linter-CUpiAi7p.cjs");
|
|
3
3
|
exports.extractImportSourceDirectives = require_linter.extractImportSourceDirectives;
|
|
4
4
|
exports.extractSourcePatterns = require_linter.extractSourcePatterns;
|
|
5
5
|
exports.lint = require_linter.lint;
|
package/dist/linter.d.cts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region node_modules/.pnpm/vscode-languageserver-types@3.18.4/node_modules/vscode-languageserver-types/lib/esm/main.d.ts
|
|
2
1
|
/**
|
|
3
2
|
* A tagging type for string properties that are actually document URIs.
|
|
4
3
|
*/
|
|
@@ -430,13 +429,15 @@ declare namespace MarkupContent {
|
|
|
430
429
|
*/
|
|
431
430
|
function is(value: any): value is MarkupContent;
|
|
432
431
|
}
|
|
433
|
-
//#endregion
|
|
434
|
-
//#region src/types.d.ts
|
|
435
432
|
interface LintFileResult {
|
|
436
433
|
path: string;
|
|
437
434
|
diagnostics: Diagnostic[];
|
|
438
435
|
fixed?: boolean;
|
|
439
436
|
fixedCount?: number;
|
|
437
|
+
/** Content before fixing; only set in dry-run mode when fixes are available */
|
|
438
|
+
originalContent?: string;
|
|
439
|
+
/** Content after fixing; only set in dry-run mode when fixes are available */
|
|
440
|
+
fixedContent?: string;
|
|
440
441
|
}
|
|
441
442
|
interface LintOptions {
|
|
442
443
|
cwd: string;
|
|
@@ -444,6 +445,8 @@ interface LintOptions {
|
|
|
444
445
|
configPath?: string;
|
|
445
446
|
autoDiscover: boolean;
|
|
446
447
|
fix?: boolean;
|
|
448
|
+
/** Compute fixes without writing them to disk (implies `fix`) */
|
|
449
|
+
dryRun?: boolean;
|
|
447
450
|
verbose?: boolean;
|
|
448
451
|
onProgress?: (current: number, total: number, file: string) => void;
|
|
449
452
|
}
|
|
@@ -452,8 +455,6 @@ interface LintResult {
|
|
|
452
455
|
totalFilesProcessed: number;
|
|
453
456
|
skippedFiles: string[];
|
|
454
457
|
}
|
|
455
|
-
//#endregion
|
|
456
|
-
//#region src/linter.d.ts
|
|
457
458
|
declare function extractSourcePatterns(cssContent: string): {
|
|
458
459
|
include: string[];
|
|
459
460
|
exclude: string[];
|
|
@@ -462,6 +463,5 @@ declare function extractImportSourceDirectives(cssContent: string): {
|
|
|
462
463
|
roots: string[];
|
|
463
464
|
disableAutoSource: boolean;
|
|
464
465
|
};
|
|
465
|
-
export declare function lint({ cwd, patterns, configPath, autoDiscover, fix, verbose, onProgress }: LintOptions): Promise<LintResult>;
|
|
466
|
-
//#endregion
|
|
466
|
+
export declare function lint({ cwd, patterns, configPath, autoDiscover, fix, dryRun, verbose, onProgress }: LintOptions): Promise<LintResult>;
|
|
467
467
|
export { type LintFileResult, type LintOptions, type LintResult, extractImportSourceDirectives, extractSourcePatterns };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tailwind-lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "A command-line tool that uses the Tailwind CSS IntelliSense plugin to show linting suggestions for your Tailwind CSS classes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -61,22 +61,22 @@
|
|
|
61
61
|
"@tailwindcss/language-service": "^0.16.0",
|
|
62
62
|
"ansis": "^4.4.0",
|
|
63
63
|
"commander": "^15.0.0",
|
|
64
|
-
"postcss": "^8.5.
|
|
64
|
+
"postcss": "^8.5.32",
|
|
65
65
|
"tinyglobby": "^0.2.17",
|
|
66
|
-
"vscode-languageserver-textdocument": "^1.0.
|
|
66
|
+
"vscode-languageserver-textdocument": "^1.0.16"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@types/node": "^26.6.
|
|
70
|
-
"oxfmt": "^0.
|
|
71
|
-
"oxlint": "^1.
|
|
72
|
-
"tsdown": "^0.23.
|
|
69
|
+
"@types/node": "^26.6.5",
|
|
70
|
+
"oxfmt": "^0.75.0",
|
|
71
|
+
"oxlint": "^1.90.0",
|
|
72
|
+
"tsdown": "^0.23.2",
|
|
73
73
|
"typescript": "^7.0.2",
|
|
74
|
-
"vitest": "^5.0.
|
|
75
|
-
"vscode-languageserver": "
|
|
74
|
+
"vitest": "^5.0.5",
|
|
75
|
+
"vscode-languageserver": "11.0.0"
|
|
76
76
|
},
|
|
77
77
|
"engines": {
|
|
78
78
|
"node": ">=22.0.0",
|
|
79
79
|
"pnpm": ">=10.0.0"
|
|
80
80
|
},
|
|
81
|
-
"packageManager": "pnpm@12.
|
|
81
|
+
"packageManager": "pnpm@12.15.0"
|
|
82
82
|
}
|