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 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
- Files are written atomically with multiple iterations to ensure all fixes are applied. The autofix process has a safety limit of 100 iterations per file to prevent infinite loops.
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-B-Bi_uRJ.cjs");
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
- //#region src/output.ts
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
- //#endregion
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 || false,
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
- async function displayResults(files, fixMode) {
129
- let totalErrors = 0;
130
- let totalWarnings = 0;
131
- let totalFixed = 0;
132
- let filesWithIssues = 0;
133
- let isFirstFile = true;
134
- for (const file of files) if (file.diagnostics.length > 0 || fixMode && file.fixed) {
135
- if (isFirstFile) {
136
- console.log();
137
- isFirstFile = false;
138
- } else console.log();
139
- console.log(ansis.default.underline.bold(file.path));
140
- if (fixMode && file.fixed) {
141
- const issueText = `${file.fixedCount || 0} issue${file.fixedCount !== 1 ? "s" : ""}`;
142
- console.log(ansis.default.green(` ✔ Fixed ${issueText}`));
143
- totalFixed += file.fixedCount || 0;
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
- const { errors, warnings } = countBySeverity(file.diagnostics);
146
- totalErrors += errors;
147
- totalWarnings += warnings;
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
- for (const diagnostic of file.diagnostics) {
150
- const line = diagnostic.range.start.line + 1;
151
- const char = diagnostic.range.start.character + 1;
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
- console.log();
159
- if (totalErrors === 0 && totalWarnings === 0) {
160
- if (totalFixed > 0) {
161
- const issueText = `${totalFixed} issue${totalFixed !== 1 ? "s" : ""}`;
162
- console.log(ansis.default.green.bold(`✔ Fixed ${issueText}`));
163
- } else console.log(ansis.default.green.bold("✔ No issues found"));
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 parts = [];
166
- if (totalErrors > 0) parts.push(`${totalErrors} error${totalErrors !== 1 ? "s" : ""}`);
167
- if (totalWarnings > 0) parts.push(`${totalWarnings} warning${totalWarnings !== 1 ? "s" : ""}`);
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 = (options.format === "json" ? "json" : "text") === "json";
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 (isJsonOutput) return;
248
- if (process.stdout.isTTY && !resolved.verbose) {
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 (process.stdout.isTTY && !resolved.verbose && !isJsonOutput) process.stdout.write(`\r${" ".repeat(80)}\r`);
255
- if (results.skippedFiles.length > 0 && !isJsonOutput) for (const skipped of results.skippedFiles) console.log(ansis.default.yellow(`⚠ Skipped ${truncateFilename(skipped)} (exceeds size limit)`));
256
- if (results.totalFilesProcessed === 0) {
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
- else await displayResults(results.files, resolved.fix);
286
- const hasErrors = results.files.some((file) => file.diagnostics.some((d) => d.severity === require_linter.SEVERITY.ERROR));
287
- process.exit(hasErrors ? 1 : 0);
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) console.log(JSON.stringify(createJsonErrorOutput({
358
+ if (isJsonOutput) return writeAndExit(`${JSON.stringify(createJsonErrorOutput({
291
359
  error: errorMessage,
292
360
  ...resolved
293
- })));
294
- else console.error(ansis.default.red("✖ Error:"), errorMessage);
295
- if (resolved.verbose && error instanceof Error && !isJsonOutput) {
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
- //#region \0rolldown/runtime.js
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
- //#endregion
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
- let node_fs_promises = require("node:fs/promises");
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
- //#endregion
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 applyFirstQuickfix(action, uri, document, content) {
155
- if (!action.edit?.changes?.[uri]) return null;
156
- const sortedEdits = [...action.edit.changes[uri]].sort((a, b) => {
157
- const lineDiff = b.range.start.line - a.range.start.line;
158
- if (lineDiff !== 0) return lineDiff;
159
- return b.range.start.character - a.range.start.character;
160
- });
161
- let newContent = content;
162
- for (const edit of sortedEdits) {
163
- const startOffset = document.offsetAt(edit.range.start);
164
- const endOffset = document.offsetAt(edit.range.end);
165
- newContent = newContent.substring(0, startOffset) + edit.newText + newContent.substring(endOffset);
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 { content: newContent };
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; iteration < 100; iteration++) {
182
- const currentDiagnostics = await (0, _tailwindcss_language_service.doValidate)(state, currentDocument);
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
- const quickfixes = await getQuickfixes(state, currentDocument, uri, currentDiagnostics);
185
- if (quickfixes.length === 0) break;
186
- const fixResult = applyFirstQuickfix(quickfixes[0], uri, currentDocument, currentContent);
187
- if (!fixResult) break;
188
- currentContent = fixResult.content;
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- if (designSystem.candidatesToAst) return designSystem.candidatesToAst(classes);
644
- if (designSystem.candidatesToCss) return designSystem.candidatesToCss(classes).map((css) => {
645
- if (!css) return [];
646
- try {
647
- return fromPostcss(postcss.default.parse(css).nodes);
648
- } catch {
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
- //#endregion
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
- //#endregion
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
- //#endregion
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
- for (let i = 0; i < files.length; i += 10) {
1025
- const batch = files.slice(i, i + 10);
1026
- const batchResults = await Promise.all(batch.map(async (file, batchIndex) => {
1027
- if (onProgress) onProgress(i + batchIndex + 1, files.length, file);
1028
- return processFile(state, cwd, file, fix);
1029
- }));
1030
- results.push(...batchResults.filter((r) => r !== null));
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
- if (!fileExists(absolutePath)) return null;
1037
- const content = readFileSync(absolutePath);
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
- writeFileSync(absolutePath, fixResult.content);
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 = await validateDocument(state, absolutePath, fixResult.content);
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-B-Bi_uRJ.cjs");
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
- declare type DocumentUri = string;
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
- declare type URI = string;
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
- declare type integer = number;
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
- declare type uinteger = number;
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
- declare type LSPAny = any;
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`](#clientCapabilities).
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`](#serverCapabilities). If the string value
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
- declare type DiagnosticSeverity = 1 | 2 | 3 | 4;
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
- declare type DiagnosticTag = 1 | 2;
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. Can be omitted. If omitted it is up to the
297
- * client to interpret diagnostics as error, warning, info or hint.
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.13.2",
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.28",
64
+ "postcss": "^8.5.32",
65
65
  "tinyglobby": "^0.2.17",
66
- "vscode-languageserver-textdocument": "^1.0.15"
66
+ "vscode-languageserver-textdocument": "^1.0.16"
67
67
  },
68
68
  "devDependencies": {
69
- "@types/node": "^26.6.4",
70
- "oxfmt": "^0.71.0",
71
- "oxlint": "^1.86.0",
72
- "tsdown": "^0.23.0",
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.3",
75
- "vscode-languageserver": "8.1.0"
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.8.1"
81
+ "packageManager": "pnpm@12.15.0"
82
82
  }