tailwind-lint 0.13.2 → 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 +106 -27
- 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,8 +1,7 @@
|
|
|
1
|
-
//#region node_modules/.pnpm/vscode-languageserver-types@3.17.3/node_modules/vscode-languageserver-types/lib/umd/main.d.ts
|
|
2
1
|
/**
|
|
3
2
|
* A tagging type for string properties that are actually document URIs.
|
|
4
3
|
*/
|
|
5
|
-
|
|
4
|
+
type DocumentUri = string;
|
|
6
5
|
declare namespace DocumentUri {
|
|
7
6
|
function is(value: any): value is DocumentUri;
|
|
8
7
|
}
|
|
@@ -11,14 +10,14 @@ declare namespace DocumentUri {
|
|
|
11
10
|
*
|
|
12
11
|
* @since 3.16.0
|
|
13
12
|
*/
|
|
14
|
-
|
|
13
|
+
type URI = string;
|
|
15
14
|
declare namespace URI {
|
|
16
15
|
function is(value: any): value is URI;
|
|
17
16
|
}
|
|
18
17
|
/**
|
|
19
18
|
* Defines an integer in the range of -2^31 to 2^31 - 1.
|
|
20
19
|
*/
|
|
21
|
-
|
|
20
|
+
type integer = number;
|
|
22
21
|
declare namespace integer {
|
|
23
22
|
const MIN_VALUE = -2147483648;
|
|
24
23
|
const MAX_VALUE = 2147483647;
|
|
@@ -27,7 +26,7 @@ declare namespace integer {
|
|
|
27
26
|
/**
|
|
28
27
|
* Defines an unsigned integer in the range of 0 to 2^31 - 1.
|
|
29
28
|
*/
|
|
30
|
-
|
|
29
|
+
type uinteger = number;
|
|
31
30
|
declare namespace uinteger {
|
|
32
31
|
const MIN_VALUE = 0;
|
|
33
32
|
const MAX_VALUE = 2147483647;
|
|
@@ -56,7 +55,7 @@ declare namespace uinteger {
|
|
|
56
55
|
*
|
|
57
56
|
* @since 3.17.0
|
|
58
57
|
*/
|
|
59
|
-
|
|
58
|
+
type LSPAny = any;
|
|
60
59
|
/**
|
|
61
60
|
* Position in a text document expressed as zero-based line and character
|
|
62
61
|
* offset. Prior to 3.17 the offsets were always based on a UTF-16 string
|
|
@@ -65,14 +64,14 @@ declare type LSPAny = any;
|
|
|
65
64
|
* offset of b is 3 since `𐐀` is represented using two code units in UTF-16.
|
|
66
65
|
* Since 3.17 clients and servers can agree on a different string encoding
|
|
67
66
|
* representation (e.g. UTF-8). The client announces it's supported encoding
|
|
68
|
-
* via the client capability [`general.positionEncodings`](
|
|
67
|
+
* via the client capability [`general.positionEncodings`](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#clientCapabilities).
|
|
69
68
|
* The value is an array of position encodings the client supports, with
|
|
70
69
|
* decreasing preference (e.g. the encoding at index `0` is the most preferred
|
|
71
70
|
* one). To stay backwards compatible the only mandatory encoding is UTF-16
|
|
72
71
|
* represented via the string `utf-16`. The server can pick one of the
|
|
73
72
|
* encodings offered by the client and signals that encoding back to the
|
|
74
73
|
* client via the initialize result's property
|
|
75
|
-
* [`capabilities.positionEncoding`](
|
|
74
|
+
* [`capabilities.positionEncoding`](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#serverCapabilities). If the string value
|
|
76
75
|
* `utf-16` is missing from the client's capability `general.positionEncodings`
|
|
77
76
|
* servers can safely assume that the client supports UTF-16. If the server
|
|
78
77
|
* omits the position encoding in its initialize result the encoding defaults
|
|
@@ -89,9 +88,6 @@ declare type LSPAny = any;
|
|
|
89
88
|
interface Position {
|
|
90
89
|
/**
|
|
91
90
|
* Line position in a document (zero-based).
|
|
92
|
-
*
|
|
93
|
-
* If a line number is greater than the number of lines in a document, it defaults back to the number of lines in the document.
|
|
94
|
-
* If a line number is negative, it defaults to 0.
|
|
95
91
|
*/
|
|
96
92
|
line: uinteger;
|
|
97
93
|
/**
|
|
@@ -99,9 +95,6 @@ interface Position {
|
|
|
99
95
|
*
|
|
100
96
|
* The meaning of this offset is determined by the negotiated
|
|
101
97
|
* `PositionEncodingKind`.
|
|
102
|
-
*
|
|
103
|
-
* If the character value is greater than the line length it defaults back to the
|
|
104
|
-
* line length.
|
|
105
98
|
*/
|
|
106
99
|
character: uinteger;
|
|
107
100
|
}
|
|
@@ -242,7 +235,7 @@ declare namespace DiagnosticSeverity {
|
|
|
242
235
|
*/
|
|
243
236
|
const Hint: 4;
|
|
244
237
|
}
|
|
245
|
-
|
|
238
|
+
type DiagnosticSeverity = 1 | 2 | 3 | 4;
|
|
246
239
|
/**
|
|
247
240
|
* The diagnostic tags.
|
|
248
241
|
*
|
|
@@ -263,7 +256,7 @@ declare namespace DiagnosticTag {
|
|
|
263
256
|
*/
|
|
264
257
|
const Deprecated: 2;
|
|
265
258
|
}
|
|
266
|
-
|
|
259
|
+
type DiagnosticTag = 1 | 2;
|
|
267
260
|
/**
|
|
268
261
|
* Structure to capture a description for an error code.
|
|
269
262
|
*
|
|
@@ -293,8 +286,9 @@ interface Diagnostic {
|
|
|
293
286
|
*/
|
|
294
287
|
range: Range;
|
|
295
288
|
/**
|
|
296
|
-
* The diagnostic's severity.
|
|
297
|
-
*
|
|
289
|
+
* The diagnostic's severity. To avoid interpretation mismatches when a
|
|
290
|
+
* server is used with different clients it is highly recommended that servers
|
|
291
|
+
* always provide a severity value.
|
|
298
292
|
*/
|
|
299
293
|
severity?: DiagnosticSeverity;
|
|
300
294
|
/**
|
|
@@ -315,9 +309,12 @@ interface Diagnostic {
|
|
|
315
309
|
*/
|
|
316
310
|
source?: string;
|
|
317
311
|
/**
|
|
318
|
-
* The diagnostic's message. It usually appears in the user interface
|
|
312
|
+
* The diagnostic's message. It usually appears in the user interface.
|
|
313
|
+
*
|
|
314
|
+
* @since 3.18.0 - support for MarkupContent. This is guarded by the client
|
|
315
|
+
* capability `textDocument.diagnostic.markupMessageSupport`.
|
|
319
316
|
*/
|
|
320
|
-
message: string;
|
|
317
|
+
message: string | MarkupContent;
|
|
321
318
|
/**
|
|
322
319
|
* Additional metadata about the diagnostic.
|
|
323
320
|
*
|
|
@@ -345,19 +342,102 @@ declare namespace Diagnostic {
|
|
|
345
342
|
/**
|
|
346
343
|
* Creates a new Diagnostic literal.
|
|
347
344
|
*/
|
|
348
|
-
function create(range: Range, message: string, severity?: DiagnosticSeverity, code?: integer | string, source?: string, relatedInformation?: DiagnosticRelatedInformation[]): Diagnostic;
|
|
345
|
+
function create(range: Range, message: string | MarkupContent, severity?: DiagnosticSeverity, code?: integer | string, source?: string, relatedInformation?: DiagnosticRelatedInformation[]): Diagnostic;
|
|
349
346
|
/**
|
|
350
347
|
* Checks whether the given literal conforms to the {@link Diagnostic} interface.
|
|
351
348
|
*/
|
|
352
349
|
function is(value: any): value is Diagnostic;
|
|
350
|
+
/**
|
|
351
|
+
* Checks whether the given diagnostic's message conforms to the 3.17.0
|
|
352
|
+
* version of the protocol where the message is a string.
|
|
353
|
+
*
|
|
354
|
+
* @param value the diagnostic
|
|
355
|
+
* @returns true if the diagnostic's message is a string, false otherwise.
|
|
356
|
+
*/
|
|
357
|
+
function is3_17(value: Diagnostic): value is Omit<Diagnostic, 'message'> & {
|
|
358
|
+
message: string;
|
|
359
|
+
};
|
|
360
|
+
/**
|
|
361
|
+
* Gets the message string of a diagnostic. If the message is already a
|
|
362
|
+
* string, it is returned as is. If the message is a MarkupContent,
|
|
363
|
+
* the value of the MarkupContent is returned. Otherwise an error is thrown.
|
|
364
|
+
*
|
|
365
|
+
* @param diagnostic the diagnostic to get the message string from.
|
|
366
|
+
* @returns the message string of the given diagnostic.
|
|
367
|
+
*/
|
|
368
|
+
function getMessageString(diagnostic: Diagnostic): string;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Describes the content type that a client supports in various
|
|
372
|
+
* result literals like `Hover`, `ParameterInfo` or `CompletionItem`.
|
|
373
|
+
*
|
|
374
|
+
* Please note that `MarkupKinds` must not start with a `$`. This kinds
|
|
375
|
+
* are reserved for internal usage.
|
|
376
|
+
*/
|
|
377
|
+
declare namespace MarkupKind {
|
|
378
|
+
/**
|
|
379
|
+
* Plain text is supported as a content format
|
|
380
|
+
*/
|
|
381
|
+
const PlainText: 'plaintext';
|
|
382
|
+
/**
|
|
383
|
+
* Markdown is supported as a content format
|
|
384
|
+
*/
|
|
385
|
+
const Markdown: 'markdown';
|
|
386
|
+
/**
|
|
387
|
+
* Checks whether the given value is a value of the {@link MarkupKind} type.
|
|
388
|
+
*/
|
|
389
|
+
function is(value: any): value is MarkupKind;
|
|
390
|
+
}
|
|
391
|
+
type MarkupKind = 'plaintext' | 'markdown';
|
|
392
|
+
/**
|
|
393
|
+
* A `MarkupContent` literal represents a string value which content is interpreted base on its
|
|
394
|
+
* kind flag. Currently the protocol supports `plaintext` and `markdown` as markup kinds.
|
|
395
|
+
*
|
|
396
|
+
* If the kind is `markdown` then the value can contain fenced code blocks like in GitHub issues.
|
|
397
|
+
* See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting
|
|
398
|
+
*
|
|
399
|
+
* Here is an example how such a string can be constructed using JavaScript / TypeScript:
|
|
400
|
+
* ```ts
|
|
401
|
+
* let markdown: MarkdownContent = {
|
|
402
|
+
* kind: MarkupKind.Markdown,
|
|
403
|
+
* value: [
|
|
404
|
+
* '# Header',
|
|
405
|
+
* 'Some text',
|
|
406
|
+
* '```typescript',
|
|
407
|
+
* 'someCode();',
|
|
408
|
+
* '```'
|
|
409
|
+
* ].join('\n')
|
|
410
|
+
* };
|
|
411
|
+
* ```
|
|
412
|
+
*
|
|
413
|
+
* *Please Note* that clients might sanitize the return markdown. A client could decide to
|
|
414
|
+
* remove HTML from the markdown to avoid script execution.
|
|
415
|
+
*/
|
|
416
|
+
interface MarkupContent {
|
|
417
|
+
/**
|
|
418
|
+
* The type of the Markup
|
|
419
|
+
*/
|
|
420
|
+
kind: MarkupKind;
|
|
421
|
+
/**
|
|
422
|
+
* The content itself
|
|
423
|
+
*/
|
|
424
|
+
value: string;
|
|
425
|
+
}
|
|
426
|
+
declare namespace MarkupContent {
|
|
427
|
+
/**
|
|
428
|
+
* Checks whether the given value conforms to the {@link MarkupContent} interface.
|
|
429
|
+
*/
|
|
430
|
+
function is(value: any): value is MarkupContent;
|
|
353
431
|
}
|
|
354
|
-
//#endregion
|
|
355
|
-
//#region src/types.d.ts
|
|
356
432
|
interface LintFileResult {
|
|
357
433
|
path: string;
|
|
358
434
|
diagnostics: Diagnostic[];
|
|
359
435
|
fixed?: boolean;
|
|
360
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;
|
|
361
441
|
}
|
|
362
442
|
interface LintOptions {
|
|
363
443
|
cwd: string;
|
|
@@ -365,6 +445,8 @@ interface LintOptions {
|
|
|
365
445
|
configPath?: string;
|
|
366
446
|
autoDiscover: boolean;
|
|
367
447
|
fix?: boolean;
|
|
448
|
+
/** Compute fixes without writing them to disk (implies `fix`) */
|
|
449
|
+
dryRun?: boolean;
|
|
368
450
|
verbose?: boolean;
|
|
369
451
|
onProgress?: (current: number, total: number, file: string) => void;
|
|
370
452
|
}
|
|
@@ -373,8 +455,6 @@ interface LintResult {
|
|
|
373
455
|
totalFilesProcessed: number;
|
|
374
456
|
skippedFiles: string[];
|
|
375
457
|
}
|
|
376
|
-
//#endregion
|
|
377
|
-
//#region src/linter.d.ts
|
|
378
458
|
declare function extractSourcePatterns(cssContent: string): {
|
|
379
459
|
include: string[];
|
|
380
460
|
exclude: string[];
|
|
@@ -383,6 +463,5 @@ declare function extractImportSourceDirectives(cssContent: string): {
|
|
|
383
463
|
roots: string[];
|
|
384
464
|
disableAutoSource: boolean;
|
|
385
465
|
};
|
|
386
|
-
export declare function lint({ cwd, patterns, configPath, autoDiscover, fix, verbose, onProgress }: LintOptions): Promise<LintResult>;
|
|
387
|
-
//#endregion
|
|
466
|
+
export declare function lint({ cwd, patterns, configPath, autoDiscover, fix, dryRun, verbose, onProgress }: LintOptions): Promise<LintResult>;
|
|
388
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
|
}
|